Skip to main content

concinnity_core/render/uniforms/
transparent.rs

1//! What the transparent pass binds: the shared per-frame view block, and the
2//! per-record tunables of its producers -- glass panes, see-through glass
3//! meshes, and water surfaces.
4
5/// Per-frame view inputs shared by every draw in the transparent pass (water,
6/// glass), bound once for the whole pass. Matches `TransparentView` in
7/// `shaders/glass.slang` and `shaders/water.slang`. 240 bytes.
8#[derive(Copy, Clone, bytemuck::NoUninit)]
9#[repr(C)]
10pub struct TransparentView {
11    /// View-projection matrix, column-major.
12    pub vp: [[f32; 4]; 4],
13    /// Inverse view-projection matrix, column-major.
14    pub inv_vp: [[f32; 4]; 4],
15    /// World-space camera position (xyz). `.w` is ignored by the shader.
16    pub camera_pos: [f32; 4],
17    /// Render-target width / height in pixels: the shader uses this to
18    /// turn its fragment position into a normalised screen UV.
19    pub viewport: [f32; 2],
20    /// Wall-clock seconds since startup, fed to the Gerstner sum.
21    pub time: f32,
22    /// Mip count of the bound IBL prefilter cube; 0 signals "no environment map",
23    /// where the glass reflection falls back to a white rim. Per-frame state, so
24    /// it rides the shared view rather than a per-draw params block.
25    pub prefilter_mip_count: f32,
26    /// Rows of the rotation taking a world-space direction into the environment
27    /// cubemap's baked frame, mirroring `ViewUniforms.sky_rot` so this pass's
28    /// sky taps turn with the main one. One `float4` per row; `w` is unused.
29    pub sky_rot: [[f32; 4]; 3],
30    /// `[x, y, z, _]`: unit direction toward the scene's sun, the first
31    /// directional light. Zero when the world declares none.
32    pub sun_dir: [f32; 4],
33    /// `[r, g, b, _]`: that light's colour times its intensity, which the water
34    /// glint scales by. Zero when the world declares no directional light, and
35    /// the shader draws no glint.
36    pub sun_color: [f32; 4],
37}
38
39/// Per-panel tunables for a `GlassPanel`, uploaded once per panel per frame.
40/// The vec3-ish fields are `[f32; 4]` so the layout is byte-identical to the
41/// shader's `float4`. Matches `GlassParams` in `shaders/glass.slang`. 64 bytes.
42#[derive(Copy, Clone, bytemuck::NoUninit)]
43#[repr(C)]
44pub struct GlassParams {
45    /// `[x, y, z, _]`: world-space panel centre.
46    pub centre: [f32; 4],
47    /// `[nx, ny, nz, _]`: unit panel normal (facing direction).
48    pub normal: [f32; 4],
49    /// `[r, g, b, _]`: colour multiplied into the refracted scene.
50    pub tint: [f32; 4],
51    /// Base alpha at normal incidence.
52    pub opacity: f32,
53    /// Screen-space refraction offset strength.
54    pub refraction_strength: f32,
55    /// Schlick-Fresnel exponent for the grazing-angle rim.
56    pub fresnel_power: f32,
57    /// Planar reflection strength: `> 0.5` selects the sharp planar reflection
58    /// (the scene re-rendered mirrored across this pane's plane, sampled
59    /// projectively at screen UV) over the probe / sky cube. 0 when planar is off
60    /// (RT on, no planar slot, or the plane overflowed the budget), keeping the
61    /// probe / sky path. Patched per-frame in `collect_glass_transparent_draws`.
62    pub planar: f32,
63}
64
65/// Per-draw tunables for a see-through glass MESH: a `Material` flagged
66/// `see_through` on an RT-capable device, drawn in the transparent pass instead
67/// of the opaque one. Unlike `GlassParams` (a pre-baked world-space pane), a
68/// mesh is LOCAL-space, so this carries the model matrix the vertex stage
69/// applies; the fragment uses the interpolated per-vertex world normal. Matches
70/// `GlassMeshParams` in `shaders/glass_mesh.slang`. 96 bytes (model is the first
71/// field, so its 16-byte GPU alignment is satisfied at offset 0).
72#[derive(Copy, Clone, bytemuck::NoUninit)]
73#[repr(C)]
74pub struct GlassMeshParams {
75    /// Column-major local-to-world model matrix.
76    pub model: [[f32; 4]; 4],
77    /// `[r, g, b, _]`: colour multiplied into the refracted scene (material tint).
78    pub tint: [f32; 4],
79    /// Base alpha at normal incidence (from `Material.opacity`).
80    pub opacity: f32,
81    /// Screen-space refraction offset strength.
82    pub refraction_strength: f32,
83    /// Schlick-Fresnel exponent for the grazing-angle rim.
84    pub fresnel_power: f32,
85    /// Mip count of the bound IBL prefilter cube (ray-miss fallback); 0 = none.
86    pub prefilter_mip_count: f32,
87}
88
89/// Maximum waves summed per `WaterParams`. Mirrors `MAX_WATER_WAVES` in
90/// `shaders/water.slang` and in the `WaterSurface` asset.
91pub const WATER_MAX_WAVES: usize = 4;
92
93/// One Gerstner wave coefficient set, packed into two `float4` lanes so the
94/// layout is identical on every target. Matches `WaterWave` in
95/// `shaders/water.slang`. 32 bytes.
96#[derive(Copy, Clone, Default, bytemuck::Zeroable, bytemuck::Pod)]
97#[repr(C)]
98pub struct WaterWaveGpu {
99    /// `[direction.x, direction.y, amplitude, wavelength]`.
100    pub dir_amp_wave: [f32; 4],
101    /// `[speed, steepness, _, _]`.
102    pub speed_steep_pad: [f32; 4],
103}
104
105/// Per-surface tunables for a `WaterSurface`, uploaded once per surface. The
106/// vec3-ish fields are `[f32; 4]` so the layout is byte-identical to the
107/// shader's `float4`. Matches `WaterParams` in `shaders/water.slang`. 224 bytes.
108#[derive(Copy, Clone, bytemuck::NoUninit)]
109#[repr(C)]
110pub struct WaterParams {
111    /// `[x, y, z, _]`: world-space surface centre.
112    pub centre: [f32; 4],
113    /// `[r, g, b, _]`: water tint at full column depth.
114    pub deep_colour: [f32; 4],
115    /// `[r, g, b, _]`: water tint just above the seabed.
116    pub shallow_colour: [f32; 4],
117    /// Depth over which the tint blends from shallow to deep, in metres.
118    pub depth_falloff: f32,
119    /// Width of the shoreline foam band, in world units.
120    pub foam_width: f32,
121    /// Foam brightness multiplier.
122    pub foam_intensity: f32,
123    /// Exponent of the Fresnel reflectance curve.
124    pub fresnel_power: f32,
125    /// Perceptual roughness in `[0, 1]`; picks the reflection's prefilter mip.
126    pub roughness: f32,
127    /// How far refraction offsets the sampled background.
128    pub refraction_strength: f32,
129    /// Live entries in `waves`.
130    pub wave_count: u32,
131    /// Padding so the field layout matches the shader-side struct.
132    pub _pad: f32,
133    /// Wave coefficients; the first `wave_count` entries are live.
134    pub waves: [WaterWaveGpu; WATER_MAX_WAVES],
135    /// Planar reflection control: `[strength, distortion, _, _]`. `strength >
136    /// 0.5` selects the sharp planar reflection (the scene re-rendered mirrored
137    /// across this surface's rest plane, sampled at screen UV) over the probe /
138    /// sky cube; `distortion` scales the wave-normal ripple offset of that
139    /// lookup, see [`WaterParams::planar_lane`]. 0 when planar is off (RT on,
140    /// no planar slot, or the plane overflowed the budget), keeping the probe /
141    /// sky path.
142    pub planar: [f32; 4],
143}
144
145// How far the mirror lookup is pushed per unit of wave slope, per unit of the
146// surface's authored roughness, and the ceiling a very rough surface stops at.
147// The planar target is a flat-plane render, so the offset only fakes the
148// ripple; a near-mirror surface barely moves it, a choppy one moves it more.
149const PLANAR_DISTORTION_PER_ROUGHNESS: f32 = 0.6;
150const PLANAR_DISTORTION_MAX: f32 = 0.06;
151
152impl WaterParams {
153    /// The `planar` lane for a surface of `roughness`: the mirror selected
154    /// with its ripple offset scaled by the roughness when `mirrored`, else
155    /// zeroed so the shader keeps the probe / sky path.
156    pub fn planar_lane(roughness: f32, mirrored: bool) -> [f32; 4] {
157        if !mirrored {
158            return [0.0; 4];
159        }
160        let distortion =
161            (roughness.max(0.0) * PLANAR_DISTORTION_PER_ROUGHNESS).min(PLANAR_DISTORTION_MAX);
162        [1.0, distortion, 0.0, 0.0]
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use core::mem::{offset_of, size_of};
170
171    // Every backend binds this block under the same layout, so it is checked
172    // here rather than per backend: a float4x4 model, a float4 tint, then four
173    // scalars. `model` is first, so its 16-byte GPU alignment is satisfied at
174    // offset 0 and the Rust `[[f32; 4]; 4]` matches byte-for-byte.
175    #[test]
176    fn glass_mesh_params_layout_matches_shader() {
177        assert_eq!(size_of::<GlassMeshParams>(), 96);
178        assert_eq!(offset_of!(GlassMeshParams, model), 0);
179        assert_eq!(offset_of!(GlassMeshParams, tint), 64);
180        assert_eq!(offset_of!(GlassMeshParams, opacity), 80);
181        assert_eq!(offset_of!(GlassMeshParams, refraction_strength), 84);
182        assert_eq!(offset_of!(GlassMeshParams, fresnel_power), 88);
183        assert_eq!(offset_of!(GlassMeshParams, prefilter_mip_count), 92);
184        assert_eq!(size_of::<GlassMeshParams>() % 16, 0);
185    }
186
187    // The per-frame block every transparent draw shares. `sky_rot` is a float4
188    // array, so it needs the 16-byte boundary the scalars ahead of it land on.
189    #[test]
190    fn transparent_view_layout_matches_shader() {
191        assert_eq!(size_of::<TransparentView>(), 240);
192        assert_eq!(offset_of!(TransparentView, vp), 0);
193        assert_eq!(offset_of!(TransparentView, inv_vp), 64);
194        assert_eq!(offset_of!(TransparentView, camera_pos), 128);
195        assert_eq!(offset_of!(TransparentView, viewport), 144);
196        assert_eq!(offset_of!(TransparentView, time), 152);
197        assert_eq!(offset_of!(TransparentView, prefilter_mip_count), 156);
198        assert_eq!(offset_of!(TransparentView, sky_rot), 160);
199        assert_eq!(offset_of!(TransparentView, sun_dir), 208);
200        assert_eq!(offset_of!(TransparentView, sun_color), 224);
201        assert_eq!(size_of::<TransparentView>() % 16, 0);
202    }
203
204    // The ripple offset follows the surface's roughness: a mirror barely moves
205    // its lookup, a rough surface moves it up to the ceiling, and a surface
206    // with no mirror slot carries nothing.
207    #[test]
208    fn the_planar_lane_scales_the_ripple_offset_by_roughness() {
209        assert_eq!(WaterParams::planar_lane(0.0, true), [1.0, 0.0, 0.0, 0.0]);
210        let default_roughness = WaterParams::planar_lane(0.05, true);
211        assert!(
212            (default_roughness[1] - 0.03).abs() < 1e-6,
213            "{default_roughness:?}"
214        );
215        let mirror = WaterParams::planar_lane(0.01, true)[1];
216        let rough = WaterParams::planar_lane(0.2, true)[1];
217        assert!(mirror < default_roughness[1] && default_roughness[1] < rough);
218        assert_eq!(
219            WaterParams::planar_lane(1.0, true)[1],
220            PLANAR_DISTORTION_MAX
221        );
222        assert_eq!(WaterParams::planar_lane(-1.0, true)[1], 0.0);
223        assert_eq!(WaterParams::planar_lane(0.5, false), [0.0; 4]);
224    }
225
226    // Two float4s and four scalars, in one 16-byte-aligned block.
227    #[test]
228    fn glass_params_layout_matches_shader() {
229        assert_eq!(size_of::<GlassParams>(), 64);
230        assert_eq!(offset_of!(GlassParams, centre), 0);
231        assert_eq!(offset_of!(GlassParams, normal), 16);
232        assert_eq!(offset_of!(GlassParams, tint), 32);
233        assert_eq!(offset_of!(GlassParams, opacity), 48);
234        assert_eq!(offset_of!(GlassParams, refraction_strength), 52);
235        assert_eq!(offset_of!(GlassParams, fresnel_power), 56);
236        assert_eq!(offset_of!(GlassParams, planar), 60);
237    }
238
239    // `waves` is an array of float4-carrying structs, so the shader aligns it to
240    // 16 and the seven live scalars ahead of it need `_pad` to reach that
241    // boundary. Nothing else pins the pad, so it is asserted here.
242    #[test]
243    fn water_params_layout_matches_shader() {
244        assert_eq!(size_of::<WaterParams>(), 224);
245        assert_eq!(offset_of!(WaterParams, centre), 0);
246        assert_eq!(offset_of!(WaterParams, deep_colour), 16);
247        assert_eq!(offset_of!(WaterParams, shallow_colour), 32);
248        assert_eq!(offset_of!(WaterParams, depth_falloff), 48);
249        assert_eq!(offset_of!(WaterParams, foam_width), 52);
250        assert_eq!(offset_of!(WaterParams, foam_intensity), 56);
251        assert_eq!(offset_of!(WaterParams, fresnel_power), 60);
252        assert_eq!(offset_of!(WaterParams, roughness), 64);
253        assert_eq!(offset_of!(WaterParams, refraction_strength), 68);
254        assert_eq!(offset_of!(WaterParams, wave_count), 72);
255        assert_eq!(offset_of!(WaterParams, _pad), 76);
256        assert_eq!(offset_of!(WaterParams, waves), 80);
257        assert_eq!(offset_of!(WaterParams, planar), 208);
258        assert_eq!(size_of::<WaterWaveGpu>(), 32);
259    }
260}