mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-22 08:06:01 +00:00
Object picking (#8)
This PR implements basic object selection, with help from `bevy_mod_picking` and `bevy_mod_outline`. ### Work Done * Click to select objects in the world * Selected objects become visually highlighted * The selected entity is available globally on the `Res<Selection>` resource * Selected entities can also be queried via the `Selected` marker component * Pressing `F` will "focus" the camera on the selected entity * Pressing `Esc` will clear the active selection ### Known Issues * We're currently using `bevy_mod_picking`'s "Selection" feature, which isn't aware of our own input events. As a consequence, it will select the object under the cursor when releasing `LMB` after orbiting the camera, which is not what we want. This should be pretty easily fixable by dropping `bevy_mod_picking`'s selection feature and reading the object under the cursor in our own click listener that only runs if the LMB event is unhandled. * The skinned mesh is not currently selectable. This is likely fixable by adjusting the logic in `scene::mark_scene_objects`... somehow. The `Gltf` data structure is kind of a nightmare to work with, so this will take some investigation. ### Future Enhancements * For parity with other DCC apps, the "Focus" action should also adjust the camera zoom level to "frame" the selected object in the view. Punting on that for now because I'm not super confident about the math involved. * Group selection with Shift/Ctrl and/or marquee selection would be really nice to have, but probably not critical for now.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::uniforms::DepthMode;
|
||||
|
||||
/// A component for storing the computed depth at which the outline lies.
|
||||
#[derive(Clone, Component, Default)]
|
||||
pub struct ComputedOutlineDepth {
|
||||
pub(crate) world_origin: Vec3,
|
||||
pub(crate) depth_mode: DepthMode,
|
||||
pub(crate) inherited: Option<Entity>,
|
||||
}
|
||||
|
||||
/// A component which specifies how the outline depth should be computed.
|
||||
#[derive(Clone, Component)]
|
||||
#[non_exhaustive]
|
||||
pub enum SetOutlineDepth {
|
||||
/// A flat plane facing the camera and intersecting the specified point in model-space.
|
||||
Flat { model_origin: Vec3 },
|
||||
/// Real model-space.
|
||||
Real,
|
||||
}
|
||||
|
||||
/// A component which specifies that this outline lies at the same depth as its parent.
|
||||
#[derive(Clone, Component, Default)]
|
||||
pub struct InheritOutlineDepth;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn compute_outline_depth(
|
||||
mut root_query: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut ComputedOutlineDepth,
|
||||
&GlobalTransform,
|
||||
Changed<GlobalTransform>,
|
||||
Option<(&SetOutlineDepth, Changed<SetOutlineDepth>)>,
|
||||
Option<&Children>,
|
||||
),
|
||||
Without<InheritOutlineDepth>,
|
||||
>,
|
||||
mut computed_query: Query<&mut ComputedOutlineDepth, With<InheritOutlineDepth>>,
|
||||
child_query: Query<&Children>,
|
||||
) {
|
||||
for (entity, mut computed, transform, changed_transform, set_depth, children) in
|
||||
root_query.iter_mut()
|
||||
{
|
||||
let changed = !computed.depth_mode.is_valid()
|
||||
|| computed.inherited.is_some()
|
||||
|| changed_transform
|
||||
|| set_depth.filter(|(_, c)| *c).is_some();
|
||||
if changed {
|
||||
let (origin, depth_mode) = if let Some((sd, _)) = set_depth {
|
||||
match sd {
|
||||
SetOutlineDepth::Flat {
|
||||
model_origin: origin,
|
||||
} => (*origin, DepthMode::Flat),
|
||||
SetOutlineDepth::Real => (Vec3::NAN, DepthMode::Real),
|
||||
}
|
||||
} else {
|
||||
(Vec3::ZERO, DepthMode::Flat)
|
||||
};
|
||||
let matrix = transform.compute_matrix();
|
||||
computed.world_origin = matrix.project_point3(origin);
|
||||
computed.depth_mode = depth_mode;
|
||||
computed.inherited = None;
|
||||
}
|
||||
if let Some(cs) = children {
|
||||
for child in cs.iter() {
|
||||
propagate_outline_depth(
|
||||
&computed,
|
||||
changed,
|
||||
entity,
|
||||
*child,
|
||||
&mut computed_query,
|
||||
&child_query,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_outline_depth(
|
||||
root_computed: &ComputedOutlineDepth,
|
||||
mut changed: bool,
|
||||
parent: Entity,
|
||||
entity: Entity,
|
||||
computed_query: &mut Query<&mut ComputedOutlineDepth, With<InheritOutlineDepth>>,
|
||||
child_query: &Query<&Children>,
|
||||
) {
|
||||
if let Ok(mut computed) = computed_query.get_mut(entity) {
|
||||
changed |= !computed.depth_mode.is_valid() | (computed.inherited != Some(parent));
|
||||
if changed {
|
||||
*computed = root_computed.clone();
|
||||
computed.inherited = Some(parent);
|
||||
}
|
||||
if let Ok(cs) = child_query.get(entity) {
|
||||
for child in cs.iter() {
|
||||
propagate_outline_depth(
|
||||
root_computed,
|
||||
changed,
|
||||
entity,
|
||||
*child,
|
||||
computed_query,
|
||||
child_query,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use bevy::pbr::{DrawMesh, 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::renderer::RenderAdapterInfo;
|
||||
use bevy::render::view::{ExtractedView, RenderLayers};
|
||||
use wgpu_types::Backend;
|
||||
|
||||
use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline};
|
||||
use crate::pipeline::{OutlinePipeline, PassType, PipelineKey};
|
||||
use crate::uniforms::{
|
||||
DepthMode, OutlineFragmentUniform, OutlineStencilFlags, OutlineStencilUniform,
|
||||
OutlineVolumeFlags, OutlineVolumeUniform, SetOutlineStencilBindGroup,
|
||||
SetOutlineVolumeBindGroup,
|
||||
};
|
||||
use crate::view_uniforms::SetOutlineViewBindGroup;
|
||||
use crate::OutlineRenderLayers;
|
||||
|
||||
pub(crate) type DrawStencil = (
|
||||
SetItemPipeline,
|
||||
SetMeshViewBindGroup<0>,
|
||||
SetMeshBindGroup<1>,
|
||||
SetOutlineViewBindGroup<2>,
|
||||
SetOutlineStencilBindGroup<3>,
|
||||
DrawMesh,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
pub(crate) fn queue_outline_stencil_mesh(
|
||||
stencil_draw_functions: Res<DrawFunctions<StencilOutline>>,
|
||||
stencil_pipeline: Res<OutlinePipeline>,
|
||||
msaa: Res<Msaa>,
|
||||
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
render_meshes: Res<RenderAssets<Mesh>>,
|
||||
adapter_info: Res<RenderAdapterInfo>,
|
||||
material_meshes: Query<(
|
||||
Entity,
|
||||
&Handle<Mesh>,
|
||||
&OutlineStencilUniform,
|
||||
&OutlineStencilFlags,
|
||||
&OutlineRenderLayers,
|
||||
)>,
|
||||
mut views: Query<(
|
||||
&ExtractedView,
|
||||
&mut RenderPhase<StencilOutline>,
|
||||
Option<&RenderLayers>,
|
||||
)>,
|
||||
) {
|
||||
let draw_stencil = stencil_draw_functions
|
||||
.read()
|
||||
.get_id::<DrawStencil>()
|
||||
.unwrap();
|
||||
|
||||
let base_key = PipelineKey::new()
|
||||
.with_msaa(*msaa)
|
||||
.with_pass_type(PassType::Stencil)
|
||||
.with_opengl_workaround(adapter_info.0.backend == Backend::Gl);
|
||||
|
||||
for (view, mut stencil_phase, view_mask) in views.iter_mut() {
|
||||
let rangefinder = view.rangefinder3d();
|
||||
let view_mask = view_mask.copied().unwrap_or_default();
|
||||
for (entity, mesh_handle, stencil_uniform, stencil_flags, outline_mask) in
|
||||
material_meshes.iter()
|
||||
{
|
||||
if !view_mask.intersects(outline_mask) {
|
||||
continue; // Layer not enabled
|
||||
}
|
||||
if stencil_flags.depth_mode == DepthMode::Invalid {
|
||||
continue; // DepthMode not propagated
|
||||
}
|
||||
let Some(mesh) = render_meshes.get(mesh_handle) else {
|
||||
continue; // No mesh
|
||||
};
|
||||
let key = base_key
|
||||
.with_primitive_topology(mesh.primitive_topology)
|
||||
.with_depth_mode(stencil_flags.depth_mode)
|
||||
.with_offset_zero(stencil_uniform.offset == 0.0)
|
||||
.with_morph_targets(mesh.morph_targets.is_some());
|
||||
let Ok(pipeline) =
|
||||
pipelines.specialize(&pipeline_cache, &stencil_pipeline, key, &mesh.layout)
|
||||
else {
|
||||
continue; // No pipeline
|
||||
};
|
||||
let distance = rangefinder.distance(&Mat4::from_translation(stencil_uniform.origin));
|
||||
stencil_phase.add(StencilOutline {
|
||||
entity,
|
||||
pipeline,
|
||||
draw_function: draw_stencil,
|
||||
distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type DrawOutline = (
|
||||
SetItemPipeline,
|
||||
SetMeshViewBindGroup<0>,
|
||||
SetMeshBindGroup<1>,
|
||||
SetOutlineViewBindGroup<2>,
|
||||
SetOutlineVolumeBindGroup<3>,
|
||||
DrawMesh,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
pub(crate) fn queue_outline_volume_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>>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
render_meshes: Res<RenderAssets<Mesh>>,
|
||||
adapter_info: Res<RenderAdapterInfo>,
|
||||
material_meshes: Query<(
|
||||
Entity,
|
||||
&Handle<Mesh>,
|
||||
&OutlineVolumeUniform,
|
||||
&OutlineVolumeFlags,
|
||||
&OutlineFragmentUniform,
|
||||
&OutlineRenderLayers,
|
||||
)>,
|
||||
mut views: Query<(
|
||||
&ExtractedView,
|
||||
&mut RenderPhase<OpaqueOutline>,
|
||||
&mut RenderPhase<TransparentOutline>,
|
||||
Option<&RenderLayers>,
|
||||
)>,
|
||||
) {
|
||||
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 = PipelineKey::new()
|
||||
.with_msaa(*msaa)
|
||||
.with_opengl_workaround(adapter_info.0.backend == Backend::Gl);
|
||||
|
||||
for (view, mut opaque_phase, mut transparent_phase, view_mask) in views.iter_mut() {
|
||||
let view_mask = view_mask.copied().unwrap_or_default();
|
||||
let rangefinder = view.rangefinder3d();
|
||||
for (entity, mesh_handle, volume_uniform, volume_flags, fragment_uniform, outline_mask) in
|
||||
material_meshes.iter()
|
||||
{
|
||||
if !view_mask.intersects(outline_mask) {
|
||||
continue; // Layer not enabled
|
||||
}
|
||||
if volume_flags.depth_mode == DepthMode::Invalid {
|
||||
continue; // DepthMode not propagated
|
||||
}
|
||||
let Some(mesh) = render_meshes.get(mesh_handle) else {
|
||||
continue; // No mesh
|
||||
};
|
||||
let transparent = fragment_uniform.colour[3] < 1.0;
|
||||
let key = base_key
|
||||
.with_primitive_topology(mesh.primitive_topology)
|
||||
.with_pass_type(if transparent {
|
||||
PassType::Transparent
|
||||
} else {
|
||||
PassType::Opaque
|
||||
})
|
||||
.with_depth_mode(volume_flags.depth_mode)
|
||||
.with_offset_zero(volume_uniform.offset == 0.0)
|
||||
.with_hdr_format(view.hdr)
|
||||
.with_morph_targets(mesh.morph_targets.is_some());
|
||||
let Ok(pipeline) =
|
||||
pipelines.specialize(&pipeline_cache, &outline_pipeline, key, &mesh.layout)
|
||||
else {
|
||||
continue; // No pipeline
|
||||
};
|
||||
let distance = rangefinder.distance(&Mat4::from_translation(volume_uniform.origin));
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
struct FragmentOutput {
|
||||
@location(0) colour: vec4<f32>,
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
@builtin(frag_depth) frag_depth: f32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct OutlineFragmentUniform {
|
||||
@align(16)
|
||||
colour: vec4<f32>,
|
||||
};
|
||||
|
||||
#ifdef VOLUME
|
||||
@group(3) @binding(1)
|
||||
var<uniform> fstage: OutlineFragmentUniform;
|
||||
#endif
|
||||
|
||||
@fragment
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
fn fragment(@location(0) normalised_depth: f32) -> FragmentOutput {
|
||||
#else
|
||||
fn fragment() -> FragmentOutput {
|
||||
#endif
|
||||
var out: FragmentOutput;
|
||||
#ifdef VOLUME
|
||||
out.colour = fstage.colour;
|
||||
#endif
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
out.frag_depth = normalised_depth;
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{
|
||||
mesh::{Indices, VertexAttributeValues},
|
||||
render_resource::{PrimitiveTopology, VertexFormat},
|
||||
},
|
||||
utils::{FloatOrd, HashMap, HashSet},
|
||||
};
|
||||
|
||||
use crate::ATTRIBUTE_OUTLINE_NORMAL;
|
||||
|
||||
enum IndexIterator<'a> {
|
||||
ExplicitU16(std::slice::Iter<'a, u16>),
|
||||
ExplicitU32(std::slice::Iter<'a, u32>),
|
||||
Implicit(std::ops::Range<usize>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Mesh> for IndexIterator<'a> {
|
||||
fn from(value: &'a Mesh) -> Self {
|
||||
match value.indices() {
|
||||
Some(Indices::U16(vec)) => IndexIterator::ExplicitU16(vec.iter()),
|
||||
Some(Indices::U32(vec)) => IndexIterator::ExplicitU32(vec.iter()),
|
||||
None => IndexIterator::Implicit(0..value.count_vertices()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for IndexIterator<'_> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
IndexIterator::ExplicitU16(iter) => iter.next().map(|val| *val as usize),
|
||||
IndexIterator::ExplicitU32(iter) => iter.next().map(|val| *val as usize),
|
||||
IndexIterator::Implicit(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self {
|
||||
IndexIterator::ExplicitU16(iter) => iter.size_hint(),
|
||||
IndexIterator::ExplicitU32(iter) => iter.size_hint(),
|
||||
IndexIterator::Implicit(iter) => iter.size_hint(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for IndexIterator<'_> {}
|
||||
|
||||
/// Failed to generate outline normals for the mesh.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum GenerateOutlineNormalsError {
|
||||
#[error("unsupported primitive topology '{0:?}'")]
|
||||
UnsupportedPrimitiveTopology(PrimitiveTopology),
|
||||
#[error("missing vertex attributes '{0}'")]
|
||||
MissingVertexAttribute(&'static str),
|
||||
#[error("the '{0}' vertex attribute should have {1:?} format, but had {2:?} format")]
|
||||
InvalidVertexAttributeFormat(&'static str, VertexFormat, VertexFormat),
|
||||
}
|
||||
|
||||
/// Extension methods for [`Mesh`].
|
||||
pub trait OutlineMeshExt {
|
||||
/// Generates outline normals for the mesh.
|
||||
///
|
||||
/// Vertex extrusion only works for meshes with smooth surface normals. Hard edges cause
|
||||
/// visual artefacts. This function generates faux-smooth normals for outlining purposes
|
||||
/// by grouping vertices by their position and averaging the normals at each point. These
|
||||
/// outline normals are then inserted as a separate vertex attribute so that the regular
|
||||
/// normals remain untouched. However, insofar as the outline normals are not
|
||||
/// perpendicular to the surface of the mesh, this technique may result in non-uniform
|
||||
/// outline thickness.
|
||||
///
|
||||
/// This function only supports meshes with TriangleList topology.
|
||||
fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError>;
|
||||
}
|
||||
|
||||
impl OutlineMeshExt for Mesh {
|
||||
fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError> {
|
||||
if self.primitive_topology() != PrimitiveTopology::TriangleList {
|
||||
return Err(GenerateOutlineNormalsError::UnsupportedPrimitiveTopology(
|
||||
self.primitive_topology(),
|
||||
));
|
||||
}
|
||||
let positions = match self.attribute(Mesh::ATTRIBUTE_POSITION).ok_or(
|
||||
GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name),
|
||||
)? {
|
||||
VertexAttributeValues::Float32x3(p) => Ok(p),
|
||||
v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat(
|
||||
Mesh::ATTRIBUTE_POSITION.name,
|
||||
VertexFormat::Float32x3,
|
||||
v.into(),
|
||||
)),
|
||||
}?;
|
||||
let normals = match self.attribute(Mesh::ATTRIBUTE_NORMAL) {
|
||||
Some(VertexAttributeValues::Float32x3(p)) => Some(p),
|
||||
_ => None,
|
||||
};
|
||||
let mut map = HashMap::<[FloatOrd; 3], Vec3>::with_capacity(positions.len());
|
||||
let mut it = IndexIterator::from(&*self);
|
||||
while let (Some(i0), Some(i1), Some(i2)) = (it.next(), it.next(), it.next()) {
|
||||
for (j0, j1, j2) in [(i0, i1, i2), (i1, i2, i0), (i2, i0, i1)] {
|
||||
let p0 = Vec3::from(positions[j0]);
|
||||
let p1 = Vec3::from(positions[j1]);
|
||||
let p2 = Vec3::from(positions[j2]);
|
||||
let angle = (p1 - p0).angle_between(p2 - p0);
|
||||
let n = map
|
||||
.entry([FloatOrd(p0.x), FloatOrd(p0.y), FloatOrd(p0.z)])
|
||||
.or_default();
|
||||
*n += angle
|
||||
* if let Some(ns) = normals {
|
||||
// Use vertex normal
|
||||
Vec3::from(ns[j0])
|
||||
} else {
|
||||
// Calculate face normal
|
||||
(p1 - p0).cross(p2 - p0).normalize_or_zero()
|
||||
};
|
||||
}
|
||||
}
|
||||
let mut outlines = Vec::with_capacity(positions.len());
|
||||
for p in positions.iter() {
|
||||
let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])];
|
||||
outlines.push(
|
||||
map.get(&key)
|
||||
.copied()
|
||||
.unwrap_or(Vec3::ZERO)
|
||||
.normalize_or_zero()
|
||||
.to_array(),
|
||||
);
|
||||
}
|
||||
self.insert_attribute(
|
||||
ATTRIBUTE_OUTLINE_NORMAL,
|
||||
VertexAttributeValues::Float32x3(outlines),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_generate_outline_normals(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut events: EventReader<'_, '_, AssetEvent<Mesh>>,
|
||||
mut squelch: Local<HashSet<Handle<Mesh>>>,
|
||||
) {
|
||||
for event in events.iter() {
|
||||
match event {
|
||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
|
||||
if squelch.contains(handle) {
|
||||
// Suppress modification events created by this system
|
||||
squelch.remove(handle);
|
||||
} else if let Some(mesh) = meshes.get_mut(handle) {
|
||||
let _ = mesh.generate_outline_normals();
|
||||
squelch.insert(handle.clone_weak());
|
||||
}
|
||||
}
|
||||
AssetEvent::Removed { handle } => {
|
||||
squelch.remove(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Automatically runs [`generate_outline_normals`](OutlineMeshExt::generate_outline_normals)
|
||||
/// on every mesh.
|
||||
///
|
||||
/// This is provided as a convenience for simple projects. It runs the outline normal
|
||||
/// generator every time a mesh asset is created or modified without consideration for
|
||||
/// whether this is necessary or appropriate.
|
||||
pub struct AutoGenerateOutlineNormalsPlugin;
|
||||
|
||||
impl Plugin for AutoGenerateOutlineNormalsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, auto_generate_outline_normals);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//! This crate provides a Bevy plugin, [`OutlinePlugin`], and associated components for
|
||||
//! rendering outlines around meshes using the vertex extrusion method.
|
||||
//!
|
||||
//! Outlines are rendered in a seperate pass following the main 3D pass. The effect of this
|
||||
//! pass is to present the outlines in depth sorted order according to the model translation
|
||||
//! of each mesh. This ensures that outlines are not clipped by other geometry.
|
||||
//!
|
||||
//! The [`OutlineVolume`] component will, by itself, cover the original object entirely with
|
||||
//! the outline colour. The [`OutlineStencil`] component must also be added to prevent the body
|
||||
//! of an object from being filled it. This must be added to any entity which needs to appear on
|
||||
//! top of an outline.
|
||||
//!
|
||||
//! The [`OutlineBundle`] and [`OutlineStencilBundle`] bundles can be used to add the right
|
||||
//! components, including the required [`ComputedOutlineDepth`] component. Optionally, the
|
||||
//! [`SetOutlineDepth`] and [`InheritOutlineDepth`] components may also be added to control the
|
||||
//! depth ordering of outlines.
|
||||
//!
|
||||
//! Vertex extrusion works best with meshes that have smooth surfaces. To avoid visual
|
||||
//! artefacts when outlining meshes with hard edges, see the
|
||||
//! [`OutlineMeshExt::generate_outline_normals`] function and the
|
||||
//! [`AutoGenerateOutlineNormalsPlugin`].
|
||||
|
||||
use bevy::asset::load_internal_asset;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::extract_component::{
|
||||
ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
|
||||
};
|
||||
use bevy::render::mesh::MeshVertexAttribute;
|
||||
use bevy::render::render_graph::RenderGraph;
|
||||
use bevy::render::render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions};
|
||||
use bevy::render::render_resource::{SpecializedMeshPipelines, VertexFormat};
|
||||
use bevy::render::view::RenderLayers;
|
||||
use bevy::render::{Render, RenderApp, RenderSet};
|
||||
use bevy::transform::TransformSystem;
|
||||
use interpolation::Lerp;
|
||||
|
||||
use crate::draw::{
|
||||
queue_outline_stencil_mesh, queue_outline_volume_mesh, DrawOutline, DrawStencil,
|
||||
};
|
||||
use crate::node::{OpaqueOutline, OutlineNode, StencilOutline, TransparentOutline};
|
||||
use crate::pipeline::{OutlinePipeline, FRAGMENT_SHADER_HANDLE, OUTLINE_SHADER_HANDLE};
|
||||
use crate::uniforms::{
|
||||
extract_outline_stencil_uniforms, extract_outline_volume_uniforms,
|
||||
queue_outline_stencil_bind_group, queue_outline_volume_bind_group, OutlineFragmentUniform,
|
||||
OutlineStencilUniform, OutlineVolumeUniform,
|
||||
};
|
||||
use crate::view_uniforms::{
|
||||
extract_outline_view_uniforms, queue_outline_view_bind_group, OutlineViewUniform,
|
||||
};
|
||||
|
||||
mod computed;
|
||||
mod draw;
|
||||
mod generate;
|
||||
mod node;
|
||||
mod pipeline;
|
||||
mod uniforms;
|
||||
mod view_uniforms;
|
||||
|
||||
pub use computed::*;
|
||||
pub use generate::*;
|
||||
|
||||
// See https://alexanderameye.github.io/notes/rendering-outlines/
|
||||
|
||||
/// The direction to extrude the vertex when rendering the outline.
|
||||
pub const ATTRIBUTE_OUTLINE_NORMAL: MeshVertexAttribute =
|
||||
MeshVertexAttribute::new("Outline_Normal", 1585570526, VertexFormat::Float32x3);
|
||||
|
||||
/// Name of the render graph node which draws the outlines.
|
||||
///
|
||||
/// This node runs after the main 3D passes and before the UI pass. The name can be used to
|
||||
/// add additional constraints on node execution order with respect to other passes.
|
||||
pub const OUTLINE_PASS_NODE_NAME: &str = "bevy_mod_outline_node";
|
||||
|
||||
/// A component for stenciling meshes during outline rendering.
|
||||
#[derive(Clone, Component)]
|
||||
pub struct OutlineStencil {
|
||||
/// Enable rendering of the stencil
|
||||
pub enabled: bool,
|
||||
/// Offset of the stencil in logical pixels
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
impl Default for OutlineStencil {
|
||||
fn default() -> Self {
|
||||
OutlineStencil {
|
||||
enabled: true,
|
||||
offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp_bool(this: bool, other: bool, scalar: f32) -> bool {
|
||||
if scalar <= 0.0 {
|
||||
this
|
||||
} else if scalar >= 1.0 {
|
||||
other
|
||||
} else {
|
||||
this | other
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for OutlineStencil {
|
||||
type Scalar = f32;
|
||||
|
||||
fn lerp(&self, other: &Self, scalar: &Self::Scalar) -> Self {
|
||||
OutlineStencil {
|
||||
enabled: lerp_bool(self.enabled, other.enabled, *scalar),
|
||||
offset: self.offset.lerp(&other.offset, scalar),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A component for rendering outlines around meshes.
|
||||
#[derive(Clone, Component, Default)]
|
||||
pub struct OutlineVolume {
|
||||
/// Enable rendering of the outline
|
||||
pub visible: bool,
|
||||
/// Width of the outline in logical pixels
|
||||
pub width: f32,
|
||||
/// Colour of the outline
|
||||
pub colour: Color,
|
||||
}
|
||||
|
||||
impl Lerp for OutlineVolume {
|
||||
type Scalar = f32;
|
||||
|
||||
fn lerp(&self, other: &Self, scalar: &Self::Scalar) -> Self {
|
||||
OutlineVolume {
|
||||
visible: lerp_bool(self.visible, other.visible, *scalar),
|
||||
width: self.width.lerp(&other.width, scalar),
|
||||
colour: {
|
||||
let [r, g, b, a] = self
|
||||
.colour
|
||||
.as_linear_rgba_f32()
|
||||
.lerp(&other.colour.as_linear_rgba_f32(), scalar);
|
||||
Color::rgba_linear(r, g, b, a)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A component for specifying what layer(s) the outline should be rendered for.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, Default)]
|
||||
pub struct OutlineRenderLayers(pub RenderLayers);
|
||||
|
||||
impl ExtractComponent for OutlineRenderLayers {
|
||||
type Query = (
|
||||
Option<&'static OutlineRenderLayers>,
|
||||
Option<&'static RenderLayers>,
|
||||
);
|
||||
type Filter = Or<(With<OutlineVolume>, With<OutlineStencil>)>;
|
||||
type Out = Self;
|
||||
|
||||
fn extract_component(
|
||||
(outline_mask, object_mask): (Option<&OutlineRenderLayers>, Option<&RenderLayers>),
|
||||
) -> Option<Self> {
|
||||
Some(
|
||||
outline_mask
|
||||
.copied()
|
||||
.or_else(|| object_mask.copied().map(OutlineRenderLayers))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A bundle for rendering stenciled outlines around meshes.
|
||||
#[derive(Bundle, Clone, Default)]
|
||||
pub struct OutlineBundle {
|
||||
pub outline: OutlineVolume,
|
||||
pub stencil: OutlineStencil,
|
||||
pub plane: ComputedOutlineDepth,
|
||||
}
|
||||
|
||||
/// A bundle for stenciling meshes in the outlining pass.
|
||||
#[derive(Bundle, Clone, Default)]
|
||||
pub struct OutlineStencilBundle {
|
||||
pub stencil: OutlineStencil,
|
||||
pub plane: ComputedOutlineDepth,
|
||||
}
|
||||
|
||||
/// Adds support for rendering outlines.
|
||||
pub struct OutlinePlugin;
|
||||
|
||||
impl Plugin for OutlinePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
load_internal_asset!(
|
||||
app,
|
||||
OUTLINE_SHADER_HANDLE,
|
||||
"outline.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
load_internal_asset!(
|
||||
app,
|
||||
FRAGMENT_SHADER_HANDLE,
|
||||
"fragment.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
|
||||
app.add_plugins((
|
||||
ExtractComponentPlugin::<OutlineRenderLayers>::default(),
|
||||
UniformComponentPlugin::<OutlineStencilUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineVolumeUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineFragmentUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineViewUniform>::default(),
|
||||
))
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
compute_outline_depth.after(TransformSystem::TransformPropagate),
|
||||
)
|
||||
.sub_app_mut(RenderApp)
|
||||
.init_resource::<DrawFunctions<StencilOutline>>()
|
||||
.init_resource::<DrawFunctions<OpaqueOutline>>()
|
||||
.init_resource::<DrawFunctions<TransparentOutline>>()
|
||||
.init_resource::<SpecializedMeshPipelines<OutlinePipeline>>()
|
||||
.add_render_command::<StencilOutline, DrawStencil>()
|
||||
.add_render_command::<OpaqueOutline, DrawOutline>()
|
||||
.add_render_command::<TransparentOutline, DrawOutline>()
|
||||
.add_systems(ExtractSchedule, extract_outline_view_uniforms)
|
||||
.add_systems(ExtractSchedule, extract_outline_stencil_uniforms)
|
||||
.add_systems(ExtractSchedule, extract_outline_volume_uniforms)
|
||||
.add_systems(
|
||||
Render,
|
||||
sort_phase_system::<StencilOutline>.in_set(RenderSet::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
sort_phase_system::<OpaqueOutline>.in_set(RenderSet::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
sort_phase_system::<TransparentOutline>.in_set(RenderSet::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
queue_outline_view_bind_group.in_set(RenderSet::Queue),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
queue_outline_stencil_bind_group.in_set(RenderSet::Queue),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
queue_outline_volume_bind_group.in_set(RenderSet::Queue),
|
||||
)
|
||||
.add_systems(Render, queue_outline_stencil_mesh.in_set(RenderSet::Queue))
|
||||
.add_systems(Render, queue_outline_volume_mesh.in_set(RenderSet::Queue));
|
||||
|
||||
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(OUTLINE_PASS_NODE_NAME, node);
|
||||
|
||||
// Run after main 3D pass, but before UI psss
|
||||
draw_3d_graph.add_node_edge(
|
||||
bevy::core_pipeline::core_3d::graph::node::END_MAIN_PASS,
|
||||
OUTLINE_PASS_NODE_NAME,
|
||||
);
|
||||
#[cfg(feature = "bevy_ui")]
|
||||
draw_3d_graph.add_node_edge(
|
||||
OUTLINE_PASS_NODE_NAME,
|
||||
bevy::ui::draw_ui_graph::node::UI_PASS,
|
||||
);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
app.sub_app_mut(RenderApp)
|
||||
.init_resource::<OutlinePipeline>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use bevy::ecs::system::lifetimeless::Read;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::camera::ExtractedCamera;
|
||||
use bevy::render::render_graph::NodeRunError;
|
||||
use bevy::render::render_phase::{
|
||||
CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, RenderPhase,
|
||||
};
|
||||
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(crate) struct StencilOutline {
|
||||
pub distance: f32,
|
||||
pub pipeline: CachedRenderPipelineId,
|
||||
pub entity: Entity,
|
||||
pub draw_function: DrawFunctionId,
|
||||
}
|
||||
|
||||
impl PhaseItem for StencilOutline {
|
||||
type SortKey = Reverse<FloatOrd>;
|
||||
|
||||
#[inline]
|
||||
fn entity(&self) -> Entity {
|
||||
self.entity
|
||||
}
|
||||
|
||||
fn sort_key(&self) -> Self::SortKey {
|
||||
Reverse(FloatOrd(self.distance))
|
||||
}
|
||||
|
||||
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
|
||||
self.draw_function
|
||||
}
|
||||
}
|
||||
|
||||
impl CachedRenderPipelinePhaseItem for StencilOutline {
|
||||
#[inline]
|
||||
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||
self.pipeline
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct OpaqueOutline {
|
||||
pub distance: f32,
|
||||
pub pipeline: CachedRenderPipelineId,
|
||||
pub entity: Entity,
|
||||
pub draw_function: DrawFunctionId,
|
||||
}
|
||||
|
||||
impl PhaseItem for OpaqueOutline {
|
||||
type SortKey = Reverse<FloatOrd>;
|
||||
|
||||
#[inline]
|
||||
fn entity(&self) -> Entity {
|
||||
self.entity
|
||||
}
|
||||
|
||||
fn sort_key(&self) -> Self::SortKey {
|
||||
Reverse(FloatOrd(self.distance))
|
||||
}
|
||||
|
||||
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
|
||||
self.draw_function
|
||||
}
|
||||
}
|
||||
|
||||
impl CachedRenderPipelinePhaseItem for OpaqueOutline {
|
||||
#[inline]
|
||||
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||
self.pipeline
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TransparentOutline {
|
||||
pub distance: f32,
|
||||
pub pipeline: CachedRenderPipelineId,
|
||||
pub entity: Entity,
|
||||
pub draw_function: DrawFunctionId,
|
||||
}
|
||||
|
||||
impl PhaseItem for TransparentOutline {
|
||||
type SortKey = FloatOrd;
|
||||
|
||||
#[inline]
|
||||
fn entity(&self) -> Entity {
|
||||
self.entity
|
||||
}
|
||||
|
||||
fn sort_key(&self) -> Self::SortKey {
|
||||
FloatOrd(self.distance)
|
||||
}
|
||||
|
||||
fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId {
|
||||
self.draw_function
|
||||
}
|
||||
}
|
||||
|
||||
impl CachedRenderPipelinePhaseItem for TransparentOutline {
|
||||
#[inline]
|
||||
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||
self.pipeline
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) 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(crate) fn new(world: &mut World) -> Self {
|
||||
Self {
|
||||
query: world.query_filtered(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for OutlineNode {
|
||||
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.view_entity();
|
||||
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
|
||||
};
|
||||
|
||||
// If drawing anything, run stencil pass to clear the depth buffer
|
||||
if !opaque_phase.items.is_empty() || !transparent_phase.items.is_empty() {
|
||||
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 mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor);
|
||||
if let Some(viewport) = camera.viewport.as_ref() {
|
||||
tracked_pass.set_camera_viewport(viewport);
|
||||
}
|
||||
stencil_phase.render(&mut tracked_pass, world, view_entity);
|
||||
}
|
||||
|
||||
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 mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor);
|
||||
if let Some(viewport) = camera.viewport.as_ref() {
|
||||
tracked_pass.set_camera_viewport(viewport);
|
||||
}
|
||||
opaque_phase.render(&mut tracked_pass, world, view_entity);
|
||||
}
|
||||
|
||||
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 mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor);
|
||||
if let Some(viewport) = camera.viewport.as_ref() {
|
||||
tracked_pass.set_camera_viewport(viewport);
|
||||
}
|
||||
transparent_phase.render(&mut tracked_pass, world, view_entity);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#import bevy_render::view View
|
||||
#import bevy_pbr::mesh_types Mesh
|
||||
#import bevy_pbr::mesh_types SkinnedMesh
|
||||
|
||||
struct Vertex {
|
||||
#ifdef VERTEX_POSITIONS
|
||||
@location(0) position: vec3<f32>,
|
||||
#endif
|
||||
#ifndef OFFSET_ZERO
|
||||
@location(1) outline_normal: vec3<f32>,
|
||||
#endif
|
||||
#ifdef SKINNED
|
||||
@location(5) joint_indices: vec4<u32>,
|
||||
@location(6) joint_weights: vec4<f32>,
|
||||
#endif
|
||||
#ifdef MORPH_TARGETS
|
||||
@builtin(vertex_index) index: u32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
@location(0) normalised_depth: f32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct OutlineViewUniform {
|
||||
@align(16)
|
||||
scale: vec2<f32>,
|
||||
};
|
||||
|
||||
struct OutlineVertexUniform {
|
||||
@align(16)
|
||||
origin: vec3<f32>,
|
||||
offset: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> view: View;
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<uniform> mesh: Mesh;
|
||||
|
||||
#import bevy_pbr::skinning
|
||||
#import bevy_pbr::morph
|
||||
|
||||
@group(2) @binding(0)
|
||||
var<uniform> view_uniform: OutlineViewUniform;
|
||||
|
||||
@group(3) @binding(0)
|
||||
var<uniform> vstage: OutlineVertexUniform;
|
||||
|
||||
#ifdef MORPH_TARGETS
|
||||
fn morph_vertex(vertex_in: Vertex) -> Vertex {
|
||||
var vertex = vertex_in;
|
||||
let weight_count = bevy_pbr::morph::layer_count();
|
||||
for (var i: u32 = 0u; i < weight_count; i ++) {
|
||||
let weight = bevy_pbr::morph::weight_at(i);
|
||||
if weight == 0.0 {
|
||||
continue;
|
||||
}
|
||||
vertex.position += weight * bevy_pbr::morph::morph(vertex.index, bevy_pbr::morph::position_offset, i);
|
||||
}
|
||||
return vertex;
|
||||
}
|
||||
#endif
|
||||
|
||||
fn mat4to3(m: mat4x4<f32>) -> mat3x3<f32> {
|
||||
return mat3x3<f32>(
|
||||
m[0].xyz, m[1].xyz, m[2].xyz
|
||||
);
|
||||
}
|
||||
|
||||
fn model_origin_z(plane: vec3<f32>, view_proj: mat4x4<f32>) -> f32 {
|
||||
var proj_zw = mat4x2<f32>(
|
||||
view_proj[0].zw, view_proj[1].zw,
|
||||
view_proj[2].zw, view_proj[3].zw);
|
||||
var zw = proj_zw * vec4<f32>(plane, 1.0);
|
||||
return zw.x / zw.y;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
|
||||
#ifdef MORPH_TARGETS
|
||||
var vertex = morph_vertex(vertex_no_morph);
|
||||
#else
|
||||
var vertex = vertex_no_morph;
|
||||
#endif
|
||||
#ifdef SKINNED
|
||||
let model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
|
||||
#else
|
||||
let model = mesh.model;
|
||||
#endif
|
||||
let clip_pos = view.view_proj * (model * vec4<f32>(vertex.position, 1.0));
|
||||
#ifdef FLAT_DEPTH
|
||||
let out_z = model_origin_z(vstage.origin, view.view_proj) * clip_pos.w;
|
||||
#else
|
||||
let out_z = clip_pos.z;
|
||||
#endif
|
||||
#ifdef OFFSET_ZERO
|
||||
let out_xy = clip_pos.xy;
|
||||
#else
|
||||
let clip_norm = mat4to3(view.view_proj) * (mat4to3(model) * vertex.outline_normal);
|
||||
let ndc_delta = vstage.offset * normalize(clip_norm.xy) * view_uniform.scale * clip_pos.w;
|
||||
let out_xy = clip_pos.xy + ndc_delta;
|
||||
#endif
|
||||
var out: VertexOutput;
|
||||
out.position = vec4<f32>(out_xy, out_z, clip_pos.w);
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
out.normalised_depth = 10.0 + 0.5 * (out_z / clip_pos.w);
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bevy::pbr::{setup_morph_and_skinning_defs, MeshPipelineKey};
|
||||
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, PrimitiveTopology, ShaderDefVal, ShaderSize, ShaderStages, StencilState,
|
||||
TextureFormat, VertexState,
|
||||
};
|
||||
use bevy::render::renderer::RenderDevice;
|
||||
use bevy::render::texture::BevyDefault;
|
||||
use bevy::render::view::ViewTarget;
|
||||
use bevy::{
|
||||
pbr::MeshPipeline,
|
||||
render::{
|
||||
mesh::MeshVertexBufferLayout,
|
||||
render_resource::{
|
||||
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
|
||||
},
|
||||
},
|
||||
};
|
||||
use bitfield::{bitfield_bitrange, bitfield_fields};
|
||||
|
||||
use crate::uniforms::{
|
||||
DepthMode, OutlineFragmentUniform, OutlineStencilUniform, OutlineVolumeUniform,
|
||||
};
|
||||
use crate::view_uniforms::OutlineViewUniform;
|
||||
use crate::ATTRIBUTE_OUTLINE_NORMAL;
|
||||
|
||||
pub(crate) const OUTLINE_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2101625026478770097);
|
||||
|
||||
pub(crate) const FRAGMENT_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 12033806834125368121);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum PassType {
|
||||
Stencil = 1,
|
||||
Opaque = 2,
|
||||
Transparent = 3,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct PipelineKey(u32);
|
||||
bitfield_bitrange! {struct PipelineKey(u32)}
|
||||
|
||||
impl PipelineKey {
|
||||
bitfield_fields! {
|
||||
u32;
|
||||
msaa_samples_minus_one, set_msaa_samples_minus_one: 5, 0;
|
||||
primitive_topology_int, set_primitive_topology_int: 8, 6;
|
||||
pass_type_int, set_pass_type_int: 10, 9;
|
||||
depth_mode_int, set_depth_mode_int: 12, 11;
|
||||
pub offset_zero, set_offset_zero: 13;
|
||||
pub hdr_format, set_hdr_format: 14;
|
||||
pub opengl_workaround, set_opengl_workaround: 15;
|
||||
pub morph_targets, set_morph_targets: 16;
|
||||
}
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
PipelineKey(0)
|
||||
}
|
||||
|
||||
pub(crate) fn with_msaa(mut self, msaa: Msaa) -> Self {
|
||||
self.set_msaa_samples_minus_one(msaa as u32 - 1);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn msaa(&self) -> Msaa {
|
||||
match self.msaa_samples_minus_one() + 1 {
|
||||
x if x == Msaa::Off as u32 => Msaa::Off,
|
||||
x if x == Msaa::Sample2 as u32 => Msaa::Sample2,
|
||||
x if x == Msaa::Sample4 as u32 => Msaa::Sample4,
|
||||
x if x == Msaa::Sample8 as u32 => Msaa::Sample8,
|
||||
x => panic!("Invalid value for Msaa: {}", x),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_primitive_topology(mut self, primitive_topology: PrimitiveTopology) -> Self {
|
||||
self.set_primitive_topology_int(primitive_topology as u32);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn primitive_topology(&self) -> PrimitiveTopology {
|
||||
match self.primitive_topology_int() {
|
||||
x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList,
|
||||
x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList,
|
||||
x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip,
|
||||
x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList,
|
||||
x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip,
|
||||
x => panic!("Invalid value for PrimitiveTopology: {}", x),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_pass_type(mut self, pass_type: PassType) -> Self {
|
||||
self.set_pass_type_int(pass_type as u32);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn pass_type(&self) -> PassType {
|
||||
match self.pass_type_int() {
|
||||
x if x == PassType::Stencil as u32 => PassType::Stencil,
|
||||
x if x == PassType::Opaque as u32 => PassType::Opaque,
|
||||
x if x == PassType::Transparent as u32 => PassType::Transparent,
|
||||
x => panic!("Invalid value for PassType: {}", x),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_depth_mode(mut self, depth_mode: DepthMode) -> Self {
|
||||
self.set_depth_mode_int(depth_mode as u32);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn depth_mode(&self) -> DepthMode {
|
||||
match self.depth_mode_int() {
|
||||
x if x == DepthMode::Flat as u32 => DepthMode::Flat,
|
||||
x if x == DepthMode::Real as u32 => DepthMode::Real,
|
||||
x => panic!("Invalid value for DepthMode: {}", x),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_offset_zero(mut self, offset_zero: bool) -> Self {
|
||||
self.set_offset_zero(offset_zero);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_hdr_format(mut self, hdr_format: bool) -> Self {
|
||||
self.set_hdr_format(hdr_format);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_opengl_workaround(mut self, opengl_workaround: bool) -> Self {
|
||||
self.set_opengl_workaround(opengl_workaround);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_morph_targets(mut self, morph_targets: bool) -> Self {
|
||||
self.set_morph_targets(morph_targets);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipelineKey> for MeshPipelineKey {
|
||||
fn from(key: PipelineKey) -> Self {
|
||||
if key.morph_targets() {
|
||||
MeshPipelineKey::empty() | MeshPipelineKey::MORPH_TARGETS
|
||||
} else {
|
||||
MeshPipelineKey::empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct OutlinePipeline {
|
||||
mesh_pipeline: MeshPipeline,
|
||||
pub outline_view_bind_group_layout: BindGroupLayout,
|
||||
pub outline_stencil_bind_group_layout: BindGroupLayout,
|
||||
pub outline_volume_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_volume_bind_group_layout =
|
||||
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
label: Some("outline_volume_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(
|
||||
OutlineVolumeUniform::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,
|
||||
},
|
||||
],
|
||||
});
|
||||
let outline_stencil_bind_group_layout =
|
||||
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
label: Some("outline_stencil_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(OutlineStencilUniform::SHADER_SIZE.get()),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
OutlinePipeline {
|
||||
mesh_pipeline,
|
||||
outline_view_bind_group_layout,
|
||||
outline_stencil_bind_group_layout,
|
||||
outline_volume_bind_group_layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecializedMeshPipeline for OutlinePipeline {
|
||||
type Key = PipelineKey;
|
||||
|
||||
fn specialize(
|
||||
&self,
|
||||
key: Self::Key,
|
||||
layout: &MeshVertexBufferLayout,
|
||||
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
|
||||
let mut targets = vec![];
|
||||
let mut vertex_defs = vec!["MESH_BINDGROUP_1".into()];
|
||||
let mut fragment_defs = vec![];
|
||||
let mut buffer_attrs = Vec::new();
|
||||
|
||||
if layout.contains(Mesh::ATTRIBUTE_POSITION) {
|
||||
vertex_defs.push("VERTEX_POSITIONS".into());
|
||||
buffer_attrs.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
|
||||
}
|
||||
|
||||
let mut bind_layouts = vec![if key.msaa() == Msaa::Off {
|
||||
self.mesh_pipeline.view_layout.clone()
|
||||
} else {
|
||||
self.mesh_pipeline.view_layout_multisampled.clone()
|
||||
}];
|
||||
|
||||
bind_layouts.push(setup_morph_and_skinning_defs(
|
||||
&self.mesh_pipeline.mesh_layouts,
|
||||
layout,
|
||||
5,
|
||||
&key.into(),
|
||||
&mut vertex_defs,
|
||||
&mut buffer_attrs,
|
||||
));
|
||||
|
||||
bind_layouts.push(self.outline_view_bind_group_layout.clone());
|
||||
let cull_mode;
|
||||
if key.depth_mode() == DepthMode::Flat {
|
||||
vertex_defs.push(ShaderDefVal::from("FLAT_DEPTH"));
|
||||
cull_mode = Some(Face::Back);
|
||||
} else if key.pass_type() == PassType::Stencil {
|
||||
cull_mode = Some(Face::Back);
|
||||
} else {
|
||||
cull_mode = Some(Face::Front);
|
||||
}
|
||||
if key.offset_zero() {
|
||||
vertex_defs.push(ShaderDefVal::from("OFFSET_ZERO"));
|
||||
} else {
|
||||
buffer_attrs.push(
|
||||
if layout.contains(ATTRIBUTE_OUTLINE_NORMAL) {
|
||||
ATTRIBUTE_OUTLINE_NORMAL
|
||||
} else {
|
||||
Mesh::ATTRIBUTE_NORMAL
|
||||
}
|
||||
.at_shader_location(1),
|
||||
);
|
||||
}
|
||||
match key.pass_type() {
|
||||
PassType::Stencil => {
|
||||
bind_layouts.push(self.outline_stencil_bind_group_layout.clone());
|
||||
}
|
||||
PassType::Opaque | PassType::Transparent => {
|
||||
fragment_defs.push(ShaderDefVal::from("VOLUME"));
|
||||
targets.push(Some(ColorTargetState {
|
||||
format: if key.hdr_format() {
|
||||
ViewTarget::TEXTURE_FORMAT_HDR
|
||||
} else {
|
||||
TextureFormat::bevy_default()
|
||||
},
|
||||
blend: Some(if key.pass_type() == PassType::Transparent {
|
||||
BlendState::ALPHA_BLENDING
|
||||
} else {
|
||||
BlendState::REPLACE
|
||||
}),
|
||||
write_mask: ColorWrites::ALL,
|
||||
}));
|
||||
|
||||
bind_layouts.push(self.outline_volume_bind_group_layout.clone());
|
||||
}
|
||||
}
|
||||
if key.opengl_workaround() {
|
||||
let val = ShaderDefVal::from("OPENGL_WORKAROUND");
|
||||
vertex_defs.push(val.clone());
|
||||
fragment_defs.push(val);
|
||||
}
|
||||
let buffers = vec![layout.get_layout(&buffer_attrs)?];
|
||||
Ok(RenderPipelineDescriptor {
|
||||
vertex: VertexState {
|
||||
shader: OUTLINE_SHADER_HANDLE.typed::<Shader>(),
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: vertex_defs,
|
||||
buffers,
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: FRAGMENT_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader_defs: fragment_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets,
|
||||
}),
|
||||
layout: bind_layouts,
|
||||
primitive: PrimitiveState {
|
||||
front_face: FrontFace::Ccw,
|
||||
cull_mode,
|
||||
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: if key.depth_mode() == DepthMode::Flat && key.pass_type() == PassType::Stencil
|
||||
{
|
||||
DepthBiasState {
|
||||
// Values determined empirically
|
||||
constant: 3,
|
||||
slope_scale: 1.0,
|
||||
..default()
|
||||
}
|
||||
} else {
|
||||
default()
|
||||
},
|
||||
}),
|
||||
multisample: MultisampleState {
|
||||
count: key.msaa().samples(),
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
push_constant_ranges: default(),
|
||||
label: Some(Cow::Borrowed("outline_pipeline")),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use bevy::{
|
||||
ecs::system::{
|
||||
lifetimeless::{Read, SRes},
|
||||
SystemParamItem,
|
||||
},
|
||||
prelude::*,
|
||||
render::{
|
||||
extract_component::{ComponentUniforms, DynamicUniformIndex},
|
||||
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
|
||||
render_resource::{BindGroup, BindGroupDescriptor, BindGroupEntry, ShaderType},
|
||||
renderer::RenderDevice,
|
||||
Extract,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
node::StencilOutline, pipeline::OutlinePipeline, ComputedOutlineDepth, OutlineStencil,
|
||||
OutlineVolume,
|
||||
};
|
||||
|
||||
#[derive(Clone, Component, ShaderType)]
|
||||
pub(crate) struct OutlineStencilUniform {
|
||||
#[align(16)]
|
||||
pub origin: Vec3,
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Component, ShaderType)]
|
||||
pub(crate) struct OutlineVolumeUniform {
|
||||
#[align(16)]
|
||||
pub origin: Vec3,
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Component, ShaderType)]
|
||||
pub(crate) struct OutlineFragmentUniform {
|
||||
#[align(16)]
|
||||
pub colour: Vec4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
pub(crate) enum DepthMode {
|
||||
#[default]
|
||||
Invalid = 0,
|
||||
Flat = 1,
|
||||
Real = 2,
|
||||
}
|
||||
|
||||
impl DepthMode {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
*self != DepthMode::Invalid
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct OutlineStencilFlags {
|
||||
pub depth_mode: DepthMode,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct OutlineVolumeFlags {
|
||||
pub depth_mode: DepthMode,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct OutlineStencilBindGroup {
|
||||
pub bind_group: BindGroup,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct OutlineVolumeBindGroup {
|
||||
pub bind_group: BindGroup,
|
||||
}
|
||||
|
||||
pub(crate) fn extract_outline_stencil_uniforms(
|
||||
mut commands: Commands,
|
||||
query: Extract<Query<(Entity, &OutlineStencil, &ComputedOutlineDepth)>>,
|
||||
) {
|
||||
for (entity, stencil, computed) in query.iter() {
|
||||
if !stencil.enabled {
|
||||
continue;
|
||||
}
|
||||
commands
|
||||
.get_or_spawn(entity)
|
||||
.insert(OutlineStencilUniform {
|
||||
origin: computed.world_origin,
|
||||
offset: stencil.offset,
|
||||
})
|
||||
.insert(OutlineStencilFlags {
|
||||
depth_mode: computed.depth_mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn extract_outline_volume_uniforms(
|
||||
mut commands: Commands,
|
||||
query: Extract<Query<(Entity, &OutlineVolume, &ComputedOutlineDepth)>>,
|
||||
) {
|
||||
for (entity, outline, computed) in query.iter() {
|
||||
if !outline.visible || outline.colour.a() == 0.0 {
|
||||
continue;
|
||||
}
|
||||
commands
|
||||
.get_or_spawn(entity)
|
||||
.insert(OutlineVolumeUniform {
|
||||
origin: computed.world_origin,
|
||||
offset: outline.width,
|
||||
})
|
||||
.insert(OutlineFragmentUniform {
|
||||
colour: outline.colour.as_linear_rgba_f32().into(),
|
||||
})
|
||||
.insert(OutlineVolumeFlags {
|
||||
depth_mode: computed.depth_mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn queue_outline_stencil_bind_group(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
vertex: Res<ComponentUniforms<OutlineStencilUniform>>,
|
||||
) {
|
||||
if let Some(vertex_binding) = vertex.binding() {
|
||||
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: vertex_binding.clone(),
|
||||
}],
|
||||
label: Some("outline_stencil_bind_group"),
|
||||
layout: &outline_pipeline.outline_stencil_bind_group_layout,
|
||||
});
|
||||
commands.insert_resource(OutlineStencilBindGroup { bind_group });
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn queue_outline_volume_bind_group(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
vertex: Res<ComponentUniforms<OutlineVolumeUniform>>,
|
||||
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_volume_bind_group"),
|
||||
layout: &outline_pipeline.outline_volume_bind_group_layout,
|
||||
});
|
||||
commands.insert_resource(OutlineVolumeBindGroup { bind_group });
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SetOutlineStencilBindGroup<const I: usize>();
|
||||
|
||||
impl<const I: usize> RenderCommand<StencilOutline> for SetOutlineStencilBindGroup<I> {
|
||||
type ViewWorldQuery = ();
|
||||
type ItemWorldQuery = Read<DynamicUniformIndex<OutlineStencilUniform>>;
|
||||
type Param = SRes<OutlineStencilBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &StencilOutline,
|
||||
_view_data: (),
|
||||
entity_data: &DynamicUniformIndex<OutlineStencilUniform>,
|
||||
bind_group: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
pass.set_bind_group(
|
||||
I,
|
||||
&bind_group.into_inner().bind_group,
|
||||
&[entity_data.index()],
|
||||
);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SetOutlineVolumeBindGroup<const I: usize>();
|
||||
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetOutlineVolumeBindGroup<I> {
|
||||
type ViewWorldQuery = ();
|
||||
type ItemWorldQuery = (
|
||||
Read<DynamicUniformIndex<OutlineVolumeUniform>>,
|
||||
Read<DynamicUniformIndex<OutlineFragmentUniform>>,
|
||||
);
|
||||
type Param = SRes<OutlineVolumeBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
_view_data: (),
|
||||
entity_data: (
|
||||
&DynamicUniformIndex<OutlineVolumeUniform>,
|
||||
&DynamicUniformIndex<OutlineFragmentUniform>,
|
||||
),
|
||||
bind_group: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
let (vertex, fragment) = entity_data;
|
||||
pass.set_bind_group(
|
||||
I,
|
||||
&bind_group.into_inner().bind_group,
|
||||
&[vertex.index(), fragment.index()],
|
||||
);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use bevy::ecs::system::lifetimeless::{Read, SRes};
|
||||
use bevy::ecs::system::SystemParamItem;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::extract_component::{ComponentUniforms, DynamicUniformIndex};
|
||||
use bevy::render::render_phase::{
|
||||
PhaseItem, RenderCommand, 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::view::RenderLayers;
|
||||
use bevy::render::Extract;
|
||||
|
||||
use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline};
|
||||
use crate::pipeline::OutlinePipeline;
|
||||
|
||||
#[derive(Clone, Component, ShaderType)]
|
||||
pub(crate) struct OutlineViewUniform {
|
||||
#[align(16)]
|
||||
scale: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct OutlineViewBindGroup {
|
||||
bind_group: BindGroup,
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn extract_outline_view_uniforms(
|
||||
mut commands: Commands,
|
||||
query: Extract<Query<(Entity, &Camera, Option<&RenderLayers>), With<Camera3d>>>,
|
||||
) {
|
||||
for (entity, camera, view_mask) in query.iter() {
|
||||
if !camera.is_active {
|
||||
continue;
|
||||
}
|
||||
if let Some(size) = camera.logical_viewport_size() {
|
||||
let mut entity_commands = commands.get_or_spawn(entity);
|
||||
entity_commands
|
||||
.insert(OutlineViewUniform { scale: 2.0 / size })
|
||||
.insert(RenderPhase::<StencilOutline>::default())
|
||||
.insert(RenderPhase::<OpaqueOutline>::default())
|
||||
.insert(RenderPhase::<TransparentOutline>::default());
|
||||
|
||||
if let Some(view_mask) = view_mask {
|
||||
entity_commands.insert(*view_mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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(crate) struct SetOutlineViewBindGroup<const I: usize>();
|
||||
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetOutlineViewBindGroup<I> {
|
||||
type ViewWorldQuery = Read<DynamicUniformIndex<OutlineViewUniform>>;
|
||||
type ItemWorldQuery = ();
|
||||
type Param = SRes<OutlineViewBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
view_data: &DynamicUniformIndex<OutlineViewUniform>,
|
||||
_entity_data: (),
|
||||
bind_group: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
pass.set_bind_group(I, &bind_group.into_inner().bind_group, &[view_data.index()]);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user