git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/draw/lit.rs (9K)

  1 //! The lit, textured mesh path of the 3D scene pass, beside the
  2 //! `Vertex3D` one and drawn in the same pass.
  3 //!
  4 //! A [`Vertex3D`](super::scene::Vertex3D) is a position and a colour, shaded
  5 //! flat or pre-lit by the host. A [`LitVertex`] carries what a model file
  6 //! carries — a normal, a texture coordinate and a colour — and is lit in the
  7 //! fragment shader (`scene3d_lit.wgsl`): a base colour (the material's,
  8 //! times the vertex colour, times a texel of its base-colour image), a
  9 //! metallic-roughness specular from GGX, a key and a fill light and a
 10 //! sky/ground ambient, all view-dependent, so a highlight moves as the
 11 //! camera does. It exists for files that bring their own normals and
 12 //! textures (glTF, OBJ with an MTL `map_Kd`), which the flat path could
 13 //! only show in a factor colour.
 14 //!
 15 //! It is ADDITIVE: [`Stage3D::lit`](super::scene::Stage3D::lit) answers
 16 //! `None` unless a renderer implements it (the Vulkan one does; the WebGPU
 17 //! one does not yet), so a host checks and keeps the flat path otherwise,
 18 //! and nothing that does not ask for it changed.
 19 //!
 20 //! Textures are ordinary image ids (`upload_rgba`): RGBA8 sRGB, so a texel
 21 //! reaches the shader linear, as an albedo must. They die with the renderer
 22 //! like every image id; a host re-uploads them in `init_3d`. The image
 23 //! sampler clamps, so the shader wraps texture coordinates itself (glTF
 24 //! repeats by default), sampling with the unwrapped coordinates' gradients
 25 //! so the wrap leaves no seam.
 26 
 27 /// One corner of a lit triangle. `uv` is in image convention: (0, 0) the
 28 /// top-left texel, as glTF has it (an OBJ `vt` needs its v flipped).
 29 #[repr(C)]
 30 #[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
 31 pub struct LitVertex {
 32     pub position: [f32; 3],
 33     /// Unit length; the shader flips it for a face seen from behind, so open
 34     /// and single-sided meshes light from both sides.
 35     pub normal: [f32; 3],
 36     pub uv: [f32; 2],
 37     /// Linear RGB, multiplying the material's base colour: white for none.
 38     pub color: [f32; 3],
 39 }
 40 
 41 /// A lit mesh a renderer holds, as [`LitStage3D::create_lit_mesh`] named it.
 42 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 43 pub struct LitMeshId(pub(crate) usize);
 44 
 45 /// How a lit surface answers the light (glTF's metallic-roughness model).
 46 #[derive(Debug, Clone, Copy, PartialEq)]
 47 pub struct LitMaterial {
 48     /// Linear RGB.
 49     pub base_color: [f32; 3],
 50     /// An image id (`upload_rgba`) whose texels multiply `base_color`, or
 51     /// `None`. A texture whose upload has not landed draws as `None`.
 52     pub texture: Option<u32>,
 53     /// 0 a dielectric (plastic, paint, wood), 1 a metal.
 54     pub metallic: f32,
 55     /// 0 a mirror, 1 fully rough.
 56     pub roughness: f32,
 57 }
 58 
 59 impl Default for LitMaterial {
 60     fn default() -> Self {
 61         Self { base_color: [1.0; 3], texture: None, metallic: 0.0, roughness: 0.8 }
 62     }
 63 }
 64 
 65 /// The light every lit draw is shaded by: a key and a fill (directions
 66 /// TOWARD them, world space, any length; colours linear and may exceed 1),
 67 /// and an ambient that blends from `ground` (a surface facing down) to `sky`
 68 /// (facing up), which also stands in for the environment a metal reflects.
 69 #[derive(Debug, Clone, Copy, PartialEq)]
 70 pub struct LitLight {
 71     pub key_toward: [f32; 3],
 72     pub key_color: [f32; 3],
 73     pub fill_toward: [f32; 3],
 74     pub fill_color: [f32; 3],
 75     pub sky: [f32; 3],
 76     pub ground: [f32; 3],
 77 }
 78 
 79 impl Default for LitLight {
 80     fn default() -> Self {
 81         Self {
 82             key_toward: [-0.35, 0.75, 0.55],
 83             key_color: [0.95; 3],
 84             fill_toward: [0.75, 0.1, -0.1],
 85             fill_color: [0.25; 3],
 86             sky: [0.2; 3],
 87             ground: [0.06; 3],
 88         }
 89     }
 90 }
 91 
 92 /// One lit draw in the staged scene.
 93 #[derive(Debug, Clone, Copy, PartialEq)]
 94 pub struct LitDraw {
 95     pub mesh: LitMeshId,
 96     pub mvp: [[f32; 4]; 4],
 97     /// The camera's position in the space the mesh is in, for the specular.
 98     pub eye: [f32; 3],
 99     pub material: LitMaterial,
100     /// Whole-draw alpha multiplier (1 = opaque), as `SceneDraw::opacity`.
101     pub opacity: f32,
102     /// The width of a wire pass that will ride on this fill (0 = none), as
103     /// `SceneDraw::wire_base_width`: the fill is pushed back so the wires
104     /// win the depth test.
105     pub wire_base_width: f32,
106     /// Draw order: this draw renders before the `SceneDraw` at this index
107     /// of the staged list, `u32::MAX` after them all — as
108     /// `SceneImage::before`. A host drawing a background and a floor as
109     /// scene draws and wires over the model puts the model between.
110     pub before: u32,
111 }
112 
113 /// What a renderer offers for lit meshes, reached through
114 /// [`Stage3D::lit`](super::scene::Stage3D::lit).
115 pub trait LitStage3D {
116     fn create_lit_mesh(&mut self, verts: &[LitVertex]) -> LitMeshId;
117     /// Replace a lit mesh's vertices (a new model in the same slot).
118     fn update_lit_mesh(&mut self, id: LitMeshId, verts: &[LitVertex]);
119     /// The light every lit draw is shaded by, from now on.
120     fn set_lit_light(&mut self, light: LitLight);
121     /// This frame's lit draws, after [`Stage3D::stage_scene`](super::scene::Stage3D::stage_scene)
122     /// (nothing is drawn when no scene is staged).
123     fn stage_lit(&mut self, draws: Vec<LitDraw>);
124 }
125 
126 /// `scene3d_lit.wgsl`'s uniform block. Its head is the scene block's (mvp,
127 /// window size, corner radius and shape), so the corner cut is shared.
128 #[repr(C)]
129 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
130 pub(crate) struct LitUniforms {
131     mvp: [[f32; 4]; 4],
132     window_size: [f32; 2],
133     window_radius: f32,
134     corner_shape: f32,
135     /// xyz the eye, w unused.
136     eye: [f32; 4],
137     /// rgb the base colour, a the draw's opacity.
138     base: [f32; 4],
139     /// metallic, roughness, 1 when textured, unused.
140     surface: [f32; 4],
141     key_toward: [f32; 4],
142     key_color: [f32; 4],
143     fill_toward: [f32; 4],
144     fill_color: [f32; 4],
145     sky: [f32; 4],
146     ground: [f32; 4],
147 }
148 
149 #[cfg_attr(target_arch = "wasm32", allow(dead_code))] // the Vulkan renderer's; WebGPU has no lit pass yet
150 pub(crate) const LIT_UNIFORM_SIZE: usize = std::mem::size_of::<LitUniforms>();
151 
152 #[cfg_attr(target_arch = "wasm32", allow(dead_code))] // the Vulkan renderer's; WebGPU has no lit pass yet
153 fn unit4(v: [f32; 3]) -> [f32; 4] {
154     let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
155     if l > 0.0 {
156         [v[0] / l, v[1] / l, v[2] / l, 0.0]
157     } else {
158         [0.0, 1.0, 0.0, 0.0]
159     }
160 }
161 
162 #[cfg_attr(target_arch = "wasm32", allow(dead_code))] // the Vulkan renderer's; WebGPU has no lit pass yet
163 fn pad(v: [f32; 3]) -> [f32; 4] {
164     [v[0], v[1], v[2], 0.0]
165 }
166 
167 /// The staged lit draws' uniform blocks, in order. A texture not resident
168 /// in the renderer is bound as its white fallback, so "textured" with no
169 /// texture yet draws the plain base colour.
170 #[cfg_attr(target_arch = "wasm32", allow(dead_code))] // the Vulkan renderer's; WebGPU has no lit pass yet
171 pub(crate) fn lit_uniforms(
172     draws: &[LitDraw],
173     light: &LitLight,
174     window_size: [f32; 2],
175     corner_radius_px: f32,
176 ) -> Vec<LitUniforms> {
177     let corner_shape = crate::layout::corner_shape();
178     draws
179         .iter()
180         .map(|d| {
181             let m = &d.material;
182             LitUniforms {
183                 mvp: d.mvp,
184                 window_size,
185                 window_radius: corner_radius_px,
186                 corner_shape,
187                 eye: pad(d.eye),
188                 base: [m.base_color[0], m.base_color[1], m.base_color[2], d.opacity],
189                 surface: [
190                     m.metallic.clamp(0.0, 1.0),
191                     m.roughness.clamp(0.04, 1.0),
192                     if m.texture.is_some() { 1.0 } else { 0.0 },
193                     0.0,
194                 ],
195                 key_toward: unit4(light.key_toward),
196                 key_color: pad(light.key_color),
197                 fill_toward: unit4(light.fill_toward),
198                 fill_color: pad(light.fill_color),
199                 sky: pad(light.sky),
200                 ground: pad(light.ground),
201             }
202         })
203         .collect()
204 }
205 
206 #[cfg(test)]
207 mod tests {
208     use super::*;
209 
210     #[test]
211     fn the_uniform_block_is_what_the_shader_reads() {
212         // mvp, then window size/radius/shape, then nine vec4s.
213         assert_eq!(LIT_UNIFORM_SIZE, 64 + 16 + 9 * 16);
214         assert_eq!(std::mem::size_of::<LitVertex>(), 11 * 4);
215     }
216 
217     #[test]
218     fn a_textured_draw_says_so_and_lights_are_normalized() {
219         let draw = LitDraw {
220             mesh: LitMeshId(0),
221             mvp: [[0.0; 4]; 4],
222             eye: [0.0, 0.0, 5.0],
223             material: LitMaterial { texture: Some(7), ..LitMaterial::default() },
224             opacity: 1.0,
225             wire_base_width: 0.0,
226             before: u32::MAX,
227         };
228         let light = LitLight { key_toward: [0.0, 3.0, 0.0], ..LitLight::default() };
229         let plain = LitDraw { material: LitMaterial::default(), ..draw };
230         let blocks = lit_uniforms(&[draw, plain], &light, [100.0, 100.0], 0.0);
231         assert_eq!(blocks[0].surface[2], 1.0);
232         assert_eq!(blocks[1].surface[2], 0.0);
233         assert_eq!(blocks[0].key_toward, [0.0, 1.0, 0.0, 0.0]);
234     }
235 }