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 }