add globals to mesh view bind group (#5409)

# Objective

- It's often really useful to have access to the time when writing shaders.

## Solution

- Add a UnifformBuffer in the mesh view bind group
- This buffer contains the time, delta time and a wrapping frame count

https://user-images.githubusercontent.com/8348954/180130314-97948c2a-2d11-423d-a9c4-fb5c9d1892c7.mp4

---

## Changelog

- Added a `GlobalsUniform` at position 9 of the mesh view bind group

## Notes

The implementation is currently split between bevy_render and bevy_pbr because I was basing my implementation on the `ViewPlugin`. I'm not sure if that's the right way to structure it.

I named this `globals` instead of just time because we could potentially add more things to it.

## References in other engines

- Godot: <https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/canvas_item_shader.html#global-built-ins>
    - Global time since startup, in seconds, by default resets to 0 after 3600 seconds
    - Doesn't seem to have anything else
- Unreal: <https://docs.unrealengine.com/4.26/en-US/RenderingAndGraphics/Materials/ExpressionReference/Constant/>
    - Generic time value that updates every frame. Can be paused or scaled.
    - Frame count node, doesn't seem to be an equivalent for shaders: <https://docs.unrealengine.com/4.26/en-US/BlueprintAPI/Utilities/GetFrameCount/>
- Unity: <https://docs.unity3d.com/Manual/SL-UnityShaderVariables.html>
    - time since startup in seconds. No mention of time wrapping. Stored as a `vec4(t/20, t, t*2, t*3)` where `t` is the value in seconds
    - Also has delta time, sin time and cos time
- ShaderToy: <https://www.shadertoy.com/howto>
    - iTime is the time since startup in seconds.
    - iFrameRate
    - iTimeDelta
    - iFrame frame counter

Co-authored-by: Charles <IceSentry@users.noreply.github.com>
This commit is contained in:
Charles 2022-09-28 04:20:27 +00:00
parent 018509c3a1
commit 8073362039
10 changed files with 175 additions and 310 deletions

View file

@ -1,39 +1,7 @@
#import bevy_pbr::mesh_types
// The time since startup data is in the globals binding which is part of the mesh_view_bindings import
#import bevy_pbr::mesh_view_bindings
@group(1) @binding(0)
var<uniform> mesh: Mesh;
// NOTE: Bindings must come before functions that use them!
#import bevy_pbr::mesh_functions
struct Vertex {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = mesh_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
out.uv = vertex.uv;
return out;
}
struct Time {
time_since_startup: f32,
};
@group(2) @binding(0)
var<uniform> time: Time;
fn oklab_to_linear_srgb(c: vec3<f32>) -> vec3<f32> {
let L = c.x;
let a = c.y;
@ -43,22 +11,28 @@ fn oklab_to_linear_srgb(c: vec3<f32>) -> vec3<f32> {
let m_ = L - 0.1055613458 * a - 0.0638541728 * b;
let s_ = L - 0.0894841775 * a - 1.2914855480 * b;
let l = l_*l_*l_;
let m = m_*m_*m_;
let s = s_*s_*s_;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
return vec3<f32>(
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
);
}
struct FragmentInput {
#import bevy_pbr::mesh_vertex_output
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
let speed = 2.0;
let t_1 = sin(time.time_since_startup * speed) * 0.5 + 0.5;
let t_2 = cos(time.time_since_startup * speed);
// The globals binding contains various global values like time
// which is the time since startup in seconds
let t_1 = sin(globals.time * speed) * 0.5 + 0.5;
let t_2 = cos(globals.time * speed);
let distance_to_center = distance(in.uv, vec2<f32>(0.5)) * 1.4;

View file

@ -4,6 +4,7 @@
mod name;
mod task_pool_options;
use bevy_ecs::system::Resource;
pub use bytemuck::{bytes_of, cast_slice, Pod, Zeroable};
pub use name::*;
pub use task_pool_options::*;
@ -37,6 +38,8 @@ impl Plugin for CorePlugin {
register_rust_types(app);
register_math_types(app);
app.init_resource::<FrameCount>();
}
}
@ -83,3 +86,9 @@ fn register_math_types(app: &mut App) {
.register_type::<bevy_math::DQuat>()
.register_type::<bevy_math::Quat>();
}
/// Keeps a count of rendered frames since the start of the app
///
/// Wraps to 0 when it reaches the maximum u32 value
#[derive(Default, Resource, Clone, Copy)]
pub struct FrameCount(pub u32);

View file

@ -16,3 +16,4 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.9.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.9.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.9.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.9.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.9.0-dev" }

View file

@ -1,21 +1,16 @@
use crate::{Diagnostic, DiagnosticId, Diagnostics};
use bevy_app::prelude::*;
use bevy_ecs::system::{Res, ResMut, Resource};
use bevy_core::FrameCount;
use bevy_ecs::system::{Res, ResMut};
use bevy_time::Time;
/// Adds "frame time" diagnostic to an App, specifically "frame time", "fps" and "frame count"
#[derive(Default)]
pub struct FrameTimeDiagnosticsPlugin;
#[derive(Resource)]
pub struct FrameTimeDiagnosticsState {
frame_count: u64,
}
impl Plugin for FrameTimeDiagnosticsPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_startup_system(Self::setup_system)
.insert_resource(FrameTimeDiagnosticsState { frame_count: 0 })
.add_system(Self::diagnostic_system);
}
}
@ -36,12 +31,9 @@ impl FrameTimeDiagnosticsPlugin {
pub fn diagnostic_system(
mut diagnostics: ResMut<Diagnostics>,
time: Res<Time>,
mut state: ResMut<FrameTimeDiagnosticsState>,
frame_count: Res<FrameCount>,
) {
diagnostics.add_measurement(Self::FRAME_COUNT, || {
state.frame_count = state.frame_count.wrapping_add(1);
state.frame_count as f64
});
diagnostics.add_measurement(Self::FRAME_COUNT, || frame_count.0 as f64);
if time.delta_seconds_f64() == 0.0 {
return;
@ -52,9 +44,3 @@ impl FrameTimeDiagnosticsPlugin {
diagnostics.add_measurement(Self::FPS, || 1.0 / time.delta_seconds_f64());
}
}
impl FrameTimeDiagnosticsState {
pub fn reset_frame_count(&mut self) {
self.frame_count = 0;
}
}

View file

@ -13,6 +13,7 @@ use bevy_math::{Mat3A, Mat4, Vec2};
use bevy_reflect::TypeUuid;
use bevy_render::{
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
globals::{GlobalsBuffer, GlobalsUniform},
mesh::{
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
GpuBufferInfo, Mesh, MeshVertexBufferLayout,
@ -383,6 +384,16 @@ impl FromWorld for MeshPipeline {
},
count: None,
},
BindGroupLayoutEntry {
binding: 9,
visibility: ShaderStages::VERTEX_FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(GlobalsUniform::min_size()),
},
count: None,
},
],
label: Some("mesh_view_layout"),
});
@ -469,6 +480,7 @@ impl FromWorld for MeshPipeline {
),
}
};
MeshPipeline {
view_layout,
mesh_layout,
@ -764,11 +776,13 @@ pub fn queue_mesh_view_bind_groups(
global_light_meta: Res<GlobalLightMeta>,
view_uniforms: Res<ViewUniforms>,
views: Query<(Entity, &ViewShadowBindings, &ViewClusterBindings)>,
globals_buffer: Res<GlobalsBuffer>,
) {
if let (Some(view_binding), Some(light_binding), Some(point_light_binding)) = (
if let (Some(view_binding), Some(light_binding), Some(point_light_binding), Some(globals)) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
global_light_meta.gpu_point_lights.binding(),
globals_buffer.buffer.binding(),
) {
for (entity, view_shadow_bindings, view_cluster_bindings) in &views {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
@ -815,6 +829,10 @@ pub fn queue_mesh_view_bind_groups(
binding: 8,
resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(),
},
BindGroupEntry {
binding: 9,
resource: globals.clone(),
},
],
label: Some("mesh_view_bind_group"),
layout: &mesh_pipeline.view_layout,

View file

@ -40,3 +40,6 @@ var<storage> cluster_light_index_lists: ClusterLightIndexLists;
@group(0) @binding(8)
var<storage> cluster_offsets_and_counts: ClusterOffsetsAndCounts;
#endif
@group(0) @binding(9)
var<uniform> globals: Globals;

View file

@ -85,3 +85,14 @@ struct ClusterOffsetsAndCounts {
data: array<vec4<u32>>,
};
#endif
struct Globals {
// The time since startup in seconds
// Wraps to 0 after 1 hour.
time: f32,
// The delta time since the previous frame in seconds
delta_time: f32,
// Frame count since the start of the app.
// It wraps to zero when it reaches the maximum value of a u32.
frame_count: u32,
}

View file

@ -0,0 +1,67 @@
use crate::{
extract_resource::ExtractResource,
render_resource::{ShaderType, UniformBuffer},
renderer::{RenderDevice, RenderQueue},
Extract, RenderApp, RenderStage,
};
use bevy_app::{App, Plugin};
use bevy_core::FrameCount;
use bevy_ecs::prelude::*;
use bevy_reflect::Reflect;
use bevy_time::Time;
pub struct GlobalsPlugin;
impl Plugin for GlobalsPlugin {
fn build(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<GlobalsBuffer>()
.init_resource::<Time>()
.add_system_to_stage(RenderStage::Extract, extract_time)
.add_system_to_stage(RenderStage::Prepare, prepare_globals_buffer);
}
}
}
fn extract_time(mut commands: Commands, time: Extract<Res<Time>>) {
commands.insert_resource(time.clone());
}
/// Contains global values useful when writing shaders.
/// Currently only contains values related to time.
#[derive(Default, Clone, Resource, ExtractResource, Reflect, ShaderType)]
#[reflect(Resource)]
pub struct GlobalsUniform {
/// The time since startup in seconds.
/// Wraps to 0 after 1 hour.
time: f32,
/// The delta time since the previous frame in seconds
delta_time: f32,
/// Frame count since the start of the app.
/// It wraps to zero when it reaches the maximum value of a u32.
frame_count: u32,
}
/// The buffer containing the [`GlobalsUniform`]
#[derive(Resource, Default)]
pub struct GlobalsBuffer {
pub buffer: UniformBuffer<GlobalsUniform>,
}
fn prepare_globals_buffer(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut globals_buffer: ResMut<GlobalsBuffer>,
time: Res<Time>,
frame_count: Res<FrameCount>,
) {
let buffer = globals_buffer.buffer.get_mut();
buffer.time = time.seconds_since_startup_wrapped_f32();
buffer.delta_time = time.delta_seconds();
buffer.frame_count = frame_count.0;
globals_buffer
.buffer
.write_buffer(&render_device, &render_queue);
}

View file

@ -5,6 +5,7 @@ pub mod color;
pub mod extract_component;
mod extract_param;
pub mod extract_resource;
pub mod globals;
pub mod mesh;
pub mod primitives;
pub mod rangefinder;
@ -18,6 +19,7 @@ mod spatial_bundle;
pub mod texture;
pub mod view;
use bevy_core::FrameCount;
use bevy_hierarchy::ValidParentCheckPlugin;
pub use extract_param::Extract;
@ -34,6 +36,7 @@ pub mod prelude {
};
}
use globals::GlobalsPlugin;
pub use once_cell;
use prelude::ComputedVisibility;
@ -324,7 +327,9 @@ impl Plugin for RenderPlugin {
.add_plugin(MeshPlugin)
// NOTE: Load this after renderer initialization so that it knows about the supported
// compressed texture formats
.add_plugin(ImagePlugin);
.add_plugin(ImagePlugin)
.add_plugin(GlobalsPlugin)
.add_plugin(FrameCountPlugin);
}
}
@ -358,3 +363,22 @@ fn extract(app_world: &mut World, render_app: &mut App) {
// see <https://github.com/bevyengine/bevy/issues/5082>
extract.apply_buffers(running_world);
}
pub struct FrameCountPlugin;
impl Plugin for FrameCountPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_system(update_frame_count);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_system_to_stage(RenderStage::Extract, extract_frame_count);
}
}
}
fn update_frame_count(mut frame_count: ResMut<FrameCount>) {
frame_count.0 = frame_count.0.wrapping_add(1);
}
fn extract_frame_count(mut commands: Commands, frame_count: Extract<Res<FrameCount>>) {
commands.insert_resource(**frame_count);
}

View file

@ -1,52 +1,28 @@
//! A shader that uses dynamic data like the time since startup.
//!
//! This example uses a specialized pipeline.
//! The time data is in the globals binding which is part of the `mesh_view_bindings` shader import.
use bevy::{
core_pipeline::core_3d::Transparent3d,
ecs::system::{
lifetimeless::{Read, SRes},
SystemParamItem,
},
pbr::{
DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup,
SetMeshViewBindGroup,
},
prelude::*,
render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
extract_resource::{ExtractResource, ExtractResourcePlugin},
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_phase::{
AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase,
SetItemPipeline, TrackedRenderPass,
},
render_resource::*,
renderer::{RenderDevice, RenderQueue},
view::{ComputedVisibility, ExtractedView, Msaa, Visibility},
RenderApp, RenderStage,
},
};
use bevy::{prelude::*, reflect::TypeUuid, render::render_resource::*};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(CustomMaterialPlugin)
.add_plugin(MaterialPlugin::<CustomMaterial>::default())
.add_startup_system(setup)
.run();
}
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
// cube
commands.spawn((
meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
Transform::from_xyz(0.0, 0.5, 0.0),
GlobalTransform::default(),
CustomMaterial,
Visibility::default(),
ComputedVisibility::default(),
));
commands.spawn(MaterialMeshBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
material: materials.add(CustomMaterial {}),
..default()
});
// camera
commands.spawn(Camera3dBundle {
@ -55,216 +31,12 @@ fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
});
}
#[derive(Component)]
struct CustomMaterial;
#[derive(AsBindGroup, TypeUuid, Debug, Clone)]
#[uuid = "a3d71c04-d054-4946-80f8-ba6cfbc90cad"]
struct CustomMaterial {}
pub struct CustomMaterialPlugin;
impl Plugin for CustomMaterialPlugin {
fn build(&self, app: &mut App) {
let render_device = app.world.resource::<RenderDevice>();
let buffer = render_device.create_buffer(&BufferDescriptor {
label: Some("time uniform buffer"),
size: std::mem::size_of::<f32>() as u64,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
app.add_plugin(ExtractComponentPlugin::<CustomMaterial>::default())
.add_plugin(ExtractResourcePlugin::<ExtractedTime>::default());
app.sub_app_mut(RenderApp)
.add_render_command::<Transparent3d, DrawCustom>()
.insert_resource(TimeMeta {
buffer,
bind_group: None,
})
.init_resource::<CustomPipeline>()
.init_resource::<SpecializedMeshPipelines<CustomPipeline>>()
.add_system_to_stage(RenderStage::Prepare, prepare_time)
.add_system_to_stage(RenderStage::Queue, queue_custom)
.add_system_to_stage(RenderStage::Queue, queue_time_bind_group);
}
}
impl ExtractComponent for CustomMaterial {
type Query = Read<CustomMaterial>;
type Filter = ();
fn extract_component(_: bevy::ecs::query::QueryItem<Self::Query>) -> Self {
CustomMaterial
}
}
// add each entity with a mesh and a `CustomMaterial` to every view's `Transparent3d` render phase using the `CustomPipeline`
#[allow(clippy::too_many_arguments)]
fn queue_custom(
transparent_3d_draw_functions: Res<DrawFunctions<Transparent3d>>,
custom_pipeline: Res<CustomPipeline>,
msaa: Res<Msaa>,
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
mut pipeline_cache: ResMut<PipelineCache>,
render_meshes: Res<RenderAssets<Mesh>>,
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>), With<CustomMaterial>>,
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
) {
let draw_custom = transparent_3d_draw_functions
.read()
.get_id::<DrawCustom>()
.unwrap();
let key = MeshPipelineKey::from_msaa_samples(msaa.samples)
| MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList);
for (view, mut transparent_phase) in &mut views {
let rangefinder = view.rangefinder3d();
for (entity, mesh_uniform, mesh_handle) in &material_meshes {
if let Some(mesh) = render_meshes.get(mesh_handle) {
let pipeline = pipelines
.specialize(&mut pipeline_cache, &custom_pipeline, key, &mesh.layout)
.unwrap();
transparent_phase.add(Transparent3d {
entity,
pipeline,
draw_function: draw_custom,
distance: rangefinder.distance(&mesh_uniform.transform),
});
}
}
}
}
#[derive(Resource, Default)]
struct ExtractedTime {
seconds_since_startup: f32,
}
impl ExtractResource for ExtractedTime {
type Source = Time;
fn extract_resource(time: &Self::Source) -> Self {
ExtractedTime {
seconds_since_startup: time.seconds_since_startup() as f32,
}
}
}
#[derive(Resource)]
struct TimeMeta {
buffer: Buffer,
bind_group: Option<BindGroup>,
}
// write the extracted time into the corresponding uniform buffer
fn prepare_time(
time: Res<ExtractedTime>,
time_meta: ResMut<TimeMeta>,
render_queue: Res<RenderQueue>,
) {
render_queue.write_buffer(
&time_meta.buffer,
0,
bevy::core::cast_slice(&[time.seconds_since_startup]),
);
}
// create a bind group for the time uniform buffer
fn queue_time_bind_group(
render_device: Res<RenderDevice>,
mut time_meta: ResMut<TimeMeta>,
pipeline: Res<CustomPipeline>,
) {
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &pipeline.time_bind_group_layout,
entries: &[BindGroupEntry {
binding: 0,
resource: time_meta.buffer.as_entire_binding(),
}],
});
time_meta.bind_group = Some(bind_group);
}
#[derive(Resource)]
pub struct CustomPipeline {
shader: Handle<Shader>,
mesh_pipeline: MeshPipeline,
time_bind_group_layout: BindGroupLayout,
}
impl FromWorld for CustomPipeline {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();
let shader = asset_server.load("shaders/animate_shader.wgsl");
let render_device = world.resource::<RenderDevice>();
let time_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("time bind group"),
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: BufferSize::new(std::mem::size_of::<f32>() as u64),
},
count: None,
}],
});
let mesh_pipeline = world.resource::<MeshPipeline>();
CustomPipeline {
shader,
mesh_pipeline: mesh_pipeline.clone(),
time_bind_group_layout,
}
}
}
impl SpecializedMeshPipeline for CustomPipeline {
type Key = MeshPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
descriptor.vertex.shader = self.shader.clone();
descriptor.fragment.as_mut().unwrap().shader = self.shader.clone();
descriptor.layout = Some(vec![
self.mesh_pipeline.view_layout.clone(),
self.mesh_pipeline.mesh_layout.clone(),
self.time_bind_group_layout.clone(),
]);
Ok(descriptor)
}
}
type DrawCustom = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
SetTimeBindGroup<2>,
DrawMesh,
);
struct SetTimeBindGroup<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetTimeBindGroup<I> {
type Param = SRes<TimeMeta>;
fn render<'w>(
_view: Entity,
_item: Entity,
time_meta: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let time_bind_group = time_meta.into_inner().bind_group.as_ref().unwrap();
pass.set_bind_group(I, time_bind_group, &[]);
RenderCommandResult::Success
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/animate_shader.wgsl".into()
}
}