Make visibility range (HLOD) dithering work when prepasses are enabled. (#16286)

Currently, the prepass has no support for visibility ranges, so
artifacts appear when using dithering visibility ranges in conjunction
with a prepass. This patch fixes that problem.

Note that this patch changes the prepass to use sparse bind group
indices instead of sequential ones. I figured this is cleaner, because
it allows for greater sharing of WGSL code between the forward pipeline
and the prepass pipeline.

The `visibility_range` example has been updated to allow the prepass to
be toggled on and off.
This commit is contained in:
Patrick Walton 2024-12-04 09:34:36 -08:00 committed by GitHub
parent c9fa975977
commit 56c70f8463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 118 additions and 21 deletions

View file

@ -5,6 +5,7 @@ use bevy_render::{
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
render_resource::binding_types::uniform_buffer,
sync_world::RenderEntity,
view::{RenderVisibilityRanges, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT},
};
pub use prepass_bindings::*;
@ -19,7 +20,7 @@ use bevy_ecs::{
SystemParamItem,
},
};
use bevy_math::Affine3A;
use bevy_math::{Affine3A, Vec4};
use bevy_render::{
globals::{GlobalsBuffer, GlobalsUniform},
prelude::{Camera, Mesh},
@ -261,30 +262,53 @@ impl<M: Material> FromWorld for PrepassPipeline<M> {
let render_device = world.resource::<RenderDevice>();
let asset_server = world.resource::<AssetServer>();
let visibility_ranges_buffer_binding_type = render_device
.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT);
let view_layout_motion_vectors = render_device.create_bind_group_layout(
"prepass_view_layout_motion_vectors",
&BindGroupLayoutEntries::sequential(
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX_FRAGMENT,
(
// View
uniform_buffer::<ViewUniform>(true),
(0, uniform_buffer::<ViewUniform>(true)),
// Globals
uniform_buffer::<GlobalsUniform>(false),
(1, uniform_buffer::<GlobalsUniform>(false)),
// PreviousViewUniforms
uniform_buffer::<PreviousViewData>(true),
(2, uniform_buffer::<PreviousViewData>(true)),
// VisibilityRanges
(
14,
buffer_layout(
visibility_ranges_buffer_binding_type,
false,
Some(Vec4::min_size()),
)
.visibility(ShaderStages::VERTEX),
),
),
),
);
let view_layout_no_motion_vectors = render_device.create_bind_group_layout(
"prepass_view_layout_no_motion_vectors",
&BindGroupLayoutEntries::sequential(
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX_FRAGMENT,
(
// View
uniform_buffer::<ViewUniform>(true),
(0, uniform_buffer::<ViewUniform>(true)),
// Globals
uniform_buffer::<GlobalsUniform>(false),
(1, uniform_buffer::<GlobalsUniform>(false)),
// VisibilityRanges
(
14,
buffer_layout(
visibility_ranges_buffer_binding_type,
false,
Some(Vec4::min_size()),
)
.visibility(ShaderStages::VERTEX),
),
),
),
);
@ -470,6 +494,13 @@ where
shader_defs.push("HAS_PREVIOUS_MORPH".into());
}
if key
.mesh_key
.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER)
{
shader_defs.push("VISIBILITY_RANGE_DITHER".into());
}
if key.mesh_key.intersects(
MeshPipelineKey::NORMAL_PREPASS
| MeshPipelineKey::MOTION_VECTOR_PREPASS
@ -666,26 +697,33 @@ pub fn prepare_prepass_view_bind_group<M: Material>(
view_uniforms: Res<ViewUniforms>,
globals_buffer: Res<GlobalsBuffer>,
previous_view_uniforms: Res<PreviousViewUniforms>,
visibility_ranges: Res<RenderVisibilityRanges>,
mut prepass_view_bind_group: ResMut<PrepassViewBindGroup>,
) {
if let (Some(view_binding), Some(globals_binding)) = (
if let (Some(view_binding), Some(globals_binding), Some(visibility_ranges_buffer)) = (
view_uniforms.uniforms.binding(),
globals_buffer.buffer.binding(),
visibility_ranges.buffer().buffer(),
) {
prepass_view_bind_group.no_motion_vectors = Some(render_device.create_bind_group(
"prepass_view_no_motion_vectors_bind_group",
&prepass_pipeline.view_layout_no_motion_vectors,
&BindGroupEntries::sequential((view_binding.clone(), globals_binding.clone())),
&BindGroupEntries::with_indices((
(0, view_binding.clone()),
(1, globals_binding.clone()),
(14, visibility_ranges_buffer.as_entire_binding()),
)),
));
if let Some(previous_view_uniforms_binding) = previous_view_uniforms.uniforms.binding() {
prepass_view_bind_group.motion_vectors = Some(render_device.create_bind_group(
"prepass_view_motion_vectors_bind_group",
&prepass_pipeline.view_layout_motion_vectors,
&BindGroupEntries::sequential((
view_binding,
globals_binding,
previous_view_uniforms_binding,
&BindGroupEntries::with_indices((
(0, view_binding),
(1, globals_binding),
(2, previous_view_uniforms_binding),
(14, visibility_ranges_buffer.as_entire_binding()),
)),
));
}
@ -713,6 +751,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
render_lightmaps: Res<RenderLightmaps>,
render_visibility_ranges: Res<RenderVisibilityRanges>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
mut opaque_prepass_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dPrepass>>,
mut alpha_mask_prepass_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dPrepass>>,
@ -854,6 +893,10 @@ pub fn queue_prepass_material_meshes<M: Material>(
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
}
if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) {
mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER;
}
// If the previous frame has skins or morph targets, note that.
if motion_vector_prepass.is_some() {
if mesh_instance

View file

@ -66,12 +66,14 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var vertex = vertex_no_morph;
#endif
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
#else // SKINNED
// 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 world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
var world_from_local = mesh_world_from_local;
#endif // SKINNED
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4<f32>(vertex.position, 1.0));
@ -161,6 +163,11 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
out.instance_index = vertex_no_morph.instance_index;
#endif
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, mesh_world_from_local[3]);
#endif // VISIBILITY_RANGE_DITHER
return out;
}

View file

@ -70,6 +70,10 @@ struct VertexOutput {
#ifdef VERTEX_COLORS
@location(8) color: vec4<f32>,
#endif
#ifdef VISIBILITY_RANGE_DITHER
@location(9) @interpolate(flat) visibility_range_dither: i32,
#endif // VISIBILITY_RANGE_DITHER
}
#ifdef PREPASS_FRAGMENT

View file

@ -41,12 +41,14 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var vertex = vertex_no_morph;
#endif
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(vertex.joint_indices, vertex.joint_weights);
#else
// 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 world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
var world_from_local = mesh_world_from_local;
#endif
#ifdef VERTEX_NORMALS
@ -96,7 +98,7 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, world_from_local[3]);
vertex_no_morph.instance_index, mesh_world_from_local[3]);
#endif
return out;

View file

@ -172,7 +172,7 @@ impl From<Option<&ViewPrepassTextures>> for MeshPipelineViewLayoutKey {
}
}
fn buffer_layout(
pub(crate) fn buffer_layout(
buffer_binding_type: BufferBindingType,
has_dynamic_offset: bool,
min_binding_size: Option<NonZero<u64>>,

View file

@ -26,9 +26,16 @@ fn fragment(
#ifdef MESHLET_MESH_MATERIAL_PASS
let in = resolve_vertex_output(frag_coord);
let is_front = true;
#else
#else // MESHLET_MESH_MATERIAL_PASS
// If we're in the crossfade section of a visibility range, conditionally
// discard the fragment according to the visibility pattern.
#ifdef VISIBILITY_RANGE_DITHER
pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither);
#endif // VISIBILITY_RANGE_DITHER
pbr_prepass_functions::prepass_alpha_discard(in);
#endif
#endif // MESHLET_MESH_MATERIAL_PASS
var out: prepass_io::FragmentOutput;

View file

@ -3,6 +3,7 @@
use std::f32::consts::PI;
use bevy::{
core_pipeline::prepass::{DepthPrepass, NormalPrepass},
input::mouse::MouseWheel,
math::vec3,
pbr::{light_consts::lux::FULL_DAYLIGHT, CascadeShadowConfigBuilder},
@ -63,6 +64,8 @@ enum MainModel {
struct AppStatus {
// Whether to show only one model.
show_one_model_only: Option<MainModel>,
// Whether to enable the prepass.
prepass: bool,
}
// Sets up the app.
@ -84,6 +87,7 @@ fn main() {
set_visibility_ranges,
update_help_text,
update_mode,
toggle_prepass,
),
)
.run();
@ -288,6 +292,34 @@ fn update_mode(
}
}
// Toggles the prepass if the user requests.
fn toggle_prepass(
mut commands: Commands,
cameras: Query<Entity, With<Camera3d>>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_status: ResMut<AppStatus>,
) {
if !keyboard_input.just_pressed(KeyCode::Space) {
return;
}
app_status.prepass = !app_status.prepass;
for camera in cameras.iter() {
if app_status.prepass {
commands
.entity(camera)
.insert(DepthPrepass)
.insert(NormalPrepass);
} else {
commands
.entity(camera)
.remove::<DepthPrepass>()
.remove::<NormalPrepass>();
}
}
}
// A system that updates the help text.
fn update_help_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
for mut text in text_query.iter_mut() {
@ -304,7 +336,8 @@ impl AppStatus {
{} (2) Show only the high-poly model
{} (3) Show only the low-poly model
Press 1, 2, or 3 to switch which model is shown
Press WASD or use the mouse wheel to move the camera",
Press WASD or use the mouse wheel to move the camera
Press Space to {} the prepass",
if self.show_one_model_only.is_none() {
'>'
} else {
@ -320,6 +353,7 @@ Press WASD or use the mouse wheel to move the camera",
} else {
' '
},
if self.prepass { "disable" } else { "enable" }
)
.into()
}