mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
888ab36716
# Objective - Fix startup crash on 3d_shapes example and anything that uses wireframes - Make sure extra_buffer_usages set arent lost on recovery ## Solution - Move extra_buffer_usages to MeshAllocatorSettings ## Testing - 3d_example doesnt crash
321 lines
11 KiB
Rust
321 lines
11 KiB
Rust
//! This example shows how to initialize an empty mesh with a Handle
|
|
//! and a render-world only usage. That buffer is then filled by a
|
|
//! compute shader on the GPU without transferring data back
|
|
//! to the CPU.
|
|
//!
|
|
//! The `mesh_allocator` is used to get references to the relevant slabs
|
|
//! that contain the mesh data we're interested in.
|
|
//!
|
|
//! This example does not remove the `GenerateMesh` component after
|
|
//! generating the mesh.
|
|
|
|
use std::ops::Not;
|
|
|
|
use bevy::{
|
|
asset::RenderAssetUsages,
|
|
color::palettes::tailwind::{RED_400, SKY_400},
|
|
core_pipeline::schedule::camera_driver,
|
|
mesh::Indices,
|
|
platform::collections::HashSet,
|
|
prelude::*,
|
|
render::{
|
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
|
mesh::allocator::{MeshAllocator, MeshAllocatorSettings},
|
|
render_resource::{
|
|
binding_types::{storage_buffer, uniform_buffer},
|
|
*,
|
|
},
|
|
renderer::{RenderContext, RenderGraph, RenderQueue},
|
|
Render, RenderApp, RenderStartup,
|
|
},
|
|
};
|
|
|
|
/// This example uses a shader source file from the assets subdirectory
|
|
const SHADER_ASSET_PATH: &str = "shaders/compute_mesh.wgsl";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((
|
|
DefaultPlugins,
|
|
ComputeShaderMeshGeneratorPlugin,
|
|
ExtractComponentPlugin::<GenerateMesh>::default(),
|
|
))
|
|
.insert_resource(ClearColor(Color::BLACK))
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
// We need a plugin to organize all the systems and render node required for this example
|
|
struct ComputeShaderMeshGeneratorPlugin;
|
|
impl Plugin for ComputeShaderMeshGeneratorPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app
|
|
.init_resource::<ChunksToProcess>()
|
|
.add_systems(RenderStartup, init_compute_pipeline)
|
|
.add_systems(Render, prepare_chunks)
|
|
.add_systems(RenderGraph, compute_mesh.before(camera_driver));
|
|
}
|
|
fn finish(&self, app: &mut App) {
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
render_app
|
|
.world_mut()
|
|
.resource_mut::<MeshAllocatorSettings>()
|
|
// This allows using the mesh allocator slabs as
|
|
// storage buffers directly in the compute shader.
|
|
// Which means that we can write from our compute
|
|
// shader directly to the allocated mesh slabs.
|
|
.extra_buffer_usages = BufferUsages::STORAGE;
|
|
}
|
|
}
|
|
|
|
/// Holds a handle to the empty mesh that should be filled
|
|
/// by the compute shader.
|
|
#[derive(Component, ExtractComponent, Clone)]
|
|
struct GenerateMesh(Handle<Mesh>);
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// a truly empty mesh will error if used in Mesh3d
|
|
// so we set up the data to be what we want the compute shader to output
|
|
// We're using 36 indices and 24 vertices which is directly taken from
|
|
// the Bevy Cuboid mesh implementation.
|
|
//
|
|
// We allocate 50 spots for each attribute here because
|
|
// it is *very important* that the amount of data allocated here is
|
|
// *bigger* than (or exactly equal to) the amount of data we intend to
|
|
// write from the compute shader. This amount of data defines how big
|
|
// the buffer we get from the mesh_allocator will be, which in turn
|
|
// defines how big the buffer is when we're in the compute shader.
|
|
//
|
|
// If it turns out you don't need all of the space when the compute shader
|
|
// is writing data, you can write NaN to the rest of the data.
|
|
let empty_mesh = {
|
|
let mut mesh = Mesh::new(
|
|
PrimitiveTopology::TriangleList,
|
|
RenderAssetUsages::RENDER_WORLD,
|
|
)
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vec![[0.; 3]; 50])
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.; 3]; 50])
|
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.; 2]; 50])
|
|
.with_inserted_indices(Indices::U32(vec![0; 50]));
|
|
|
|
mesh.asset_usage = RenderAssetUsages::RENDER_WORLD;
|
|
mesh
|
|
};
|
|
|
|
let handle = meshes.add(empty_mesh);
|
|
|
|
// we spawn two "users" of the mesh handle,
|
|
// but only insert `GenerateMesh` on one of them
|
|
// to show that the mesh handle works as usual
|
|
commands.spawn((
|
|
GenerateMesh(handle.clone()),
|
|
Mesh3d(handle.clone()),
|
|
MeshMaterial3d(materials.add(StandardMaterial {
|
|
base_color: RED_400.into(),
|
|
..default()
|
|
})),
|
|
Transform::from_xyz(-2.5, 1.5, 0.),
|
|
));
|
|
|
|
commands.spawn((
|
|
Mesh3d(handle),
|
|
MeshMaterial3d(materials.add(StandardMaterial {
|
|
base_color: SKY_400.into(),
|
|
..default()
|
|
})),
|
|
Transform::from_xyz(2.5, 1.5, 0.),
|
|
));
|
|
|
|
// some additional scene elements.
|
|
// This mesh specifically is here so that we don't assume
|
|
// mesh_allocator offsets that would only work if we had
|
|
// one mesh in the scene.
|
|
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)),
|
|
));
|
|
commands.spawn((
|
|
PointLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
// camera
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
}
|
|
|
|
/// This is called `ChunksToProcess` because this example originated
|
|
/// from a use case of generating chunks of landscape or voxels
|
|
/// It only exists in the render world.
|
|
#[derive(Resource, Default)]
|
|
struct ChunksToProcess(Vec<AssetId<Mesh>>);
|
|
|
|
/// `processed` is a `HashSet` contains the `AssetId`s that have been
|
|
/// processed. We use that to remove `AssetId`s that have already
|
|
/// been processed, which means each unique `GenerateMesh` will result
|
|
/// in one compute shader mesh generation process instead of generating
|
|
/// the mesh every frame.
|
|
fn prepare_chunks(
|
|
meshes_to_generate: Query<&GenerateMesh>,
|
|
mut chunks: ResMut<ChunksToProcess>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
pipeline: Res<ComputePipeline>,
|
|
mut processed: Local<HashSet<AssetId<Mesh>>>,
|
|
) {
|
|
// If the pipeline isn't ready, then meshes
|
|
// won't be processed. So we want to wait until
|
|
// the pipeline is ready before considering any mesh processed.
|
|
if pipeline_cache
|
|
.get_compute_pipeline(pipeline.pipeline)
|
|
.is_some()
|
|
{
|
|
// get the AssetId for each Handle<Mesh>
|
|
// which we'll use later to get the relevant buffers
|
|
// from the mesh_allocator
|
|
let chunk_data: Vec<AssetId<Mesh>> = meshes_to_generate
|
|
.iter()
|
|
.filter_map(|gmesh| {
|
|
let id = gmesh.0.id();
|
|
processed.contains(&id).not().then_some(id)
|
|
})
|
|
.collect();
|
|
|
|
// Cache any meshes we're going to process this frame
|
|
for id in &chunk_data {
|
|
processed.insert(*id);
|
|
}
|
|
|
|
chunks.0 = chunk_data;
|
|
}
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct ComputePipeline {
|
|
layout: BindGroupLayoutDescriptor,
|
|
pipeline: CachedComputePipelineId,
|
|
}
|
|
|
|
// init only happens once
|
|
fn init_compute_pipeline(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
) {
|
|
let layout = BindGroupLayoutDescriptor::new(
|
|
"",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::COMPUTE,
|
|
(
|
|
// offsets
|
|
uniform_buffer::<DataRanges>(false),
|
|
// vertices
|
|
storage_buffer::<Vec<u32>>(false),
|
|
// indices
|
|
storage_buffer::<Vec<u32>>(false),
|
|
),
|
|
),
|
|
);
|
|
let shader = asset_server.load(SHADER_ASSET_PATH);
|
|
let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
|
label: Some("Mesh generation compute shader".into()),
|
|
layout: vec![layout.clone()],
|
|
shader: shader.clone(),
|
|
..default()
|
|
});
|
|
commands.insert_resource(ComputePipeline { layout, pipeline });
|
|
}
|
|
|
|
// A uniform that holds the vertex and index offsets
|
|
// for the vertex/index mesh_allocator buffer slabs
|
|
#[derive(ShaderType)]
|
|
struct DataRanges {
|
|
vertex_start: u32,
|
|
vertex_end: u32,
|
|
index_start: u32,
|
|
index_end: u32,
|
|
}
|
|
|
|
fn compute_mesh(
|
|
mut render_context: RenderContext,
|
|
chunks: Res<ChunksToProcess>,
|
|
mesh_allocator: Res<MeshAllocator>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
pipeline: Res<ComputePipeline>,
|
|
render_queue: Res<RenderQueue>,
|
|
) {
|
|
let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) else {
|
|
return;
|
|
};
|
|
|
|
for mesh_id in &chunks.0 {
|
|
info!(?mesh_id, "processing mesh");
|
|
|
|
// the mesh_allocator holds slabs of meshes, so the buffers we get here
|
|
// can contain more data than just the mesh we're asking for.
|
|
// That's why there is a range field.
|
|
// You should *not* touch data in these buffers that is outside of the range.
|
|
let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(mesh_id).unwrap();
|
|
let index_buffer_slice = mesh_allocator.mesh_index_slice(mesh_id).unwrap();
|
|
|
|
let first = DataRanges {
|
|
// there are 8 vertex data values (pos, normal, uv) per vertex
|
|
// and the vertex_buffer_slice.range.start is in "vertex elements"
|
|
// which includes all of that data, so each index is worth 8 indices
|
|
// to our shader code.
|
|
vertex_start: vertex_buffer_slice.range.start * 8,
|
|
vertex_end: vertex_buffer_slice.range.end * 8,
|
|
// but each vertex index is a single value, so the index of the
|
|
// vertex indices is exactly what the value is
|
|
index_start: index_buffer_slice.range.start,
|
|
index_end: index_buffer_slice.range.end,
|
|
};
|
|
|
|
let mut uniforms = UniformBuffer::from(first);
|
|
uniforms.write_buffer(render_context.render_device(), &render_queue);
|
|
|
|
// pass in the full mesh_allocator slabs as well as the first index
|
|
// offsets for the vertex and index buffers
|
|
let bind_group = render_context.render_device().create_bind_group(
|
|
None,
|
|
&pipeline_cache.get_bind_group_layout(&pipeline.layout),
|
|
&BindGroupEntries::sequential((
|
|
&uniforms,
|
|
vertex_buffer_slice.buffer.as_entire_buffer_binding(),
|
|
index_buffer_slice.buffer.as_entire_buffer_binding(),
|
|
)),
|
|
);
|
|
|
|
let mut pass =
|
|
render_context
|
|
.command_encoder()
|
|
.begin_compute_pass(&ComputePassDescriptor {
|
|
label: Some("Mesh generation compute pass"),
|
|
..default()
|
|
});
|
|
pass.push_debug_group("compute_mesh");
|
|
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.set_pipeline(init_pipeline);
|
|
// we only dispatch 1,1,1 workgroup here, but a real compute shader
|
|
// would take advantage of more and larger size workgroups
|
|
pass.dispatch_workgroups(1, 1, 1);
|
|
|
|
pass.pop_debug_group();
|
|
}
|
|
}
|