Use GpuArrayBuffer for MeshUniform (#9254)

# Objective

- Reduce the number of rebindings to enable batching of draw commands

## Solution

- Use the new `GpuArrayBuffer` for `MeshUniform` data to store all
`MeshUniform` data in arrays within fewer bindings
- Sort opaque/alpha mask prepass, opaque/alpha mask main, and shadow
phases also by the batch per-object data binding dynamic offset to
improve performance on WebGL2.

---

## Changelog

- Changed: Per-object `MeshUniform` data is now managed by
`GpuArrayBuffer` as arrays in buffers that need to be indexed into.

## Migration Guide

Accessing the `model` member of an individual mesh object's shader
`Mesh` struct the old way where each `MeshUniform` was stored at its own
dynamic offset:
```rust
struct Vertex {
    @location(0) position: vec3<f32>,
};

fn vertex(vertex: Vertex) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = mesh_position_local_to_clip(
        mesh.model,
        vec4<f32>(vertex.position, 1.0)
    );
    return out;
}
```

The new way where one needs to index into the array of `Mesh`es for the
batch:
```rust
struct Vertex {
    @builtin(instance_index) instance_index: u32,
    @location(0) position: vec3<f32>,
};

fn vertex(vertex: Vertex) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = mesh_position_local_to_clip(
        mesh[vertex.instance_index].model,
        vec4<f32>(vertex.position, 1.0)
    );
    return out;
}
```
Note that using the instance_index is the default way to pass the
per-object index into the shader, but if you wish to do custom rendering
approaches you can pass it in however you like.

---------

Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
Co-authored-by: Elabajaba <Elabajaba@users.noreply.github.com>
This commit is contained in:
Robert Swain 2023-07-30 15:17:08 +02:00 committed by GitHub
parent fb9c5a6cbb
commit e6405bb7b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 335 additions and 151 deletions

View file

@ -8,6 +8,7 @@ struct CustomMaterial {
var<uniform> material: CustomMaterial; var<uniform> material: CustomMaterial;
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
@location(1) blend_color: vec4<f32>, @location(1) blend_color: vec4<f32>,
}; };
@ -21,8 +22,8 @@ struct VertexOutput {
fn vertex(vertex: Vertex) -> VertexOutput { fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.clip_position = mesh_position_local_to_clip( out.clip_position = mesh_position_local_to_clip(
mesh.model, mesh[vertex.instance_index].model,
vec4<f32>(vertex.position, 1.0) vec4<f32>(vertex.position, 1.0),
); );
out.blend_color = vertex.blend_color; out.blend_color = vertex.blend_color;
return out; return out;

View file

@ -19,8 +19,12 @@ struct VertexOutput {
fn vertex(vertex: Vertex) -> VertexOutput { fn vertex(vertex: Vertex) -> VertexOutput {
let position = vertex.position * vertex.i_pos_scale.w + vertex.i_pos_scale.xyz; let position = vertex.position * vertex.i_pos_scale.w + vertex.i_pos_scale.xyz;
var out: VertexOutput; var out: VertexOutput;
// NOTE: The 0 index into the Mesh array is a hack for this example as the
// instance_index builtin would map to the wrong index in the Mesh array.
// This index could be passed in via another uniform instead but it's
// unnecessary for the example.
out.clip_position = mesh_position_local_to_clip( out.clip_position = mesh_position_local_to_clip(
mesh.model, mesh[0].model,
vec4<f32>(position, 1.0) vec4<f32>(position, 1.0)
); );
out.color = vertex.i_color; out.color = vertex.i_color;

View file

@ -136,14 +136,18 @@ impl Plugin for Core3dPlugin {
pub struct Opaque3d { pub struct Opaque3d {
pub distance: f32, pub distance: f32,
// Per-object data may be bound at different dynamic offsets within a buffer. If it is, then
// each batch of per-object data starts at the same dynamic offset.
pub per_object_binding_dynamic_offset: u32,
pub pipeline: CachedRenderPipelineId, pub pipeline: CachedRenderPipelineId,
pub entity: Entity, pub entity: Entity,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
} }
impl PhaseItem for Opaque3d { impl PhaseItem for Opaque3d {
// NOTE: (dynamic offset, -distance)
// NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort. // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort.
type SortKey = Reverse<FloatOrd>; type SortKey = (u32, Reverse<FloatOrd>);
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
@ -152,7 +156,10 @@ impl PhaseItem for Opaque3d {
#[inline] #[inline]
fn sort_key(&self) -> Self::SortKey { fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance)) (
self.per_object_binding_dynamic_offset,
Reverse(FloatOrd(self.distance)),
)
} }
#[inline] #[inline]
@ -163,7 +170,9 @@ impl PhaseItem for Opaque3d {
#[inline] #[inline]
fn sort(items: &mut [Self]) { fn sort(items: &mut [Self]) {
// Key negated to match reversed SortKey ordering // Key negated to match reversed SortKey ordering
radsort::sort_by_key(items, |item| -item.distance); radsort::sort_by_key(items, |item| {
(item.per_object_binding_dynamic_offset, -item.distance)
});
} }
} }
@ -176,14 +185,18 @@ impl CachedRenderPipelinePhaseItem for Opaque3d {
pub struct AlphaMask3d { pub struct AlphaMask3d {
pub distance: f32, pub distance: f32,
// Per-object data may be bound at different dynamic offsets within a buffer. If it is, then
// each batch of per-object data starts at the same dynamic offset.
pub per_object_binding_dynamic_offset: u32,
pub pipeline: CachedRenderPipelineId, pub pipeline: CachedRenderPipelineId,
pub entity: Entity, pub entity: Entity,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
} }
impl PhaseItem for AlphaMask3d { impl PhaseItem for AlphaMask3d {
// NOTE: (dynamic offset, -distance)
// NOTE: Values increase towards the camera. Front-to-back ordering for alpha mask means we need a descending sort. // NOTE: Values increase towards the camera. Front-to-back ordering for alpha mask means we need a descending sort.
type SortKey = Reverse<FloatOrd>; type SortKey = (u32, Reverse<FloatOrd>);
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
@ -192,7 +205,10 @@ impl PhaseItem for AlphaMask3d {
#[inline] #[inline]
fn sort_key(&self) -> Self::SortKey { fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance)) (
self.per_object_binding_dynamic_offset,
Reverse(FloatOrd(self.distance)),
)
} }
#[inline] #[inline]
@ -203,7 +219,9 @@ impl PhaseItem for AlphaMask3d {
#[inline] #[inline]
fn sort(items: &mut [Self]) { fn sort(items: &mut [Self]) {
// Key negated to match reversed SortKey ordering // Key negated to match reversed SortKey ordering
radsort::sort_by_key(items, |item| -item.distance); radsort::sort_by_key(items, |item| {
(item.per_object_binding_dynamic_offset, -item.distance)
});
} }
} }

View file

@ -80,14 +80,18 @@ pub struct ViewPrepassTextures {
/// Used to render all 3D meshes with materials that have no transparency. /// Used to render all 3D meshes with materials that have no transparency.
pub struct Opaque3dPrepass { pub struct Opaque3dPrepass {
pub distance: f32, pub distance: f32,
// Per-object data may be bound at different dynamic offsets within a buffer. If it is, then
// each batch of per-object data starts at the same dynamic offset.
pub per_object_binding_dynamic_offset: u32,
pub entity: Entity, pub entity: Entity,
pub pipeline_id: CachedRenderPipelineId, pub pipeline_id: CachedRenderPipelineId,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
} }
impl PhaseItem for Opaque3dPrepass { impl PhaseItem for Opaque3dPrepass {
// NOTE: (dynamic offset, -distance)
// NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort. // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort.
type SortKey = Reverse<FloatOrd>; type SortKey = (u32, Reverse<FloatOrd>);
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
@ -96,7 +100,10 @@ impl PhaseItem for Opaque3dPrepass {
#[inline] #[inline]
fn sort_key(&self) -> Self::SortKey { fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance)) (
self.per_object_binding_dynamic_offset,
Reverse(FloatOrd(self.distance)),
)
} }
#[inline] #[inline]
@ -107,7 +114,9 @@ impl PhaseItem for Opaque3dPrepass {
#[inline] #[inline]
fn sort(items: &mut [Self]) { fn sort(items: &mut [Self]) {
// Key negated to match reversed SortKey ordering // Key negated to match reversed SortKey ordering
radsort::sort_by_key(items, |item| -item.distance); radsort::sort_by_key(items, |item| {
(item.per_object_binding_dynamic_offset, -item.distance)
});
} }
} }
@ -125,14 +134,18 @@ impl CachedRenderPipelinePhaseItem for Opaque3dPrepass {
/// Used to render all meshes with a material with an alpha mask. /// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dPrepass { pub struct AlphaMask3dPrepass {
pub distance: f32, pub distance: f32,
// Per-object data may be bound at different dynamic offsets within a buffer. If it is, then
// each batch of per-object data starts at the same dynamic offset.
pub per_object_binding_dynamic_offset: u32,
pub entity: Entity, pub entity: Entity,
pub pipeline_id: CachedRenderPipelineId, pub pipeline_id: CachedRenderPipelineId,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
} }
impl PhaseItem for AlphaMask3dPrepass { impl PhaseItem for AlphaMask3dPrepass {
// NOTE: Values increase towards the camera. Front-to-back ordering for alpha mask means we need a descending sort. // NOTE: (dynamic offset, -distance)
type SortKey = Reverse<FloatOrd>; // NOTE: Values increase towards the camera. Front-to-back ordering for opaque means we need a descending sort.
type SortKey = (u32, Reverse<FloatOrd>);
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
@ -141,7 +154,10 @@ impl PhaseItem for AlphaMask3dPrepass {
#[inline] #[inline]
fn sort_key(&self) -> Self::SortKey { fn sort_key(&self) -> Self::SortKey {
Reverse(FloatOrd(self.distance)) (
self.per_object_binding_dynamic_offset,
Reverse(FloatOrd(self.distance)),
)
} }
#[inline] #[inline]
@ -152,7 +168,9 @@ impl PhaseItem for AlphaMask3dPrepass {
#[inline] #[inline]
fn sort(items: &mut [Self]) { fn sort(items: &mut [Self]) {
// Key negated to match reversed SortKey ordering // Key negated to match reversed SortKey ordering
radsort::sort_by_key(items, |item| -item.distance); radsort::sort_by_key(items, |item| {
(item.per_object_binding_dynamic_offset, -item.distance)
});
} }
} }

View file

@ -29,5 +29,6 @@ bevy_derive = { path = "../bevy_derive", version = "0.12.0-dev" }
bitflags = "2.3" bitflags = "2.3"
# direct dependency required for derive macro # direct dependency required for derive macro
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
radsort = "0.1"
naga_oil = "0.8" naga_oil = "0.8"
radsort = "0.1"
smallvec = "1.6"

View file

@ -30,9 +30,9 @@ use bevy_render::{
RenderPhase, SetItemPipeline, TrackedRenderPass, RenderPhase, SetItemPipeline, TrackedRenderPass,
}, },
render_resource::{ render_resource::{
AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource, AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, GpuArrayBufferIndex,
PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef,
SpecializedMeshPipelineError, SpecializedMeshPipelines, SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
}, },
renderer::RenderDevice, renderer::RenderDevice,
texture::FallbackImage, texture::FallbackImage,
@ -379,7 +379,12 @@ pub fn queue_material_meshes<M: Material>(
msaa: Res<Msaa>, msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<Mesh>>, render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderMaterials<M>>, render_materials: Res<RenderMaterials<M>>,
material_meshes: Query<(&Handle<M>, &Handle<Mesh>, &MeshUniform)>, material_meshes: Query<(
&Handle<M>,
&Handle<Mesh>,
&MeshUniform,
&GpuArrayBufferIndex<MeshUniform>,
)>,
images: Res<RenderAssets<Image>>, images: Res<RenderAssets<Image>>,
mut views: Query<( mut views: Query<(
&ExtractedView, &ExtractedView,
@ -463,7 +468,7 @@ pub fn queue_material_meshes<M: Material>(
let rangefinder = view.rangefinder3d(); let rangefinder = view.rangefinder3d();
for visible_entity in &visible_entities.entities { for visible_entity in &visible_entities.entities {
if let Ok((material_handle, mesh_handle, mesh_uniform)) = if let Ok((material_handle, mesh_handle, mesh_uniform, batch_indices)) =
material_meshes.get(*visible_entity) material_meshes.get(*visible_entity)
{ {
if let (Some(mesh), Some(material)) = ( if let (Some(mesh), Some(material)) = (
@ -520,6 +525,9 @@ pub fn queue_material_meshes<M: Material>(
draw_function: draw_opaque_pbr, draw_function: draw_opaque_pbr,
pipeline: pipeline_id, pipeline: pipeline_id,
distance, distance,
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
AlphaMode::Mask(_) => { AlphaMode::Mask(_) => {
@ -528,6 +536,9 @@ pub fn queue_material_meshes<M: Material>(
draw_function: draw_alpha_mask_pbr, draw_function: draw_alpha_mask_pbr,
pipeline: pipeline_id, pipeline: pipeline_id,
distance, distance,
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
AlphaMode::Blend AlphaMode::Blend

View file

@ -30,9 +30,9 @@ use bevy_render::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType,
ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState,
DynamicUniformBuffer, FragmentState, FrontFace, MultisampleState, PipelineCache, DynamicUniformBuffer, FragmentState, FrontFace, GpuArrayBufferIndex, MultisampleState,
PolygonMode, PrimitiveState, RenderPipelineDescriptor, Shader, ShaderRef, ShaderStages, PipelineCache, PolygonMode, PrimitiveState, RenderPipelineDescriptor, Shader, ShaderRef,
ShaderType, SpecializedMeshPipeline, SpecializedMeshPipelineError, ShaderStages, ShaderType, SpecializedMeshPipeline, SpecializedMeshPipelineError,
SpecializedMeshPipelines, StencilFaceState, StencilState, TextureSampleType, SpecializedMeshPipelines, StencilFaceState, StencilState, TextureSampleType,
TextureViewDimension, VertexState, TextureViewDimension, VertexState,
}, },
@ -751,7 +751,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
msaa: Res<Msaa>, msaa: Res<Msaa>,
render_meshes: Res<RenderAssets<Mesh>>, render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderMaterials<M>>, render_materials: Res<RenderMaterials<M>>,
material_meshes: Query<(&Handle<M>, &Handle<Mesh>, &MeshUniform)>, material_meshes: Query<(
&Handle<M>,
&Handle<Mesh>,
&MeshUniform,
&GpuArrayBufferIndex<MeshUniform>,
)>,
mut views: Query<( mut views: Query<(
&ExtractedView, &ExtractedView,
&VisibleEntities, &VisibleEntities,
@ -796,7 +801,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
let rangefinder = view.rangefinder3d(); let rangefinder = view.rangefinder3d();
for visible_entity in &visible_entities.entities { for visible_entity in &visible_entities.entities {
let Ok((material_handle, mesh_handle, mesh_uniform)) = material_meshes.get(*visible_entity) else { let Ok((material_handle, mesh_handle, mesh_uniform, batch_indices)) = material_meshes.get(*visible_entity) else {
continue; continue;
}; };
@ -848,6 +853,9 @@ pub fn queue_prepass_material_meshes<M: Material>(
draw_function: opaque_draw_prepass, draw_function: opaque_draw_prepass,
pipeline_id, pipeline_id,
distance, distance,
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
AlphaMode::Mask(_) => { AlphaMode::Mask(_) => {
@ -856,6 +864,9 @@ pub fn queue_prepass_material_meshes<M: Material>(
draw_function: alpha_mask_draw_prepass, draw_function: alpha_mask_draw_prepass,
pipeline_id, pipeline_id,
distance, distance,
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
AlphaMode::Blend AlphaMode::Blend

View file

@ -7,6 +7,7 @@
// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can // Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can
// pass them to custom prepass shaders like pbr_prepass.wgsl. // pass them to custom prepass shaders like pbr_prepass.wgsl.
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
#ifdef VERTEX_UVS #ifdef VERTEX_UVS
@ -88,7 +89,9 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
#ifdef SKINNED #ifdef SKINNED
var model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights); var model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
#else // SKINNED #else // SKINNED
var model = mesh.model; // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
var model = mesh[vertex_no_morph.instance_index].model;
#endif // SKINNED #endif // SKINNED
out.clip_position = bevy_pbr::mesh_functions::mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); out.clip_position = bevy_pbr::mesh_functions::mesh_position_local_to_clip(model, vec4(vertex.position, 1.0));
@ -105,17 +108,33 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
#ifdef SKINNED #ifdef SKINNED
out.world_normal = bevy_pbr::skinning::skin_normals(model, vertex.normal); out.world_normal = bevy_pbr::skinning::skin_normals(model, vertex.normal);
#else // SKINNED #else // SKINNED
out.world_normal = bevy_pbr::mesh_functions::mesh_normal_local_to_world(vertex.normal); out.world_normal = bevy_pbr::mesh_functions::mesh_normal_local_to_world(
vertex.normal,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif // SKINNED #endif // SKINNED
#ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS
out.world_tangent = bevy_pbr::mesh_functions::mesh_tangent_local_to_world(model, vertex.tangent); out.world_tangent = bevy_pbr::mesh_functions::mesh_tangent_local_to_world(
model,
vertex.tangent,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif // VERTEX_TANGENTS #endif // VERTEX_TANGENTS
#endif // NORMAL_PREPASS #endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS #ifdef MOTION_VECTOR_PREPASS
out.world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world(model, vec4<f32>(vertex.position, 1.0)); out.world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world(model, vec4<f32>(vertex.position, 1.0));
out.previous_world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world(mesh.previous_model, vec4<f32>(vertex.position, 1.0)); // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
out.previous_world_position = bevy_pbr::mesh_functions::mesh_position_local_to_world(
mesh[vertex_no_morph.instance_index].previous_model,
vec4<f32>(vertex.position, 1.0)
);
#endif // MOTION_VECTOR_PREPASS #endif // MOTION_VECTOR_PREPASS
return out; return out;

View file

@ -15,5 +15,4 @@ var<uniform> previous_view_proj: mat4x4<f32>;
// Material bindings will be in @group(1) // Material bindings will be in @group(1)
@group(2) @binding(0) #import bevy_pbr::mesh_bindings mesh
var<uniform> mesh: bevy_pbr::mesh_types::Mesh;

View file

@ -3,8 +3,8 @@ use crate::{
CascadeShadowConfig, Cascades, CascadesVisibleEntities, Clusters, CubemapVisibleEntities, CascadeShadowConfig, Cascades, CascadesVisibleEntities, Clusters, CubemapVisibleEntities,
DirectionalLight, DirectionalLightShadowMap, DrawPrepass, EnvironmentMapLight, DirectionalLight, DirectionalLightShadowMap, DrawPrepass, EnvironmentMapLight,
GlobalVisiblePointLights, Material, MaterialPipelineKey, MeshPipeline, MeshPipelineKey, GlobalVisiblePointLights, Material, MaterialPipelineKey, MeshPipeline, MeshPipelineKey,
NotShadowCaster, PointLight, PointLightShadowMap, PrepassPipeline, RenderMaterials, SpotLight, MeshUniform, NotShadowCaster, PointLight, PointLightShadowMap, PrepassPipeline,
VisiblePointLights, RenderMaterials, SpotLight, VisiblePointLights,
}; };
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_core_pipeline::core_3d::Transparent3d; use bevy_core_pipeline::core_3d::Transparent3d;
@ -1556,7 +1556,10 @@ pub fn prepare_clusters(
pub fn queue_shadows<M: Material>( pub fn queue_shadows<M: Material>(
shadow_draw_functions: Res<DrawFunctions<Shadow>>, shadow_draw_functions: Res<DrawFunctions<Shadow>>,
prepass_pipeline: Res<PrepassPipeline<M>>, prepass_pipeline: Res<PrepassPipeline<M>>,
casting_meshes: Query<(&Handle<Mesh>, &Handle<M>), Without<NotShadowCaster>>, casting_meshes: Query<
(&GpuArrayBufferIndex<MeshUniform>, &Handle<Mesh>, &Handle<M>),
Without<NotShadowCaster>,
>,
render_meshes: Res<RenderAssets<Mesh>>, render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderMaterials<M>>, render_materials: Res<RenderMaterials<M>>,
mut pipelines: ResMut<SpecializedMeshPipelines<PrepassPipeline<M>>>, mut pipelines: ResMut<SpecializedMeshPipelines<PrepassPipeline<M>>>,
@ -1601,7 +1604,9 @@ pub fn queue_shadows<M: Material>(
// NOTE: Lights with shadow mapping disabled will have no visible entities // NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued // so no meshes will be queued
for entity in visible_entities.iter().copied() { for entity in visible_entities.iter().copied() {
if let Ok((mesh_handle, material_handle)) = casting_meshes.get(entity) { if let Ok((batch_indices, mesh_handle, material_handle)) =
casting_meshes.get(entity)
{
if let (Some(mesh), Some(material)) = ( if let (Some(mesh), Some(material)) = (
render_meshes.get(mesh_handle), render_meshes.get(mesh_handle),
render_materials.get(material_handle), render_materials.get(material_handle),
@ -1647,7 +1652,10 @@ pub fn queue_shadows<M: Material>(
draw_function: draw_shadow_mesh, draw_function: draw_shadow_mesh,
pipeline: pipeline_id, pipeline: pipeline_id,
entity, entity,
distance: 0.0, // TODO: sort back-to-front distance: 0.0, // TODO: sort front-to-back
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
} }
@ -1658,13 +1666,16 @@ pub fn queue_shadows<M: Material>(
pub struct Shadow { pub struct Shadow {
pub distance: f32, pub distance: f32,
// Per-object data may be bound at different dynamic offsets within a buffer. If it is, then
// each batch of per-object data starts at the same dynamic offset.
pub per_object_binding_dynamic_offset: u32,
pub entity: Entity, pub entity: Entity,
pub pipeline: CachedRenderPipelineId, pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
} }
impl PhaseItem for Shadow { impl PhaseItem for Shadow {
type SortKey = usize; type SortKey = (usize, u32);
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
@ -1673,7 +1684,7 @@ impl PhaseItem for Shadow {
#[inline] #[inline]
fn sort_key(&self) -> Self::SortKey { fn sort_key(&self) -> Self::SortKey {
self.pipeline.id() (self.pipeline.id(), self.per_object_binding_dynamic_offset)
} }
#[inline] #[inline]
@ -1686,7 +1697,7 @@ impl PhaseItem for Shadow {
// The shadow phase is sorted by pipeline id for performance reasons. // The shadow phase is sorted by pipeline id for performance reasons.
// Grouping all draw commands using the same pipeline together performs // Grouping all draw commands using the same pipeline together performs
// better than rebinding everything at a high rate. // better than rebinding everything at a high rate.
radsort::sort_by_key(items, |item| item.pipeline.id()); radsort::sort_by_key(items, |item| item.sort_key());
} }
} }

View file

@ -21,8 +21,8 @@ use bevy_ecs::{
use bevy_math::{Mat3A, Mat4, Vec2}; use bevy_math::{Mat3A, Mat4, Vec2};
use bevy_reflect::TypeUuid; use bevy_reflect::TypeUuid;
use bevy_render::{ use bevy_render::{
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
globals::{GlobalsBuffer, GlobalsUniform}, globals::{GlobalsBuffer, GlobalsUniform},
gpu_component_array_buffer::GpuComponentArrayBufferPlugin,
mesh::{ mesh::{
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
GpuBufferInfo, InnerMeshVertexBufferLayout, Mesh, MeshVertexBufferLayout, GpuBufferInfo, InnerMeshVertexBufferLayout, Mesh, MeshVertexBufferLayout,
@ -105,12 +105,6 @@ impl Plugin for MeshRenderPlugin {
Shader::from_wgsl Shader::from_wgsl
); );
load_internal_asset!(app, MESH_TYPES_HANDLE, "mesh_types.wgsl", Shader::from_wgsl); load_internal_asset!(app, MESH_TYPES_HANDLE, "mesh_types.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
MESH_BINDINGS_HANDLE,
"mesh_bindings.wgsl",
Shader::from_wgsl
);
load_internal_asset!( load_internal_asset!(
app, app,
MESH_FUNCTIONS_HANDLE, MESH_FUNCTIONS_HANDLE,
@ -121,7 +115,7 @@ impl Plugin for MeshRenderPlugin {
load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl);
load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl); load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl);
app.add_plugins(UniformComponentPlugin::<MeshUniform>::default()); app.add_plugins(GpuComponentArrayBufferPlugin::<MeshUniform>::default());
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app
@ -145,9 +139,30 @@ impl Plugin for MeshRenderPlugin {
} }
fn finish(&self, app: &mut bevy_app::App) { fn finish(&self, app: &mut bevy_app::App) {
let mut mesh_bindings_shader_defs = Vec::with_capacity(1);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
if let Some(per_object_buffer_batch_size) = GpuArrayBuffer::<MeshUniform>::batch_size(
render_app.world.resource::<RenderDevice>(),
) {
mesh_bindings_shader_defs.push(ShaderDefVal::UInt(
"PER_OBJECT_BUFFER_BATCH_SIZE".into(),
per_object_buffer_batch_size,
));
}
render_app.init_resource::<MeshPipeline>(); render_app.init_resource::<MeshPipeline>();
} }
// Load the mesh_bindings shader module here as it depends on runtime information about
// whether storage buffers are supported, or the maximum uniform buffer binding size.
load_internal_asset!(
app,
MESH_BINDINGS_HANDLE,
"mesh_bindings.wgsl",
Shader::from_wgsl_with_defs,
mesh_bindings_shader_defs
);
} }
} }
@ -309,7 +324,6 @@ pub struct MeshPipeline {
// This dummy white texture is to be used in place of optional StandardMaterial textures // This dummy white texture is to be used in place of optional StandardMaterial textures
pub dummy_white_gpu_image: GpuImage, pub dummy_white_gpu_image: GpuImage,
pub clustered_forward_buffer_binding_type: BufferBindingType, pub clustered_forward_buffer_binding_type: BufferBindingType,
pub mesh_layouts: MeshLayouts, pub mesh_layouts: MeshLayouts,
} }
@ -709,6 +723,8 @@ impl SpecializedMeshPipeline for MeshPipeline {
let mut shader_defs = Vec::new(); let mut shader_defs = Vec::new();
let mut vertex_attributes = Vec::new(); let mut vertex_attributes = Vec::new();
shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into());
if layout.contains(Mesh::ATTRIBUTE_POSITION) { if layout.contains(Mesh::ATTRIBUTE_POSITION) {
shader_defs.push("VERTEX_POSITIONS".into()); shader_defs.push("VERTEX_POSITIONS".into());
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
@ -932,29 +948,29 @@ pub fn queue_mesh_bind_group(
mut groups: ResMut<MeshBindGroups>, mut groups: ResMut<MeshBindGroups>,
mesh_pipeline: Res<MeshPipeline>, mesh_pipeline: Res<MeshPipeline>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
mesh_uniforms: Res<ComponentUniforms<MeshUniform>>, mesh_uniforms: Res<GpuArrayBuffer<MeshUniform>>,
skinned_mesh_uniform: Res<SkinnedMeshUniform>, skinned_mesh_uniform: Res<SkinnedMeshUniform>,
weights_uniform: Res<MorphUniform>, weights_uniform: Res<MorphUniform>,
) { ) {
groups.reset(); groups.reset();
let layouts = &mesh_pipeline.mesh_layouts; let layouts = &mesh_pipeline.mesh_layouts;
let Some(model) = mesh_uniforms.buffer() else { let Some(model) = mesh_uniforms.binding() else {
return; return;
}; };
groups.model_only = Some(layouts.model_only(&render_device, model)); groups.model_only = Some(layouts.model_only(&render_device, &model));
let skin = skinned_mesh_uniform.buffer.buffer(); let skin = skinned_mesh_uniform.buffer.buffer();
if let Some(skin) = skin { if let Some(skin) = skin {
groups.skinned = Some(layouts.skinned(&render_device, model, skin)); groups.skinned = Some(layouts.skinned(&render_device, &model, skin));
} }
if let Some(weights) = weights_uniform.buffer.buffer() { if let Some(weights) = weights_uniform.buffer.buffer() {
for (id, gpu_mesh) in meshes.iter() { for (id, gpu_mesh) in meshes.iter() {
if let Some(targets) = gpu_mesh.morph_targets.as_ref() { if let Some(targets) = gpu_mesh.morph_targets.as_ref() {
let group = if let Some(skin) = skin.filter(|_| is_skinned(&gpu_mesh.layout)) { let group = if let Some(skin) = skin.filter(|_| is_skinned(&gpu_mesh.layout)) {
layouts.morphed_skinned(&render_device, model, skin, weights, targets) layouts.morphed_skinned(&render_device, &model, skin, weights, targets)
} else { } else {
layouts.morphed(&render_device, model, weights, targets) layouts.morphed(&render_device, &model, weights, targets)
}; };
groups.morph_targets.insert(id.id(), group); groups.morph_targets.insert(id.id(), group);
} }
@ -1198,7 +1214,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
type ViewWorldQuery = (); type ViewWorldQuery = ();
type ItemWorldQuery = ( type ItemWorldQuery = (
Read<Handle<Mesh>>, Read<Handle<Mesh>>,
Read<DynamicUniformIndex<MeshUniform>>, Read<GpuArrayBufferIndex<MeshUniform>>,
Option<Read<SkinnedMeshJoints>>, Option<Read<SkinnedMeshJoints>>,
Option<Read<MorphIndex>>, Option<Read<MorphIndex>>,
); );
@ -1207,7 +1223,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
fn render<'w>( fn render<'w>(
_item: &P, _item: &P,
_view: (), _view: (),
(mesh, mesh_index, skin_index, morph_index): ROQueryItem<Self::ItemWorldQuery>, (mesh, batch_indices, skin_index, morph_index): ROQueryItem<Self::ItemWorldQuery>,
bind_groups: SystemParamItem<'w, '_, Self::Param>, bind_groups: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>, pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult { ) -> RenderCommandResult {
@ -1223,14 +1239,23 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
); );
return RenderCommandResult::Failure; return RenderCommandResult::Failure;
}; };
let mut set_bind_group = |indices: &[u32]| pass.set_bind_group(I, bind_group, indices);
let mesh_index = mesh_index.index(); let mut dynamic_offsets: [u32; 3] = Default::default();
match (skin_index, morph_index) { let mut index_count = 0;
(None, None) => set_bind_group(&[mesh_index]), if let Some(mesh_index) = batch_indices.dynamic_offset {
(Some(skin), None) => set_bind_group(&[mesh_index, skin.index]), dynamic_offsets[index_count] = mesh_index;
(None, Some(morph)) => set_bind_group(&[mesh_index, morph.index]), index_count += 1;
(Some(skin), Some(morph)) => set_bind_group(&[mesh_index, skin.index, morph.index]), }
}; if let Some(skin_index) = skin_index {
dynamic_offsets[index_count] = skin_index.index;
index_count += 1;
}
if let Some(morph_index) = morph_index {
dynamic_offsets[index_count] = morph_index.index;
index_count += 1;
}
pass.set_bind_group(I, bind_group, &dynamic_offsets[0..index_count]);
RenderCommandResult::Success RenderCommandResult::Success
} }
} }
@ -1239,12 +1264,12 @@ pub struct DrawMesh;
impl<P: PhaseItem> RenderCommand<P> for DrawMesh { impl<P: PhaseItem> RenderCommand<P> for DrawMesh {
type Param = SRes<RenderAssets<Mesh>>; type Param = SRes<RenderAssets<Mesh>>;
type ViewWorldQuery = (); type ViewWorldQuery = ();
type ItemWorldQuery = Read<Handle<Mesh>>; type ItemWorldQuery = (Read<GpuArrayBufferIndex<MeshUniform>>, Read<Handle<Mesh>>);
#[inline] #[inline]
fn render<'w>( fn render<'w>(
_item: &P, _item: &P,
_view: (), _view: (),
mesh_handle: ROQueryItem<'_, Self::ItemWorldQuery>, (batch_indices, mesh_handle): ROQueryItem<'_, Self::ItemWorldQuery>,
meshes: SystemParamItem<'w, '_, Self::Param>, meshes: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>, pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult { ) -> RenderCommandResult {
@ -1257,10 +1282,13 @@ impl<P: PhaseItem> RenderCommand<P> for DrawMesh {
count, count,
} => { } => {
pass.set_index_buffer(buffer.slice(..), 0, *index_format); pass.set_index_buffer(buffer.slice(..), 0, *index_format);
pass.draw_indexed(0..*count, 0, 0..1); pass.draw_indexed(0..*count, 0, batch_indices.index..batch_indices.index + 1);
} }
GpuBufferInfo::NonIndexed => { GpuBufferInfo::NonIndexed => {
pass.draw(0..gpu_mesh.vertex_count, 0..1); pass.draw(
0..gpu_mesh.vertex_count,
batch_indices.index..batch_indices.index + 1,
);
} }
} }
RenderCommandResult::Success RenderCommandResult::Success

View file

@ -5,6 +5,7 @@
#import bevy_pbr::mesh_vertex_output MeshVertexOutput #import bevy_pbr::mesh_vertex_output MeshVertexOutput
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32,
#ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
#endif #endif
@ -63,14 +64,21 @@ fn vertex(vertex_no_morph: Vertex) -> MeshVertexOutput {
#ifdef SKINNED #ifdef SKINNED
var model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights); var model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
#else #else
var model = mesh.model; // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
var model = mesh[vertex_no_morph.instance_index].model;
#endif #endif
#ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS
#ifdef SKINNED #ifdef SKINNED
out.world_normal = bevy_pbr::skinning::skin_normals(model, vertex.normal); out.world_normal = bevy_pbr::skinning::skin_normals(model, vertex.normal);
#else #else
out.world_normal = mesh_functions::mesh_normal_local_to_world(vertex.normal); out.world_normal = mesh_functions::mesh_normal_local_to_world(
vertex.normal,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif #endif
#endif #endif
@ -84,13 +92,25 @@ fn vertex(vertex_no_morph: Vertex) -> MeshVertexOutput {
#endif #endif
#ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(model, vertex.tangent); out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
model,
vertex.tangent,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif #endif
#ifdef VERTEX_COLORS #ifdef VERTEX_COLORS
out.color = vertex.color; out.color = vertex.color;
#endif #endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
out.instance_index = vertex_no_morph.instance_index;
#endif
return out; return out;
} }

View file

@ -3,8 +3,8 @@
use bevy_render::{ use bevy_render::{
mesh::morph::MAX_MORPH_WEIGHTS, mesh::morph::MAX_MORPH_WEIGHTS,
render_resource::{ render_resource::{
BindGroup, BindGroupDescriptor, BindGroupLayout, BindGroupLayoutDescriptor, Buffer, BindGroup, BindGroupDescriptor, BindGroupLayout, BindGroupLayoutDescriptor,
TextureView, BindingResource, Buffer, TextureView,
}, },
renderer::RenderDevice, renderer::RenderDevice,
}; };
@ -17,9 +17,12 @@ mod layout_entry {
use super::MORPH_BUFFER_SIZE; use super::MORPH_BUFFER_SIZE;
use crate::render::mesh::JOINT_BUFFER_SIZE; use crate::render::mesh::JOINT_BUFFER_SIZE;
use crate::MeshUniform; use crate::MeshUniform;
use bevy_render::render_resource::{ use bevy_render::{
BindGroupLayoutEntry, BindingType, BufferBindingType, BufferSize, ShaderStages, ShaderType, render_resource::{
TextureSampleType, TextureViewDimension, BindGroupLayoutEntry, BindingType, BufferBindingType, BufferSize, GpuArrayBuffer,
ShaderStages, TextureSampleType, TextureViewDimension,
},
renderer::RenderDevice,
}; };
fn buffer(binding: u32, size: u64, visibility: ShaderStages) -> BindGroupLayoutEntry { fn buffer(binding: u32, size: u64, visibility: ShaderStages) -> BindGroupLayoutEntry {
@ -34,9 +37,12 @@ mod layout_entry {
}, },
} }
} }
pub(super) fn model(binding: u32) -> BindGroupLayoutEntry { pub(super) fn model(render_device: &RenderDevice, binding: u32) -> BindGroupLayoutEntry {
let size = MeshUniform::min_size().get(); GpuArrayBuffer::<MeshUniform>::binding_layout(
buffer(binding, size, ShaderStages::VERTEX | ShaderStages::FRAGMENT) binding,
ShaderStages::VERTEX_FRAGMENT,
render_device,
)
} }
pub(super) fn skinning(binding: u32) -> BindGroupLayoutEntry { pub(super) fn skinning(binding: u32) -> BindGroupLayoutEntry {
buffer(binding, JOINT_BUFFER_SIZE as u64, ShaderStages::VERTEX) buffer(binding, JOINT_BUFFER_SIZE as u64, ShaderStages::VERTEX)
@ -62,9 +68,8 @@ mod layout_entry {
mod entry { mod entry {
use super::MORPH_BUFFER_SIZE; use super::MORPH_BUFFER_SIZE;
use crate::render::mesh::JOINT_BUFFER_SIZE; use crate::render::mesh::JOINT_BUFFER_SIZE;
use crate::MeshUniform;
use bevy_render::render_resource::{ use bevy_render::render_resource::{
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, ShaderType, TextureView, BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, TextureView,
}; };
fn entry(binding: u32, size: u64, buffer: &Buffer) -> BindGroupEntry { fn entry(binding: u32, size: u64, buffer: &Buffer) -> BindGroupEntry {
@ -77,8 +82,8 @@ mod entry {
}), }),
} }
} }
pub(super) fn model(binding: u32, buffer: &Buffer) -> BindGroupEntry { pub(super) fn model(binding: u32, resource: BindingResource) -> BindGroupEntry {
entry(binding, MeshUniform::min_size().get(), buffer) BindGroupEntry { binding, resource }
} }
pub(super) fn skinning(binding: u32, buffer: &Buffer) -> BindGroupEntry { pub(super) fn skinning(binding: u32, buffer: &Buffer) -> BindGroupEntry {
entry(binding, JOINT_BUFFER_SIZE as u64, buffer) entry(binding, JOINT_BUFFER_SIZE as u64, buffer)
@ -132,20 +137,23 @@ impl MeshLayouts {
fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayout { fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[layout_entry::model(0)], entries: &[layout_entry::model(render_device, 0)],
label: Some("mesh_layout"), label: Some("mesh_layout"),
}) })
} }
fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[layout_entry::model(0), layout_entry::skinning(1)], entries: &[
layout_entry::model(render_device, 0),
layout_entry::skinning(1),
],
label: Some("skinned_mesh_layout"), label: Some("skinned_mesh_layout"),
}) })
} }
fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout { fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
layout_entry::model(0), layout_entry::model(render_device, 0),
layout_entry::weights(2), layout_entry::weights(2),
layout_entry::targets(3), layout_entry::targets(3),
], ],
@ -155,7 +163,7 @@ impl MeshLayouts {
fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
layout_entry::model(0), layout_entry::model(render_device, 0),
layout_entry::skinning(1), layout_entry::skinning(1),
layout_entry::weights(2), layout_entry::weights(2),
layout_entry::targets(3), layout_entry::targets(3),
@ -166,9 +174,9 @@ impl MeshLayouts {
// ---------- BindGroup methods ---------- // ---------- BindGroup methods ----------
pub fn model_only(&self, render_device: &RenderDevice, model: &Buffer) -> BindGroup { pub fn model_only(&self, render_device: &RenderDevice, model: &BindingResource) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor { render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model)], entries: &[entry::model(0, model.clone())],
layout: &self.model_only, layout: &self.model_only,
label: Some("model_only_mesh_bind_group"), label: Some("model_only_mesh_bind_group"),
}) })
@ -176,11 +184,11 @@ impl MeshLayouts {
pub fn skinned( pub fn skinned(
&self, &self,
render_device: &RenderDevice, render_device: &RenderDevice,
model: &Buffer, model: &BindingResource,
skin: &Buffer, skin: &Buffer,
) -> BindGroup { ) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor { render_device.create_bind_group(&BindGroupDescriptor {
entries: &[entry::model(0, model), entry::skinning(1, skin)], entries: &[entry::model(0, model.clone()), entry::skinning(1, skin)],
layout: &self.skinned, layout: &self.skinned,
label: Some("skinned_mesh_bind_group"), label: Some("skinned_mesh_bind_group"),
}) })
@ -188,13 +196,13 @@ impl MeshLayouts {
pub fn morphed( pub fn morphed(
&self, &self,
render_device: &RenderDevice, render_device: &RenderDevice,
model: &Buffer, model: &BindingResource,
weights: &Buffer, weights: &Buffer,
targets: &TextureView, targets: &TextureView,
) -> BindGroup { ) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor { render_device.create_bind_group(&BindGroupDescriptor {
entries: &[ entries: &[
entry::model(0, model), entry::model(0, model.clone()),
entry::weights(2, weights), entry::weights(2, weights),
entry::targets(3, targets), entry::targets(3, targets),
], ],
@ -205,14 +213,14 @@ impl MeshLayouts {
pub fn morphed_skinned( pub fn morphed_skinned(
&self, &self,
render_device: &RenderDevice, render_device: &RenderDevice,
model: &Buffer, model: &BindingResource,
skin: &Buffer, skin: &Buffer,
weights: &Buffer, weights: &Buffer,
targets: &TextureView, targets: &TextureView,
) -> BindGroup { ) -> BindGroup {
render_device.create_bind_group(&BindGroupDescriptor { render_device.create_bind_group(&BindGroupDescriptor {
entries: &[ entries: &[
entry::model(0, model), entry::model(0, model.clone()),
entry::skinning(1, skin), entry::skinning(1, skin),
entry::weights(2, weights), entry::weights(2, weights),
entry::targets(3, targets), entry::targets(3, targets),

View file

@ -4,12 +4,22 @@
#ifdef MESH_BINDGROUP_1 #ifdef MESH_BINDGROUP_1
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(1) @binding(0) @group(1) @binding(0)
var<uniform> mesh: Mesh; var<uniform> mesh: array<Mesh, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else #else
@group(1) @binding(0)
var<storage> mesh: array<Mesh>;
#endif // PER_OBJECT_BUFFER_BATCH_SIZE
#else // MESH_BINDGROUP_1
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(2) @binding(0) @group(2) @binding(0)
var<uniform> mesh: Mesh; var<uniform> mesh: array<Mesh, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else
@group(2) @binding(0)
var<storage> mesh: array<Mesh>;
#endif // PER_OBJECT_BUFFER_BATCH_SIZE
#endif #endif // MESH_BINDGROUP_1

View file

@ -20,7 +20,7 @@ fn mesh_position_local_to_clip(model: mat4x4<f32>, vertex_position: vec4<f32>) -
return mesh_position_world_to_clip(world_position); return mesh_position_world_to_clip(world_position);
} }
fn mesh_normal_local_to_world(vertex_normal: vec3<f32>) -> vec3<f32> { fn mesh_normal_local_to_world(vertex_normal: vec3<f32>, instance_index: u32) -> vec3<f32> {
// NOTE: The mikktspace method of normal mapping requires that the world normal is // NOTE: The mikktspace method of normal mapping requires that the world normal is
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity, // and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
@ -29,23 +29,23 @@ fn mesh_normal_local_to_world(vertex_normal: vec3<f32>) -> vec3<f32> {
// http://www.mikktspace.com/ // http://www.mikktspace.com/
return normalize( return normalize(
mat3x3<f32>( mat3x3<f32>(
mesh.inverse_transpose_model[0].xyz, mesh[instance_index].inverse_transpose_model[0].xyz,
mesh.inverse_transpose_model[1].xyz, mesh[instance_index].inverse_transpose_model[1].xyz,
mesh.inverse_transpose_model[2].xyz mesh[instance_index].inverse_transpose_model[2].xyz
) * vertex_normal ) * vertex_normal
); );
} }
// Calculates the sign of the determinant of the 3x3 model matrix based on a // Calculates the sign of the determinant of the 3x3 model matrix based on a
// mesh flag // mesh flag
fn sign_determinant_model_3x3m() -> f32 { fn sign_determinant_model_3x3m(instance_index: u32) -> f32 {
// bool(u32) is false if 0u else true // bool(u32) is false if 0u else true
// f32(bool) is 1.0 if true else 0.0 // f32(bool) is 1.0 if true else 0.0
// * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively // * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively
return f32(bool(mesh.flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0; return f32(bool(mesh[instance_index].flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0;
} }
fn mesh_tangent_local_to_world(model: mat4x4<f32>, vertex_tangent: vec4<f32>) -> vec4<f32> { fn mesh_tangent_local_to_world(model: mat4x4<f32>, vertex_tangent: vec4<f32>, instance_index: u32) -> vec4<f32> {
// NOTE: The mikktspace method of normal mapping requires that the world tangent is // NOTE: The mikktspace method of normal mapping requires that the world tangent is
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity, // and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
@ -62,6 +62,6 @@ fn mesh_tangent_local_to_world(model: mat4x4<f32>, vertex_tangent: vec4<f32>) ->
), ),
// NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for // NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for
// situations such as negative scaling. // situations such as negative scaling.
vertex_tangent.w * sign_determinant_model_3x3m() vertex_tangent.w * sign_determinant_model_3x3m(instance_index)
); );
} }

View file

@ -15,4 +15,7 @@ struct MeshVertexOutput {
#ifdef VERTEX_COLORS #ifdef VERTEX_COLORS
@location(4) color: vec4<f32>, @location(4) color: vec4<f32>,
#endif #endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(5) instance_index: u32,
#endif
} }

View file

@ -138,7 +138,7 @@ fn fragment(
pbr_input.V = V; pbr_input.V = V;
pbr_input.occlusion = occlusion; pbr_input.occlusion = occlusion;
pbr_input.flags = mesh.flags; pbr_input.flags = mesh[in.instance_index].flags;
output_color = pbr_functions::pbr(pbr_input); output_color = pbr_functions::pbr(pbr_input);
} else { } else {

View file

@ -223,7 +223,7 @@ fn pbr(
for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1]; i = i + 1u) { for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1]; i = i + 1u) {
let light_id = clustering::get_light_id(i); let light_id = clustering::get_light_id(i);
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal);
} }
@ -236,7 +236,7 @@ fn pbr(
let light_id = clustering::get_light_id(i); let light_id = clustering::get_light_id(i);
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal); shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal);
} }
@ -248,7 +248,7 @@ fn pbr(
let n_directional_lights = view_bindings::lights.n_directional_lights; let n_directional_lights = view_bindings::lights.n_directional_lights;
for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) {
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z);
} }

View file

@ -6,6 +6,7 @@
#endif #endif
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
#ifdef SKINNED #ifdef SKINNED
@location(4) joint_indexes: vec4<u32>, @location(4) joint_indexes: vec4<u32>,
@ -22,7 +23,7 @@ fn vertex(vertex: Vertex) -> VertexOutput {
#ifdef SKINNED #ifdef SKINNED
let model = bevy_pbr::skinning::skin_model(vertex.joint_indexes, vertex.joint_weights); let model = bevy_pbr::skinning::skin_model(vertex.joint_indexes, vertex.joint_weights);
#else #else
let model = mesh.model; let model = mesh[vertex.instance_index].model;
#endif #endif
var out: VertexOutput; var out: VertexOutput;

View file

@ -7,6 +7,7 @@ use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::std_traits::ReflectDefault;
use bevy_reflect::{Reflect, TypeUuid}; use bevy_reflect::{Reflect, TypeUuid};
use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin}; use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin};
use bevy_render::render_resource::GpuArrayBufferIndex;
use bevy_render::Render; use bevy_render::Render;
use bevy_render::{ use bevy_render::{
extract_resource::{ExtractResource, ExtractResourcePlugin}, extract_resource::{ExtractResource, ExtractResourcePlugin},
@ -117,8 +118,21 @@ fn queue_wireframes(
pipeline_cache: Res<PipelineCache>, pipeline_cache: Res<PipelineCache>,
msaa: Res<Msaa>, msaa: Res<Msaa>,
mut material_meshes: ParamSet<( mut material_meshes: ParamSet<(
Query<(Entity, &Handle<Mesh>, &MeshUniform)>, Query<(
Query<(Entity, &Handle<Mesh>, &MeshUniform), With<Wireframe>>, Entity,
&Handle<Mesh>,
&MeshUniform,
&GpuArrayBufferIndex<MeshUniform>,
)>,
Query<
(
Entity,
&Handle<Mesh>,
&MeshUniform,
&GpuArrayBufferIndex<MeshUniform>,
),
With<Wireframe>,
>,
)>, )>,
mut views: Query<(&ExtractedView, &VisibleEntities, &mut RenderPhase<Opaque3d>)>, mut views: Query<(&ExtractedView, &VisibleEntities, &mut RenderPhase<Opaque3d>)>,
) { ) {
@ -128,17 +142,17 @@ fn queue_wireframes(
let rangefinder = view.rangefinder3d(); let rangefinder = view.rangefinder3d();
let view_key = msaa_key | MeshPipelineKey::from_hdr(view.hdr); let view_key = msaa_key | MeshPipelineKey::from_hdr(view.hdr);
let add_render_phase = let add_render_phase = |(entity, mesh_handle, mesh_uniform, batch_indices): (
|(entity, mesh_handle, mesh_uniform): (Entity, &Handle<Mesh>, &MeshUniform)| { Entity,
&Handle<Mesh>,
&MeshUniform,
&GpuArrayBufferIndex<MeshUniform>,
)| {
if let Some(mesh) = render_meshes.get(mesh_handle) { if let Some(mesh) = render_meshes.get(mesh_handle) {
let key = view_key let key =
| MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); view_key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
let pipeline_id = pipelines.specialize( let pipeline_id =
&pipeline_cache, pipelines.specialize(&pipeline_cache, &wireframe_pipeline, key, &mesh.layout);
&wireframe_pipeline,
key,
&mesh.layout,
);
let pipeline_id = match pipeline_id { let pipeline_id = match pipeline_id {
Ok(id) => id, Ok(id) => id,
Err(err) => { Err(err) => {
@ -151,6 +165,9 @@ fn queue_wireframes(
pipeline: pipeline_id, pipeline: pipeline_id,
draw_function: draw_custom, draw_function: draw_custom,
distance: rangefinder.distance(&mesh_uniform.transform), distance: rangefinder.distance(&mesh_uniform.transform),
per_object_binding_dynamic_offset: batch_indices
.dynamic_offset
.unwrap_or_default(),
}); });
} }
}; };

View file

@ -18,16 +18,20 @@ pub struct GpuComponentArrayBufferPlugin<C: Component + GpuArrayBufferable>(Phan
impl<C: Component + GpuArrayBufferable> Plugin for GpuComponentArrayBufferPlugin<C> { impl<C: Component + GpuArrayBufferable> Plugin for GpuComponentArrayBufferPlugin<C> {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app render_app.add_systems(
.insert_resource(GpuArrayBuffer::<C>::new(
render_app.world.resource::<RenderDevice>(),
))
.add_systems(
Render, Render,
prepare_gpu_component_array_buffers::<C>.in_set(RenderSet::Prepare), prepare_gpu_component_array_buffers::<C>.in_set(RenderSet::Prepare),
); );
} }
} }
fn finish(&self, app: &mut App) {
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.insert_resource(GpuArrayBuffer::<C>::new(
render_app.world.resource::<RenderDevice>(),
));
}
}
} }
impl<C: Component + GpuArrayBufferable> Default for GpuComponentArrayBufferPlugin<C> { impl<C: Component + GpuArrayBufferable> Default for GpuComponentArrayBufferPlugin<C> {