diff --git a/Cargo.toml b/Cargo.toml index 2846f82..f9a6d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ bevy = { version = "0.7", default-features = false, features = [ "render", ] } bevy_asset = { version = "0.7", default-features = false } +libm = "0.2" [dev-dependencies] bevy = { version = "0.7", default-features = false, features = [ diff --git a/examples/cube.rs b/examples/cube.rs index 62a9ab8..8ec18fc 100644 --- a/examples/cube.rs +++ b/examples/cube.rs @@ -22,6 +22,7 @@ struct TheCube(); fn setup( mut commands: Commands, mut meshes: ResMut>, + mut outlines: ResMut>, mut materials: ResMut>, ) { // Spawn cube et al. @@ -35,16 +36,16 @@ fn setup( mesh: meshes.add(Mesh::from(RoundedBox { size: Vec3::new(1., 1., 1.), radius: 0.3, - subdivisions: 3, + subdivisions: 5, })), material: materials.add(Color::rgb(0.1, 0.1, 0.9).into()), transform: Transform::from_xyz(0.0, 1.0, 0.0), ..default() }) - .insert(Outline { - colour: Color::rgb(1.0, 0.0, 0.0), - offset: 1.0, - }) + .insert(outlines.add(Outline { + colour: Color::rgba(0.0, 1.0, 0.0, 0.5), + width: 50.0, + })) .insert(TheCube()); commands.spawn_bundle(PointLightBundle { point_light: PointLight { diff --git a/src/lib.rs b/src/lib.rs index 9ad0f70..19b5326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,44 +1,116 @@ use bevy::asset::load_internal_asset; -use bevy::core_pipeline::Transparent3d; +use bevy::core_pipeline::{Opaque3d, Transparent3d}; +use bevy::ecs::system::lifetimeless::{Read, SQuery, SRes}; +use bevy::ecs::system::SystemParamItem; use bevy::pbr::{ DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup, SetMeshViewBindGroup, }; use bevy::prelude::*; use bevy::reflect::TypeUuid; use bevy::render::mesh::{MeshVertexBufferLayout, PrimitiveTopology}; -use bevy::render::render_asset::RenderAssets; -use bevy::render::render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}; -use bevy::render::render_resource::{ - Face, PipelineCache, RenderPipelineDescriptor, SpecializedMeshPipeline, - SpecializedMeshPipelineError, SpecializedMeshPipelines, +use bevy::render::render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets}; +use bevy::render::render_component::ExtractComponentPlugin; +use bevy::render::render_phase::{ + AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, + SetItemPipeline, TrackedRenderPass, }; +use bevy::render::render_resource::std140::{AsStd140, Std140}; +use bevy::render::render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferInitDescriptor, BufferSize, + BufferUsages, Face, PipelineCache, RenderPipelineDescriptor, ShaderStages, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, +}; +use bevy::render::renderer::RenderDevice; use bevy::render::view::ExtractedView; use bevy::render::{RenderApp, RenderStage}; +use libm::nextafterf; -pub const OUTLINE_SHADER_HANDLE: HandleUntyped = +const OUTLINE_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2101625026478770097); -#[derive(Clone, Component)] +#[derive(Clone, TypeUuid)] +#[uuid = "552e416b-2766-4e6a-9ee5-9ebd0e8c0230"] pub struct Outline { pub colour: Color, - pub offset: f32, + pub width: f32, +} + +impl RenderAsset for Outline { + type ExtractedAsset = Outline; + + type PreparedAsset = GpuOutline; + + type Param = (SRes, SRes); + + fn extract_asset(&self) -> Self::ExtractedAsset { + self.clone() + } + + fn prepare_asset( + outline: Self::ExtractedAsset, + (render_device, outline_pipeline): &mut bevy::ecs::system::SystemParamItem, + ) -> Result< + Self::PreparedAsset, + bevy::render::render_asset::PrepareAssetError, + > { + let colour = outline.colour.as_linear_rgba_f32().into(); + let vbuffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("outline_vertex_stage_uniform_buffer"), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + contents: VertexStageData { + width: outline.width, + } + .as_std140() + .as_bytes(), + }); + let fbuffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("outline_fragment_stage_uniform_buffer"), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + contents: FragmentStageData { colour }.as_std140().as_bytes(), + }); + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("outline_bind_group"), + layout: &outline_pipeline.bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: vbuffer.as_entire_binding(), + }, + BindGroupEntry { + binding: 1, + resource: fbuffer.as_entire_binding(), + }, + ], + }); + Ok(GpuOutline { + _vertex_stage_buffer: vbuffer, + _fragment_stage_buffer: fbuffer, + bind_group, + transparent: colour.w < 1.0, + }) + } +} + +#[derive(Clone, AsStd140)] +struct VertexStageData { + width: f32, +} + +#[derive(Clone, AsStd140)] +struct FragmentStageData { + colour: Vec4, +} + +pub struct GpuOutline { + _vertex_stage_buffer: Buffer, + _fragment_stage_buffer: Buffer, + bind_group: BindGroup, + transparent: bool, } pub struct OutlinePlugin; -fn extract_outline( - mut commands: Commands, - mut previous_len: Local, - mut query: Query<(Entity, &Outline)>, -) { - let mut values = Vec::with_capacity(*previous_len); - for (entity, outline) in query.iter_mut() { - values.push((entity, (outline.clone(),))); - } - *previous_len = values.len(); - commands.insert_or_spawn_batch(values); -} - impl Plugin for OutlinePlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -47,11 +119,14 @@ impl Plugin for OutlinePlugin { "outline.wgsl", Shader::from_wgsl ); - app.sub_app_mut(RenderApp) + app.add_asset::() + .add_plugin(ExtractComponentPlugin::>::default()) + .add_plugin(RenderAssetPlugin::::default()) + .sub_app_mut(RenderApp) + .add_render_command::() .add_render_command::() .init_resource::() .init_resource::>() - .add_system_to_stage(RenderStage::Extract, extract_outline) .add_system_to_stage(RenderStage::Queue, queue_outline); } } @@ -60,56 +135,139 @@ type DrawOutline = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetMeshBindGroup<1>, + SetOutlineBindGroup<2>, DrawMesh, ); +struct SetOutlineBindGroup(); + +impl EntityRenderCommand for SetOutlineBindGroup { + type Param = (SRes>, SQuery>>); + fn render<'w>( + _view: Entity, + item: Entity, + (outlines, query): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let outline_handle = query.get(item).unwrap(); + let outline = outlines.into_inner().get(outline_handle).unwrap(); + pass.set_bind_group(I, &outline.bind_group, &[]); + RenderCommandResult::Success + } +} + #[allow(clippy::too_many_arguments)] fn queue_outline( + opaque_3d_draw_functions: Res>, transparent_3d_draw_functions: Res>, outline_pipeline: Res, msaa: Res, mut pipelines: ResMut>, mut pipeline_cache: ResMut, render_meshes: Res>, - material_meshes: Query<(Entity, &MeshUniform, &Handle, &Outline)>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + render_outlines: Res>, + material_meshes: Query<(Entity, &MeshUniform, &Handle, &Handle)>, + mut views: Query<( + &ExtractedView, + &mut RenderPhase, + &mut RenderPhase, + )>, ) { - let draw_outline = transparent_3d_draw_functions + let draw_opaque_outline = opaque_3d_draw_functions + .read() + .get_id::() + .unwrap(); + let draw_transparent_outline = transparent_3d_draw_functions .read() .get_id::() .unwrap(); - let key = MeshPipelineKey::from_msaa_samples(msaa.samples) + let base_key = MeshPipelineKey::from_msaa_samples(msaa.samples) | MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); - 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, mesh_handle, _) in material_meshes.iter() { + for (view, mut opaque_phase, mut transparent_phase) in views.iter_mut() { + let inverse_view_matrix = view.transform.compute_matrix().inverse(); + let inverse_view_row_2 = inverse_view_matrix.row(2); + for (entity, mesh_uniform, mesh_handle, outline_handle) in material_meshes.iter() { if let Some(mesh) = render_meshes.get(mesh_handle) { - let pipeline = pipelines - .specialize(&mut pipeline_cache, &outline_pipeline, key, &mesh.layout) - .unwrap(); - transparent_phase.add(Transparent3d { - entity, - pipeline, - draw_function: draw_outline, - distance: view_row_2.dot(mesh_uniform.transform.col(3)), - }); + if let Some(outline) = render_outlines.get(outline_handle) { + let key = if outline.transparent { + base_key | MeshPipelineKey::TRANSPARENT_MAIN_PASS + } else { + base_key + }; + let pipeline = pipelines + .specialize(&mut pipeline_cache, &outline_pipeline, key, &mesh.layout) + .unwrap(); + let distance = nextafterf( + inverse_view_row_2.dot(mesh_uniform.transform.col(3)), + f32::NEG_INFINITY, + ); + if outline.transparent { + transparent_phase.add(Transparent3d { + entity, + pipeline, + draw_function: draw_transparent_outline, + distance, + }); + } else { + opaque_phase.add(Opaque3d { + entity, + pipeline, + draw_function: draw_opaque_outline, + distance: -distance, + }); + } + } } } } } -struct OutlinePipeline { +pub struct OutlinePipeline { mesh_pipeline: MeshPipeline, + bind_group_layout: BindGroupLayout, } impl FromWorld for OutlinePipeline { fn from_world(world: &mut World) -> Self { let world = world.cell(); let mesh_pipeline = world.get_resource::().unwrap().clone(); - OutlinePipeline { mesh_pipeline } + let render_device = world.get_resource::().unwrap(); + let bind_group_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("outline_pipeline_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: BufferSize::new( + VertexStageData::std140_size_static() as u64, + ), + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: BufferSize::new( + FragmentStageData::std140_size_static() as u64, + ), + }, + count: None, + }, + ], + }); + OutlinePipeline { + mesh_pipeline, + bind_group_layout, + } } } @@ -123,13 +281,12 @@ impl SpecializedMeshPipeline for OutlinePipeline { ) -> Result { let mut descriptor = self.mesh_pipeline.specialize(key, layout)?; descriptor.primitive.cull_mode = Some(Face::Front); - //descriptor.depth_stencil.as_mut().unwrap().depth_write_enabled = false; - //descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always; descriptor.vertex.shader = OUTLINE_SHADER_HANDLE.typed(); descriptor.fragment.as_mut().unwrap().shader = OUTLINE_SHADER_HANDLE.typed(); descriptor.layout = Some(vec![ self.mesh_pipeline.view_layout.clone(), self.mesh_pipeline.mesh_layout.clone(), + self.bind_group_layout.clone(), ]); Ok(descriptor) } diff --git a/src/outline.wgsl b/src/outline.wgsl index 39f2fa8..b2b2163 100644 --- a/src/outline.wgsl +++ b/src/outline.wgsl @@ -10,9 +10,23 @@ struct VertexOutput { [[builtin(position)]] clip_position: vec4; }; +struct VertexStageData { + width: f32; +}; + +struct FragmentStageData { + colour: vec4; +}; + [[group(1), binding(0)]] var mesh: Mesh; +[[group(2), binding(0)]] +var vstage: VertexStageData; + +[[group(2), binding(1)]] +var fstage: FragmentStageData; + fn mat4to3(m: mat4x4) -> mat3x3 { return mat3x3( m[0].xyz, m[1].xyz, m[2].xyz @@ -24,8 +38,8 @@ fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; var clip_pos = view.view_proj * (mesh.model * vec4(vertex.position, 1.0)); var clip_norm = mat4to3(view.view_proj) * (mat4to3(mesh.model) * normalize(vertex.normal)); - var n_pos = clip_pos + vec4(50.0 * normalize(clip_norm.xy) * clip_pos.w / vec2(view.width, view.height), 0.0, 0.0); - out.clip_position = n_pos + vec4(0.0,0.0,0.0,0.0); + var clip_delta = vec4(vstage.width * normalize(clip_norm.xy) * clip_pos.w / vec2(view.width, view.height), 0.0, 0.0); + out.clip_position = clip_pos + clip_delta; return out; } @@ -35,5 +49,5 @@ struct FragmentInput { [[stage(fragment)]] fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { - return vec4(1.0, 0.0, 0.0, 1.0); + return fstage.colour; }