diff --git a/Cargo.toml b/Cargo.toml index 76b6199116..af4f416784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ anyhow = "1.0.4" rand = "0.8.0" ron = "0.7.0" serde = { version = "1", features = ["derive"] } +bytemuck = "1.7" # Needed to poll Task examples futures-lite = "1.11.3" @@ -418,6 +419,22 @@ path = "examples/shader/shader_defs.rs" name = "shader_material" path = "examples/shader/shader_material.rs" +[[example]] +name = "shader_material_glsl" +path = "examples/shader/shader_material_glsl.rs" + +[[example]] +name = "shader_instancing" +path = "examples/shader/shader_instancing.rs" + +[[example]] +name = "animate_shader" +path = "examples/shader/animate_shader.rs" + +[[example]] +name = "compute_shader_game_of_life" +path = "examples/shader/compute_shader_game_of_life.rs" + # Tools [[example]] name = "bevymark" diff --git a/assets/shaders/animate_shader.wgsl b/assets/shaders/animate_shader.wgsl new file mode 100644 index 0000000000..fdc60da00b --- /dev/null +++ b/assets/shaders/animate_shader.wgsl @@ -0,0 +1,72 @@ +#import bevy_pbr::mesh_view_bind_group +#import bevy_pbr::mesh_struct + +[[group(1), binding(0)]] +var mesh: Mesh; + +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] normal: vec3; + [[location(2)]] uv: vec2; +}; + +struct VertexOutput { + [[builtin(position)]] clip_position: vec4; + [[location(0)]] uv: vec2; +}; + +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + let world_position = mesh.model * vec4(vertex.position, 1.0); + + var out: VertexOutput; + out.clip_position = view.view_proj * world_position; + out.uv = vertex.uv; + return out; +} + + +struct Time { + time_since_startup: f32; +}; +[[group(2), binding(0)]] +var time: Time; + + +fn oklab_to_linear_srgb(c: vec3) -> vec3 { + let L = c.x; + let a = c.y; + let b = c.z; + + let l_ = L + 0.3963377774 * a + 0.2158037573 * b; + 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_; + + return vec3( + 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, + ); +} + +[[stage(fragment)]] +fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { + 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); + + let distance_to_center = distance(in.uv, vec2(0.5)) * 1.4; + + // blending is done in a perceptual color space: https://bottosson.github.io/posts/oklab/ + let red = vec3(0.627955, 0.224863, 0.125846); + let green = vec3(0.86644, -0.233887, 0.179498); + let blue = vec3(0.701674, 0.274566, -0.169156); + let white = vec3(1.0, 0.0, 0.0); + let mixed = mix(mix(red, blue, t_1), mix(green, white, t_2), distance_to_center); + + return vec4(oklab_to_linear_srgb(mixed), 1.0); +} diff --git a/assets/shaders/custom_material.frag b/assets/shaders/custom_material.frag new file mode 100644 index 0000000000..c18a67b4e3 --- /dev/null +++ b/assets/shaders/custom_material.frag @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) out vec4 o_Target; + +layout(set = 1, binding = 0) uniform CustomMaterial { + vec4 Color; +}; + +void main() { + o_Target = Color; +} diff --git a/assets/shaders/custom_material.vert b/assets/shaders/custom_material.vert new file mode 100644 index 0000000000..1dbf4a8829 --- /dev/null +++ b/assets/shaders/custom_material.vert @@ -0,0 +1,26 @@ +#version 450 + +layout(location = 0) in vec3 Vertex_Position; +layout(location = 1) in vec3 Vertex_Normal; +layout(location = 2) in vec2 Vertex_Uv; + +layout(set = 0, binding = 0) uniform CameraViewProj { + mat4 ViewProj; + mat4 InverseView; + mat4 Projection; + vec3 WorldPosition; + float near; + float far; + float width; + float height; +}; + +layout(set = 2, binding = 0) uniform Mesh { + mat4 Model; + mat4 InverseTransposeModel; + uint flags; +}; + +void main() { + gl_Position = ViewProj * Model * vec4(Vertex_Position, 1.0); +} diff --git a/assets/shaders/game_of_life.wgsl b/assets/shaders/game_of_life.wgsl new file mode 100644 index 0000000000..6acb095fe7 --- /dev/null +++ b/assets/shaders/game_of_life.wgsl @@ -0,0 +1,67 @@ +[[group(0), binding(0)]] +var texture: texture_storage_2d; + +fn hash(value: u32) -> u32 { + var state = value; + state = state ^ 2747636419u; + state = state * 2654435769u; + state = state ^ state >> 16u; + state = state * 2654435769u; + state = state ^ state >> 16u; + state = state * 2654435769u; + return state; +} +fn randomFloat(value: u32) -> f32 { + return f32(hash(value)) / 4294967295.0; +} + +[[stage(compute), workgroup_size(8, 8, 1)]] +fn init([[builtin(global_invocation_id)]] invocation_id: vec3, [[builtin(num_workgroups)]] num_workgroups: vec3) { + let location = vec2(i32(invocation_id.x), i32(invocation_id.y)); + let location_f32 = vec2(f32(invocation_id.x), f32(invocation_id.y)); + + let randomNumber = randomFloat(invocation_id.y * num_workgroups.x + invocation_id.x); + let alive = randomNumber > 0.9; + let color = vec4(f32(alive)); + + textureStore(texture, location, color); +} + + +fn get(location: vec2, offset_x: i32, offset_y: i32) -> i32 { + let value: vec4 = textureLoad(texture, location + vec2(offset_x, offset_y)); + return i32(value.x); +} + +fn count_alive(location: vec2) -> i32 { + return get(location, -1, -1) + + get(location, -1, 0) + + get(location, -1, 1) + + get(location, 0, -1) + + get(location, 0, 1) + + get(location, 1, -1) + + get(location, 1, 0) + + get(location, 1, 1); +} + +[[stage(compute), workgroup_size(8, 8, 1)]] +fn update([[builtin(global_invocation_id)]] invocation_id: vec3) { + let location = vec2(i32(invocation_id.x), i32(invocation_id.y)); + + let n_alive = count_alive(location); + let color = vec4(f32(n_alive) / 8.0); + + var alive: bool; + if (n_alive == 3) { + alive = true; + } else if (n_alive == 2) { + let currently_alive = get(location, 0, 0); + alive = bool(currently_alive); + } else { + alive = false; + } + + storageBarrier(); + + textureStore(texture, location, vec4(f32(alive))); +} \ No newline at end of file diff --git a/assets/shaders/hot.frag b/assets/shaders/hot.frag deleted file mode 100644 index 18c41c8cd5..0000000000 --- a/assets/shaders/hot.frag +++ /dev/null @@ -1,11 +0,0 @@ -#version 450 - -layout(location = 0) out vec4 o_Target; - -layout(set = 2, binding = 0) uniform MyMaterial_color { - vec4 color; -}; - -void main() { - o_Target = color * 0.5; -} diff --git a/assets/shaders/hot.vert b/assets/shaders/hot.vert deleted file mode 100644 index 1809b220df..0000000000 --- a/assets/shaders/hot.vert +++ /dev/null @@ -1,15 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 Vertex_Position; - -layout(set = 0, binding = 0) uniform CameraViewProj { - mat4 ViewProj; -}; - -layout(set = 1, binding = 0) uniform Transform { - mat4 Model; -}; - -void main() { - gl_Position = ViewProj * Model * vec4(Vertex_Position, 1.0); -} diff --git a/assets/shaders/instancing.wgsl b/assets/shaders/instancing.wgsl new file mode 100644 index 0000000000..262df7224b --- /dev/null +++ b/assets/shaders/instancing.wgsl @@ -0,0 +1,35 @@ +#import bevy_pbr::mesh_view_bind_group +#import bevy_pbr::mesh_struct + +[[group(1), binding(0)]] +var mesh: Mesh; + +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] normal: vec3; + [[location(2)]] uv: vec2; + + [[location(3)]] i_pos_scale: vec4; + [[location(4)]] i_color: vec4; +}; + +struct VertexOutput { + [[builtin(position)]] clip_position: vec4; + [[location(0)]] color: vec4; +}; + +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + let position = vertex.position * vertex.i_pos_scale.w + vertex.i_pos_scale.xyz; + let world_position = mesh.model * vec4(position, 1.0); + + var out: VertexOutput; + out.clip_position = view.view_proj * world_position; + out.color = vertex.i_color; + return out; +} + +[[stage(fragment)]] +fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { + return in.color; +} diff --git a/examples/README.md b/examples/README.md index dc8342214f..84d61d12be 100644 --- a/examples/README.md +++ b/examples/README.md @@ -215,6 +215,10 @@ Example | File | Description Example | File | Description --- | --- | --- `shader_material` | [`shader/shader_material.rs`](./shader/shader_material.rs) | Illustrates creating a custom material and a shader that uses it +`shader_material_glsl` | [`shader/shader_material_glsl.rs`](./shader/shader_material_glsl.rs) | A custom shader using the GLSL shading language. +`shader_instancing` | [`shader/shader_instancing.rs`](./shader/shader_instancing.rs) | A custom shader showing off rendering a mesh multiple times in one draw call. +`animate_shader` | [`shader/animate_shader.rs`](./shader/animate_shader.rs) | Shows how to pass changing data like the time since startup into a shader. +`compute_shader_game_of_life` | [`shader/compute_shader_game_of_life.rs`](./shader/compute_shader_game_of_life.rs) | A compute shader simulating Conway's Game of Life `shader_defs` | [`shader/shader_defs.rs`](./shader/shader_defs.rs) | Demonstrates creating a custom material that uses "shaders defs" (a tool to selectively toggle parts of a shader) ## Tests diff --git a/examples/shader/animate_shader.rs b/examples/shader/animate_shader.rs new file mode 100644 index 0000000000..6e8f4b00af --- /dev/null +++ b/examples/shader/animate_shader.rs @@ -0,0 +1,249 @@ +use bevy::{ + core_pipeline::Transparent3d, + ecs::system::{lifetimeless::SRes, SystemParamItem}, + pbr::{ + DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup, + SetMeshViewBindGroup, + }, + prelude::*, + render::{ + render_phase::{ + AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, + SetItemPipeline, TrackedRenderPass, + }, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + view::{ComputedVisibility, ExtractedView, Msaa, Visibility}, + RenderApp, RenderStage, + }, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(CustomMaterialPlugin) + .add_startup_system(setup) + .run(); +} + +fn setup(mut commands: Commands, mut meshes: ResMut>) { + // cube + commands.spawn().insert_bundle(( + 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(), + )); + + // camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..Default::default() + }); +} + +#[derive(Component)] +struct CustomMaterial; + +pub struct CustomMaterialPlugin; + +impl Plugin for CustomMaterialPlugin { + fn build(&self, app: &mut App) { + let render_device = app.world.get_resource::().unwrap(); + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("time uniform buffer"), + size: std::mem::size_of::() as u64, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + app.sub_app_mut(RenderApp) + .add_render_command::() + .insert_resource(TimeMeta { + buffer, + bind_group: None, + }) + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Extract, extract_time) + .add_system_to_stage(RenderStage::Extract, extract_custom_material) + .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); + } +} + +// extract the `CustomMaterial` component into the render world +fn extract_custom_material( + mut commands: Commands, + mut previous_len: Local, + mut query: Query>, +) { + let mut values = Vec::with_capacity(*previous_len); + for entity in query.iter_mut() { + values.push((entity, (CustomMaterial,))); + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + +// add each entity with a mesh and a `CustomMaterial` to every view's `Transparent3d` render phase using the `CustomPipeline` +fn queue_custom( + transparent_3d_draw_functions: Res>, + custom_pipeline: Res, + msaa: Res, + mut pipelines: ResMut>, + mut pipeline_cache: ResMut, + material_meshes: Query<(Entity, &MeshUniform), (With>, With)>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) { + let draw_custom = transparent_3d_draw_functions + .read() + .get_id::() + .unwrap(); + + let key = MeshPipelineKey::from_msaa_samples(msaa.samples) + | MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + let pipeline = pipelines.specialize(&mut pipeline_cache, &custom_pipeline, key); + + for (view, mut transparent_phase) in views.iter_mut() { + let view_matrix = view.transform.compute_matrix(); + let view_row_2 = view_matrix.row(2); + for (entity, mesh_uniform) in material_meshes.iter() { + transparent_phase.add(Transparent3d { + entity, + pipeline, + draw_function: draw_custom, + distance: view_row_2.dot(mesh_uniform.transform.col(3)), + }); + } + } +} + +#[derive(Default)] +struct ExtractedTime { + seconds_since_startup: f32, +} + +// extract the passed time into a resource in the render world +fn extract_time(mut commands: Commands, time: Res