Skip to content
421 changes: 421 additions & 0 deletions native/shared/src/renderer/froxel.rs

Large diffs are not rendered by default.

96 changes: 95 additions & 1 deletion native/shared/src/renderer/hiz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! policy); pipelines and the mip chain stay fields on [`Renderer`].

use super::formats::HIZ_MIP_COUNT;
use super::{HizDownsampleParams, HizLinearizeParams, SsaoBlurParams};
use super::formats::halton;
use super::{HizDownsampleParams, HizLinearizeParams, SsaoBlurParams, SsaoParams};
use super::Renderer;

impl Renderer {
Expand Down Expand Up @@ -169,3 +170,96 @@ impl Renderer {
}
}
}

impl Renderer {
/// GTAO compute dispatch (half-res, Hi-Z-accelerated, temporal EMA
/// ping-pong). Caller guards on `ssao_enabled` and passes the
/// projection terms. Split from end_frame_with_scene.
#[allow(clippy::too_many_arguments)]
pub(super) fn record_gtao(
&mut self,
encoder: &mut wgpu::CommandEncoder,
profiler: &mut crate::profiler::Profiler,
half_w: u32,
half_h: u32,
p00: f32,
p11: f32,
p20: f32,
p21: f32,
) {
let p22 = self.current_proj_matrix[2][2];
let p32 = self.current_proj_matrix[3][2];
// --- SSAO (compute GTAO, samples Hi-Z pyramid) --------------
let ld = self.lighting_uniforms.light_dir;
let v = &self.current_view_matrix;
let light_dir_vs = [
v[0][0]*ld[0] + v[1][0]*ld[1] + v[2][0]*ld[2],
v[0][1]*ld[0] + v[1][1]*ld[1] + v[2][1]*ld[2],
v[0][2]*ld[0] + v[1][2]*ld[1] + v[2][2]*ld[2],
0.0,
];
// Temporal accumulation: ping-pong history textures.
// `write_idx` is the current-frame output; `read_idx` the
// previous frame's result. First 4 frames force alpha=1
// so the initial clear never contaminates the signal.
let write_idx = self.ssao_history_idx;
let read_idx = 1 - write_idx;
let frame_phase = self.ssao_history_frame % 4;
let force_refresh = if self.ssao_history_frame < 4 { 1u32 } else { 0u32 };
// 4-frame EMA: alpha = 1/4 = 0.25 gives equal weight to
// each of the 4 phases at steady state.
let alpha = 0.25_f32;
// Halton-5 rotation: uncorrelated with TAA's base-2/3 jitter
// so the two noise patterns don't resonate.
let halton5 = halton(self.ssao_history_frame + 1, 5);
let sp = SsaoParams {
params: [
1.0 / half_w as f32,
1.0 / half_h as f32,
self.ssao_radius,
self.ssao_strength,
],
proj_row01: [p00, p11, p20, p21],
proj_z: [p22, p32, 1.0 / p00, 1.0 / p11],
light_dir_vs,
size: [half_w, half_h, frame_phase, force_refresh],
temporal: [alpha, halton5, 0.0, 0.0],
};
self.queue.write_buffer(&self.ssao_uniform_buffer, 0, bytemuck::bytes_of(&sp));

if self.ssao_bg_cache[write_idx].is_none() {
self.ssao_bg_cache[write_idx] = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ssao_bg"),
layout: &self.ssao_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: self.ssao_uniform_buffer.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.ssao_rt_view) },
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.hiz_sampler) },
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.hiz_views[0]) },
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self.hiz_views[1]) },
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.hiz_views[2]) },
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self.hiz_views[3]) },
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&self.hiz_views[4]) },
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self.velocity_rt_view) },
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::TextureView(&self.ssao_history_views[read_idx]) },
wgpu::BindGroupEntry { binding: 10, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
wgpu::BindGroupEntry { binding: 11, resource: wgpu::BindingResource::TextureView(&self.ssao_history_views[write_idx]) },
],
}));
}
let bg = self.ssao_bg_cache[write_idx].as_ref().unwrap();

let ssao_ts = profiler.compute_pass_timestamp_writes("ssao_pass");
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("ssao_pass"),
timestamp_writes: ssao_ts,
});
pass.set_pipeline(&self.ssao_pipeline);
pass.set_bind_group(0, bg, &[]);
pass.dispatch_workgroups((half_w + 7) / 8, (half_h + 7) / 8, 1);

// Flip ping-pong indices for the next frame.
self.ssao_history_idx = read_idx;
self.ssao_history_frame = self.ssao_history_frame.wrapping_add(1);
}
}
153 changes: 153 additions & 0 deletions native/shared/src/renderer/lighting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Group-1 lighting bind group — layout and construction.
//!
//! The scene + immediate-mode 3D pipelines share one bind-group layout
//! for lighting data: the Lighting UBO, env/IBL textures, the shadow
//! cascade, and (on clustered devices) the froxel buffers at bindings
//! 10-12. The bind group is rebuilt whenever the env source changes
//! (HDR load, panorama, procedural sky); every rebuild goes through
//! [`Renderer::make_lighting_bind_group`] so the entry list exists in
//! exactly one place and cannot drift between call sites.

use super::{froxel, Renderer};

/// Create the group-1 layout. `clustered` appends the froxel bindings —
/// set when [`froxel::FroxelPass::supported`] holds for the device.
/// Pipelines whose shaders don't reference bindings 10-12 (pipeline_3d)
/// share the layout unaffected; extra entries are legal as long as the
/// bind group provides them.
pub(super) fn create_lighting_layout(
device: &wgpu::Device,
clustered: bool,
) -> wgpu::BindGroupLayout {
let tex_float = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
};
let tex_depth = wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
};
let frag = wgpu::ShaderStages::FRAGMENT;
let mut entries = vec![
// 0: Lighting UBO
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: frag,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// 1/2: env (IBL specular) texture + sampler
wgpu::BindGroupLayoutEntry { binding: 1, visibility: frag, ty: tex_float, count: None },
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: frag,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// 3/4: BRDF LUT + sampler
wgpu::BindGroupLayoutEntry { binding: 3, visibility: frag, ty: tex_float, count: None },
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: frag,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// 5-7: shadow cascades, 8: comparison sampler
wgpu::BindGroupLayoutEntry { binding: 5, visibility: frag, ty: tex_depth, count: None },
wgpu::BindGroupLayoutEntry { binding: 6, visibility: frag, ty: tex_depth, count: None },
wgpu::BindGroupLayoutEntry { binding: 7, visibility: frag, ty: tex_depth, count: None },
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: frag,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
// 9: env diffuse (IBL irradiance)
wgpu::BindGroupLayoutEntry { binding: 9, visibility: frag, ty: tex_float, count: None },
];
if clustered {
entries.extend(froxel::extra_lighting_layout_entries());
}
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("lighting_layout"),
entries: &entries,
})
}

/// Everything a lighting bind group references besides the env views.
/// `Renderer::new` builds one from constructor locals (before `self`
/// exists); [`Renderer::make_lighting_bind_group`] from fields.
pub(super) struct LightingBindSources<'a> {
pub lighting_buffer: &'a wgpu::Buffer,
pub env_sampler: &'a wgpu::Sampler,
pub brdf_lut_view: &'a wgpu::TextureView,
pub brdf_lut_sampler: &'a wgpu::Sampler,
pub shadow_map: &'a crate::shadows::ShadowMap,
pub froxel: Option<&'a froxel::FroxelPass>,
}

/// The single source of truth for the group-1 entry list — every
/// lighting bind group the renderer ever creates goes through here.
pub(super) fn create_lighting_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
label: &str,
src: &LightingBindSources<'_>,
env_view: &wgpu::TextureView,
diffuse_view: &wgpu::TextureView,
) -> wgpu::BindGroup {
let mut entries = vec![
wgpu::BindGroupEntry { binding: 0, resource: src.lighting_buffer.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(env_view) },
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(src.env_sampler) },
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(src.brdf_lut_view) },
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(src.brdf_lut_sampler) },
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[0]) },
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[1]) },
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[2]) },
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(&src.shadow_map.sampler) },
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::TextureView(diffuse_view) },
];
if let Some(f) = src.froxel {
entries.extend(f.extra_lighting_bind_entries());
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &entries,
})
}

impl Renderer {
/// Build a group-1 lighting bind group for the given env-specular /
/// env-diffuse views. Everything else (UBO, BRDF LUT, shadow
/// cascade, froxel buffers when clustered) comes from `self`.
pub(super) fn make_lighting_bind_group(
&self,
label: &str,
env_view: &wgpu::TextureView,
diffuse_view: &wgpu::TextureView,
) -> wgpu::BindGroup {
create_lighting_bind_group(
&self.device,
&self.lighting_layout,
label,
&LightingBindSources {
lighting_buffer: &self.lighting_buffer,
env_sampler: &self.env_sampler,
brdf_lut_view: &self.brdf_lut_view,
brdf_lut_sampler: &self.brdf_lut_sampler,
shadow_map: &self.shadow_map,
froxel: self.froxel.as_ref(),
},
env_view,
diffuse_view,
)
}
}
Loading
Loading