New WGSL files:
mask.wgsl Sun-cone × sky-alpha mask at ¼ resolution. Marks sky
pixels (scene_color.a == 0) that fall within a tight
cone around the sun direction. Sun direction derived
from scene time via the injected DAY_PERIOD /
SUN_OFFSET constants — same source as sim::lighting,
so visual rays align with the mechanical sun.
shafts.wgsl Radial blur at ¼ resolution. Projects the sun
direction-at-infinity to screen space via view_proj,
steps 32 samples from each pixel toward the
sun_screen_pos accumulating mask intensity with
exponential decay, outputs sun-tinted ray color.
shader.wgsl:
- fs_sky now writes alpha = 0 so the mask pass can identify sky
pixels without a separate occluder pass. Terrain / outline /
remote-player pipelines continue writing alpha = 1.
post.wgsl (rewritten):
- Reads scene_color + shafts.
- Cheap edge-aware FXAA (5-tap diagonal blur, blends toward neighbor
average where luminance gradient exceeds threshold). Catches the
axis-aligned staircase aliasing that voxel games produce.
- Adds shafts additively before tonemap so rays go through the
filmic curve and don't blow out.
render/scene_target.rs:
- create_image_bind_group (single texture + sampler) — used by
mask pass (binds scene_color) and shafts pass (binds mask_view).
- create_composite_bind_group (scene + shafts + sampler) — used by
the final post pass.
- create_quarter_res_view (¹⁄₁₆ fillrate, RENDER_ATTACHMENT +
TEXTURE_BINDING) — used for both mask and shafts targets.
render/pipelines.rs:
- image_bgl / composite_bgl as separate layouts.
- fullscreen_pipeline factory replaces the post-specific one;
takes vs/fs entry-point names so mask, shafts, and post all
build through the same shape.
render/mod.rs:
- Renderer grows mask_view, shafts_view, image_bgl, composite_bgl,
mask_pipeline, shafts_pipeline, mask_bg, shafts_bg fields. The
old single-pass post_pipeline becomes the final composite pass.
- render() now does scene → mask → shafts → post (each in its own
encoder block).
- resize() recreates all three render targets and all three bind
groups in the right order.
Tests: 63 passing (added 2 for mask/shafts source assembly). Native
+ wasm release clean.
Visual change: when you face roughly toward the sun and there's
geometry blocking the disc, you'll see warm light shafts radiating
outward. FXAA softens the worst pixel-step edges. Tonemap (from
Round A) is now the final step of the new pipeline.
This commit is contained in:
parent
94585b1ab2
commit
bd6b3fadb0
8 changed files with 492 additions and 44 deletions
67
src/mask.wgsl
Normal file
67
src/mask.wgsl
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Sun-cone sky mask. Generates the input to the radial-blur shafts
|
||||
// pass: white where (a) this pixel is sky (scene_color.a == 0) AND
|
||||
// (b) the view direction is close to the sun direction; black
|
||||
// elsewhere. Runs at ¼ resolution to save fillrate.
|
||||
//
|
||||
// Sun direction is derived from scene time via the injected DAY_PERIOD
|
||||
// / SUN_OFFSET constants — same source of truth as sim::lighting and
|
||||
// the terrain shader, so the visual rays line up with the mechanical
|
||||
// sun direction by construction.
|
||||
|
||||
struct Camera {
|
||||
view_proj: mat4x4<f32>,
|
||||
inv_view_proj: mat4x4<f32>,
|
||||
eye: vec4<f32>,
|
||||
frame: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> camera: Camera;
|
||||
@group(1) @binding(0) var scene_tex: texture_2d<f32>;
|
||||
@group(1) @binding(1) var scene_sampler: sampler;
|
||||
|
||||
fn scene_time() -> f32 { return camera.frame.x; }
|
||||
|
||||
fn sun_direction(t: f32) -> vec3<f32> {
|
||||
let a = (t / DAY_PERIOD + SUN_OFFSET) * 6.28318530718;
|
||||
return normalize(vec3<f32>(cos(a), sin(a), 0.25));
|
||||
}
|
||||
|
||||
struct MaskOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_mask(@builtin(vertex_index) idx: u32) -> MaskOut {
|
||||
var corners = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>( 3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
);
|
||||
let p = corners[idx];
|
||||
var out: MaskOut;
|
||||
out.clip = vec4<f32>(p, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x * 0.5 + 0.5, p.y * -0.5 + 0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_mask(in: MaskOut) -> @location(0) vec4<f32> {
|
||||
let scene = textureSample(scene_tex, scene_sampler, in.uv);
|
||||
// Sky pixels carry alpha = 0; geometry carries alpha = 1.
|
||||
let is_sky = 1.0 - scene.a;
|
||||
|
||||
// Reconstruct world-space view direction at this pixel.
|
||||
let ndc_x = in.uv.x * 2.0 - 1.0;
|
||||
let ndc_y = 1.0 - in.uv.y * 2.0;
|
||||
let far_h = camera.inv_view_proj * vec4<f32>(ndc_x, ndc_y, 1.0, 1.0);
|
||||
let world_far = far_h.xyz / far_h.w;
|
||||
let view_dir = normalize(world_far - camera.eye.xyz);
|
||||
|
||||
let sun = sun_direction(scene_time());
|
||||
// Tight cone around sun. pow exponent controls cone width.
|
||||
let cone = pow(max(dot(view_dir, sun), 0.0), 8.0);
|
||||
|
||||
let mask = is_sky * cone;
|
||||
return vec4<f32>(mask, mask, mask, 1.0);
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
// Post pipeline. Currently:
|
||||
// 1. Sample the offscreen scene_color (linear HDR-ish)
|
||||
// 2. ACES tonemap to compress highlights gracefully
|
||||
// 3. Output to the surface (which sRGB-encodes automatically)
|
||||
//
|
||||
// Effects layered on top (god rays, FXAA) will slot in between (1) and (2)
|
||||
// — tonemap stays last so everything benefits from the curve.
|
||||
// Post composite. Pipeline (in order):
|
||||
// 1. Read scene_color (linear HDR-ish, alpha encodes sky/geometry)
|
||||
// 2. Cheap edge-detect FXAA (4-tap diagonal blur)
|
||||
// 3. Additively composite sun shafts on top
|
||||
// 4. ACES tonemap to compress highlights into [0,1]
|
||||
// 5. Output to surface (sRGB-encoded by GPU)
|
||||
|
||||
@group(0) @binding(0) var scene_color_tex: texture_2d<f32>;
|
||||
@group(0) @binding(1) var scene_color_sampler: sampler;
|
||||
@group(0) @binding(1) var shafts_tex: texture_2d<f32>;
|
||||
@group(0) @binding(2) var post_sampler: sampler;
|
||||
|
||||
struct PostOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
|
|
@ -24,14 +24,30 @@ fn vs_post(@builtin(vertex_index) idx: u32) -> PostOut {
|
|||
let p = corners[idx];
|
||||
var out: PostOut;
|
||||
out.clip = vec4<f32>(p, 0.0, 1.0);
|
||||
// Texture origin is top-left; flip Y so screen coords map to texel coords.
|
||||
out.uv = vec2<f32>(p.x * 0.5 + 0.5, p.y * -0.5 + 0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Narkowicz's ACES filmic approximation. Brightens midtones, compresses
|
||||
// highlights smoothly into [0, 1]. Output is linear; the sRGB surface
|
||||
// encodes for display.
|
||||
// Cheap edge-aware blur — a small FXAA. Samples the center and four
|
||||
// diagonal neighbors, blends toward the average where the local
|
||||
// luminance gradient exceeds a threshold. Works well for voxel games
|
||||
// where edges are axis-aligned and high-contrast.
|
||||
fn fxaa(uv: vec2<f32>, texel: vec2<f32>) -> vec3<f32> {
|
||||
let c = textureSample(scene_color_tex, post_sampler, uv).rgb;
|
||||
let nw = textureSample(scene_color_tex, post_sampler, uv + texel * vec2<f32>(-0.5, -0.5)).rgb;
|
||||
let ne = textureSample(scene_color_tex, post_sampler, uv + texel * vec2<f32>( 0.5, -0.5)).rgb;
|
||||
let sw = textureSample(scene_color_tex, post_sampler, uv + texel * vec2<f32>(-0.5, 0.5)).rgb;
|
||||
let se = textureSample(scene_color_tex, post_sampler, uv + texel * vec2<f32>( 0.5, 0.5)).rgb;
|
||||
let avg = (nw + ne + sw + se) * 0.25;
|
||||
let luma_w = vec3<f32>(0.299, 0.587, 0.114);
|
||||
let lc = dot(c, luma_w);
|
||||
let la = dot(avg, luma_w);
|
||||
let edge = clamp(abs(lc - la) * 4.0, 0.0, 1.0);
|
||||
return mix(c, avg, edge);
|
||||
}
|
||||
|
||||
// Narkowicz ACES filmic approximation. Output is linear; the sRGB
|
||||
// surface encodes for display.
|
||||
fn aces_tonemap(c: vec3<f32>) -> vec3<f32> {
|
||||
let a = 2.51;
|
||||
let b = 0.03;
|
||||
|
|
@ -43,7 +59,13 @@ fn aces_tonemap(c: vec3<f32>) -> vec3<f32> {
|
|||
|
||||
@fragment
|
||||
fn fs_post(in: PostOut) -> @location(0) vec4<f32> {
|
||||
let scene = textureSample(scene_color_tex, scene_color_sampler, in.uv);
|
||||
let tonemapped = aces_tonemap(scene.rgb);
|
||||
let dims = vec2<f32>(textureDimensions(scene_color_tex));
|
||||
let texel = vec2<f32>(1.0) / dims;
|
||||
|
||||
let aa = fxaa(in.uv, texel);
|
||||
let shafts = textureSample(shafts_tex, post_sampler, in.uv).rgb;
|
||||
// Additive composite — sun shafts brighten the underlying scene.
|
||||
let composed = aa + shafts;
|
||||
let tonemapped = aces_tonemap(composed);
|
||||
return vec4<f32>(tonemapped, 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ pub struct Renderer {
|
|||
pipeline: wgpu::RenderPipeline,
|
||||
sky_pipeline: wgpu::RenderPipeline,
|
||||
outline_pipeline: wgpu::RenderPipeline,
|
||||
post_pipeline: wgpu::RenderPipeline,
|
||||
|
||||
outline_buffer: wgpu::Buffer,
|
||||
depth_view: wgpu::TextureView,
|
||||
|
|
@ -52,12 +51,21 @@ pub struct Renderer {
|
|||
remote_ib: wgpu::Buffer,
|
||||
remote_index_count: u32,
|
||||
|
||||
// ---- Post processing (Step 1: pass-through scene → surface) ----
|
||||
// ---- Post processing chain: scene → mask → shafts → composite. ----
|
||||
scene_color: wgpu::TextureView,
|
||||
scene_color_format: wgpu::TextureFormat,
|
||||
post_sampler: wgpu::Sampler,
|
||||
post_bgl: wgpu::BindGroupLayout,
|
||||
post_bind_group: wgpu::BindGroup,
|
||||
image_bgl: wgpu::BindGroupLayout,
|
||||
composite_bgl: wgpu::BindGroupLayout,
|
||||
// ¼-res god-rays chain
|
||||
mask_view: wgpu::TextureView,
|
||||
shafts_view: wgpu::TextureView,
|
||||
mask_pipeline: wgpu::RenderPipeline,
|
||||
shafts_pipeline: wgpu::RenderPipeline,
|
||||
post_pipeline: wgpu::RenderPipeline,
|
||||
mask_bg: wgpu::BindGroup, // binds scene_color → input to mask pass
|
||||
shafts_bg: wgpu::BindGroup, // binds mask_view → input to shafts pass
|
||||
post_bind_group: wgpu::BindGroup, // binds scene + shafts → input to post
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
|
|
@ -252,10 +260,16 @@ impl Renderer {
|
|||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
// ---- Post pipeline + scene-color target ----
|
||||
// ---- Post chain: scene-color + ¼-res mask + ¼-res shafts → composite. ----
|
||||
let scene_color_format = config.format;
|
||||
let scene_color =
|
||||
scene_target::create_scene_color_view(&device, width, height, scene_color_format);
|
||||
let mask_view = scene_target::create_quarter_res_view(
|
||||
&device, width, height, scene_color_format,
|
||||
);
|
||||
let shafts_view = scene_target::create_quarter_res_view(
|
||||
&device, width, height, scene_color_format,
|
||||
);
|
||||
let post_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("post sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
|
|
@ -266,11 +280,53 @@ impl Renderer {
|
|||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
let post_bgl = pipelines::post_bgl(&device);
|
||||
let post_bind_group =
|
||||
scene_target::create_post_bind_group(&device, &post_bgl, &scene_color, &post_sampler);
|
||||
let post_pl = pipelines::pipeline_layout(&device, "post pl", &[&post_bgl]);
|
||||
let post_pipeline = pipelines::post_pipeline(&device, &post_pl, &post_shader, config.format);
|
||||
|
||||
// Bind-group layouts
|
||||
let image_bgl = pipelines::image_bgl(&device);
|
||||
let composite_bgl = pipelines::composite_bgl(&device);
|
||||
|
||||
// Shaders for the new mask + shafts passes
|
||||
let mask_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("mask shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
crate::shader_source::mask_shader_source().into(),
|
||||
),
|
||||
});
|
||||
let shafts_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("shafts shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(
|
||||
crate::shader_source::shafts_shader_source().into(),
|
||||
),
|
||||
});
|
||||
|
||||
// Pipeline layouts
|
||||
let mask_pl =
|
||||
pipelines::pipeline_layout(&device, "mask pl", &[&camera_bgl, &image_bgl]);
|
||||
let shafts_pl =
|
||||
pipelines::pipeline_layout(&device, "shafts pl", &[&camera_bgl, &image_bgl]);
|
||||
let post_pl = pipelines::pipeline_layout(&device, "post pl", &[&composite_bgl]);
|
||||
|
||||
// Pipelines (all full-screen-triangle, no depth)
|
||||
let mask_pipeline = pipelines::fullscreen_pipeline(
|
||||
&device, "mask pipeline", &mask_pl, &mask_shader, "vs_mask", "fs_mask", config.format,
|
||||
);
|
||||
let shafts_pipeline = pipelines::fullscreen_pipeline(
|
||||
&device, "shafts pipeline", &shafts_pl, &shafts_shader, "vs_shafts", "fs_shafts", config.format,
|
||||
);
|
||||
let post_pipeline = pipelines::fullscreen_pipeline(
|
||||
&device, "post pipeline", &post_pl, &post_shader, "vs_post", "fs_post", config.format,
|
||||
);
|
||||
|
||||
// Bind groups
|
||||
let mask_bg = scene_target::create_image_bind_group(
|
||||
&device, &image_bgl, &scene_color, &post_sampler,
|
||||
);
|
||||
let shafts_bg = scene_target::create_image_bind_group(
|
||||
&device, &image_bgl, &mask_view, &post_sampler,
|
||||
);
|
||||
let post_bind_group = scene_target::create_composite_bind_group(
|
||||
&device, &composite_bgl, &scene_color, &shafts_view, &post_sampler,
|
||||
);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
|
|
@ -294,9 +350,16 @@ impl Renderer {
|
|||
scene_color,
|
||||
scene_color_format,
|
||||
post_sampler,
|
||||
post_bgl,
|
||||
post_bind_group,
|
||||
image_bgl,
|
||||
composite_bgl,
|
||||
mask_view,
|
||||
shafts_view,
|
||||
mask_pipeline,
|
||||
shafts_pipeline,
|
||||
post_pipeline,
|
||||
mask_bg,
|
||||
shafts_bg,
|
||||
post_bind_group,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -384,12 +447,37 @@ impl Renderer {
|
|||
height,
|
||||
self.scene_color_format,
|
||||
);
|
||||
self.post_bind_group = scene_target::create_post_bind_group(
|
||||
self.mask_view = scene_target::create_quarter_res_view(
|
||||
&self.device,
|
||||
&self.post_bgl,
|
||||
width,
|
||||
height,
|
||||
self.scene_color_format,
|
||||
);
|
||||
self.shafts_view = scene_target::create_quarter_res_view(
|
||||
&self.device,
|
||||
width,
|
||||
height,
|
||||
self.scene_color_format,
|
||||
);
|
||||
self.mask_bg = scene_target::create_image_bind_group(
|
||||
&self.device,
|
||||
&self.image_bgl,
|
||||
&self.scene_color,
|
||||
&self.post_sampler,
|
||||
);
|
||||
self.shafts_bg = scene_target::create_image_bind_group(
|
||||
&self.device,
|
||||
&self.image_bgl,
|
||||
&self.mask_view,
|
||||
&self.post_sampler,
|
||||
);
|
||||
self.post_bind_group = scene_target::create_composite_bind_group(
|
||||
&self.device,
|
||||
&self.composite_bgl,
|
||||
&self.scene_color,
|
||||
&self.shafts_view,
|
||||
&self.post_sampler,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn rebuild_chunk(&mut self, coord: IVec3, world: &World) {
|
||||
|
|
@ -508,7 +596,51 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Post pass: scene_color → surface (effects later). ----
|
||||
// ---- 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"),
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ pub fn camera_bgl(device: &Device) -> BindGroupLayout {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn post_bgl(device: &Device) -> BindGroupLayout {
|
||||
/// Single-image input layout: one filterable texture + one filtering
|
||||
/// sampler. Used by the mask pass (reads scene_color) and the shafts
|
||||
/// pass (reads the mask). Same shape as the historical `post_bgl`.
|
||||
pub fn image_bgl(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("post bgl"),
|
||||
label: Some("image bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
|
|
@ -48,6 +51,42 @@ pub fn post_bgl(device: &Device) -> BindGroupLayout {
|
|||
})
|
||||
}
|
||||
|
||||
/// Composite layout used by the final post pass: scene_color +
|
||||
/// shafts + shared sampler.
|
||||
pub fn composite_bgl(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("composite bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pipeline_layout(
|
||||
device: &Device,
|
||||
label: &str,
|
||||
|
|
@ -200,26 +239,31 @@ pub fn outline_pipeline(
|
|||
})
|
||||
}
|
||||
|
||||
/// Pass-through post: full-screen triangle that samples `scene_color`
|
||||
/// back to the surface. Foundation for FXAA / sun shafts / tonemap.
|
||||
pub fn post_pipeline(
|
||||
/// Generic full-screen-triangle pipeline. Takes the vertex + fragment
|
||||
/// entry point names so the same factory builds mask, shafts, and
|
||||
/// composite passes with different shaders. No depth, no culling, no
|
||||
/// blending — pure post-process.
|
||||
pub fn fullscreen_pipeline(
|
||||
device: &Device,
|
||||
label: &str,
|
||||
layout: &PipelineLayout,
|
||||
shader: &ShaderModule,
|
||||
vs_entry: &str,
|
||||
fs_entry: &str,
|
||||
format: TextureFormat,
|
||||
) -> RenderPipeline {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("post pipeline"),
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_post"),
|
||||
entry_point: Some(vs_entry),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_post"),
|
||||
entry_point: Some(fs_entry),
|
||||
targets: &[Some(color_target(format))],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -49,19 +49,21 @@ pub fn create_scene_color_view(
|
|||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn create_post_bind_group(
|
||||
/// One-image bind group: a single texture + a sampler. Used by the
|
||||
/// mask pass (binds scene_color) and the shafts pass (binds the mask).
|
||||
pub fn create_image_bind_group(
|
||||
device: &Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
scene: &TextureView,
|
||||
image: &TextureView,
|
||||
sampler: &Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("post bg"),
|
||||
label: Some("image bg"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(scene),
|
||||
resource: wgpu::BindingResource::TextureView(image),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
|
|
@ -70,3 +72,58 @@ pub fn create_post_bind_group(
|
|||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Composite bind group for the final post pass: scene_color + shafts
|
||||
/// + shared sampler.
|
||||
pub fn create_composite_bind_group(
|
||||
device: &Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
scene: &TextureView,
|
||||
shafts: &TextureView,
|
||||
sampler: &Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("composite bg"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(scene),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(shafts),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Quarter-resolution render target for the god-rays mask and shafts
|
||||
/// passes. ¼ dimensions = ¹⁄₁₆ fillrate of full screen — cheap enough
|
||||
/// to run 32-sample radial blur each frame.
|
||||
pub fn create_quarter_res_view(
|
||||
device: &Device,
|
||||
w: u32,
|
||||
h: u32,
|
||||
format: TextureFormat,
|
||||
) -> TextureView {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("¼-res shafts target"),
|
||||
size: wgpu::Extent3d {
|
||||
width: (w / 4).max(1),
|
||||
height: (h / 4).max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
tex.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,7 +367,9 @@ fn fs_sky(in: SkyOut) -> @location(0) vec4<f32> {
|
|||
let far_h = camera.inv_view_proj * vec4<f32>(in.ndc.x, in.ndc.y, 1.0, 1.0);
|
||||
let world_pos = far_h.xyz / far_h.w;
|
||||
let dir = normalize(world_pos - eye_world());
|
||||
return vec4<f32>(sky_color(dir), 1.0);
|
||||
// alpha = 0 flags this pixel as "sky" for the god-rays mask pass;
|
||||
// terrain pipeline writes alpha = 1 where it overdraws.
|
||||
return vec4<f32>(sky_color(dir), 0.0);
|
||||
}
|
||||
|
||||
// ---------------- 5c. Outline ----------------
|
||||
|
|
|
|||
|
|
@ -26,8 +26,21 @@ pub fn terrain_shader_source() -> String {
|
|||
format!("{}{}", wgsl_constants_header(), include_str!("shader.wgsl"))
|
||||
}
|
||||
|
||||
/// Post-process pipeline source. Doesn't depend on the shared
|
||||
/// constants; passed through for symmetry with `terrain_shader_source`.
|
||||
/// God-rays sun-cone mask shader. Reads scene_color, gates by sun
|
||||
/// direction. Needs DAY_PERIOD / SUN_OFFSET injected.
|
||||
pub fn mask_shader_source() -> String {
|
||||
format!("{}{}", wgsl_constants_header(), include_str!("mask.wgsl"))
|
||||
}
|
||||
|
||||
/// God-rays radial-blur pass. Reads the mask texture, accumulates
|
||||
/// samples toward the sun's screen-space position. Needs the same
|
||||
/// injected sun constants.
|
||||
pub fn shafts_shader_source() -> String {
|
||||
format!("{}{}", wgsl_constants_header(), include_str!("shafts.wgsl"))
|
||||
}
|
||||
|
||||
/// Post-composite pipeline source: FXAA + shafts composite + tonemap.
|
||||
/// Doesn't reference sun-direction math; no constants needed.
|
||||
pub fn post_shader_source() -> &'static str {
|
||||
include_str!("post.wgsl")
|
||||
}
|
||||
|
|
@ -51,4 +64,20 @@ mod tests {
|
|||
assert!(src.contains("fn sun_direction"));
|
||||
assert!(src.contains("fn fs_main"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_shader_source_has_constants_and_entry_points() {
|
||||
let src = mask_shader_source();
|
||||
assert!(src.contains("DAY_PERIOD"));
|
||||
assert!(src.contains("fn vs_mask"));
|
||||
assert!(src.contains("fn fs_mask"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shafts_shader_source_has_constants_and_entry_points() {
|
||||
let src = shafts_shader_source();
|
||||
assert!(src.contains("DAY_PERIOD"));
|
||||
assert!(src.contains("fn vs_shafts"));
|
||||
assert!(src.contains("fn fs_shafts"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
95
src/shafts.wgsl
Normal file
95
src/shafts.wgsl
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Screen-space radial blur from the sun's projected screen position.
|
||||
// Reads the sun-cone sky mask, accumulates samples along the ray from
|
||||
// this pixel toward the sun, and outputs sun-tinted ray intensity for
|
||||
// the post composite to add onto the scene.
|
||||
//
|
||||
// Runs at ¼ resolution. 32 samples with exponential decay.
|
||||
|
||||
struct Camera {
|
||||
view_proj: mat4x4<f32>,
|
||||
inv_view_proj: mat4x4<f32>,
|
||||
eye: vec4<f32>,
|
||||
frame: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> camera: Camera;
|
||||
@group(1) @binding(0) var mask_tex: texture_2d<f32>;
|
||||
@group(1) @binding(1) var mask_sampler: sampler;
|
||||
|
||||
fn scene_time() -> f32 { return camera.frame.x; }
|
||||
|
||||
fn sun_direction(t: f32) -> vec3<f32> {
|
||||
let a = (t / DAY_PERIOD + SUN_OFFSET) * 6.28318530718;
|
||||
return normalize(vec3<f32>(cos(a), sin(a), 0.25));
|
||||
}
|
||||
|
||||
fn twilight_amount(sun: vec3<f32>) -> f32 {
|
||||
return smoothstep(-0.10, 0.05, sun.y) - smoothstep(0.05, 0.30, sun.y);
|
||||
}
|
||||
|
||||
fn sun_tint(sun: vec3<f32>) -> vec3<f32> {
|
||||
let twi = twilight_amount(sun);
|
||||
return mix(vec3<f32>(1.00, 0.95, 0.85), vec3<f32>(1.00, 0.55, 0.30), twi);
|
||||
}
|
||||
|
||||
struct ShaftsOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_shafts(@builtin(vertex_index) idx: u32) -> ShaftsOut {
|
||||
var corners = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>( 3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
);
|
||||
let p = corners[idx];
|
||||
var out: ShaftsOut;
|
||||
out.clip = vec4<f32>(p, 0.0, 1.0);
|
||||
out.uv = vec2<f32>(p.x * 0.5 + 0.5, p.y * -0.5 + 0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
const N_SAMPLES: i32 = 32;
|
||||
const DECAY: f32 = 0.965;
|
||||
const WEIGHT: f32 = 0.42;
|
||||
const EXPOSURE: f32 = 0.30;
|
||||
|
||||
@fragment
|
||||
fn fs_shafts(in: ShaftsOut) -> @location(0) vec4<f32> {
|
||||
let sun = sun_direction(scene_time());
|
||||
if (sun.y < -0.05) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Project the sun direction at infinity to screen space.
|
||||
// view_proj × (sun, 0.0) yields the homogeneous coords of a point
|
||||
// infinitely far along the sun direction.
|
||||
let sun_clip = camera.view_proj * vec4<f32>(sun, 0.0);
|
||||
if (sun_clip.w <= 0.0) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
let sun_ndc = sun_clip.xyz / sun_clip.w;
|
||||
let sun_uv = vec2<f32>(sun_ndc.x * 0.5 + 0.5, sun_ndc.y * -0.5 + 0.5);
|
||||
|
||||
// Step from this pixel toward the sun. Sample mask each step,
|
||||
// accumulate with exponential illumination decay.
|
||||
let delta = (sun_uv - in.uv) / f32(N_SAMPLES);
|
||||
var coord = in.uv;
|
||||
var illum_decay = 1.0;
|
||||
var accum = 0.0;
|
||||
for (var i = 0; i < N_SAMPLES; i = i + 1) {
|
||||
coord = coord + delta;
|
||||
let m = textureSample(mask_tex, mask_sampler, coord).r;
|
||||
accum = accum + m * illum_decay * WEIGHT;
|
||||
illum_decay = illum_decay * DECAY;
|
||||
}
|
||||
|
||||
// Gate by sun altitude (no shafts at night). At low sun the rays
|
||||
// are most dramatic — keep intensity flat over the visible range.
|
||||
let visibility = smoothstep(-0.05, 0.15, sun.y);
|
||||
let intensity = accum * EXPOSURE * visibility;
|
||||
|
||||
return vec4<f32>(sun_tint(sun) * intensity, 1.0);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue