Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions native/shared/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,8 +885,10 @@ pub struct Renderer {
pub ssao_blur_uniform_buffer: wgpu::Buffer,
/// Strength multiplier for SSAO (0 = off, 1 = full). Default 1.0.
pub ssao_strength: f32,
/// Sample radius in UV units (default ~0.005, gives a soft AO
/// signal a few pixels wide on a 1024-tall surface).
/// Sample radius in WORLD-SPACE METERS (default 2.0; the shader
/// projects it to a screen radius per pixel via depth and clamps the
/// result — see `clamped_radius` in ao.rs). NOTE: earlier docs called
/// this "UV units ~0.005" — stale from before the world-space refactor.
pub ssao_radius: f32,
/// Skip the SSAO + bilateral-blur passes entirely when false; the
/// blur RT gets a WHITE clear (no occlusion) so composite stays
Expand Down Expand Up @@ -6122,7 +6124,10 @@ impl Renderer {
ssao_blur_pipeline,
ssao_blur_layout,
ssao_blur_uniform_buffer,
ssao_strength: 1.0,
// 0.7, not full 1.0: with the screen-radius now capped to a
// local contact term (see ao.rs), full strength read as harsh
// near-black creases. 0.7 keeps AO as a grounding cue.
ssao_strength: 0.7,
ssao_enabled: true,
bloom_enabled: true,
ssao_bg_cache: [None, None],
Expand Down Expand Up @@ -6706,9 +6711,10 @@ impl Renderer {
self.ssao_strength = strength.max(0.0);
}

/// SSAO sample radius in UV units. 0.006 (~0.6% of viewport
/// height) is the default — wider radius catches larger AO
/// features but also blurs detail and increases halo risk.
/// SSAO sample radius in WORLD-SPACE METERS (default 2.0). The shader
/// converts it to a per-pixel screen radius (scaled by depth) and caps
/// that at 0.05 of the viewport, so a large world radius still stays a
/// local contact term rather than a scene-wide dimming wash.
pub fn set_ssao_radius(&mut self, radius: f32) {
self.ssao_radius = radius.max(0.0001);
}
Expand Down
18 changes: 16 additions & 2 deletions native/shared/src/renderer/shaders/ao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ const N_PHASES: u32 = 4u;
const N_STEPS: u32 = 8u;
const PI: f32 = 3.14159265;
const HIZ_MAX_MIP: i32 = 4;
// Screen-space contact shadows (the SSAO 'G' channel). Disabled: this
// 12-step march is half-res, unblurred, and — unlike the horizon AO — has
// NO temporal accumulation, so over the dense wind-animated grass it hits
// swaying blades a little differently every frame, producing a sharp,
// pixelated, dark flickering layer. It is also independent of ssao_radius /
// ssao_strength, which is why tuning those changed nothing. Off until it can
// be gated to exclude foliage. Flip to true to restore.
const CONTACT_SHADOWS_ENABLED: bool = false;


fn hiz_sample(uv: vec2<f32>, mip: i32) -> f32 {
Expand Down Expand Up @@ -220,7 +228,13 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
let proj_scale_x = abs(u.proj_row01.x);
let proj_scale_y = abs(u.proj_row01.y);
let screen_radius = radius_ws * 0.5 * (proj_scale_x + proj_scale_y) / abs(P.z);
let clamped_radius = clamp(screen_radius, 2.0 * max(inv_sz.x, inv_sz.y), 0.25);
// Cap the screen-space radius. The old 0.25 ceiling let a near-field
// pixel gather horizon occlusion across a QUARTER of the screen — over
// dense instanced grass that pulls thousands of blades into every AO
// sample, painting a broad, blotchy dark layer that also shimmers with
// the grass wind. 0.05 keeps AO a local contact/crevice term (what it's
// for) instead of a scene-wide dimming wash.
let clamped_radius = clamp(screen_radius, 2.0 * max(inv_sz.x, inv_sz.y), 0.05);

var ao_sum = 0.0;
let step_size = clamped_radius / f32(N_STEPS);
Expand Down Expand Up @@ -320,7 +334,7 @@ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
let light_vs = normalize(u.light_dir_vs.xyz);
var contact = 1.0;
let n_dot_l = dot(N, light_vs);
if (n_dot_l > 0.1) {
if (CONTACT_SHADOWS_ENABLED && n_dot_l > 0.1) {
let cs_steps = 12u;
let cs_max_dist = 0.2;
let cs_step = cs_max_dist / f32(cs_steps);
Expand Down
64 changes: 30 additions & 34 deletions native/shared/src/shadows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,41 +630,37 @@ impl ShadowMap {
let c_near = splits[c];
let c_far = splits[c + 1];

// Build a sub-projection that matches the camera's projection
// but with the near/far planes replaced by cascade limits.
// For a perspective projection, that means re-deriving the
// z-mapping while keeping the x/y fields intact.
let mut sub_proj = camera_proj;
// camera_proj is column-major, standard wgpu perspective layout:
// col2 row2 = (far+near)/(near-far)
// col3 row2 = 2*far*near/(near-far)
// col2 row3 = -1
let nf = 1.0 / (c_near - c_far);
sub_proj[2][2] = (c_far + c_near) * nf;
sub_proj[3][2] = 2.0 * c_far * c_near * nf;

let sub_inv_vp = crate::renderer::mat4_invert(
crate::renderer::mat4_multiply(sub_proj, camera_view),
);

// The 8 NDC corners of a clip cube. wgpu uses z in [0,1].
let ndc_corners: [[f32; 4]; 8] = [
[-1.0, -1.0, 0.0, 1.0],
[ 1.0, -1.0, 0.0, 1.0],
[-1.0, 1.0, 0.0, 1.0],
[ 1.0, 1.0, 0.0, 1.0],
[-1.0, -1.0, 1.0, 1.0],
[ 1.0, -1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0],
];

// Unproject to world space
// Frustum-slice corners for [c_near, c_far], computed DIRECTLY in
// view space from the camera FOV, then transformed to world by the
// (affine, well-conditioned) view inverse.
//
// We deliberately do NOT invert the perspective projection here.
// `mat4_perspective` uses the OpenGL [-1,1] NDC-z convention, so
// unprojecting its NDC clip corners handed the near plane a NEGATIVE
// homogeneous w — every corner then divided down onto the z=-1 plane
// and the frustum bounding sphere collapsed to ~0.12 m. The cascade
// ortho covered almost nothing, so terrain a few metres from the
// camera projected outside the shadow frustum and self-shadowed:
// the reported "moving dark patch" that scaled disproportionately
// with the camera. Half-extents per unit view depth come straight
// from the projection (no inversion, no convention hazard):
// tan(fovy/2) = 1 / proj[1][1]
// tan(fovy/2) * aspect = 1 / proj[0][0]
let inv_view = crate::renderer::mat4_invert(camera_view);
let half_w_per_d = 1.0 / camera_proj[0][0];
let half_h_per_d = 1.0 / camera_proj[1][1];
let mut world_corners = [[0.0f32; 3]; 8];
for i in 0..8 {
let h = crate::renderer::mat4_mul_vec4(&sub_inv_vp, &ndc_corners[i]);
let w = if h[3].abs() > 1e-8 { h[3] } else { 1.0 };
world_corners[i] = [h[0] / w, h[1] / w, h[2] / w];
let mut ci = 0usize;
for &d in [c_near, c_far].iter() {
let hw = d * half_w_per_d;
let hh = d * half_h_per_d;
for &(sx, sy) in [(-1.0f32, -1.0f32), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)].iter() {
// View space looks down -Z, so this slice sits at z = -d.
let vp = [sx * hw, sy * hh, -d, 1.0];
let wc = crate::renderer::mat4_mul_vec4(&inv_view, &vp);
world_corners[ci] = [wc[0], wc[1], wc[2]];
ci += 1;
}
}

// Bounding sphere of this cascade's frustum slice. Sphere
Expand Down
Loading