shader.wgsl:
- Camera.frame layout now documented slot-by-slot in the struct
comment; named accessors scene_time() / exposure_bias() / eye_world()
so call sites read as intent. exposure_bias plumbed (default 1.0)
as the foundation for a future Settings.exposure slider. [#20]
- Leaves now get sun-aware backlit translucency: leaf_translucency()
peaks when the sun rakes across the leaf plane AND comes from
behind the surface. Cheap stand-in for subsurface scattering —
reads as "dappled sun through canopy" without per-pixel ray
sampling. [#25]
render/mod.rs:
- run_fullscreen_pass(encoder, label, target, pipeline, bgs, clear)
helper. The three post-chain steps (mask → shafts → composite)
are now three sequential calls instead of three copies of the
same begin_render_pass boilerplate. Adding a new effect (bloom,
motion blur, vignette) is one extra row. [#23]
- upload_camera fills frame[1] = 1.0 (exposure_bias default).
render/uniform.rs:
- CameraUniform.frame doc updated to enumerate each slot.
Tests: 63 passing. Native + wasm release clean.
This commit is contained in:
parent
bd6b3fadb0
commit
0dca49f475
3 changed files with 98 additions and 70 deletions
|
|
@ -520,7 +520,8 @@ impl Renderer {
|
|||
view_proj: vp.to_cols_array_2d(),
|
||||
inv_view_proj: inv.to_cols_array_2d(),
|
||||
eye: [camera.position.x, camera.position.y, camera.position.z, 1.0],
|
||||
frame: [time, 0.0, 0.0, 0.0],
|
||||
// [scene_time, exposure_bias, reserved, reserved]
|
||||
frame: [time, 1.0, 0.0, 0.0],
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(&uni));
|
||||
|
|
@ -596,70 +597,29 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Mask pass: ¼-res, sun-cone × sky-alpha = god-ray seeds. ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("mask pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.mask_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.mask_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_bind_group(1, &self.mask_bg, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
// ---- Shafts pass: ¼-res, radial blur from sun screen-pos. ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("shafts pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.shafts_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.shafts_pipeline);
|
||||
pass.set_bind_group(0, &self.camera_bind_group, &[]);
|
||||
pass.set_bind_group(1, &self.shafts_bg, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
// ---- Post pass: FXAA + shafts composite + ACES tonemap. ----
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("post pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &surface_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.post_pipeline);
|
||||
pass.set_bind_group(0, &self.post_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
// ---- Post chain: mask → shafts → composite. Each step is a
|
||||
// full-screen-triangle pass with the same shape, so the chain
|
||||
// is just three calls of run_fullscreen_pass with different
|
||||
// (pipeline, target, bind groups). To add a new effect (bloom,
|
||||
// motion blur, vignette), insert another row here. ----
|
||||
run_fullscreen_pass(
|
||||
&mut encoder, "mask pass", &self.mask_view,
|
||||
&self.mask_pipeline,
|
||||
&[&self.camera_bind_group, &self.mask_bg],
|
||||
Some(wgpu::Color::BLACK),
|
||||
);
|
||||
run_fullscreen_pass(
|
||||
&mut encoder, "shafts pass", &self.shafts_view,
|
||||
&self.shafts_pipeline,
|
||||
&[&self.camera_bind_group, &self.shafts_bg],
|
||||
Some(wgpu::Color::BLACK),
|
||||
);
|
||||
run_fullscreen_pass(
|
||||
&mut encoder, "post pass", &surface_view,
|
||||
&self.post_pipeline,
|
||||
&[&self.post_bind_group],
|
||||
None,
|
||||
);
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
|
|
@ -667,6 +627,44 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Run a single full-screen-triangle render pass: clear target if
|
||||
/// `clear` is `Some`, load otherwise; set pipeline; bind groups in
|
||||
/// order; draw three vertices. Used for every post-chain step so the
|
||||
/// chain reads as a declarative sequence of effects rather than
|
||||
/// boilerplate.
|
||||
fn run_fullscreen_pass(
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
label: &str,
|
||||
target: &wgpu::TextureView,
|
||||
pipeline: &wgpu::RenderPipeline,
|
||||
bind_groups: &[&wgpu::BindGroup],
|
||||
clear: Option<wgpu::Color>,
|
||||
) {
|
||||
let load = match clear {
|
||||
Some(c) => wgpu::LoadOp::Clear(c),
|
||||
None => wgpu::LoadOp::Load,
|
||||
};
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some(label),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(pipeline);
|
||||
for (i, bg) in bind_groups.iter().enumerate() {
|
||||
pass.set_bind_group(i as u32, *bg, &[]);
|
||||
}
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
// ---- Browser-only compat helpers (probe WebGPU, render init-failure overlay) ----
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm_compat {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ pub struct CameraUniform {
|
|||
pub view_proj: [[f32; 4]; 4],
|
||||
pub inv_view_proj: [[f32; 4]; 4],
|
||||
pub eye: [f32; 4],
|
||||
/// `.x` = scene time in seconds (drives day/night cycle + leaf
|
||||
/// sway). `.y/.z/.w` reserved. Matches WGSL `camera.frame` with
|
||||
/// `scene_time()` accessor.
|
||||
/// Per-frame scalars; matches WGSL `camera.frame`. Accessors in
|
||||
/// the shader expose each slot by name:
|
||||
///
|
||||
/// .x scene_time seconds since session start
|
||||
/// .y exposure_bias tonemap multiplier (default 1.0)
|
||||
/// .z reserved
|
||||
/// .w reserved
|
||||
pub frame: [f32; 4],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,18 +16,26 @@
|
|||
|
||||
// ---------------- 1. Camera ----------------
|
||||
|
||||
// Camera uniform — layout mirrored in `render::uniform::CameraUniform`.
|
||||
// The `frame` vec4 is the "per-frame scalars" slot; the accessor
|
||||
// functions below name each component so callsites read as intent
|
||||
// rather than `frame.x` / `frame.y` / etc.
|
||||
//
|
||||
// frame.x scene_time seconds since session start
|
||||
// frame.y exposure_bias tonemap multiplier (default 1.0)
|
||||
// frame.z reserved for future fog density / weather etc.
|
||||
// frame.w reserved
|
||||
struct Camera {
|
||||
view_proj: mat4x4<f32>,
|
||||
inv_view_proj: mat4x4<f32>,
|
||||
eye: vec4<f32>,
|
||||
/// .x = scene time in seconds (drives day/night cycle + leaf sway)
|
||||
/// .y/.z/.w reserved
|
||||
frame: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> camera: Camera;
|
||||
|
||||
fn scene_time() -> f32 { return camera.frame.x; }
|
||||
fn exposure_bias() -> f32 { return camera.frame.y; }
|
||||
fn eye_world() -> vec3<f32> { return camera.eye.xyz; }
|
||||
|
||||
// ---------------- 2. Sky horizon math (shared with ambient) ----------------
|
||||
|
|
@ -239,6 +247,21 @@ fn leaf_jitter(world_pos: vec3<f32>) -> f32 {
|
|||
return 0.88 + n * 0.18;
|
||||
}
|
||||
|
||||
/// Approximated leaf translucency. When the sun is behind the leaf
|
||||
/// (relative to the surface normal), some light transmits through to
|
||||
/// the viewer's side and the leaf glows softly with sun-tinted color.
|
||||
/// Cheap stand-in for full subsurface scattering; reads as "sun
|
||||
/// through canopy" without per-pixel sampling.
|
||||
fn leaf_translucency(normal: vec3<f32>, sun: vec3<f32>, day: f32) -> f32 {
|
||||
// peak transmittance when sun rakes across the leaf plane — i.e.
|
||||
// when sun is roughly perpendicular to the normal (grazing).
|
||||
let grazing = 1.0 - abs(dot(normal, sun));
|
||||
// backlit bias: prefer light coming from BEHIND the leaf so we
|
||||
// don't double-count the front-side Lambert from direct_sun_term.
|
||||
let back = max(dot(-normal, sun), 0.0);
|
||||
return grazing * back * 0.55 * day;
|
||||
}
|
||||
|
||||
/// Distance fog. Returns 0 (no fog) → 1 (fully obscured).
|
||||
fn fog_factor(dist: f32) -> f32 {
|
||||
let fog_start = 90.0;
|
||||
|
|
@ -331,6 +354,9 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|||
|
||||
if (in.leaf > 0.5) {
|
||||
lit = lit * leaf_jitter(in.world_pos);
|
||||
// Sun-aware translucency — leaves glow when backlit.
|
||||
let trans = leaf_translucency(n, sun, day);
|
||||
lit = lit + in.color * sun_col * trans;
|
||||
}
|
||||
|
||||
let to_eye = eye_world() - in.world_pos;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue