
# 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>
477 lines
19 KiB
Rust
477 lines
19 KiB
Rust
//! Demonstrates how to define and use specialized mesh pipeline
|
|
//!
|
|
//! This example shows how to use the built-in [`SpecializedMeshPipeline`]
|
|
//! functionality with a custom [`RenderCommand`] to allow custom mesh rendering with
|
|
//! more flexibility than the material api.
|
|
//!
|
|
//! [`SpecializedMeshPipeline`] let's you customize the entire pipeline used when rendering a mesh.
|
|
|
|
use bevy::{
|
|
core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
|
|
ecs::{component::Tick, system::StaticSystemParam},
|
|
math::{vec3, vec4},
|
|
pbr::{
|
|
DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances,
|
|
SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewEmptyBindGroup,
|
|
},
|
|
prelude::*,
|
|
render::{
|
|
batching::{
|
|
gpu_preprocessing::{
|
|
self, PhaseBatchedInstanceBuffers, PhaseIndirectParametersBuffers,
|
|
PreprocessWorkItem, UntypedPhaseBatchedInstanceBuffers,
|
|
},
|
|
GetBatchData, GetFullBatchData,
|
|
},
|
|
experimental::occlusion_culling::OcclusionCulling,
|
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
|
mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology, RenderMesh},
|
|
render_asset::{RenderAssetUsages, RenderAssets},
|
|
render_phase::{
|
|
AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, SetItemPipeline,
|
|
ViewBinnedRenderPhases,
|
|
},
|
|
render_resource::{
|
|
ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, Face, FragmentState,
|
|
FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState,
|
|
RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
|
|
SpecializedMeshPipelines, TextureFormat, VertexState,
|
|
},
|
|
view::NoIndirectDrawing,
|
|
view::{self, ExtractedView, RenderVisibleEntities, ViewTarget, VisibilityClass},
|
|
Render, RenderApp, RenderSystems,
|
|
},
|
|
};
|
|
|
|
const SHADER_ASSET_PATH: &str = "shaders/specialized_mesh_pipeline.wgsl";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_plugins(CustomRenderedMeshPipelinePlugin)
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
/// Spawns the objects in the scene.
|
|
fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
|
|
// Build a custom triangle mesh with colors
|
|
// We define a custom mesh because the examples only uses a limited
|
|
// set of vertex attributes for simplicity
|
|
let mesh = Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::default(),
|
|
)
|
|
.with_inserted_indices(Indices::U32(vec![0, 1, 2]))
|
|
.with_inserted_attribute(
|
|
Mesh::ATTRIBUTE_POSITION,
|
|
vec![
|
|
vec3(-0.5, -0.5, 0.0),
|
|
vec3(0.5, -0.5, 0.0),
|
|
vec3(0.0, 0.25, 0.0),
|
|
],
|
|
)
|
|
.with_inserted_attribute(
|
|
Mesh::ATTRIBUTE_COLOR,
|
|
vec![
|
|
vec4(1.0, 0.0, 0.0, 1.0),
|
|
vec4(0.0, 1.0, 0.0, 1.0),
|
|
vec4(0.0, 0.0, 1.0, 1.0),
|
|
],
|
|
);
|
|
|
|
// spawn 3 triangles to show that batching works
|
|
for (x, y) in [-0.5, 0.0, 0.5].into_iter().zip([-0.25, 0.5, -0.25]) {
|
|
// Spawn an entity with all the required components for it to be rendered with our custom pipeline
|
|
commands.spawn((
|
|
// We use a marker component to identify the mesh that will be rendered
|
|
// with our specialized pipeline
|
|
CustomRenderedEntity,
|
|
// We need to add the mesh handle to the entity
|
|
Mesh3d(meshes.add(mesh.clone())),
|
|
Transform::from_xyz(x, y, 0.0),
|
|
));
|
|
}
|
|
|
|
// Spawn the camera.
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
// Move the camera back a bit to see all the triangles
|
|
Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
}
|
|
|
|
// When writing custom rendering code it's generally recommended to use a plugin.
|
|
// The main reason for this is that it gives you access to the finish() hook
|
|
// which is called after rendering resources are initialized.
|
|
struct CustomRenderedMeshPipelinePlugin;
|
|
impl Plugin for CustomRenderedMeshPipelinePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default());
|
|
|
|
// We make sure to add these to the render app, not the main app.
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
render_app
|
|
// This is needed to tell bevy about your custom pipeline
|
|
.init_resource::<SpecializedMeshPipelines<CustomMeshPipeline>>()
|
|
// We need to use a custom draw command so we need to register it
|
|
.add_render_command::<Opaque3d, DrawSpecializedPipelineCommands>()
|
|
.add_systems(
|
|
Render,
|
|
queue_custom_mesh_pipeline.in_set(RenderSystems::Queue),
|
|
);
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
// Creating this pipeline needs the RenderDevice and RenderQueue
|
|
// which are only available once rendering plugins are initialized.
|
|
render_app.init_resource::<CustomMeshPipeline>();
|
|
}
|
|
}
|
|
|
|
/// A marker component that represents an entity that is to be rendered using
|
|
/// our specialized pipeline.
|
|
///
|
|
/// Note the [`ExtractComponent`] trait implementation: this is necessary to
|
|
/// tell Bevy that this object should be pulled into the render world. Also note
|
|
/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
|
|
/// that entities with this component need to be examined for visibility.
|
|
#[derive(Clone, Component, ExtractComponent)]
|
|
#[require(VisibilityClass)]
|
|
#[component(on_add = view::add_visibility_class::<CustomRenderedEntity>)]
|
|
struct CustomRenderedEntity;
|
|
|
|
/// The custom draw commands that Bevy executes for each entity we enqueue into
|
|
/// the render phase.
|
|
type DrawSpecializedPipelineCommands = (
|
|
// Set the pipeline
|
|
SetItemPipeline,
|
|
// Set the view uniform at bind group 0
|
|
SetMeshViewBindGroup<0>,
|
|
// Set an empty material bind group at bind group 1
|
|
SetMeshViewEmptyBindGroup<1>,
|
|
// Set the mesh uniform at bind group 2
|
|
SetMeshBindGroup<2>,
|
|
// Draw the mesh
|
|
DrawMesh,
|
|
);
|
|
|
|
// This contains the state needed to specialize a mesh pipeline
|
|
#[derive(Resource)]
|
|
struct CustomMeshPipeline {
|
|
/// The base mesh pipeline defined by bevy
|
|
///
|
|
/// This isn't required, but if you want to use a bevy `Mesh` it's easier when you
|
|
/// have access to the base `MeshPipeline` that bevy already defines
|
|
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 CustomMeshPipeline {
|
|
fn from_world(world: &mut World) -> Self {
|
|
// Load the shader
|
|
let shader_handle: Handle<Shader> = world.resource::<AssetServer>().load(SHADER_ASSET_PATH);
|
|
Self {
|
|
mesh_pipeline: MeshPipeline::from_world(world),
|
|
shader_handle,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SpecializedMeshPipeline for CustomMeshPipeline {
|
|
/// Pipeline use keys to determine how to specialize it.
|
|
/// The key is also used by the pipeline cache to determine if
|
|
/// it needs to create a new pipeline or not
|
|
///
|
|
/// In this example we just use the base `MeshPipelineKey` defined by bevy, but this could be anything.
|
|
/// For example, if you want to make a pipeline with a procedural shader you could add the Handle<Shader> to the key.
|
|
type Key = MeshPipelineKey;
|
|
|
|
fn specialize(
|
|
&self,
|
|
mesh_key: Self::Key,
|
|
layout: &MeshVertexBufferLayoutRef,
|
|
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
|
|
// Define the vertex attributes based on a standard bevy [`Mesh`]
|
|
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));
|
|
}
|
|
if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
|
|
// Make sure this matches the shader location
|
|
vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(1));
|
|
}
|
|
// 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(mesh_key));
|
|
|
|
Ok(RenderPipelineDescriptor {
|
|
label: Some("Specialized Mesh Pipeline".into()),
|
|
layout: vec![
|
|
view_layout.main_layout.clone(),
|
|
view_layout.empty_layout.clone(),
|
|
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(),
|
|
// Customize how to store the meshes' vertex attributes in the vertex buffer
|
|
buffers: vec![vertex_buffer_layout],
|
|
},
|
|
fragment: Some(FragmentState {
|
|
shader: self.shader_handle.clone(),
|
|
shader_defs: vec![],
|
|
entry_point: "fragment".into(),
|
|
targets: vec![Some(ColorTargetState {
|
|
// This isn't required, but bevy supports HDR and non-HDR rendering
|
|
// so it's generally recommended to specialize the pipeline for that
|
|
format: if mesh_key.contains(MeshPipelineKey::HDR) {
|
|
ViewTarget::TEXTURE_FORMAT_HDR
|
|
} else {
|
|
TextureFormat::bevy_default()
|
|
},
|
|
// For this example we only use opaque meshes,
|
|
// but if you wanted to use alpha blending you would need to set it here
|
|
blend: None,
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
primitive: PrimitiveState {
|
|
topology: mesh_key.primitive_topology(),
|
|
front_face: FrontFace::Ccw,
|
|
cull_mode: Some(Face::Back),
|
|
polygon_mode: PolygonMode::Fill,
|
|
..default()
|
|
},
|
|
// Note that if your view has no depth buffer this will need to be
|
|
// changed.
|
|
depth_stencil: Some(DepthStencilState {
|
|
format: CORE_3D_DEPTH_FORMAT,
|
|
depth_write_enabled: true,
|
|
depth_compare: CompareFunction::GreaterEqual,
|
|
stencil: default(),
|
|
bias: default(),
|
|
}),
|
|
// It's generally recommended to specialize your pipeline for MSAA,
|
|
// but it's not always possible
|
|
multisample: MultisampleState {
|
|
count: mesh_key.msaa_samples(),
|
|
..MultisampleState::default()
|
|
},
|
|
zero_initialize_workgroup_memory: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A render-world system that enqueues the entity with custom rendering into
|
|
/// the opaque render phases of each view.
|
|
fn queue_custom_mesh_pipeline(
|
|
pipeline_cache: Res<PipelineCache>,
|
|
custom_mesh_pipeline: Res<CustomMeshPipeline>,
|
|
(mut opaque_render_phases, opaque_draw_functions): (
|
|
ResMut<ViewBinnedRenderPhases<Opaque3d>>,
|
|
Res<DrawFunctions<Opaque3d>>,
|
|
),
|
|
mut specialized_mesh_pipelines: ResMut<SpecializedMeshPipelines<CustomMeshPipeline>>,
|
|
views: Query<(
|
|
&RenderVisibleEntities,
|
|
&ExtractedView,
|
|
&Msaa,
|
|
Has<NoIndirectDrawing>,
|
|
Has<OcclusionCulling>,
|
|
)>,
|
|
(render_meshes, render_mesh_instances): (
|
|
Res<RenderAssets<RenderMesh>>,
|
|
Res<RenderMeshInstances>,
|
|
),
|
|
param: StaticSystemParam<<MeshPipeline as GetBatchData>::Param>,
|
|
mut phase_batched_instance_buffers: ResMut<
|
|
PhaseBatchedInstanceBuffers<Opaque3d, <MeshPipeline as GetBatchData>::BufferData>,
|
|
>,
|
|
mut phase_indirect_parameters_buffers: ResMut<PhaseIndirectParametersBuffers<Opaque3d>>,
|
|
mut change_tick: Local<Tick>,
|
|
) {
|
|
let system_param_item = param.into_inner();
|
|
|
|
let UntypedPhaseBatchedInstanceBuffers {
|
|
ref mut data_buffer,
|
|
ref mut work_item_buffers,
|
|
ref mut late_indexed_indirect_parameters_buffer,
|
|
ref mut late_non_indexed_indirect_parameters_buffer,
|
|
..
|
|
} = phase_batched_instance_buffers.buffers;
|
|
|
|
// Get the id for our custom draw function
|
|
let draw_function_id = opaque_draw_functions
|
|
.read()
|
|
.id::<DrawSpecializedPipelineCommands>();
|
|
|
|
// Render phases are per-view, so we need to iterate over all views so that
|
|
// the entity appears in them. (In this example, we have only one view, but
|
|
// it's good practice to loop over all views anyway.)
|
|
for (view_visible_entities, view, msaa, no_indirect_drawing, gpu_occlusion_culling) in
|
|
views.iter()
|
|
{
|
|
let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
|
|
continue;
|
|
};
|
|
|
|
// Create *work item buffers* if necessary. Work item buffers store the
|
|
// indices of meshes that are to be rendered when indirect drawing is
|
|
// enabled.
|
|
let work_item_buffer = gpu_preprocessing::get_or_create_work_item_buffer::<Opaque3d>(
|
|
work_item_buffers,
|
|
view.retained_view_entity,
|
|
no_indirect_drawing,
|
|
gpu_occlusion_culling,
|
|
);
|
|
|
|
// Initialize those work item buffers in preparation for this new frame.
|
|
gpu_preprocessing::init_work_item_buffers(
|
|
work_item_buffer,
|
|
late_indexed_indirect_parameters_buffer,
|
|
late_non_indexed_indirect_parameters_buffer,
|
|
);
|
|
|
|
// 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);
|
|
|
|
// Set up a slot to hold information about the batch set we're going to
|
|
// create. If there are any of our custom meshes in the scene, we'll
|
|
// need this information in order for Bevy to kick off the rendering.
|
|
let mut mesh_batch_set_info = None;
|
|
|
|
// Find all the custom rendered entities that are visible from this
|
|
// view.
|
|
for &(render_entity, visible_entity) in
|
|
view_visible_entities.get::<CustomRenderedEntity>().iter()
|
|
{
|
|
// Get the mesh instance
|
|
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
// Get the mesh data
|
|
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());
|
|
|
|
// Initialize the batch set information if this was the first custom
|
|
// mesh we saw. We'll need that information later to create the
|
|
// batch set.
|
|
if mesh_batch_set_info.is_none() {
|
|
mesh_batch_set_info = Some(MeshBatchSetInfo {
|
|
indirect_parameters_index: phase_indirect_parameters_buffers
|
|
.buffers
|
|
.allocate(mesh.indexed(), 1),
|
|
is_indexed: mesh.indexed(),
|
|
});
|
|
}
|
|
let mesh_info = mesh_batch_set_info.unwrap();
|
|
|
|
// Allocate some input and output indices. We'll need these to
|
|
// create the *work item* below.
|
|
let Some(input_index) =
|
|
MeshPipeline::get_binned_index(&system_param_item, visible_entity)
|
|
else {
|
|
continue;
|
|
};
|
|
let output_index = data_buffer.add() as u32;
|
|
|
|
// Finally, we can specialize the pipeline based on the key
|
|
let pipeline_id = specialized_mesh_pipelines
|
|
.specialize(
|
|
&pipeline_cache,
|
|
&custom_mesh_pipeline,
|
|
mesh_key,
|
|
&mesh.layout,
|
|
)
|
|
// This should never with this example, but if your pipeline specialization
|
|
// can fail you need to handle the error here
|
|
.expect("Failed to specialize mesh pipeline");
|
|
|
|
// Bump the change tick so that Bevy is forced to rebuild the bin.
|
|
let next_change_tick = change_tick.get() + 1;
|
|
change_tick.set(next_change_tick);
|
|
|
|
// Add the mesh with our specialized pipeline
|
|
opaque_phase.add(
|
|
Opaque3dBatchSetKey {
|
|
draw_function: draw_function_id,
|
|
pipeline: pipeline_id,
|
|
material_bind_group_index: None,
|
|
vertex_slab: default(),
|
|
index_slab: None,
|
|
lightmap_slab: None,
|
|
},
|
|
// The asset ID is arbitrary; we simply use [`AssetId::invalid`],
|
|
// but you can use anything you like. Note that the asset ID need
|
|
// not be the ID of a [`Mesh`].
|
|
Opaque3dBinKey {
|
|
asset_id: AssetId::<Mesh>::invalid().untyped(),
|
|
},
|
|
(render_entity, visible_entity),
|
|
mesh_instance.current_uniform_index,
|
|
// This example supports batching, but if your pipeline doesn't
|
|
// support it you can use `BinnedRenderPhaseType::UnbatchableMesh`
|
|
BinnedRenderPhaseType::BatchableMesh,
|
|
*change_tick,
|
|
);
|
|
|
|
// Create a *work item*. A work item tells the Bevy renderer to
|
|
// transform the mesh on GPU.
|
|
work_item_buffer.push(
|
|
mesh.indexed(),
|
|
PreprocessWorkItem {
|
|
input_index: input_index.into(),
|
|
output_or_indirect_parameters_index: if no_indirect_drawing {
|
|
output_index
|
|
} else {
|
|
mesh_info.indirect_parameters_index
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
// Now if there were any meshes, we need to add a command to the
|
|
// indirect parameters buffer, so that the renderer will end up
|
|
// enqueuing a command to draw the mesh.
|
|
if let Some(mesh_info) = mesh_batch_set_info {
|
|
phase_indirect_parameters_buffers
|
|
.buffers
|
|
.add_batch_set(mesh_info.is_indexed, mesh_info.indirect_parameters_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we end up having any custom meshes to draw, this contains information
|
|
// needed to create the batch set.
|
|
#[derive(Clone, Copy)]
|
|
struct MeshBatchSetInfo {
|
|
/// The first index of the mesh batch in the indirect parameters buffer.
|
|
indirect_parameters_index: u32,
|
|
/// Whether the mesh is indexed (has an index buffer).
|
|
is_indexed: bool,
|
|
}
|