
# Objective - Fixes #10909 - Fixes #8492 ## Solution - Name all matrices `x_from_y`, for example `world_from_view`. ## Testing - I've tested most of the 3D examples. The `lighting` example particularly should hit a lot of the changes and appears to run fine. --- ## Changelog - Renamed matrices across the engine to follow a `y_from_x` naming, making the space conversion more obvious. ## Migration Guide - `Frustum`'s `from_view_projection`, `from_view_projection_custom_far` and `from_view_projection_no_far` were renamed to `from_clip_from_world`, `from_clip_from_world_custom_far` and `from_clip_from_world_no_far`. - `ComputedCameraValues::projection_matrix` was renamed to `clip_from_view`. - `CameraProjection::get_projection_matrix` was renamed to `get_clip_from_view` (this affects implementations on `Projection`, `PerspectiveProjection` and `OrthographicProjection`). - `ViewRangefinder3d::from_view_matrix` was renamed to `from_world_from_view`. - `PreviousViewData`'s members were renamed to `view_from_world` and `clip_from_world`. - `ExtractedView`'s `projection`, `transform` and `view_projection` were renamed to `clip_from_view`, `world_from_view` and `clip_from_world`. - `ViewUniform`'s `view_proj`, `unjittered_view_proj`, `inverse_view_proj`, `view`, `inverse_view`, `projection` and `inverse_projection` were renamed to `clip_from_world`, `unjittered_clip_from_world`, `world_from_clip`, `world_from_view`, `view_from_world`, `clip_from_view` and `view_from_clip`. - `GpuDirectionalCascade::view_projection` was renamed to `clip_from_world`. - `MeshTransforms`' `transform` and `previous_transform` were renamed to `world_from_local` and `previous_world_from_local`. - `MeshUniform`'s `transform`, `previous_transform`, `inverse_transpose_model_a` and `inverse_transpose_model_b` were renamed to `world_from_local`, `previous_world_from_local`, `local_from_world_transpose_a` and `local_from_world_transpose_b` (the `Mesh` type in WGSL mirrors this, however `transform` and `previous_transform` were named `model` and `previous_model`). - `Mesh2dTransforms::transform` was renamed to `world_from_local`. - `Mesh2dUniform`'s `transform`, `inverse_transpose_model_a` and `inverse_transpose_model_b` were renamed to `world_from_local`, `local_from_world_transpose_a` and `local_from_world_transpose_b` (the `Mesh2d` type in WGSL mirrors this). - In WGSL, in `bevy_pbr::mesh_functions`, `get_model_matrix` and `get_previous_model_matrix` were renamed to `get_world_from_local` and `get_previous_world_from_local`. - In WGSL, `bevy_sprite::mesh2d_functions::get_model_matrix` was renamed to `get_world_from_local`.
1051 lines
42 KiB
Rust
1051 lines
42 KiB
Rust
use super::{
|
|
asset::{Meshlet, MeshletBoundingSpheres, MeshletMesh},
|
|
persistent_buffer::PersistentGpuBuffer,
|
|
};
|
|
use crate::{
|
|
Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver,
|
|
PreviousGlobalTransform, RenderMaterialInstances, ShadowView,
|
|
};
|
|
use bevy_asset::{AssetEvent, AssetId, AssetServer, Assets, Handle, UntypedAssetId};
|
|
use bevy_core_pipeline::{
|
|
core_3d::Camera3d,
|
|
prepass::{PreviousViewData, PreviousViewUniforms},
|
|
};
|
|
use bevy_ecs::{
|
|
component::Component,
|
|
entity::{Entity, EntityHashMap},
|
|
event::EventReader,
|
|
query::{AnyOf, Has},
|
|
system::{Commands, Local, Query, Res, ResMut, Resource, SystemState},
|
|
world::{FromWorld, World},
|
|
};
|
|
use bevy_math::{UVec2, Vec4Swizzles};
|
|
use bevy_render::{
|
|
render_resource::{binding_types::*, *},
|
|
renderer::{RenderDevice, RenderQueue},
|
|
texture::{CachedTexture, TextureCache},
|
|
view::{ExtractedView, RenderLayers, ViewDepthTexture, ViewUniform, ViewUniforms},
|
|
MainWorld,
|
|
};
|
|
use bevy_transform::components::GlobalTransform;
|
|
use bevy_utils::{default, HashMap, HashSet};
|
|
use encase::internal::WriteInto;
|
|
use std::{
|
|
array, iter,
|
|
mem::size_of,
|
|
ops::{DerefMut, Range},
|
|
sync::{atomic::AtomicBool, Arc},
|
|
};
|
|
|
|
/// Create and queue for uploading to the GPU [`MeshUniform`] components for
|
|
/// [`MeshletMesh`] entities, as well as queuing uploads for any new meshlet mesh
|
|
/// assets that have not already been uploaded to the GPU.
|
|
pub fn extract_meshlet_meshes(
|
|
mut gpu_scene: ResMut<MeshletGpuScene>,
|
|
// TODO: Replace main_world and system_state when Extract<ResMut<Assets<MeshletMesh>>> is possible
|
|
mut main_world: ResMut<MainWorld>,
|
|
mut system_state: Local<
|
|
Option<
|
|
SystemState<(
|
|
Query<(
|
|
Entity,
|
|
&Handle<MeshletMesh>,
|
|
&GlobalTransform,
|
|
Option<&PreviousGlobalTransform>,
|
|
Option<&RenderLayers>,
|
|
Has<NotShadowReceiver>,
|
|
Has<NotShadowCaster>,
|
|
)>,
|
|
Res<AssetServer>,
|
|
ResMut<Assets<MeshletMesh>>,
|
|
EventReader<AssetEvent<MeshletMesh>>,
|
|
)>,
|
|
>,
|
|
>,
|
|
) {
|
|
if system_state.is_none() {
|
|
*system_state = Some(SystemState::new(&mut main_world));
|
|
}
|
|
let system_state = system_state.as_mut().unwrap();
|
|
|
|
let (instances_query, asset_server, mut assets, mut asset_events) =
|
|
system_state.get_mut(&mut main_world);
|
|
|
|
// Reset all temporary data for MeshletGpuScene
|
|
gpu_scene.reset();
|
|
|
|
// Free GPU buffer space for any modified or dropped MeshletMesh assets
|
|
for asset_event in asset_events.read() {
|
|
if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event {
|
|
if let Some((
|
|
[vertex_data_slice, vertex_ids_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice],
|
|
_,
|
|
)) = gpu_scene.meshlet_mesh_slices.remove(id)
|
|
{
|
|
gpu_scene.vertex_data.mark_slice_unused(vertex_data_slice);
|
|
gpu_scene.vertex_ids.mark_slice_unused(vertex_ids_slice);
|
|
gpu_scene.indices.mark_slice_unused(indices_slice);
|
|
gpu_scene.meshlets.mark_slice_unused(meshlets_slice);
|
|
gpu_scene
|
|
.meshlet_bounding_spheres
|
|
.mark_slice_unused(meshlet_bounding_spheres_slice);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (
|
|
instance,
|
|
handle,
|
|
transform,
|
|
previous_transform,
|
|
render_layers,
|
|
not_shadow_receiver,
|
|
not_shadow_caster,
|
|
) in &instances_query
|
|
{
|
|
// Skip instances with an unloaded MeshletMesh asset
|
|
if asset_server.is_managed(handle.id())
|
|
&& !asset_server.is_loaded_with_dependencies(handle.id())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Upload the instance's MeshletMesh asset data, if not done already, along with other per-frame per-instance data.
|
|
gpu_scene.queue_meshlet_mesh_upload(
|
|
instance,
|
|
render_layers.cloned().unwrap_or(default()),
|
|
not_shadow_caster,
|
|
handle,
|
|
&mut assets,
|
|
);
|
|
|
|
// Build a MeshUniform for each instance
|
|
let transform = transform.affine();
|
|
let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform);
|
|
let mut flags = if not_shadow_receiver {
|
|
MeshFlags::empty()
|
|
} else {
|
|
MeshFlags::SHADOW_RECEIVER
|
|
};
|
|
if transform.matrix3.determinant().is_sign_positive() {
|
|
flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3;
|
|
}
|
|
let transforms = MeshTransforms {
|
|
world_from_local: (&transform).into(),
|
|
previous_world_from_local: (&previous_transform).into(),
|
|
flags: flags.bits(),
|
|
};
|
|
gpu_scene
|
|
.instance_uniforms
|
|
.get_mut()
|
|
.push(MeshUniform::new(&transforms, None));
|
|
}
|
|
}
|
|
|
|
/// Upload all newly queued [`MeshletMesh`] asset data from [`extract_meshlet_meshes`] to the GPU.
|
|
pub fn perform_pending_meshlet_mesh_writes(
|
|
mut gpu_scene: ResMut<MeshletGpuScene>,
|
|
render_queue: Res<RenderQueue>,
|
|
render_device: Res<RenderDevice>,
|
|
) {
|
|
gpu_scene
|
|
.vertex_data
|
|
.perform_writes(&render_queue, &render_device);
|
|
gpu_scene
|
|
.vertex_ids
|
|
.perform_writes(&render_queue, &render_device);
|
|
gpu_scene
|
|
.indices
|
|
.perform_writes(&render_queue, &render_device);
|
|
gpu_scene
|
|
.meshlets
|
|
.perform_writes(&render_queue, &render_device);
|
|
gpu_scene
|
|
.meshlet_bounding_spheres
|
|
.perform_writes(&render_queue, &render_device);
|
|
}
|
|
|
|
/// For each entity in the scene, record what material ID (for use with depth testing during the meshlet mesh material draw nodes)
|
|
/// its material was assigned in the `prepare_material_meshlet_meshes` systems, and note that the material is used by at least one entity in the scene.
|
|
pub fn queue_material_meshlet_meshes<M: Material>(
|
|
mut gpu_scene: ResMut<MeshletGpuScene>,
|
|
render_material_instances: Res<RenderMaterialInstances<M>>,
|
|
) {
|
|
// TODO: Ideally we could parallelize this system, both between different materials, and the loop over instances
|
|
let gpu_scene = gpu_scene.deref_mut();
|
|
|
|
for (i, (instance, _, _)) in gpu_scene.instances.iter().enumerate() {
|
|
if let Some(material_asset_id) = render_material_instances.get(instance) {
|
|
let material_asset_id = material_asset_id.untyped();
|
|
if let Some(material_id) = gpu_scene.material_id_lookup.get(&material_asset_id) {
|
|
gpu_scene.material_ids_present_in_scene.insert(*material_id);
|
|
gpu_scene.instance_material_ids.get_mut()[i] = *material_id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Try using Queue::write_buffer_with() in queue_meshlet_mesh_upload() to reduce copies
|
|
fn upload_storage_buffer<T: ShaderSize + bytemuck::NoUninit>(
|
|
buffer: &mut StorageBuffer<Vec<T>>,
|
|
render_device: &RenderDevice,
|
|
render_queue: &RenderQueue,
|
|
) where
|
|
Vec<T>: WriteInto,
|
|
{
|
|
let inner = buffer.buffer();
|
|
let capacity = inner.map_or(0, |b| b.size());
|
|
let size = buffer.get().size().get() as BufferAddress;
|
|
|
|
if capacity >= size {
|
|
let inner = inner.unwrap();
|
|
let bytes = bytemuck::must_cast_slice(buffer.get().as_slice());
|
|
render_queue.write_buffer(inner, 0, bytes);
|
|
} else {
|
|
buffer.write_buffer(render_device, render_queue);
|
|
}
|
|
}
|
|
|
|
pub fn prepare_meshlet_per_frame_resources(
|
|
mut gpu_scene: ResMut<MeshletGpuScene>,
|
|
views: Query<(
|
|
Entity,
|
|
&ExtractedView,
|
|
Option<&RenderLayers>,
|
|
AnyOf<(&Camera3d, &ShadowView)>,
|
|
)>,
|
|
mut texture_cache: ResMut<TextureCache>,
|
|
render_queue: Res<RenderQueue>,
|
|
render_device: Res<RenderDevice>,
|
|
mut commands: Commands,
|
|
) {
|
|
if gpu_scene.scene_meshlet_count == 0 {
|
|
return;
|
|
}
|
|
|
|
let gpu_scene = gpu_scene.as_mut();
|
|
|
|
gpu_scene
|
|
.instance_uniforms
|
|
.write_buffer(&render_device, &render_queue);
|
|
upload_storage_buffer(
|
|
&mut gpu_scene.instance_material_ids,
|
|
&render_device,
|
|
&render_queue,
|
|
);
|
|
upload_storage_buffer(
|
|
&mut gpu_scene.instance_meshlet_counts_prefix_sum,
|
|
&render_device,
|
|
&render_queue,
|
|
);
|
|
upload_storage_buffer(
|
|
&mut gpu_scene.instance_meshlet_slice_starts,
|
|
&render_device,
|
|
&render_queue,
|
|
);
|
|
|
|
// Early submission for GPU data uploads to start while the render graph records commands
|
|
render_queue.submit([]);
|
|
|
|
let needed_buffer_size = 4 * gpu_scene.scene_meshlet_count as u64;
|
|
match &mut gpu_scene.cluster_instance_ids {
|
|
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
|
|
slot => {
|
|
let buffer = render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("meshlet_cluster_instance_ids"),
|
|
size: needed_buffer_size,
|
|
usage: BufferUsages::STORAGE,
|
|
mapped_at_creation: false,
|
|
});
|
|
*slot = Some(buffer.clone());
|
|
buffer
|
|
}
|
|
};
|
|
match &mut gpu_scene.cluster_meshlet_ids {
|
|
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
|
|
slot => {
|
|
let buffer = render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("meshlet_cluster_meshlet_ids"),
|
|
size: needed_buffer_size,
|
|
usage: BufferUsages::STORAGE,
|
|
mapped_at_creation: false,
|
|
});
|
|
*slot = Some(buffer.clone());
|
|
buffer
|
|
}
|
|
};
|
|
|
|
let needed_buffer_size = 4 * gpu_scene.scene_triangle_count;
|
|
let visibility_buffer_draw_triangle_buffer =
|
|
match &mut gpu_scene.visibility_buffer_draw_triangle_buffer {
|
|
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
|
|
slot => {
|
|
let buffer = render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("meshlet_visibility_buffer_draw_triangle_buffer"),
|
|
size: needed_buffer_size,
|
|
usage: BufferUsages::STORAGE,
|
|
mapped_at_creation: false,
|
|
});
|
|
*slot = Some(buffer.clone());
|
|
buffer
|
|
}
|
|
};
|
|
|
|
let needed_buffer_size =
|
|
gpu_scene.scene_meshlet_count.div_ceil(u32::BITS) as u64 * size_of::<u32>() as u64;
|
|
for (view_entity, view, render_layers, (_, shadow_view)) in &views {
|
|
let instance_visibility = gpu_scene
|
|
.view_instance_visibility
|
|
.entry(view_entity)
|
|
.or_insert_with(|| {
|
|
let mut buffer = StorageBuffer::default();
|
|
buffer.set_label(Some("meshlet_view_instance_visibility"));
|
|
buffer
|
|
});
|
|
for (instance_index, (_, layers, not_shadow_caster)) in
|
|
gpu_scene.instances.iter().enumerate()
|
|
{
|
|
// If either the layers don't match the view's layers or this is a shadow view
|
|
// and the instance is not a shadow caster, hide the instance for this view
|
|
if !render_layers.unwrap_or(&default()).intersects(layers)
|
|
|| (shadow_view.is_some() && *not_shadow_caster)
|
|
{
|
|
let vec = instance_visibility.get_mut();
|
|
let index = instance_index / 32;
|
|
let bit = instance_index - index * 32;
|
|
if vec.len() <= index {
|
|
vec.extend(iter::repeat(0).take(index - vec.len() + 1));
|
|
}
|
|
vec[index] |= 1 << bit;
|
|
}
|
|
}
|
|
upload_storage_buffer(instance_visibility, &render_device, &render_queue);
|
|
let instance_visibility = instance_visibility.buffer().unwrap().clone();
|
|
|
|
let second_pass_candidates_buffer = match &mut gpu_scene.second_pass_candidates_buffer {
|
|
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
|
|
slot => {
|
|
let buffer = render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("meshlet_second_pass_candidates"),
|
|
size: needed_buffer_size,
|
|
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
*slot = Some(buffer.clone());
|
|
buffer
|
|
}
|
|
};
|
|
|
|
let visibility_buffer = TextureDescriptor {
|
|
label: Some("meshlet_visibility_buffer"),
|
|
size: Extent3d {
|
|
width: view.viewport.z,
|
|
height: view.viewport.w,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::R32Uint,
|
|
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
};
|
|
|
|
let visibility_buffer_draw_indirect_args_first =
|
|
render_device.create_buffer_with_data(&BufferInitDescriptor {
|
|
label: Some("meshlet_visibility_buffer_draw_indirect_args_first"),
|
|
contents: DrawIndirectArgs {
|
|
vertex_count: 0,
|
|
instance_count: 1,
|
|
first_vertex: 0,
|
|
first_instance: 0,
|
|
}
|
|
.as_bytes(),
|
|
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
|
|
});
|
|
let visibility_buffer_draw_indirect_args_second =
|
|
render_device.create_buffer_with_data(&BufferInitDescriptor {
|
|
label: Some("meshlet_visibility_buffer_draw_indirect_args_second"),
|
|
contents: DrawIndirectArgs {
|
|
vertex_count: 0,
|
|
instance_count: 1,
|
|
first_vertex: 0,
|
|
first_instance: 0,
|
|
}
|
|
.as_bytes(),
|
|
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
|
|
});
|
|
|
|
let depth_pyramid_size = Extent3d {
|
|
width: view.viewport.z.div_ceil(2),
|
|
height: view.viewport.w.div_ceil(2),
|
|
depth_or_array_layers: 1,
|
|
};
|
|
let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2);
|
|
let depth_pyramid = texture_cache.get(
|
|
&render_device,
|
|
TextureDescriptor {
|
|
label: Some("meshlet_depth_pyramid"),
|
|
size: depth_pyramid_size,
|
|
mip_level_count: depth_pyramid_mip_count,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::R32Float,
|
|
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
},
|
|
);
|
|
let depth_pyramid_mips = array::from_fn(|i| {
|
|
if (i as u32) < depth_pyramid_mip_count {
|
|
depth_pyramid.texture.create_view(&TextureViewDescriptor {
|
|
label: Some("meshlet_depth_pyramid_texture_view"),
|
|
format: Some(TextureFormat::R32Float),
|
|
dimension: Some(TextureViewDimension::D2),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: i as u32,
|
|
mip_level_count: Some(1),
|
|
base_array_layer: 0,
|
|
array_layer_count: Some(1),
|
|
})
|
|
} else {
|
|
gpu_scene.depth_pyramid_dummy_texture.clone()
|
|
}
|
|
});
|
|
let depth_pyramid_all_mips = depth_pyramid.default_view.clone();
|
|
|
|
let previous_depth_pyramid = match gpu_scene.previous_depth_pyramids.get(&view_entity) {
|
|
Some(texture_view) => texture_view.clone(),
|
|
None => depth_pyramid_all_mips.clone(),
|
|
};
|
|
gpu_scene
|
|
.previous_depth_pyramids
|
|
.insert(view_entity, depth_pyramid_all_mips.clone());
|
|
|
|
let material_depth_color = TextureDescriptor {
|
|
label: Some("meshlet_material_depth_color"),
|
|
size: Extent3d {
|
|
width: view.viewport.z,
|
|
height: view.viewport.w,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::R16Uint,
|
|
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
};
|
|
|
|
let material_depth = TextureDescriptor {
|
|
label: Some("meshlet_material_depth"),
|
|
size: Extent3d {
|
|
width: view.viewport.z,
|
|
height: view.viewport.w,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::Depth16Unorm,
|
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
view_formats: &[],
|
|
};
|
|
|
|
let not_shadow_view = shadow_view.is_none();
|
|
commands.entity(view_entity).insert(MeshletViewResources {
|
|
scene_meshlet_count: gpu_scene.scene_meshlet_count,
|
|
second_pass_candidates_buffer,
|
|
instance_visibility,
|
|
visibility_buffer: not_shadow_view
|
|
.then(|| texture_cache.get(&render_device, visibility_buffer)),
|
|
visibility_buffer_draw_indirect_args_first,
|
|
visibility_buffer_draw_indirect_args_second,
|
|
visibility_buffer_draw_triangle_buffer: visibility_buffer_draw_triangle_buffer.clone(),
|
|
depth_pyramid_all_mips,
|
|
depth_pyramid_mips,
|
|
depth_pyramid_mip_count,
|
|
previous_depth_pyramid,
|
|
material_depth_color: not_shadow_view
|
|
.then(|| texture_cache.get(&render_device, material_depth_color)),
|
|
material_depth: not_shadow_view
|
|
.then(|| texture_cache.get(&render_device, material_depth)),
|
|
view_size: view.viewport.zw(),
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn prepare_meshlet_view_bind_groups(
|
|
gpu_scene: Res<MeshletGpuScene>,
|
|
views: Query<(
|
|
Entity,
|
|
&MeshletViewResources,
|
|
AnyOf<(&ViewDepthTexture, &ShadowView)>,
|
|
)>,
|
|
view_uniforms: Res<ViewUniforms>,
|
|
previous_view_uniforms: Res<PreviousViewUniforms>,
|
|
render_device: Res<RenderDevice>,
|
|
mut commands: Commands,
|
|
) {
|
|
let (
|
|
Some(cluster_instance_ids),
|
|
Some(cluster_meshlet_ids),
|
|
Some(view_uniforms),
|
|
Some(previous_view_uniforms),
|
|
) = (
|
|
gpu_scene.cluster_instance_ids.as_ref(),
|
|
gpu_scene.cluster_meshlet_ids.as_ref(),
|
|
view_uniforms.uniforms.binding(),
|
|
previous_view_uniforms.uniforms.binding(),
|
|
)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let first_node = Arc::new(AtomicBool::new(true));
|
|
|
|
// TODO: Some of these bind groups can be reused across multiple views
|
|
for (view_entity, view_resources, view_depth) in &views {
|
|
let entries = BindGroupEntries::sequential((
|
|
gpu_scene
|
|
.instance_meshlet_counts_prefix_sum
|
|
.binding()
|
|
.unwrap(),
|
|
gpu_scene.instance_meshlet_slice_starts.binding().unwrap(),
|
|
cluster_instance_ids.as_entire_binding(),
|
|
cluster_meshlet_ids.as_entire_binding(),
|
|
));
|
|
let fill_cluster_buffers = render_device.create_bind_group(
|
|
"meshlet_fill_cluster_buffers",
|
|
&gpu_scene.fill_cluster_buffers_bind_group_layout,
|
|
&entries,
|
|
);
|
|
|
|
let entries = BindGroupEntries::sequential((
|
|
cluster_meshlet_ids.as_entire_binding(),
|
|
gpu_scene.meshlet_bounding_spheres.binding(),
|
|
cluster_instance_ids.as_entire_binding(),
|
|
gpu_scene.instance_uniforms.binding().unwrap(),
|
|
view_resources.instance_visibility.as_entire_binding(),
|
|
view_resources
|
|
.second_pass_candidates_buffer
|
|
.as_entire_binding(),
|
|
gpu_scene.meshlets.binding(),
|
|
view_resources
|
|
.visibility_buffer_draw_indirect_args_first
|
|
.as_entire_binding(),
|
|
view_resources
|
|
.visibility_buffer_draw_triangle_buffer
|
|
.as_entire_binding(),
|
|
&view_resources.previous_depth_pyramid,
|
|
view_uniforms.clone(),
|
|
previous_view_uniforms.clone(),
|
|
));
|
|
let culling_first = render_device.create_bind_group(
|
|
"meshlet_culling_first_bind_group",
|
|
&gpu_scene.culling_bind_group_layout,
|
|
&entries,
|
|
);
|
|
|
|
let entries = BindGroupEntries::sequential((
|
|
cluster_meshlet_ids.as_entire_binding(),
|
|
gpu_scene.meshlet_bounding_spheres.binding(),
|
|
cluster_instance_ids.as_entire_binding(),
|
|
gpu_scene.instance_uniforms.binding().unwrap(),
|
|
view_resources.instance_visibility.as_entire_binding(),
|
|
view_resources
|
|
.second_pass_candidates_buffer
|
|
.as_entire_binding(),
|
|
gpu_scene.meshlets.binding(),
|
|
view_resources
|
|
.visibility_buffer_draw_indirect_args_second
|
|
.as_entire_binding(),
|
|
view_resources
|
|
.visibility_buffer_draw_triangle_buffer
|
|
.as_entire_binding(),
|
|
&view_resources.depth_pyramid_all_mips,
|
|
view_uniforms.clone(),
|
|
previous_view_uniforms.clone(),
|
|
));
|
|
let culling_second = render_device.create_bind_group(
|
|
"meshlet_culling_second_bind_group",
|
|
&gpu_scene.culling_bind_group_layout,
|
|
&entries,
|
|
);
|
|
|
|
let view_depth_texture = match view_depth {
|
|
(Some(view_depth), None) => view_depth.view(),
|
|
(None, Some(shadow_view)) => &shadow_view.depth_attachment.view,
|
|
_ => unreachable!(),
|
|
};
|
|
let downsample_depth = render_device.create_bind_group(
|
|
"meshlet_downsample_depth_bind_group",
|
|
&gpu_scene.downsample_depth_bind_group_layout,
|
|
&BindGroupEntries::sequential((
|
|
view_depth_texture,
|
|
&view_resources.depth_pyramid_mips[0],
|
|
&view_resources.depth_pyramid_mips[1],
|
|
&view_resources.depth_pyramid_mips[2],
|
|
&view_resources.depth_pyramid_mips[3],
|
|
&view_resources.depth_pyramid_mips[4],
|
|
&view_resources.depth_pyramid_mips[5],
|
|
&view_resources.depth_pyramid_mips[6],
|
|
&view_resources.depth_pyramid_mips[7],
|
|
&view_resources.depth_pyramid_mips[8],
|
|
&view_resources.depth_pyramid_mips[9],
|
|
&view_resources.depth_pyramid_mips[10],
|
|
&view_resources.depth_pyramid_mips[11],
|
|
&gpu_scene.depth_pyramid_sampler,
|
|
)),
|
|
);
|
|
|
|
let entries = BindGroupEntries::sequential((
|
|
cluster_meshlet_ids.as_entire_binding(),
|
|
gpu_scene.meshlets.binding(),
|
|
gpu_scene.indices.binding(),
|
|
gpu_scene.vertex_ids.binding(),
|
|
gpu_scene.vertex_data.binding(),
|
|
cluster_instance_ids.as_entire_binding(),
|
|
gpu_scene.instance_uniforms.binding().unwrap(),
|
|
gpu_scene.instance_material_ids.binding().unwrap(),
|
|
view_resources
|
|
.visibility_buffer_draw_triangle_buffer
|
|
.as_entire_binding(),
|
|
view_uniforms.clone(),
|
|
));
|
|
let visibility_buffer_raster = render_device.create_bind_group(
|
|
"meshlet_visibility_raster_buffer_bind_group",
|
|
&gpu_scene.visibility_buffer_raster_bind_group_layout,
|
|
&entries,
|
|
);
|
|
|
|
let copy_material_depth =
|
|
view_resources
|
|
.material_depth_color
|
|
.as_ref()
|
|
.map(|material_depth_color| {
|
|
render_device.create_bind_group(
|
|
"meshlet_copy_material_depth_bind_group",
|
|
&gpu_scene.copy_material_depth_bind_group_layout,
|
|
&[BindGroupEntry {
|
|
binding: 0,
|
|
resource: BindingResource::TextureView(
|
|
&material_depth_color.default_view,
|
|
),
|
|
}],
|
|
)
|
|
});
|
|
|
|
let material_draw = view_resources
|
|
.visibility_buffer
|
|
.as_ref()
|
|
.map(|visibility_buffer| {
|
|
let entries = BindGroupEntries::sequential((
|
|
&visibility_buffer.default_view,
|
|
cluster_meshlet_ids.as_entire_binding(),
|
|
gpu_scene.meshlets.binding(),
|
|
gpu_scene.indices.binding(),
|
|
gpu_scene.vertex_ids.binding(),
|
|
gpu_scene.vertex_data.binding(),
|
|
cluster_instance_ids.as_entire_binding(),
|
|
gpu_scene.instance_uniforms.binding().unwrap(),
|
|
));
|
|
render_device.create_bind_group(
|
|
"meshlet_mesh_material_draw_bind_group",
|
|
&gpu_scene.material_draw_bind_group_layout,
|
|
&entries,
|
|
)
|
|
});
|
|
|
|
commands.entity(view_entity).insert(MeshletViewBindGroups {
|
|
first_node: Arc::clone(&first_node),
|
|
fill_cluster_buffers,
|
|
culling_first,
|
|
culling_second,
|
|
downsample_depth,
|
|
visibility_buffer_raster,
|
|
copy_material_depth,
|
|
material_draw,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// A resource that manages GPU data for rendering [`MeshletMesh`]'s.
|
|
#[derive(Resource)]
|
|
pub struct MeshletGpuScene {
|
|
vertex_data: PersistentGpuBuffer<Arc<[u8]>>,
|
|
vertex_ids: PersistentGpuBuffer<Arc<[u32]>>,
|
|
indices: PersistentGpuBuffer<Arc<[u8]>>,
|
|
meshlets: PersistentGpuBuffer<Arc<[Meshlet]>>,
|
|
meshlet_bounding_spheres: PersistentGpuBuffer<Arc<[MeshletBoundingSpheres]>>,
|
|
meshlet_mesh_slices: HashMap<AssetId<MeshletMesh>, ([Range<BufferAddress>; 5], u64)>,
|
|
|
|
scene_meshlet_count: u32,
|
|
scene_triangle_count: u64,
|
|
next_material_id: u32,
|
|
material_id_lookup: HashMap<UntypedAssetId, u32>,
|
|
material_ids_present_in_scene: HashSet<u32>,
|
|
/// Per-instance [`Entity`], [`RenderLayers`], and [`NotShadowCaster`]
|
|
instances: Vec<(Entity, RenderLayers, bool)>,
|
|
/// Per-instance transforms, model matrices, and render flags
|
|
instance_uniforms: StorageBuffer<Vec<MeshUniform>>,
|
|
/// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support.
|
|
view_instance_visibility: EntityHashMap<StorageBuffer<Vec<u32>>>,
|
|
instance_material_ids: StorageBuffer<Vec<u32>>,
|
|
instance_meshlet_counts_prefix_sum: StorageBuffer<Vec<u32>>,
|
|
instance_meshlet_slice_starts: StorageBuffer<Vec<u32>>,
|
|
cluster_instance_ids: Option<Buffer>,
|
|
cluster_meshlet_ids: Option<Buffer>,
|
|
second_pass_candidates_buffer: Option<Buffer>,
|
|
previous_depth_pyramids: EntityHashMap<TextureView>,
|
|
visibility_buffer_draw_triangle_buffer: Option<Buffer>,
|
|
|
|
fill_cluster_buffers_bind_group_layout: BindGroupLayout,
|
|
culling_bind_group_layout: BindGroupLayout,
|
|
visibility_buffer_raster_bind_group_layout: BindGroupLayout,
|
|
downsample_depth_bind_group_layout: BindGroupLayout,
|
|
copy_material_depth_bind_group_layout: BindGroupLayout,
|
|
material_draw_bind_group_layout: BindGroupLayout,
|
|
depth_pyramid_sampler: Sampler,
|
|
depth_pyramid_dummy_texture: TextureView,
|
|
}
|
|
|
|
impl FromWorld for MeshletGpuScene {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let render_device = world.resource::<RenderDevice>();
|
|
|
|
Self {
|
|
vertex_data: PersistentGpuBuffer::new("meshlet_vertex_data", render_device),
|
|
vertex_ids: PersistentGpuBuffer::new("meshlet_vertex_ids", render_device),
|
|
indices: PersistentGpuBuffer::new("meshlet_indices", render_device),
|
|
meshlets: PersistentGpuBuffer::new("meshlets", render_device),
|
|
meshlet_bounding_spheres: PersistentGpuBuffer::new(
|
|
"meshlet_bounding_spheres",
|
|
render_device,
|
|
),
|
|
meshlet_mesh_slices: HashMap::new(),
|
|
|
|
scene_meshlet_count: 0,
|
|
scene_triangle_count: 0,
|
|
next_material_id: 0,
|
|
material_id_lookup: HashMap::new(),
|
|
material_ids_present_in_scene: HashSet::new(),
|
|
instances: Vec::new(),
|
|
instance_uniforms: {
|
|
let mut buffer = StorageBuffer::default();
|
|
buffer.set_label(Some("meshlet_instance_uniforms"));
|
|
buffer
|
|
},
|
|
view_instance_visibility: EntityHashMap::default(),
|
|
instance_material_ids: {
|
|
let mut buffer = StorageBuffer::default();
|
|
buffer.set_label(Some("meshlet_instance_material_ids"));
|
|
buffer
|
|
},
|
|
instance_meshlet_counts_prefix_sum: {
|
|
let mut buffer = StorageBuffer::default();
|
|
buffer.set_label(Some("meshlet_instance_meshlet_counts_prefix_sum"));
|
|
buffer
|
|
},
|
|
instance_meshlet_slice_starts: {
|
|
let mut buffer = StorageBuffer::default();
|
|
buffer.set_label(Some("meshlet_instance_meshlet_slice_starts"));
|
|
buffer
|
|
},
|
|
cluster_instance_ids: None,
|
|
cluster_meshlet_ids: None,
|
|
second_pass_candidates_buffer: None,
|
|
previous_depth_pyramids: EntityHashMap::default(),
|
|
visibility_buffer_draw_triangle_buffer: None,
|
|
|
|
// TODO: Buffer min sizes
|
|
fill_cluster_buffers_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_fill_cluster_buffers_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::COMPUTE,
|
|
(
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_sized(false, None),
|
|
storage_buffer_sized(false, None),
|
|
),
|
|
),
|
|
),
|
|
culling_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_culling_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::COMPUTE,
|
|
(
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_sized(false, None),
|
|
storage_buffer_sized(false, None),
|
|
texture_2d(TextureSampleType::Float { filterable: false }),
|
|
uniform_buffer::<ViewUniform>(true),
|
|
uniform_buffer::<PreviousViewData>(true),
|
|
),
|
|
),
|
|
),
|
|
downsample_depth_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_downsample_depth_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, {
|
|
let write_only_r32float = || {
|
|
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly)
|
|
};
|
|
(
|
|
texture_depth_2d(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
texture_storage_2d(
|
|
TextureFormat::R32Float,
|
|
StorageTextureAccess::ReadWrite,
|
|
),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
write_only_r32float(),
|
|
sampler(SamplerBindingType::NonFiltering),
|
|
)
|
|
}),
|
|
),
|
|
visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_visibility_buffer_raster_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::VERTEX,
|
|
(
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
uniform_buffer::<ViewUniform>(true),
|
|
),
|
|
),
|
|
),
|
|
copy_material_depth_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_copy_material_depth_bind_group_layout",
|
|
&BindGroupLayoutEntries::single(
|
|
ShaderStages::FRAGMENT,
|
|
texture_2d(TextureSampleType::Uint),
|
|
),
|
|
),
|
|
material_draw_bind_group_layout: render_device.create_bind_group_layout(
|
|
"meshlet_mesh_material_draw_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::FRAGMENT,
|
|
(
|
|
texture_2d(TextureSampleType::Uint),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
storage_buffer_read_only_sized(false, None),
|
|
),
|
|
),
|
|
),
|
|
depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor {
|
|
label: Some("meshlet_depth_pyramid_sampler"),
|
|
..default()
|
|
}),
|
|
depth_pyramid_dummy_texture: render_device
|
|
.create_texture(&TextureDescriptor {
|
|
label: Some("meshlet_depth_pyramid_dummy_texture"),
|
|
size: Extent3d {
|
|
width: 1,
|
|
height: 1,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::R32Float,
|
|
usage: TextureUsages::STORAGE_BINDING,
|
|
view_formats: &[],
|
|
})
|
|
.create_view(&TextureViewDescriptor {
|
|
label: Some("meshlet_depth_pyramid_dummy_texture_view"),
|
|
format: Some(TextureFormat::R32Float),
|
|
dimension: Some(TextureViewDimension::D2),
|
|
aspect: TextureAspect::All,
|
|
base_mip_level: 0,
|
|
mip_level_count: Some(1),
|
|
base_array_layer: 0,
|
|
array_layer_count: Some(1),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MeshletGpuScene {
|
|
/// Clear per-frame CPU->GPU upload buffers and reset all per-frame data.
|
|
fn reset(&mut self) {
|
|
// TODO: Shrink capacity if saturation is low
|
|
self.scene_meshlet_count = 0;
|
|
self.scene_triangle_count = 0;
|
|
self.next_material_id = 0;
|
|
self.material_id_lookup.clear();
|
|
self.material_ids_present_in_scene.clear();
|
|
self.instances.clear();
|
|
self.view_instance_visibility
|
|
.values_mut()
|
|
.for_each(|b| b.get_mut().clear());
|
|
self.instance_uniforms.get_mut().clear();
|
|
self.instance_material_ids.get_mut().clear();
|
|
self.instance_meshlet_counts_prefix_sum.get_mut().clear();
|
|
self.instance_meshlet_slice_starts.get_mut().clear();
|
|
// TODO: Remove unused entries for view_instance_visibility and previous_depth_pyramids
|
|
}
|
|
|
|
fn queue_meshlet_mesh_upload(
|
|
&mut self,
|
|
instance: Entity,
|
|
render_layers: RenderLayers,
|
|
not_shadow_caster: bool,
|
|
handle: &Handle<MeshletMesh>,
|
|
assets: &mut Assets<MeshletMesh>,
|
|
) {
|
|
let queue_meshlet_mesh = |asset_id: &AssetId<MeshletMesh>| {
|
|
let meshlet_mesh = assets.remove_untracked(*asset_id).expect(
|
|
"MeshletMesh asset was already unloaded but is not registered with MeshletGpuScene",
|
|
);
|
|
|
|
let vertex_data_slice = self
|
|
.vertex_data
|
|
.queue_write(Arc::clone(&meshlet_mesh.vertex_data), ());
|
|
let vertex_ids_slice = self.vertex_ids.queue_write(
|
|
Arc::clone(&meshlet_mesh.vertex_ids),
|
|
vertex_data_slice.start,
|
|
);
|
|
let indices_slice = self
|
|
.indices
|
|
.queue_write(Arc::clone(&meshlet_mesh.indices), ());
|
|
let meshlets_slice = self.meshlets.queue_write(
|
|
Arc::clone(&meshlet_mesh.meshlets),
|
|
(vertex_ids_slice.start, indices_slice.start),
|
|
);
|
|
let meshlet_bounding_spheres_slice = self
|
|
.meshlet_bounding_spheres
|
|
.queue_write(Arc::clone(&meshlet_mesh.bounding_spheres), ());
|
|
|
|
(
|
|
[
|
|
vertex_data_slice,
|
|
vertex_ids_slice,
|
|
indices_slice,
|
|
meshlets_slice,
|
|
meshlet_bounding_spheres_slice,
|
|
],
|
|
meshlet_mesh.worst_case_meshlet_triangles,
|
|
)
|
|
};
|
|
|
|
// If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading
|
|
let ([_, _, _, meshlets_slice, _], triangle_count) = self
|
|
.meshlet_mesh_slices
|
|
.entry(handle.id())
|
|
.or_insert_with_key(queue_meshlet_mesh)
|
|
.clone();
|
|
|
|
let meshlets_slice = (meshlets_slice.start as u32 / size_of::<Meshlet>() as u32)
|
|
..(meshlets_slice.end as u32 / size_of::<Meshlet>() as u32);
|
|
|
|
// Append instance data for this frame
|
|
self.instances
|
|
.push((instance, render_layers, not_shadow_caster));
|
|
self.instance_material_ids.get_mut().push(0);
|
|
self.instance_meshlet_counts_prefix_sum
|
|
.get_mut()
|
|
.push(self.scene_meshlet_count);
|
|
self.instance_meshlet_slice_starts
|
|
.get_mut()
|
|
.push(meshlets_slice.start);
|
|
|
|
self.scene_meshlet_count += meshlets_slice.end - meshlets_slice.start;
|
|
self.scene_triangle_count += triangle_count;
|
|
}
|
|
|
|
/// Get the depth value for use with the material depth texture for a given [`Material`] asset.
|
|
pub fn get_material_id(&mut self, material_id: UntypedAssetId) -> u32 {
|
|
*self
|
|
.material_id_lookup
|
|
.entry(material_id)
|
|
.or_insert_with(|| {
|
|
self.next_material_id += 1;
|
|
self.next_material_id
|
|
})
|
|
}
|
|
|
|
pub fn material_present_in_scene(&self, material_id: &u32) -> bool {
|
|
self.material_ids_present_in_scene.contains(material_id)
|
|
}
|
|
|
|
pub fn fill_cluster_buffers_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.fill_cluster_buffers_bind_group_layout.clone()
|
|
}
|
|
|
|
pub fn culling_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.culling_bind_group_layout.clone()
|
|
}
|
|
|
|
pub fn downsample_depth_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.downsample_depth_bind_group_layout.clone()
|
|
}
|
|
|
|
pub fn visibility_buffer_raster_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.visibility_buffer_raster_bind_group_layout.clone()
|
|
}
|
|
|
|
pub fn copy_material_depth_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.copy_material_depth_bind_group_layout.clone()
|
|
}
|
|
|
|
pub fn material_draw_bind_group_layout(&self) -> BindGroupLayout {
|
|
self.material_draw_bind_group_layout.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct MeshletViewResources {
|
|
pub scene_meshlet_count: u32,
|
|
pub second_pass_candidates_buffer: Buffer,
|
|
instance_visibility: Buffer,
|
|
pub visibility_buffer: Option<CachedTexture>,
|
|
pub visibility_buffer_draw_indirect_args_first: Buffer,
|
|
pub visibility_buffer_draw_indirect_args_second: Buffer,
|
|
visibility_buffer_draw_triangle_buffer: Buffer,
|
|
depth_pyramid_all_mips: TextureView,
|
|
depth_pyramid_mips: [TextureView; 12],
|
|
pub depth_pyramid_mip_count: u32,
|
|
previous_depth_pyramid: TextureView,
|
|
pub material_depth_color: Option<CachedTexture>,
|
|
pub material_depth: Option<CachedTexture>,
|
|
pub view_size: UVec2,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct MeshletViewBindGroups {
|
|
pub first_node: Arc<AtomicBool>,
|
|
pub fill_cluster_buffers: BindGroup,
|
|
pub culling_first: BindGroup,
|
|
pub culling_second: BindGroup,
|
|
pub downsample_depth: BindGroup,
|
|
pub visibility_buffer_raster: BindGroup,
|
|
pub copy_material_depth: Option<BindGroup>,
|
|
pub material_draw: Option<BindGroup>,
|
|
}
|