
# Objective Upgrade to `wgpu` version `25.0`. Depends on https://github.com/bevyengine/naga_oil/pull/121 ## Solution ### Problem The biggest issue we face upgrading is the following requirement: > To facilitate this change, there was an additional validation rule put in place: if there is a binding array in a bind group, you may not use dynamic offset buffers or uniform buffers in that bind group. This requirement comes from vulkan rules on UpdateAfterBind descriptors. This is a major difficulty for us, as there are a number of binding arrays that are used in the view bind group. Note, this requirement does not affect merely uniform buffors that use dynamic offset but the use of *any* uniform in a bind group that also has a binding array. ### Attempted fixes The easiest fix would be to change uniforms to be storage buffers whenever binding arrays are in use: ```wgsl #ifdef BINDING_ARRAYS_ARE_USED @group(0) @binding(0) var<uniform> view: View; @group(0) @binding(1) var<uniform> lights: types::Lights; #else @group(0) @binding(0) var<storage> view: array<View>; @group(0) @binding(1) var<storage> lights: array<types::Lights>; #endif ``` This requires passing the view index to the shader so that we know where to index into the buffer: ```wgsl struct PushConstants { view_index: u32, } var<push_constant> push_constants: PushConstants; ``` Using push constants is no problem because binding arrays are only usable on native anyway. However, this greatly complicates the ability to access `view` in shaders. For example: ```wgsl #ifdef BINDING_ARRAYS_ARE_USED mesh_view_bindings::view.view_from_world[0].z #else mesh_view_bindings::view[mesh_view_bindings::view_index].view_from_world[0].z #endif ``` Using this approach would work but would have the effect of polluting our shaders with ifdef spam basically *everywhere*. Why not use a function? Unfortunately, the following is not valid wgsl as it returns a binding directly from a function in the uniform path. ```wgsl fn get_view() -> View { #if BINDING_ARRAYS_ARE_USED let view_index = push_constants.view_index; let view = views[view_index]; #endif return view; } ``` This also poses problems for things like lights where we want to return a ptr to the light data. Returning ptrs from wgsl functions isn't allowed even if both bindings were buffers. The next attempt was to simply use indexed buffers everywhere, in both the binding array and non binding array path. This would be viable if push constants were available everywhere to pass the view index, but unfortunately they are not available on webgpu. This means either passing the view index in a storage buffer (not ideal for such a small amount of state) or using push constants sometimes and uniform buffers only on webgpu. However, this kind of conditional layout infects absolutely everything. Even if we were to accept just using storage buffer for the view index, there's also the additional problem that some dynamic offsets aren't actually per-view but per-use of a setting on a camera, which would require passing that uniform data on *every* camera regardless of whether that rendering feature is being used, which is also gross. As such, although it's gross, the simplest solution just to bump binding arrays into `@group(1)` and all other bindings up one bind group. This should still bring us under the device limit of 4 for most users. ### Next steps / looking towards the future I'd like to avoid needing split our view bind group into multiple parts. In the future, if `wgpu` were to add `@builtin(draw_index)`, we could build a list of draw state in gpu processing and avoid the need for any kind of state change at all (see https://github.com/gfx-rs/wgpu/issues/6823). This would also provide significantly more flexibility to handle things like offsets into other arrays that may not be per-view. ### Testing Tested a number of examples, there are probably more that are still broken. --------- Co-authored-by: François Mockers <mockersf@gmail.com> Co-authored-by: Elabajaba <Elabajaba@users.noreply.github.com>
640 lines
24 KiB
Rust
640 lines
24 KiB
Rust
//! This example demonstrates how to write a custom phase
|
|
//!
|
|
//! Render phases in bevy are used whenever you need to draw a group of meshes in a specific way.
|
|
//! For example, bevy's main pass has an opaque phase, a transparent phase for both 2d and 3d.
|
|
//! Sometimes, you may want to only draw a subset of meshes before or after the builtin phase. In
|
|
//! those situations you need to write your own phase.
|
|
//!
|
|
//! This example showcases how writing a custom phase to draw a stencil of a bevy mesh could look
|
|
//! like. Some shortcuts have been used for simplicity.
|
|
//!
|
|
//! This example was made for 3d, but a 2d equivalent would be almost identical.
|
|
|
|
use std::ops::Range;
|
|
|
|
use bevy::pbr::SetMeshViewEmptyBindGroup;
|
|
use bevy::{
|
|
core_pipeline::core_3d::graph::{Core3d, Node3d},
|
|
ecs::{
|
|
query::QueryItem,
|
|
system::{lifetimeless::SRes, SystemParamItem},
|
|
},
|
|
math::FloatOrd,
|
|
pbr::{
|
|
DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,
|
|
MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,
|
|
},
|
|
platform::collections::HashSet,
|
|
prelude::*,
|
|
render::{
|
|
batching::{
|
|
gpu_preprocessing::{
|
|
batch_and_prepare_sorted_render_phase, IndirectParametersCpuMetadata,
|
|
UntypedPhaseIndirectParametersBuffers,
|
|
},
|
|
GetBatchData, GetFullBatchData,
|
|
},
|
|
camera::ExtractedCamera,
|
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
|
mesh::{allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh},
|
|
render_asset::RenderAssets,
|
|
render_graph::{
|
|
NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
|
|
},
|
|
render_phase::{
|
|
sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,
|
|
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,
|
|
SortedRenderPhasePlugin, ViewSortedRenderPhases,
|
|
},
|
|
render_resource::{
|
|
CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace,
|
|
MultisampleState, PipelineCache, PolygonMode, PrimitiveState, RenderPassDescriptor,
|
|
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
|
|
SpecializedMeshPipelines, TextureFormat, VertexState,
|
|
},
|
|
renderer::RenderContext,
|
|
sync_world::MainEntity,
|
|
view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},
|
|
Extract, Render, RenderApp, RenderDebugFlags, RenderSystems,
|
|
},
|
|
};
|
|
use nonmax::NonMaxU32;
|
|
|
|
const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wgsl";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((DefaultPlugins, MeshStencilPhasePlugin))
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// circular base
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Circle::new(4.0))),
|
|
MeshMaterial3d(materials.add(Color::WHITE)),
|
|
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
|
));
|
|
// cube
|
|
// This cube will be rendered by the main pass, but it will also be rendered by our custom
|
|
// pass. This should result in an unlit red cube
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
|
Transform::from_xyz(0.0, 0.5, 0.0),
|
|
// This marker component is used to identify which mesh will be used in our custom pass
|
|
// The circle doesn't have it so it won't be rendered in our pass
|
|
DrawStencil,
|
|
));
|
|
// light
|
|
commands.spawn((
|
|
PointLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
// camera
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
// disable msaa for simplicity
|
|
Msaa::Off,
|
|
));
|
|
}
|
|
|
|
#[derive(Component, ExtractComponent, Clone, Copy, Default)]
|
|
struct DrawStencil;
|
|
|
|
struct MeshStencilPhasePlugin;
|
|
impl Plugin for MeshStencilPhasePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_plugins((
|
|
ExtractComponentPlugin::<DrawStencil>::default(),
|
|
SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),
|
|
));
|
|
// We need to get the render app from the main app
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
render_app
|
|
.init_resource::<SpecializedMeshPipelines<StencilPipeline>>()
|
|
.init_resource::<DrawFunctions<Stencil3d>>()
|
|
.add_render_command::<Stencil3d, DrawMesh3dStencil>()
|
|
.init_resource::<ViewSortedRenderPhases<Stencil3d>>()
|
|
.add_systems(ExtractSchedule, extract_camera_phases)
|
|
.add_systems(
|
|
Render,
|
|
(
|
|
queue_custom_meshes.in_set(RenderSystems::QueueMeshes),
|
|
sort_phase_system::<Stencil3d>.in_set(RenderSystems::PhaseSort),
|
|
batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>
|
|
.in_set(RenderSystems::PrepareResources),
|
|
),
|
|
);
|
|
|
|
render_app
|
|
.add_render_graph_node::<ViewNodeRunner<CustomDrawNode>>(Core3d, CustomDrawPassLabel)
|
|
// Tell the node to run after the main pass
|
|
.add_render_graph_edges(Core3d, (Node3d::MainOpaquePass, CustomDrawPassLabel));
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
// We need to get the render app from the main app
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
// The pipeline needs the RenderDevice to be created and it's only available once plugins
|
|
// are initialized
|
|
render_app.init_resource::<StencilPipeline>();
|
|
}
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct StencilPipeline {
|
|
/// The base mesh pipeline defined by bevy
|
|
///
|
|
/// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default
|
|
/// pipeline as much as possible
|
|
mesh_pipeline: MeshPipeline,
|
|
/// Stores the shader used for this pipeline directly on the pipeline.
|
|
/// This isn't required, it's only done like this for simplicity.
|
|
shader_handle: Handle<Shader>,
|
|
}
|
|
|
|
impl FromWorld for StencilPipeline {
|
|
fn from_world(world: &mut World) -> Self {
|
|
Self {
|
|
mesh_pipeline: MeshPipeline::from_world(world),
|
|
shader_handle: world.resource::<AssetServer>().load(SHADER_ASSET_PATH),
|
|
}
|
|
}
|
|
}
|
|
|
|
// For more information on how SpecializedMeshPipeline work, please look at the
|
|
// specialized_mesh_pipeline example
|
|
impl SpecializedMeshPipeline for StencilPipeline {
|
|
type Key = MeshPipelineKey;
|
|
|
|
fn specialize(
|
|
&self,
|
|
key: Self::Key,
|
|
layout: &MeshVertexBufferLayoutRef,
|
|
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
|
|
// We will only use the position of the mesh in our shader so we only need to specify that
|
|
let mut vertex_attributes = Vec::new();
|
|
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
|
|
// Make sure this matches the shader location
|
|
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
|
|
}
|
|
// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
|
|
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
|
|
let view_layout = self
|
|
.mesh_pipeline
|
|
.get_view_layout(MeshPipelineViewLayoutKey::from(key));
|
|
Ok(RenderPipelineDescriptor {
|
|
label: Some("Specialized Mesh Pipeline".into()),
|
|
// We want to reuse the data from bevy so we use the same bind groups as the default
|
|
// mesh pipeline
|
|
layout: vec![
|
|
// Bind group 0 is the view uniform
|
|
view_layout.main_layout.clone(),
|
|
// Bind group 1 is empty
|
|
view_layout.empty_layout.clone(),
|
|
// Bind group 2 is the mesh uniform
|
|
self.mesh_pipeline.mesh_layouts.model_only.clone(),
|
|
],
|
|
push_constant_ranges: vec![],
|
|
vertex: VertexState {
|
|
shader: self.shader_handle.clone(),
|
|
shader_defs: vec![],
|
|
entry_point: "vertex".into(),
|
|
buffers: vec![vertex_buffer_layout],
|
|
},
|
|
fragment: Some(FragmentState {
|
|
shader: self.shader_handle.clone(),
|
|
shader_defs: vec![],
|
|
entry_point: "fragment".into(),
|
|
targets: vec![Some(ColorTargetState {
|
|
format: TextureFormat::bevy_default(),
|
|
blend: None,
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
primitive: PrimitiveState {
|
|
topology: key.primitive_topology(),
|
|
front_face: FrontFace::Ccw,
|
|
cull_mode: Some(Face::Back),
|
|
polygon_mode: PolygonMode::Fill,
|
|
..default()
|
|
},
|
|
depth_stencil: None,
|
|
// It's generally recommended to specialize your pipeline for MSAA,
|
|
// but it's not always possible
|
|
multisample: MultisampleState::default(),
|
|
zero_initialize_workgroup_memory: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// We will reuse render commands already defined by bevy to draw a 3d mesh
|
|
type DrawMesh3dStencil = (
|
|
SetItemPipeline,
|
|
// This will set the view bindings in group 0
|
|
SetMeshViewBindGroup<0>,
|
|
// This will set an empty bind group in group 1
|
|
SetMeshViewEmptyBindGroup<1>,
|
|
// This will set the mesh bindings in group 2
|
|
SetMeshBindGroup<2>,
|
|
// This will draw the mesh
|
|
DrawMesh,
|
|
);
|
|
|
|
// This is the data required per entity drawn in a custom phase in bevy. More specifically this is the
|
|
// data required when using a ViewSortedRenderPhase. This would look differently if we wanted a
|
|
// batched render phase. Sorted phases are a bit easier to implement, but a batched phase would
|
|
// look similar.
|
|
//
|
|
// If you want to see how a batched phase implementation looks, you should look at the Opaque2d
|
|
// phase.
|
|
struct Stencil3d {
|
|
pub sort_key: FloatOrd,
|
|
pub entity: (Entity, MainEntity),
|
|
pub pipeline: CachedRenderPipelineId,
|
|
pub draw_function: DrawFunctionId,
|
|
pub batch_range: Range<u32>,
|
|
pub extra_index: PhaseItemExtraIndex,
|
|
/// Whether the mesh in question is indexed (uses an index buffer in
|
|
/// addition to its vertex buffer).
|
|
pub indexed: bool,
|
|
}
|
|
|
|
// For more information about writing a phase item, please look at the custom_phase_item example
|
|
impl PhaseItem for Stencil3d {
|
|
#[inline]
|
|
fn entity(&self) -> Entity {
|
|
self.entity.0
|
|
}
|
|
|
|
#[inline]
|
|
fn main_entity(&self) -> MainEntity {
|
|
self.entity.1
|
|
}
|
|
|
|
#[inline]
|
|
fn draw_function(&self) -> DrawFunctionId {
|
|
self.draw_function
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range(&self) -> &Range<u32> {
|
|
&self.batch_range
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range_mut(&mut self) -> &mut Range<u32> {
|
|
&mut self.batch_range
|
|
}
|
|
|
|
#[inline]
|
|
fn extra_index(&self) -> PhaseItemExtraIndex {
|
|
self.extra_index.clone()
|
|
}
|
|
|
|
#[inline]
|
|
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
|
|
(&mut self.batch_range, &mut self.extra_index)
|
|
}
|
|
}
|
|
|
|
impl SortedPhaseItem for Stencil3d {
|
|
type SortKey = FloatOrd;
|
|
|
|
#[inline]
|
|
fn sort_key(&self) -> Self::SortKey {
|
|
self.sort_key
|
|
}
|
|
|
|
#[inline]
|
|
fn sort(items: &mut [Self]) {
|
|
// bevy normally uses radsort instead of the std slice::sort_by_key
|
|
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
|
|
// Since it is not re-exported by bevy, we just use the std sort for the purpose of the example
|
|
items.sort_by_key(SortedPhaseItem::sort_key);
|
|
}
|
|
|
|
#[inline]
|
|
fn indexed(&self) -> bool {
|
|
self.indexed
|
|
}
|
|
}
|
|
|
|
impl CachedRenderPipelinePhaseItem for Stencil3d {
|
|
#[inline]
|
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
|
self.pipeline
|
|
}
|
|
}
|
|
|
|
impl GetBatchData for StencilPipeline {
|
|
type Param = (
|
|
SRes<RenderMeshInstances>,
|
|
SRes<RenderAssets<RenderMesh>>,
|
|
SRes<MeshAllocator>,
|
|
);
|
|
type CompareData = AssetId<Mesh>;
|
|
type BufferData = MeshUniform;
|
|
|
|
fn get_batch_data(
|
|
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
|
|
(_entity, main_entity): (Entity, MainEntity),
|
|
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
|
|
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
|
|
error!(
|
|
"`get_batch_data` should never be called in GPU mesh uniform \
|
|
building mode"
|
|
);
|
|
return None;
|
|
};
|
|
let mesh_instance = mesh_instances.get(&main_entity)?;
|
|
let first_vertex_index =
|
|
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
|
|
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
|
|
None => 0,
|
|
};
|
|
let mesh_uniform = {
|
|
let mesh_transforms = &mesh_instance.transforms;
|
|
let (local_from_world_transpose_a, local_from_world_transpose_b) =
|
|
mesh_transforms.world_from_local.inverse_transpose_3x3();
|
|
MeshUniform {
|
|
world_from_local: mesh_transforms.world_from_local.to_transpose(),
|
|
previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),
|
|
lightmap_uv_rect: UVec2::ZERO,
|
|
local_from_world_transpose_a,
|
|
local_from_world_transpose_b,
|
|
flags: mesh_transforms.flags,
|
|
first_vertex_index,
|
|
current_skin_index: u32::MAX,
|
|
material_and_lightmap_bind_group_slot: 0,
|
|
tag: 0,
|
|
pad: 0,
|
|
}
|
|
};
|
|
Some((mesh_uniform, None))
|
|
}
|
|
}
|
|
|
|
impl GetFullBatchData for StencilPipeline {
|
|
type BufferInputData = MeshInputUniform;
|
|
|
|
fn get_index_and_compare_data(
|
|
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
|
|
main_entity: MainEntity,
|
|
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
|
|
// This should only be called during GPU building.
|
|
let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {
|
|
error!(
|
|
"`get_index_and_compare_data` should never be called in CPU mesh uniform building \
|
|
mode"
|
|
);
|
|
return None;
|
|
};
|
|
let mesh_instance = mesh_instances.get(&main_entity)?;
|
|
Some((
|
|
mesh_instance.current_uniform_index,
|
|
mesh_instance
|
|
.should_batch()
|
|
.then_some(mesh_instance.mesh_asset_id),
|
|
))
|
|
}
|
|
|
|
fn get_binned_batch_data(
|
|
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
|
|
main_entity: MainEntity,
|
|
) -> Option<Self::BufferData> {
|
|
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
|
|
error!(
|
|
"`get_binned_batch_data` should never be called in GPU mesh uniform building mode"
|
|
);
|
|
return None;
|
|
};
|
|
let mesh_instance = mesh_instances.get(&main_entity)?;
|
|
let first_vertex_index =
|
|
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
|
|
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
|
|
None => 0,
|
|
};
|
|
|
|
Some(MeshUniform::new(
|
|
&mesh_instance.transforms,
|
|
first_vertex_index,
|
|
mesh_instance.material_bindings_index.slot,
|
|
None,
|
|
None,
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn write_batch_indirect_parameters_metadata(
|
|
indexed: bool,
|
|
base_output_index: u32,
|
|
batch_set_index: Option<NonMaxU32>,
|
|
indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,
|
|
indirect_parameters_offset: u32,
|
|
) {
|
|
// Note that `IndirectParameters` covers both of these structures, even
|
|
// though they actually have distinct layouts. See the comment above that
|
|
// type for more information.
|
|
let indirect_parameters = IndirectParametersCpuMetadata {
|
|
base_output_index,
|
|
batch_set_index: match batch_set_index {
|
|
None => !0,
|
|
Some(batch_set_index) => u32::from(batch_set_index),
|
|
},
|
|
};
|
|
|
|
if indexed {
|
|
indirect_parameters_buffers
|
|
.indexed
|
|
.set(indirect_parameters_offset, indirect_parameters);
|
|
} else {
|
|
indirect_parameters_buffers
|
|
.non_indexed
|
|
.set(indirect_parameters_offset, indirect_parameters);
|
|
}
|
|
}
|
|
|
|
fn get_binned_index(
|
|
_param: &SystemParamItem<Self::Param>,
|
|
_query_item: MainEntity,
|
|
) -> Option<NonMaxU32> {
|
|
None
|
|
}
|
|
}
|
|
|
|
// When defining a phase, we need to extract it from the main world and add it to a resource
|
|
// that will be used by the render world. We need to give that resource all views that will use
|
|
// that phase
|
|
fn extract_camera_phases(
|
|
mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
|
|
cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
|
|
mut live_entities: Local<HashSet<RetainedViewEntity>>,
|
|
) {
|
|
live_entities.clear();
|
|
for (main_entity, camera) in &cameras {
|
|
if !camera.is_active {
|
|
continue;
|
|
}
|
|
// This is the main camera, so we use the first subview index (0)
|
|
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
|
|
|
|
stencil_phases.insert_or_clear(retained_view_entity);
|
|
live_entities.insert(retained_view_entity);
|
|
}
|
|
|
|
// Clear out all dead views.
|
|
stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
|
|
}
|
|
|
|
// This is a very important step when writing a custom phase.
|
|
//
|
|
// This system determines which meshes will be added to the phase.
|
|
fn queue_custom_meshes(
|
|
custom_draw_functions: Res<DrawFunctions<Stencil3d>>,
|
|
mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
custom_draw_pipeline: Res<StencilPipeline>,
|
|
render_meshes: Res<RenderAssets<RenderMesh>>,
|
|
render_mesh_instances: Res<RenderMeshInstances>,
|
|
mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
|
|
mut views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
|
|
has_marker: Query<(), With<DrawStencil>>,
|
|
) {
|
|
for (view, visible_entities, msaa) in &mut views {
|
|
let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {
|
|
continue;
|
|
};
|
|
let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();
|
|
|
|
// Create the key based on the view.
|
|
// In this case we only care about MSAA and HDR
|
|
let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
|
|
| MeshPipelineKey::from_hdr(view.hdr);
|
|
|
|
let rangefinder = view.rangefinder3d();
|
|
// Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter
|
|
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
|
|
// We only want meshes with the marker component to be queued to our phase.
|
|
if has_marker.get(*render_entity).is_err() {
|
|
continue;
|
|
}
|
|
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
|
|
continue;
|
|
};
|
|
|
|
// Specialize the key for the current mesh entity
|
|
// For this example we only specialize based on the mesh topology
|
|
// but you could have more complex keys and that's where you'd need to create those keys
|
|
let mut mesh_key = view_key;
|
|
mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
|
|
|
|
let pipeline_id = pipelines.specialize(
|
|
&pipeline_cache,
|
|
&custom_draw_pipeline,
|
|
mesh_key,
|
|
&mesh.layout,
|
|
);
|
|
let pipeline_id = match pipeline_id {
|
|
Ok(id) => id,
|
|
Err(err) => {
|
|
error!("{}", err);
|
|
continue;
|
|
}
|
|
};
|
|
let distance = rangefinder.distance_translation(&mesh_instance.translation);
|
|
// At this point we have all the data we need to create a phase item and add it to our
|
|
// phase
|
|
custom_phase.add(Stencil3d {
|
|
// Sort the data based on the distance to the view
|
|
sort_key: FloatOrd(distance),
|
|
entity: (*render_entity, *visible_entity),
|
|
pipeline: pipeline_id,
|
|
draw_function: draw_custom,
|
|
// Sorted phase items aren't batched
|
|
batch_range: 0..1,
|
|
extra_index: PhaseItemExtraIndex::None,
|
|
indexed: mesh.indexed(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render label used to order our render graph node that will render our phase
|
|
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
|
|
struct CustomDrawPassLabel;
|
|
|
|
#[derive(Default)]
|
|
struct CustomDrawNode;
|
|
impl ViewNode for CustomDrawNode {
|
|
type ViewQuery = (
|
|
&'static ExtractedCamera,
|
|
&'static ExtractedView,
|
|
&'static ViewTarget,
|
|
);
|
|
|
|
fn run<'w>(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext<'w>,
|
|
(camera, view, target): QueryItem<'w, '_, Self::ViewQuery>,
|
|
world: &'w World,
|
|
) -> Result<(), NodeRunError> {
|
|
// First, we need to get our phases resource
|
|
let Some(stencil_phases) = world.get_resource::<ViewSortedRenderPhases<Stencil3d>>() else {
|
|
return Ok(());
|
|
};
|
|
|
|
// Get the view entity from the graph
|
|
let view_entity = graph.view_entity();
|
|
|
|
// Get the phase for the current view running our node
|
|
let Some(stencil_phase) = stencil_phases.get(&view.retained_view_entity) else {
|
|
return Ok(());
|
|
};
|
|
|
|
// Render pass setup
|
|
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
|
|
label: Some("stencil pass"),
|
|
// For the purpose of the example, we will write directly to the view target. A real
|
|
// stencil pass would write to a custom texture and that texture would be used in later
|
|
// passes to render custom effects using it.
|
|
color_attachments: &[Some(target.get_color_attachment())],
|
|
// We don't bind any depth buffer for this pass
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
});
|
|
|
|
if let Some(viewport) = camera.viewport.as_ref() {
|
|
render_pass.set_camera_viewport(viewport);
|
|
}
|
|
|
|
// Render the phase
|
|
// This will execute each draw functions of each phase items queued in this phase
|
|
if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {
|
|
error!("Error encountered while rendering the stencil phase {err:?}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|