Move outline rendering to dedicated pass.

This commit is contained in:
Robin KAY 2022-08-05 02:40:04 +01:00
parent c971bddb1f
commit 2aa76309dd
11 changed files with 963 additions and 362 deletions

View File

@ -15,7 +15,6 @@ bevy = { version = "0.8", default-features = false, features = [
"bevy_asset",
"render",
] }
libm = "0.2"
[dev-dependencies]
bevy = { version = "0.8", default-features = false, features = [
@ -24,5 +23,5 @@ bevy = { version = "0.8", default-features = false, features = [
] }
[[example]]
name = "torus"
path = "examples/torus.rs"
name = "shapes"
path = "examples/shapes.rs"

View File

@ -1,4 +1,4 @@
use std::f32::consts::TAU;
use std::f32::consts::{PI, TAU};
use bevy::prelude::{shape::Torus, *};
@ -12,20 +12,22 @@ fn main() {
.add_plugins(DefaultPlugins)
.add_plugin(OutlinePlugin)
.add_startup_system(setup)
.add_system(rotate_cube)
.add_system(wobble)
.add_system(orbit)
.run();
}
#[derive(Component)]
struct TheCube();
struct Wobbles;
#[derive(Component)]
struct Orbits;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut outlines: ResMut<Assets<Outline>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn cube et al.
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(bevy::prelude::shape::Plane { size: 5.0 })),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
@ -43,11 +45,31 @@ fn setup(
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
})
.insert(outlines.add(Outline {
colour: Color::rgba(0.0, 1.0, 0.0, 0.5),
.insert(Outline {
colour: Color::rgba(0.0, 1.0, 0.0, 1.0),
width: 25.0,
}))
.insert(TheCube());
})
.insert(OutlineStencil)
.insert(Wobbles);
commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(Torus {
radius: 0.3,
ring_radius: 0.1,
subdivisions_segments: 20,
subdivisions_sides: 10,
})),
material: materials.add(Color::rgb(0.9, 0.1, 0.1).into()),
transform: Transform::from_xyz(0.0, 1.2, 2.0)
.with_rotation(Quat::from_rotation_x(0.5 * PI)),
..default()
})
.insert(Outline {
colour: Color::rgba(1.0, 0.0, 1.0, 0.3),
width: 15.0,
})
.insert(OutlineStencil)
.insert(Orbits);
commands.spawn_bundle(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
@ -63,17 +85,13 @@ fn setup(
});
}
fn rotate_cube(
mut cubes: Query<&mut Transform, With<TheCube>>,
timer: Res<Time>,
mut t: Local<f32>,
) {
fn wobble(mut query: Query<&mut Transform, With<Wobbles>>, timer: Res<Time>, mut t: Local<f32>) {
let ta = *t;
*t = (ta + 0.5 * timer.delta_seconds()) % TAU;
let tb = *t;
let i1 = tb.cos() - ta.cos();
let i2 = ta.sin() - tb.sin();
for mut transform in cubes.iter_mut() {
for mut transform in query.iter_mut() {
transform.rotate(Quat::from_rotation_z(
TAU * 20.0 * i1 * timer.delta_seconds(),
));
@ -82,3 +100,12 @@ fn rotate_cube(
));
}
}
fn orbit(mut query: Query<&mut Transform, With<Orbits>>, timer: Res<Time>) {
for mut transform in query.iter_mut() {
transform.translate_around(
Vec3::ZERO,
Quat::from_rotation_y(0.4 * timer.delta_seconds()),
)
}
}

15
src/common.wgsl Normal file
View File

@ -0,0 +1,15 @@
#define_import_path bevy_mod_outline::common
#import bevy_pbr::mesh_view_bindings
#import bevy_pbr::mesh_types
@group(1) @binding(0)
var<uniform> mesh: Mesh;
fn model_origin_z() -> f32 {
var origin = mesh.model[3];
var proj_zw = mat4x2<f32>(
view.view_proj[0].zw, view.view_proj[1].zw,
view.view_proj[2].zw, view.view_proj[3].zw);
var zw = proj_zw * origin;
return zw.x / zw.y;
}

143
src/draw.rs Normal file
View File

@ -0,0 +1,143 @@
use bevy::pbr::{DrawMesh, MeshPipelineKey, MeshUniform, SetMeshBindGroup, SetMeshViewBindGroup};
use bevy::prelude::*;
use bevy::render::render_asset::RenderAssets;
use bevy::render::render_phase::{DrawFunctions, RenderPhase, SetItemPipeline};
use bevy::render::render_resource::{PipelineCache, SpecializedMeshPipelines};
use bevy::render::view::ExtractedView;
use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline};
use crate::pipeline::{OutlinePipeline, PassType};
use crate::uniforms::{OutlineFragmentUniform, SetOutlineBindGroup};
use crate::view_uniforms::SetOutlineViewBindGroup;
use crate::OutlineStencil;
pub type DrawStencil = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
DrawMesh,
);
#[allow(clippy::too_many_arguments)]
pub fn queue_outline_stencil_mesh(
stencil_draw_functions: Res<DrawFunctions<StencilOutline>>,
stencil_pipeline: Res<OutlinePipeline>,
msaa: Res<Msaa>,
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
mut pipeline_cache: ResMut<PipelineCache>,
render_meshes: Res<RenderAssets<Mesh>>,
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>), With<OutlineStencil>>,
mut views: Query<(&ExtractedView, &mut RenderPhase<StencilOutline>)>,
) {
let draw_stencil = stencil_draw_functions
.read()
.get_id::<DrawStencil>()
.unwrap();
let base_key = MeshPipelineKey::from_msaa_samples(msaa.samples);
for (view, mut stencil_phase) in views.iter_mut() {
let rangefinder = view.rangefinder3d();
for (entity, mesh_uniform, mesh_handle) in material_meshes.iter() {
if let Some(mesh) = render_meshes.get(mesh_handle) {
let key =
base_key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
let pipeline = pipelines
.specialize(
&mut pipeline_cache,
&stencil_pipeline,
(key, PassType::Stencil),
&mesh.layout,
)
.unwrap();
let distance = rangefinder.distance(&mesh_uniform.transform);
stencil_phase.add(StencilOutline {
entity,
pipeline,
draw_function: draw_stencil,
distance,
});
}
}
}
}
pub type DrawOutline = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
SetOutlineViewBindGroup<2>,
SetOutlineBindGroup<3>,
DrawMesh,
);
#[allow(clippy::too_many_arguments)]
pub fn queue_outline_mesh(
opaque_draw_functions: Res<DrawFunctions<OpaqueOutline>>,
transparent_draw_functions: Res<DrawFunctions<TransparentOutline>>,
outline_pipeline: Res<OutlinePipeline>,
msaa: Res<Msaa>,
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
mut pipeline_cache: ResMut<PipelineCache>,
render_meshes: Res<RenderAssets<Mesh>>,
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>, &OutlineFragmentUniform)>,
mut views: Query<(
&ExtractedView,
&mut RenderPhase<OpaqueOutline>,
&mut RenderPhase<TransparentOutline>,
)>,
) {
let draw_opaque_outline = opaque_draw_functions
.read()
.get_id::<DrawOutline>()
.unwrap();
let draw_transparent_outline = transparent_draw_functions
.read()
.get_id::<DrawOutline>()
.unwrap();
let base_key = MeshPipelineKey::from_msaa_samples(msaa.samples);
for (view, mut opaque_phase, mut transparent_phase) in views.iter_mut() {
let rangefinder = view.rangefinder3d();
for (entity, mesh_uniform, mesh_handle, outline_fragment) in material_meshes.iter() {
if let Some(mesh) = render_meshes.get(mesh_handle) {
let transparent = outline_fragment.colour[3] < 1.0;
let pass_type;
let key = base_key
| MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
| if transparent {
pass_type = PassType::Transparent;
MeshPipelineKey::TRANSPARENT_MAIN_PASS
} else {
pass_type = PassType::Opaque;
MeshPipelineKey::NONE
};
let pipeline = pipelines
.specialize(
&mut pipeline_cache,
&outline_pipeline,
(key, pass_type),
&mesh.layout,
)
.unwrap();
let distance = rangefinder.distance(&mesh_uniform.transform);
if transparent {
transparent_phase.add(TransparentOutline {
entity,
pipeline,
draw_function: draw_transparent_outline,
distance,
});
} else {
opaque_phase.add(OpaqueOutline {
entity,
pipeline,
draw_function: draw_opaque_outline,
distance,
});
}
}
}
}
}

View File

@ -1,356 +1,122 @@
use bevy::asset::load_internal_asset;
use bevy::core_pipeline::core_3d::{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::ecs::query::QueryItem;
use bevy::prelude::*;
use bevy::reflect::TypeUuid;
use bevy::render::extract_component::ExtractComponentPlugin;
use bevy::render::mesh::{MeshVertexBufferLayout, PrimitiveTopology};
use bevy::render::render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets};
use bevy::render::render_phase::{
AddRenderCommand, DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase,
SetItemPipeline, TrackedRenderPass,
use bevy::render::extract_component::{
ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
};
use bevy::render::render_resource::{
AsBindGroup, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType, BufferSize,
DynamicUniformBuffer, Face, PipelineCache, PreparedBindGroup, RenderPipelineDescriptor,
ShaderSize, ShaderStages, ShaderType, SpecializedMeshPipeline, SpecializedMeshPipelineError,
SpecializedMeshPipelines,
use bevy::render::render_graph::RenderGraph;
use bevy::render::render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions};
use bevy::render::render_resource::SpecializedMeshPipelines;
use bevy::render::{RenderApp, RenderStage};
use crate::draw::{queue_outline_mesh, queue_outline_stencil_mesh, DrawOutline, DrawStencil};
use crate::node::{OpaqueOutline, OutlineNode, StencilOutline, TransparentOutline};
use crate::pipeline::{
OutlinePipeline, COMMON_SHADER_HANDLE, OUTLINE_SHADER_HANDLE, STENCIL_SHADER_HANDLE,
};
use bevy::render::renderer::{RenderDevice, RenderQueue};
use bevy::render::texture::FallbackImage;
use bevy::render::view::ExtractedView;
use bevy::render::{Extract, RenderApp, RenderStage};
use libm::nextafterf;
use crate::uniforms::{queue_outline_bind_group, OutlineFragmentUniform, OutlineVertexUniform};
use crate::view_uniforms::{
extract_outline_view_uniforms, queue_outline_view_bind_group, OutlineViewUniform,
};
mod draw;
mod node;
mod pipeline;
mod uniforms;
mod view_uniforms;
// See https://alexanderameye.github.io/notes/rendering-outlines/
const OUTLINE_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2101625026478770097);
/// A component for stenciling meshes during outline rendering
#[derive(Component, Default)]
pub struct OutlineStencil;
/// An asset for rendering outlines around meshes.
#[derive(Clone, TypeUuid, AsBindGroup)]
#[uuid = "552e416b-2766-4e6a-9ee5-9ebd0e8c0230"]
impl ExtractComponent for OutlineStencil {
type Query = ();
type Filter = With<OutlineStencil>;
fn extract_component(_item: QueryItem<Self::Query>) -> Self {
OutlineStencil
}
}
/// A component for rendering outlines around meshes.
#[derive(Clone, Component)]
pub struct Outline {
/// Colour of the outline
#[uniform(1, visibility(fragment))]
pub colour: Color,
/// Width of the outline in logical pixels
#[uniform(0, visibility(vertex))]
pub width: f32,
/// Colour of the outline
pub colour: Color,
}
impl RenderAsset for Outline {
type ExtractedAsset = Outline;
type PreparedAsset = GpuOutline;
type Param = (
SRes<RenderDevice>,
SRes<OutlinePipeline>,
SRes<RenderAssets<Image>>,
SRes<FallbackImage>,
);
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
outline: Self::ExtractedAsset,
(render_device, outline_pipeline, images, fallback_image): &mut bevy::ecs::system::SystemParamItem<Self::Param>,
) -> Result<
Self::PreparedAsset,
bevy::render::render_asset::PrepareAssetError<Self::ExtractedAsset>,
> {
if let Ok(pbg) = outline.as_bind_group(
&outline_pipeline.outline_bind_group_layout,
render_device,
images,
fallback_image,
) {
Ok(GpuOutline {
bind_group: pbg,
transparent: outline.colour.a() < 1.0,
})
} else {
Err(PrepareAssetError::RetryNextUpdate(outline))
}
}
}
#[derive(Clone, Component, ShaderType)]
struct ViewSizeUniform {
logical_size: Vec2,
}
#[derive(Default)]
struct ViewSizeUniforms {
pub uniforms: DynamicUniformBuffer<ViewSizeUniform>,
}
#[derive(Component)]
struct ViewSizeUniformOffset {
pub offset: u32,
}
struct GpuViewSize {
bind_group: BindGroup,
}
pub struct GpuOutline {
bind_group: PreparedBindGroup<Outline>,
transparent: bool,
}
/// Adds support for the [Outline] asset type.
/// Adds support for rendering outlines.
pub struct OutlinePlugin;
impl Plugin for OutlinePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, COMMON_SHADER_HANDLE, "common.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
STENCIL_SHADER_HANDLE,
"stencil.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
OUTLINE_SHADER_HANDLE,
"outline.wgsl",
Shader::from_wgsl
);
app.add_asset::<Outline>()
.add_plugin(ExtractComponentPlugin::<Handle<Outline>>::default())
.add_plugin(RenderAssetPlugin::<Outline>::default())
app.add_plugin(ExtractComponentPlugin::<OutlineStencil>::extract_visible())
.add_plugin(ExtractComponentPlugin::<OutlineVertexUniform>::default())
.add_plugin(ExtractComponentPlugin::<OutlineFragmentUniform>::default())
.add_plugin(UniformComponentPlugin::<OutlineVertexUniform>::default())
.add_plugin(UniformComponentPlugin::<OutlineFragmentUniform>::default())
.add_plugin(UniformComponentPlugin::<OutlineViewUniform>::default())
.sub_app_mut(RenderApp)
.add_render_command::<Opaque3d, DrawOutline>()
.add_render_command::<Transparent3d, DrawOutline>()
.init_resource::<DrawFunctions<StencilOutline>>()
.init_resource::<DrawFunctions<OpaqueOutline>>()
.init_resource::<DrawFunctions<TransparentOutline>>()
.init_resource::<OutlinePipeline>()
.init_resource::<SpecializedMeshPipelines<OutlinePipeline>>()
.init_resource::<ViewSizeUniforms>()
.add_system_to_stage(RenderStage::Extract, extract_view_size_uniforms)
.add_system_to_stage(RenderStage::Prepare, prepare_view_size_uniforms)
.add_system_to_stage(RenderStage::Queue, queue_outline);
}
}
type DrawOutline = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
SetViewSizeBindGroup<2>,
SetOutlineBindGroup<3>,
DrawMesh,
);
struct SetViewSizeBindGroup<const I: usize>();
impl<const I: usize> EntityRenderCommand for SetViewSizeBindGroup<I> {
type Param = (SRes<GpuViewSize>, SQuery<Read<ViewSizeUniformOffset>>);
#[inline]
fn render<'w>(
view: Entity,
_item: Entity,
(gpu_view_size, offset_query): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let uniform_offset = offset_query.get_inner(view).unwrap();
pass.set_bind_group(
I,
&gpu_view_size.into_inner().bind_group,
&[uniform_offset.offset],
);
RenderCommandResult::Success
}
}
struct SetOutlineBindGroup<const I: usize>();
impl<const I: usize> EntityRenderCommand for SetOutlineBindGroup<I> {
type Param = (SRes<RenderAssets<Outline>>, SQuery<Read<Handle<Outline>>>);
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.bind_group, &[]);
RenderCommandResult::Success
}
}
fn extract_view_size_uniforms(
mut commands: Commands,
query: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
) {
for (entity, camera) in query.iter() {
if !camera.is_active {
continue;
}
if let Some(size) = camera.logical_viewport_size() {
commands
.get_or_spawn(entity)
.insert(ViewSizeUniform { logical_size: size });
}
}
}
fn prepare_view_size_uniforms(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
outline_pipeline: Res<OutlinePipeline>,
mut view_size_uniforms: ResMut<ViewSizeUniforms>,
views: Query<(Entity, &ViewSizeUniform)>,
) {
view_size_uniforms.uniforms.clear();
for (entity, view_size_uniform) in views.iter() {
let view_size_uniforms = ViewSizeUniformOffset {
offset: view_size_uniforms.uniforms.push(view_size_uniform.clone()),
};
commands.entity(entity).insert(view_size_uniforms);
}
view_size_uniforms
.uniforms
.write_buffer(&render_device, &render_queue);
if let Some(view_binding) = view_size_uniforms.uniforms.binding() {
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[BindGroupEntry {
binding: 0,
resource: view_binding.clone(),
}],
label: Some("outline_view_size_bind_group"),
layout: &outline_pipeline.view_size_bind_group_layout,
});
commands.insert_resource(GpuViewSize { bind_group });
}
}
#[allow(clippy::too_many_arguments)]
fn queue_outline(
opaque_3d_draw_functions: Res<DrawFunctions<Opaque3d>>,
transparent_3d_draw_functions: Res<DrawFunctions<Transparent3d>>,
outline_pipeline: Res<OutlinePipeline>,
msaa: Res<Msaa>,
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
mut pipeline_cache: ResMut<PipelineCache>,
render_meshes: Res<RenderAssets<Mesh>>,
render_outlines: Res<RenderAssets<Outline>>,
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>, &Handle<Outline>)>,
mut views: Query<(
&ExtractedView,
&mut RenderPhase<Opaque3d>,
&mut RenderPhase<Transparent3d>,
)>,
) {
let draw_opaque_outline = opaque_3d_draw_functions
.read()
.get_id::<DrawOutline>()
.unwrap();
let draw_transparent_outline = transparent_3d_draw_functions
.read()
.get_id::<DrawOutline>()
.unwrap();
let base_key = MeshPipelineKey::from_msaa_samples(msaa.samples)
| MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList);
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) {
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();
// Increase distance to just behind the non-outline mesh
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,
});
}
}
}
}
}
}
pub struct OutlinePipeline {
mesh_pipeline: MeshPipeline,
view_size_bind_group_layout: BindGroupLayout,
outline_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::<MeshPipeline>().unwrap().clone();
let render_device = world.get_resource::<RenderDevice>().unwrap();
let view_size_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("outline_view_size_bind_group_layout"),
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(ViewSizeUniform::SHADER_SIZE.get()),
},
count: None,
}],
});
let outline_bind_group_layout = Outline::bind_group_layout(&render_device);
OutlinePipeline {
mesh_pipeline,
view_size_bind_group_layout,
outline_bind_group_layout,
}
}
}
impl SpecializedMeshPipeline for OutlinePipeline {
type Key = MeshPipelineKey;
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut descriptor = self.mesh_pipeline.specialize(key, layout)?;
descriptor.primitive.cull_mode = Some(Face::Front);
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.view_size_bind_group_layout.clone(),
self.outline_bind_group_layout.clone(),
]);
Ok(descriptor)
.add_render_command::<StencilOutline, DrawStencil>()
.add_render_command::<OpaqueOutline, DrawOutline>()
.add_render_command::<TransparentOutline, DrawOutline>()
.add_system_to_stage(RenderStage::Extract, extract_outline_view_uniforms)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<StencilOutline>)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<OpaqueOutline>)
.add_system_to_stage(
RenderStage::PhaseSort,
sort_phase_system::<TransparentOutline>,
)
.add_system_to_stage(RenderStage::Queue, queue_outline_view_bind_group)
.add_system_to_stage(RenderStage::Queue, queue_outline_bind_group)
.add_system_to_stage(RenderStage::Queue, queue_outline_stencil_mesh)
.add_system_to_stage(RenderStage::Queue, queue_outline_mesh);
let world = &mut app.sub_app_mut(RenderApp).world;
let node = OutlineNode::new(world);
let mut graph = world.resource_mut::<RenderGraph>();
let draw_3d_graph = graph
.get_sub_graph_mut(bevy::core_pipeline::core_3d::graph::NAME)
.unwrap();
draw_3d_graph.add_node(OutlineNode::NAME, node);
draw_3d_graph
.add_node_edge(
bevy::core_pipeline::core_3d::graph::node::MAIN_PASS,
OutlineNode::NAME,
)
.unwrap();
draw_3d_graph
.add_slot_edge(
draw_3d_graph.input_node().unwrap().id,
bevy::core_pipeline::core_3d::graph::input::VIEW_ENTITY,
OutlineNode::NAME,
OutlineNode::IN_VIEW,
)
.unwrap();
}
}

264
src/node.rs Normal file
View File

@ -0,0 +1,264 @@
use std::cmp::Reverse;
use bevy::ecs::system::lifetimeless::Read;
use bevy::prelude::*;
use bevy::render::camera::ExtractedCamera;
use bevy::render::render_graph::{NodeRunError, SlotInfo, SlotType};
use bevy::render::render_phase::{
CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, EntityPhaseItem, PhaseItem,
RenderPhase, TrackedRenderPass,
};
use bevy::render::render_resource::{
CachedRenderPipelineId, LoadOp, Operations, RenderPassDepthStencilAttachment,
RenderPassDescriptor,
};
use bevy::render::view::{ExtractedView, ViewDepthTexture, ViewTarget};
use bevy::render::{
render_graph::{Node, RenderGraphContext},
renderer::RenderContext,
};
use bevy::utils::FloatOrd;
pub struct StencilOutline {
pub distance: f32,
pub pipeline: CachedRenderPipelineId,
pub entity: Entity,
pub draw_function: DrawFunctionId,
}
impl PhaseItem for StencilOutline {
type SortKey = Reverse<FloatOrd>;
fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance))
}
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
self.draw_function
}
}
impl EntityPhaseItem for StencilOutline {
#[inline]
fn entity(&self) -> Entity {
self.entity
}
}
impl CachedRenderPipelinePhaseItem for StencilOutline {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub struct OpaqueOutline {
pub distance: f32,
pub pipeline: CachedRenderPipelineId,
pub entity: Entity,
pub draw_function: DrawFunctionId,
}
impl PhaseItem for OpaqueOutline {
type SortKey = Reverse<FloatOrd>;
fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance))
}
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
self.draw_function
}
}
impl EntityPhaseItem for OpaqueOutline {
#[inline]
fn entity(&self) -> Entity {
self.entity
}
}
impl CachedRenderPipelinePhaseItem for OpaqueOutline {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub struct TransparentOutline {
pub distance: f32,
pub pipeline: CachedRenderPipelineId,
pub entity: Entity,
pub draw_function: DrawFunctionId,
}
impl PhaseItem for TransparentOutline {
type SortKey = FloatOrd;
fn sort_key(&self) -> Self::SortKey {
FloatOrd(self.distance)
}
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
self.draw_function
}
}
impl EntityPhaseItem for TransparentOutline {
#[inline]
fn entity(&self) -> Entity {
self.entity
}
}
impl CachedRenderPipelinePhaseItem for TransparentOutline {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub struct OutlineNode {
query: QueryState<
(
Read<ExtractedCamera>,
Read<RenderPhase<StencilOutline>>,
Read<RenderPhase<OpaqueOutline>>,
Read<RenderPhase<TransparentOutline>>,
Read<Camera3d>,
Read<ViewTarget>,
Read<ViewDepthTexture>,
),
With<ExtractedView>,
>,
}
impl OutlineNode {
pub const NAME: &'static str = "outline_node";
pub const IN_VIEW: &'static str = "view";
pub fn new(world: &mut World) -> Self {
Self {
query: world.query_filtered(),
}
}
}
impl Node for OutlineNode {
fn input(&self) -> Vec<SlotInfo> {
vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)]
}
fn update(&mut self, world: &mut World) {
self.query.update_archetypes(world);
}
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (camera, stencil_phase, opaque_phase, transparent_phase, camera_3d, target, depth) =
match self.query.get_manual(world, view_entity) {
Ok(query) => query,
Err(_) => {
return Ok(());
} // No window
};
// Always run stencil pass to ensure depth buffer is cleared
{
let pass_descriptor = RenderPassDescriptor {
label: Some("outline_stencil_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth.view,
depth_ops: Some(Operations {
load: camera_3d.depth_load_op.clone().into(),
store: true,
}),
stencil_ops: None,
}),
};
let draw_functions = world.resource::<DrawFunctions<StencilOutline>>();
let render_pass = render_context
.command_encoder
.begin_render_pass(&pass_descriptor);
let mut draw_functions = draw_functions.write();
let mut tracked_pass = TrackedRenderPass::new(render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
tracked_pass.set_camera_viewport(viewport);
}
for item in &stencil_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
}
}
if !opaque_phase.items.is_empty() {
let pass_descriptor = RenderPassDescriptor {
label: Some("outline_opaque_pass"),
color_attachments: &[Some(target.get_color_attachment(Operations {
load: LoadOp::Load,
store: true,
}))],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth.view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: true,
}),
stencil_ops: None,
}),
};
let draw_functions = world.resource::<DrawFunctions<OpaqueOutline>>();
let render_pass = render_context
.command_encoder
.begin_render_pass(&pass_descriptor);
let mut draw_functions = draw_functions.write();
let mut tracked_pass = TrackedRenderPass::new(render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
tracked_pass.set_camera_viewport(viewport);
}
for item in &opaque_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
}
}
if !transparent_phase.items.is_empty() {
let pass_descriptor = RenderPassDescriptor {
label: Some("outline_transparent_pass"),
color_attachments: &[Some(target.get_color_attachment(Operations {
load: LoadOp::Load,
store: true,
}))],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth.view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: true,
}),
stencil_ops: None,
}),
};
let draw_functions = world.resource::<DrawFunctions<TransparentOutline>>();
let render_pass = render_context
.command_encoder
.begin_render_pass(&pass_descriptor);
let mut draw_functions = draw_functions.write();
let mut tracked_pass = TrackedRenderPass::new(render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
tracked_pass.set_camera_viewport(viewport);
}
for item in &transparent_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
}
}
Ok(())
}
}

View File

@ -1,7 +1,6 @@
#import bevy_pbr::mesh_types
#import bevy_pbr::mesh_view_bindings
#import bevy_mod_outline::common
struct Vertex {
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
};
@ -10,29 +9,26 @@ struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
};
struct ViewSizeUniforms {
logical_size: vec2<f32>,
struct OutlineViewUniform {
scale: vec2<f32>,
};
struct VertexStageData {
struct OutlineVertexUniform {
width: f32,
};
struct FragmentStageData {
struct OutlineFragmentUniform {
colour: vec4<f32>,
};
@group(1) @binding(0)
var<uniform> mesh: Mesh;
@group(2) @binding(0)
var<uniform> view_size: ViewSizeUniforms;
var<uniform> view_uniform: OutlineViewUniform;
@group(3) @binding(0)
var<uniform> vstage: VertexStageData;
var<uniform> vstage: OutlineVertexUniform;
@group(3) @binding(1)
var<uniform> fstage: FragmentStageData;
var<uniform> fstage: OutlineFragmentUniform;
fn mat4to3(m: mat4x4<f32>) -> mat3x3<f32> {
return mat3x3<f32>(
@ -41,12 +37,12 @@ fn mat4to3(m: mat4x4<f32>) -> mat3x3<f32> {
}
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
fn vertex(vertex: VertexInput) -> VertexOutput {
var out: VertexOutput;
var clip_pos = view.view_proj * (mesh.model * vec4<f32>(vertex.position, 1.0));
var clip_norm = mat4to3(view.view_proj) * (mat4to3(mesh.model) * normalize(vertex.normal));
var clip_delta = vec4<f32>(2.0 * vstage.width * normalize(clip_norm.xy) * clip_pos.w / view_size.logical_size, 0.0, 0.0);
out.clip_position = clip_pos + clip_delta;
var clip_delta = vec2<f32>(vstage.width * normalize(clip_norm.xy) * clip_pos.w * view_uniform.scale);
out.clip_position = vec4<f32>((clip_pos.xy + clip_delta) / clip_pos.w, model_origin_z(), 1.0);
return out;
}

180
src/pipeline.rs Normal file
View File

@ -0,0 +1,180 @@
use std::borrow::Cow;
use bevy::prelude::*;
use bevy::reflect::TypeUuid;
use bevy::render::render_resource::{
BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState,
BufferBindingType, BufferSize, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
DepthStencilState, Face, FragmentState, FrontFace, MultisampleState, PolygonMode,
PrimitiveState, ShaderSize, ShaderStages, StencilState, TextureFormat, VertexState,
};
use bevy::render::renderer::RenderDevice;
use bevy::render::texture::BevyDefault;
use bevy::{
pbr::{MeshPipeline, MeshPipelineKey},
render::{
mesh::MeshVertexBufferLayout,
render_resource::{
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
},
},
};
use crate::uniforms::{OutlineFragmentUniform, OutlineVertexUniform};
use crate::view_uniforms::OutlineViewUniform;
pub const COMMON_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9448276477068917228);
pub const STENCIL_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 12033806834125368121);
pub const OUTLINE_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2101625026478770097);
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum PassType {
Stencil,
Opaque,
Transparent,
}
pub struct OutlinePipeline {
mesh_pipeline: MeshPipeline,
pub outline_view_bind_group_layout: BindGroupLayout,
pub outline_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::<MeshPipeline>().unwrap().clone();
let render_device = world.get_resource::<RenderDevice>().unwrap();
let outline_view_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("outline_view_bind_group_layout"),
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(OutlineViewUniform::SHADER_SIZE.get()),
},
count: None,
}],
});
let outline_bind_group_layout =
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("outline_bind_group_layout"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(
OutlineVertexUniform::SHADER_SIZE.get(),
),
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(
OutlineFragmentUniform::SHADER_SIZE.get(),
),
},
count: None,
},
],
});
OutlinePipeline {
mesh_pipeline,
outline_view_bind_group_layout,
outline_bind_group_layout,
}
}
}
impl SpecializedMeshPipeline for OutlinePipeline {
type Key = (MeshPipelineKey, PassType);
fn specialize(
&self,
(key, pass_type): Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut targets = vec![];
let mut bind_layouts = vec![
self.mesh_pipeline.view_layout.clone(),
self.mesh_pipeline.mesh_layout.clone(),
];
let mut buffer_attrs = vec![Mesh::ATTRIBUTE_POSITION.at_shader_location(0)];
let shader;
match pass_type {
PassType::Stencil => {
shader = STENCIL_SHADER_HANDLE;
}
PassType::Opaque | PassType::Transparent => {
shader = OUTLINE_SHADER_HANDLE;
targets.push(Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(if pass_type == PassType::Transparent {
BlendState::ALPHA_BLENDING
} else {
BlendState::REPLACE
}),
write_mask: ColorWrites::ALL,
}));
bind_layouts.push(self.outline_view_bind_group_layout.clone());
bind_layouts.push(self.outline_bind_group_layout.clone());
buffer_attrs.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1));
}
}
let buffers = vec![layout.get_layout(&buffer_attrs)?];
Ok(RenderPipelineDescriptor {
vertex: VertexState {
shader: shader.clone().typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers,
},
fragment: Some(FragmentState {
shader: shader.clone().typed::<Shader>(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets,
}),
layout: Some(bind_layouts),
primitive: PrimitiveState {
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
topology: key.primitive_topology(),
strip_index_format: None,
},
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::Greater,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState {
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some(Cow::Borrowed("outline_stencil_pipeline")),
})
}
}

22
src/stencil.wgsl Normal file
View File

@ -0,0 +1,22 @@
#import bevy_mod_outline::common
struct VertexInput {
@location(0) position: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
};
@vertex
fn vertex(vertex: VertexInput) -> VertexOutput {
var out: VertexOutput;
var clip_pos = view.view_proj * (mesh.model * vec4<f32>(vertex.position, 1.0));
out.clip_position = vec4<f32>(clip_pos.xy / clip_pos.w, model_origin_z(), 1.0);
return out;
}
@fragment
fn fragment() {
return;
}

104
src/uniforms.rs Normal file
View File

@ -0,0 +1,104 @@
use bevy::{
ecs::{
query::QueryItem,
system::{
lifetimeless::{Read, SQuery, SRes},
SystemParamItem,
},
},
prelude::*,
render::{
extract_component::{ComponentUniforms, DynamicUniformIndex, ExtractComponent},
render_phase::{EntityRenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::{BindGroup, BindGroupDescriptor, BindGroupEntry, ShaderType},
renderer::RenderDevice,
},
};
use crate::{pipeline::OutlinePipeline, Outline};
#[derive(Clone, Component, ShaderType)]
pub struct OutlineVertexUniform {
pub width: f32,
}
impl ExtractComponent for OutlineVertexUniform {
type Query = Read<Outline>;
type Filter = ();
fn extract_component(item: QueryItem<Self::Query>) -> Self {
OutlineVertexUniform { width: item.width }
}
}
#[derive(Clone, Component, ShaderType)]
pub struct OutlineFragmentUniform {
pub colour: Vec4,
}
impl ExtractComponent for OutlineFragmentUniform {
type Query = Read<Outline>;
type Filter = ();
fn extract_component(item: QueryItem<Self::Query>) -> Self {
OutlineFragmentUniform {
colour: item.colour.as_linear_rgba_f32().into(),
}
}
}
pub struct OutlineBindGroup {
pub bind_group: BindGroup,
}
pub fn queue_outline_bind_group(
mut commands: Commands,
render_device: Res<RenderDevice>,
outline_pipeline: Res<OutlinePipeline>,
vertex: Res<ComponentUniforms<OutlineVertexUniform>>,
fragment: Res<ComponentUniforms<OutlineFragmentUniform>>,
) {
if let (Some(vertex_binding), Some(fragment_binding)) = (vertex.binding(), fragment.binding()) {
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
BindGroupEntry {
binding: 0,
resource: vertex_binding.clone(),
},
BindGroupEntry {
binding: 1,
resource: fragment_binding.clone(),
},
],
label: Some("outline_bind_group"),
layout: &outline_pipeline.outline_bind_group_layout,
});
commands.insert_resource(OutlineBindGroup { bind_group });
}
}
pub struct SetOutlineBindGroup<const I: usize>();
impl<const I: usize> EntityRenderCommand for SetOutlineBindGroup<I> {
type Param = (
SRes<OutlineBindGroup>,
SQuery<(
Read<DynamicUniformIndex<OutlineVertexUniform>>,
Read<DynamicUniformIndex<OutlineFragmentUniform>>,
)>,
);
fn render<'w>(
_view: Entity,
item: Entity,
(bind_group, query): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let (vertex, fragment) = query.get(item).unwrap();
pass.set_bind_group(
I,
&bind_group.into_inner().bind_group,
&[vertex.index(), fragment.index()],
);
RenderCommandResult::Success
}
}

85
src/view_uniforms.rs Normal file
View File

@ -0,0 +1,85 @@
use bevy::ecs::system::lifetimeless::{Read, SQuery, SRes};
use bevy::ecs::system::SystemParamItem;
use bevy::prelude::*;
use bevy::render::extract_component::{ComponentUniforms, DynamicUniformIndex};
use bevy::render::render_phase::{
EntityRenderCommand, RenderCommandResult, RenderPhase, TrackedRenderPass,
};
use bevy::render::render_resource::ShaderType;
use bevy::render::render_resource::{BindGroup, BindGroupDescriptor, BindGroupEntry};
use bevy::render::renderer::RenderDevice;
use bevy::render::Extract;
use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline};
use crate::pipeline::OutlinePipeline;
#[derive(Clone, Component, ShaderType)]
pub struct OutlineViewUniform {
#[align(16)]
scale: Vec2,
}
pub struct OutlineViewBindGroup {
bind_group: BindGroup,
}
pub fn extract_outline_view_uniforms(
mut commands: Commands,
query: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
) {
for (entity, camera) in query.iter() {
if !camera.is_active {
continue;
}
if let Some(size) = camera.logical_viewport_size() {
commands
.get_or_spawn(entity)
.insert(OutlineViewUniform { scale: 2.0 / size })
.insert(RenderPhase::<StencilOutline>::default())
.insert(RenderPhase::<OpaqueOutline>::default())
.insert(RenderPhase::<TransparentOutline>::default());
}
}
}
pub fn queue_outline_view_bind_group(
mut commands: Commands,
render_device: Res<RenderDevice>,
outline_pipeline: Res<OutlinePipeline>,
view_uniforms: Res<ComponentUniforms<OutlineViewUniform>>,
) {
if let Some(view_binding) = view_uniforms.binding() {
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[BindGroupEntry {
binding: 0,
resource: view_binding.clone(),
}],
label: Some("outline_view_bind_group"),
layout: &outline_pipeline.outline_view_bind_group_layout,
});
commands.insert_resource(OutlineViewBindGroup { bind_group });
}
}
pub struct SetOutlineViewBindGroup<const I: usize>();
impl<const I: usize> EntityRenderCommand for SetOutlineViewBindGroup<I> {
type Param = (
SRes<OutlineViewBindGroup>,
SQuery<Read<DynamicUniformIndex<OutlineViewUniform>>>,
);
fn render<'w>(
view: Entity,
_item: Entity,
(bind_group, query): SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let view_index = query.get(view).unwrap();
pass.set_bind_group(
I,
&bind_group.into_inner().bind_group,
&[view_index.index()],
);
RenderCommandResult::Success
}
}