Skip to main content

loam_render/
sky_ground.rs

1use bytemuck::{Pod, Zeroable};
2use glam::Mat4;
3use wgpu::*;
4
5/// Shared with [`crate::raymarch::HYPERSLICE_KERNEL_WGSL`].
6pub const SKY_GROUND_WGSL: &str = include_str!("sky_ground.wgsl");
7
8/// Linear light; `below` is the color at ray y = -1, `above` at ray y = +1, mixed linearly in y.
9#[derive(Copy, Clone, Debug, PartialEq)]
10pub struct Sky {
11    pub below: [f32; 3],
12    pub above: [f32; 3],
13}
14
15pub const DEFAULT_SKY: Sky = Sky {
16    below: [0.04, 0.05, 0.10],
17    above: [0.10, 0.13, 0.22],
18};
19
20impl Sky {
21    /// The color at ray y = 0 with alpha 1.
22    pub const fn horizon(self) -> Color {
23        Color {
24            r: 0.5 * (self.below[0] as f64 + self.above[0] as f64),
25            g: 0.5 * (self.below[1] as f64 + self.above[1] as f64),
26            b: 0.5 * (self.below[2] as f64 + self.above[2] as f64),
27            a: 1.0,
28        }
29    }
30}
31
32pub const SKY_HORIZON: Color = DEFAULT_SKY.horizon();
33
34/// Returns linear light.
35pub fn linear_from_srgb8(rgb: [u8; 3]) -> [f32; 3] {
36    // IEC 61966-2-1 sRGB electro-optical transfer function.
37    rgb.map(|byte| {
38        let c = f64::from(byte) / 255.0;
39        let linear = if c <= 0.04045 {
40            c / 12.92
41        } else {
42            ((c + 0.055) / 1.055).powf(2.4)
43        };
44        linear as f32
45    })
46}
47
48pub const GROUND_DARK_GREY: [f32; 3] = [0.18, 0.20, 0.24];
49pub const GROUND_LIGHT_GREY: [f32; 3] = [0.30, 0.32, 0.36];
50
51/// Half the sky is mixed in at `ln 2 / density` world units.
52pub const DEFAULT_FOG_PER_UNIT: f32 = 0.0015;
53
54#[derive(Copy, Clone, Debug)]
55pub struct Ground {
56    pub y: f32,
57    pub dark: [f32; 3],
58    pub light: [f32; 3],
59    pub fog_per_unit: f32,
60    /// Distance from the eye before any fog mixes in.
61    pub fog_start: f32,
62    pub visible: bool,
63}
64
65/// Bind group 0, binding 0; matches the node's WGSL `Uniforms`.
66#[repr(C)]
67#[derive(Copy, Clone, Debug, Pod, Zeroable)]
68pub struct SkyGroundUniforms {
69    pub view_proj: [[f32; 4]; 4],
70    pub inv_view_proj: [[f32; 4]; 4],
71    pub viewport_origin: [f32; 2],
72    pub resolution: [f32; 2],
73    pub ground_dark: [f32; 3],
74    pub ground_y: f32,
75    pub ground_light: [f32; 3],
76    pub show_ground: f32,
77    pub sky_below: [f32; 3],
78    pub fog_per_unit: f32,
79    pub sky_above: [f32; 3],
80    pub fog_start: f32,
81}
82
83impl SkyGroundUniforms {
84    /// `view_proj` must be the matrix the raster content over this pass is drawn with.
85    pub fn new(view_proj: Mat4, viewport: crate::Viewport, sky: Sky, ground: Ground) -> Self {
86        Self {
87            view_proj: view_proj.to_cols_array_2d(),
88            inv_view_proj: view_proj.inverse().to_cols_array_2d(),
89            viewport_origin: [viewport.x as f32, viewport.y as f32],
90            resolution: viewport.resolution_f32(),
91            ground_dark: ground.dark,
92            ground_y: ground.y,
93            ground_light: ground.light,
94            show_ground: if ground.visible { 1.0 } else { 0.0 },
95            sky_below: sky.below,
96            fog_per_unit: ground.fog_per_unit,
97            sky_above: sky.above,
98            fog_start: ground.fog_start,
99        }
100    }
101
102    fn sky(&self) -> Sky {
103        Sky {
104            below: self.sky_below,
105            above: self.sky_above,
106        }
107    }
108}
109
110const SKY_GROUND_NODE_WGSL: &str = concat!(
111    include_str!("sky_ground.wgsl"),
112    r#"
113struct Uniforms {
114    view_proj: mat4x4<f32>,
115    inv_view_proj: mat4x4<f32>,
116    viewport_origin: vec2<f32>,
117    resolution: vec2<f32>,
118    ground_dark: vec3<f32>,
119    ground_y: f32,
120    ground_light: vec3<f32>,
121    show_ground: f32,
122    sky_below: vec3<f32>,
123    fog_per_unit: f32,
124    sky_above: vec3<f32>,
125    fog_start: f32,
126};
127
128@group(0) @binding(0) var<uniform> u: Uniforms;
129
130const HORIZON_EPS: f32 = 1.0e-6;
131
132const LIGHT_DIR: vec3<f32> = vec3<f32>(0.5, 0.85, 0.3);
133const AMBIENT: f32 = 0.20;
134const DIFFUSE: f32 = 0.85;
135
136struct Fragment {
137    @location(0) color: vec4<f32>,
138    @builtin(frag_depth) depth: f32,
139};
140
141@vertex
142fn vs_fullscreen(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
143    let uv = vec2<f32>(f32((vid << 1u) & 2u), f32(vid & 2u));
144    return vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
145}
146
147fn unproject(ndc: vec3<f32>) -> vec3<f32> {
148    let h = u.inv_view_proj * vec4<f32>(ndc, 1.0);
149    return h.xyz / h.w;
150}
151
152fn shade(frag_pos: vec4<f32>, near_ndc: f32, far_ndc: f32, background_depth: f32) -> Fragment {
153    let uv = (frag_pos.xy - u.viewport_origin) / u.resolution;
154    let ndc_xy = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
155    let near = unproject(vec3<f32>(ndc_xy, near_ndc));
156    // The infinite reversed projection puts the far point at w = 0, so the direction is formed before the divide.
157    let far = u.inv_view_proj * vec4<f32>(ndc_xy, far_ndc, 1.0);
158    let rd = normalize(far.xyz - near * far.w);
159    // Derivatives need uniform control flow, so the plane footprint is taken before the sky branch returns.
160    let plane_t = (u.ground_y - near.y) / select(rd.y, HORIZON_EPS, abs(rd.y) <= HORIZON_EPS);
161    let plane = (near + rd * plane_t).xz;
162    let footprint = abs(dpdx(plane)) + abs(dpdy(plane));
163
164    var out: Fragment;
165
166    var t: f32 = 0.0;
167    var hit = false;
168    if (u.show_ground >= 0.5 && abs(rd.y) > HORIZON_EPS) {
169        t = (u.ground_y - near.y) / rd.y;
170        hit = t > 0.0;
171    }
172
173    if (!hit) {
174        out.color = vec4<f32>(sky(rd, u.sky_below, u.sky_above), 1.0);
175        out.depth = background_depth;
176        return out;
177    }
178
179    let p_hit = near + rd * t;
180    let fog = 1.0 - exp(-max(t - u.fog_start, 0.0) * u.fog_per_unit);
181    let base = ground_color(p_hit, footprint, u.ground_dark, u.ground_light, fog);
182    let lambert = max(dot(vec3<f32>(0.0, 1.0, 0.0), normalize(LIGHT_DIR)), 0.0);
183    let lit = base * (AMBIENT + DIFFUSE * lambert);
184    out.color = vec4<f32>(mix(lit, sky(rd, u.sky_below, u.sky_above), fog), 1.0);
185
186    let clip = u.view_proj * vec4<f32>(p_hit, 1.0);
187    out.depth = clamp(clip.z / clip.w, 0.0, 1.0);
188    return out;
189}
190
191@fragment
192fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> Fragment {
193    return shade(frag_pos, 0.0, 1.0, 1.0);
194}
195
196@fragment
197fn fs_reversed_z(@builtin(position) frag_pos: vec4<f32>) -> Fragment {
198    return shade(frag_pos, 1.0, 0.0, 0.0);
199}
200"#
201);
202
203pub struct SkyGroundNode {
204    pipeline: RenderPipeline,
205    uniform_buf: Buffer,
206    bind_group: BindGroup,
207    clear: Color,
208    depth_clear: f32,
209}
210
211impl SkyGroundNode {
212    /// `depth_format` must match the attachments passed to [`Self::record`], which owns the frame's color and depth clear.
213    pub fn new(
214        device: &Device,
215        target_format: TextureFormat,
216        depth_format: TextureFormat,
217        convention: crate::DepthConvention,
218    ) -> Self {
219        let module = device.create_shader_module(ShaderModuleDescriptor {
220            label: Some("sky_ground shader"),
221            source: ShaderSource::Wgsl(SKY_GROUND_NODE_WGSL.into()),
222        });
223
224        let uniform_buf = device.create_buffer(&BufferDescriptor {
225            label: Some("sky_ground uniforms"),
226            size: std::mem::size_of::<SkyGroundUniforms>() as u64,
227            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
228            mapped_at_creation: false,
229        });
230
231        let bgl = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
232            label: Some("sky_ground bgl"),
233            entries: &[BindGroupLayoutEntry {
234                binding: 0,
235                visibility: ShaderStages::VERTEX_FRAGMENT,
236                ty: BindingType::Buffer {
237                    ty: BufferBindingType::Uniform,
238                    has_dynamic_offset: false,
239                    min_binding_size: None,
240                },
241                count: None,
242            }],
243        });
244
245        let bind_group = device.create_bind_group(&BindGroupDescriptor {
246            label: Some("sky_ground bg"),
247            layout: &bgl,
248            entries: &[BindGroupEntry {
249                binding: 0,
250                resource: uniform_buf.as_entire_binding(),
251            }],
252        });
253
254        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
255            label: Some("sky_ground pipeline layout"),
256            bind_group_layouts: &[&bgl],
257            push_constant_ranges: &[],
258        });
259
260        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
261            label: Some("sky_ground pipeline"),
262            layout: Some(&pipeline_layout),
263            vertex: VertexState {
264                module: &module,
265                entry_point: Some("vs_fullscreen"),
266                buffers: &[],
267                compilation_options: Default::default(),
268            },
269            fragment: Some(FragmentState {
270                module: &module,
271                entry_point: Some(match convention {
272                    crate::DepthConvention::StandardZ => "fs_main",
273                    crate::DepthConvention::ReversedZ => "fs_reversed_z",
274                }),
275                targets: &[Some(ColorTargetState {
276                    format: target_format,
277                    blend: None,
278                    write_mask: ColorWrites::ALL,
279                })],
280                compilation_options: Default::default(),
281            }),
282            primitive: PrimitiveState {
283                topology: PrimitiveTopology::TriangleList,
284                ..Default::default()
285            },
286            depth_stencil: Some(DepthStencilState {
287                format: depth_format,
288                depth_write_enabled: true,
289                depth_compare: CompareFunction::Always,
290                stencil: StencilState::default(),
291                bias: DepthBiasState::default(),
292            }),
293            multisample: MultisampleState {
294                count: 1,
295                ..Default::default()
296            },
297            multiview: None,
298            cache: None,
299        });
300
301        Self {
302            pipeline,
303            uniform_buf,
304            bind_group,
305            clear: SKY_HORIZON,
306            depth_clear: match convention {
307                crate::DepthConvention::StandardZ => 1.0,
308                crate::DepthConvention::ReversedZ => crate::view::DEPTH_CLEAR,
309            },
310        }
311    }
312
313    /// Also sets the color clear to the uniforms' horizon.
314    pub fn set_uniforms(&mut self, queue: &Queue, uniforms: &SkyGroundUniforms) {
315        self.clear = uniforms.sky().horizon();
316        queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(uniforms));
317    }
318
319    /// Clears both attachments, so it records at the start of a stage the schedule lets clear; `viewport` restricts the shading, not the clear.
320    pub fn record(
321        &self,
322        encoder: &mut CommandEncoder,
323        view: &TextureView,
324        depth_view: &TextureView,
325        viewport: Option<&crate::Viewport>,
326    ) {
327        let mut rp = encoder.begin_render_pass(&RenderPassDescriptor {
328            label: Some("sky_ground pass"),
329            color_attachments: &[Some(RenderPassColorAttachment {
330                view,
331                depth_slice: None,
332                resolve_target: None,
333                ops: Operations {
334                    load: LoadOp::Clear(self.clear),
335                    store: StoreOp::Store,
336                },
337            })],
338            depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
339                view: depth_view,
340                depth_ops: Some(Operations {
341                    load: LoadOp::Clear(self.depth_clear),
342                    store: StoreOp::Store,
343                }),
344                stencil_ops: None,
345            }),
346            timestamp_writes: None,
347            occlusion_query_set: None,
348        });
349        self.draw(&mut rp, viewport);
350    }
351
352    pub fn draw(&self, rp: &mut RenderPass<'_>, viewport: Option<&crate::Viewport>) {
353        if let Some(vp) = viewport {
354            vp.apply(rp);
355        }
356        rp.set_pipeline(&self.pipeline);
357        rp.set_bind_group(0, &self.bind_group, &[]);
358        rp.draw(0..3, 0..1);
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn shader_validates_and_uniforms_match_host_layout() {
368        let module = naga::front::wgsl::parse_str(SKY_GROUND_NODE_WGSL).expect("parse");
369        naga::valid::Validator::new(
370            naga::valid::ValidationFlags::all(),
371            naga::valid::Capabilities::empty(),
372        )
373        .validate(&module)
374        .expect("sky shader validates");
375        let ty = module
376            .types
377            .iter()
378            .map(|(_, t)| t)
379            .find(|t| t.name.as_deref() == Some("Uniforms"))
380            .expect("the node's WGSL declares `Uniforms`");
381        let naga::TypeInner::Struct { members, span } = &ty.inner else {
382            panic!("`Uniforms` is not a struct");
383        };
384        assert_eq!(*span as usize, std::mem::size_of::<SkyGroundUniforms>());
385        let rust_offsets = [
386            (
387                "view_proj",
388                std::mem::offset_of!(SkyGroundUniforms, view_proj),
389            ),
390            (
391                "inv_view_proj",
392                std::mem::offset_of!(SkyGroundUniforms, inv_view_proj),
393            ),
394            (
395                "viewport_origin",
396                std::mem::offset_of!(SkyGroundUniforms, viewport_origin),
397            ),
398            (
399                "resolution",
400                std::mem::offset_of!(SkyGroundUniforms, resolution),
401            ),
402            (
403                "ground_dark",
404                std::mem::offset_of!(SkyGroundUniforms, ground_dark),
405            ),
406            (
407                "ground_y",
408                std::mem::offset_of!(SkyGroundUniforms, ground_y),
409            ),
410            (
411                "ground_light",
412                std::mem::offset_of!(SkyGroundUniforms, ground_light),
413            ),
414            (
415                "show_ground",
416                std::mem::offset_of!(SkyGroundUniforms, show_ground),
417            ),
418            (
419                "sky_below",
420                std::mem::offset_of!(SkyGroundUniforms, sky_below),
421            ),
422            (
423                "fog_per_unit",
424                std::mem::offset_of!(SkyGroundUniforms, fog_per_unit),
425            ),
426            (
427                "sky_above",
428                std::mem::offset_of!(SkyGroundUniforms, sky_above),
429            ),
430            (
431                "fog_start",
432                std::mem::offset_of!(SkyGroundUniforms, fog_start),
433            ),
434        ];
435        assert_eq!(members.len(), rust_offsets.len());
436        for (member, (name, offset)) in members.iter().zip(rust_offsets) {
437            assert_eq!(member.name.as_deref(), Some(name));
438            assert_eq!(member.offset as usize, offset, "offset of {name}");
439        }
440    }
441
442    #[test]
443    fn every_srgb_byte_survives_decode_and_the_targets_encode() {
444        for byte in 0..=u8::MAX {
445            let [linear, _, _] = linear_from_srgb8([byte; 3]);
446            let c = f64::from(linear);
447            let encoded = if c <= 0.003_130_8 {
448                12.92 * c
449            } else {
450                1.055 * c.powf(1.0 / 2.4) - 0.055
451            };
452            assert_eq!((encoded * 255.0).round() as u8, byte);
453        }
454    }
455}