1use alloc::vec;
7use alloc::vec::Vec;
8
9use crate::math::{ceil, floor, powf, sqrt};
10
11use crate::math::vec3::{cross, dot, length};
12
13use super::mesh_payload::Vertex;
14
15const VIEW_DIR: [f32; 3] = [-0.577, -0.577, -0.577];
18const LIGHT_DIR: [f32; 3] = [0.408, 0.816, 0.408];
19const AMBIENT: f32 = 0.30;
20const DIFFUSE: f32 = 0.70;
21const FIT: f32 = 0.9;
23
24pub struct RasterImage {
27 pub width: u32,
29 pub height: u32,
31 pub rgba: Vec<u8>,
33}
34
35fn normalize(v: [f32; 3]) -> [f32; 3] {
36 let len = length(v);
37 if len <= 0.0 || !len.is_finite() {
38 return [0.0, 0.0, 1.0];
39 }
40 [v[0] / len, v[1] / len, v[2] / len]
41}
42
43fn camera_basis() -> ([f32; 3], [f32; 3], [f32; 3]) {
46 let fwd = normalize(VIEW_DIR);
47 let right = normalize(cross([0.0, 1.0, 0.0], fwd));
48 let up = cross(fwd, right);
49 (right, up, fwd)
50}
51
52pub struct MeshPart<'a> {
56 pub verts: &'a [Vertex],
58 pub indices: &'a [u16],
60 pub color: [f32; 3],
62}
63
64pub fn shade_mesh(
69 verts: &[Vertex],
70 indices: &[u16],
71 size: u32,
72 base_color: [f32; 3],
73) -> RasterImage {
74 shade_parts(
75 &[MeshPart {
76 verts,
77 indices,
78 color: base_color,
79 }],
80 size,
81 )
82}
83
84pub fn shade_parts(parts: &[MeshPart], size: u32) -> RasterImage {
87 let mut img = RasterImage {
88 width: size,
89 height: size,
90 rgba: vec![0u8; (size * size * 4) as usize],
91 };
92 let mut positions = parts.iter().flat_map(|p| p.verts.iter().map(|v| v.pos));
93 let Some(first) = positions.next() else {
94 return img;
95 };
96 if size == 0 || parts.iter().all(|p| p.indices.len() < 3) {
97 return img;
98 }
99 let (right, up, fwd) = camera_basis();
100
101 let mut min = first;
104 let mut max = first;
105 for pos in positions {
106 for a in 0..3 {
107 min[a] = min[a].min(pos[a]);
108 max[a] = max[a].max(pos[a]);
109 }
110 }
111 let center = [
112 (min[0] + max[0]) * 0.5,
113 (min[1] + max[1]) * 0.5,
114 (min[2] + max[2]) * 0.5,
115 ];
116 let project = |v: &Vertex| {
117 let p = [
118 v.pos[0] - center[0],
119 v.pos[1] - center[1],
120 v.pos[2] - center[2],
121 ];
122 [dot(p, right), dot(p, up), dot(p, fwd)]
123 };
124 let extent = parts
125 .iter()
126 .flat_map(|p| p.verts.iter())
127 .map(|v| {
128 let p = project(v);
129 p[0].abs().max(p[1].abs())
130 })
131 .fold(0.0f32, f32::max);
132 if extent <= 0.0 || !extent.is_finite() {
133 return img;
134 }
135 let half = size as f32 * 0.5;
136 let scale = half * FIT / extent;
137 let light = normalize(LIGHT_DIR);
138 let mut depth = vec![f32::NEG_INFINITY; (size * size) as usize];
139 for part in parts {
140 let screen: Vec<[f32; 3]> = part
142 .verts
143 .iter()
144 .map(|v| {
145 let p = project(v);
146 [half + p[0] * scale, half - p[1] * scale, p[2]]
147 })
148 .collect();
149 for tri in part.indices.chunks_exact(3) {
150 let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
151 if i0 >= screen.len() || i1 >= screen.len() || i2 >= screen.len() {
152 continue;
153 }
154 fill_triangle(
155 &mut img,
156 &mut depth,
157 [screen[i0], screen[i1], screen[i2]],
158 [
159 part.verts[i0].normal,
160 part.verts[i1].normal,
161 part.verts[i2].normal,
162 ],
163 light,
164 part.color,
165 );
166 }
167 }
168 img
169}
170
171fn fill_triangle(
173 img: &mut RasterImage,
174 depth: &mut [f32],
175 p: [[f32; 3]; 3],
176 n: [[f32; 3]; 3],
177 light: [f32; 3],
178 base_color: [f32; 3],
179) {
180 let area =
181 (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[1][1] - p[0][1]) * (p[2][0] - p[0][0]);
182 if area.abs() <= f32::EPSILON || !area.is_finite() {
183 return;
184 }
185 let min_x = floor(p.iter().map(|v| v[0]).fold(f32::INFINITY, f32::min));
186 let max_x = ceil(p.iter().map(|v| v[0]).fold(f32::NEG_INFINITY, f32::max));
187 let min_y = floor(p.iter().map(|v| v[1]).fold(f32::INFINITY, f32::min));
188 let max_y = ceil(p.iter().map(|v| v[1]).fold(f32::NEG_INFINITY, f32::max));
189 let x0 = (min_x.max(0.0)) as u32;
190 let x1 = (max_x.min(img.width as f32 - 1.0)).max(0.0) as u32;
191 let y0 = (min_y.max(0.0)) as u32;
192 let y1 = (max_y.min(img.height as f32 - 1.0)).max(0.0) as u32;
193 for y in y0..=y1 {
194 for x in x0..=x1 {
195 let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
196 let w0 = ((p[1][0] - px) * (p[2][1] - py) - (p[1][1] - py) * (p[2][0] - px)) / area;
198 let w1 = ((p[2][0] - px) * (p[0][1] - py) - (p[2][1] - py) * (p[0][0] - px)) / area;
199 let w2 = 1.0 - w0 - w1;
200 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
201 continue;
202 }
203 let z = w0 * p[0][2] + w1 * p[1][2] + w2 * p[2][2];
204 let idx = (y * img.width + x) as usize;
205 if -z <= depth[idx] {
208 continue;
209 }
210 depth[idx] = -z;
211 let normal = normalize([
212 w0 * n[0][0] + w1 * n[1][0] + w2 * n[2][0],
213 w0 * n[0][1] + w1 * n[1][1] + w2 * n[2][1],
214 w0 * n[0][2] + w1 * n[1][2] + w2 * n[2][2],
215 ]);
216 let diff = dot(normal, light).abs().clamp(0.0, 1.0);
218 let shade = AMBIENT + DIFFUSE * diff;
219 let o = idx * 4;
220 for (c, &b) in base_color.iter().enumerate() {
221 img.rgba[o + c] = ((b * shade).clamp(0.0, 1.0) * 255.0) as u8;
222 }
223 img.rgba[o + 3] = 255;
224 }
225 }
226}
227
228pub fn shade_sphere(size: u32, albedo: [f32; 3], roughness: f32, metallic: f32) -> RasterImage {
232 let mut img = RasterImage {
233 width: size,
234 height: size,
235 rgba: vec![0u8; (size * size * 4) as usize],
236 };
237 if size == 0 {
238 return img;
239 }
240 let light = normalize([0.45, 0.65, 0.6]);
241 let view = [0.0, 0.0, 1.0];
242 let h = normalize([light[0] + view[0], light[1] + view[1], light[2] + view[2]]);
243 let rough = roughness.clamp(0.05, 1.0);
244 let metal = metallic.clamp(0.0, 1.0);
245 let shininess = 2.0 / (rough * rough) - 1.0;
246 let radius = size as f32 * 0.5 * FIT;
247 let half = size as f32 * 0.5;
248 for y in 0..size {
249 for x in 0..size {
250 let dx = (x as f32 + 0.5 - half) / radius;
251 let dy = (half - (y as f32 + 0.5)) / radius;
252 let d2 = dx * dx + dy * dy;
253 if d2 > 1.0 {
254 continue;
255 }
256 let normal = [dx, dy, sqrt(1.0 - d2)];
257 let diff = dot(normal, light).max(0.0);
258 let spec = powf(dot(normal, h).max(0.0), shininess) * (1.0 - rough * 0.6);
259 let spec_color = [
261 1.0 - metal + metal * albedo[0],
262 1.0 - metal + metal * albedo[1],
263 1.0 - metal + metal * albedo[2],
264 ];
265 let o = ((y * size + x) * 4) as usize;
266 for c in 0..3 {
267 let v = albedo[c] * (AMBIENT + DIFFUSE * diff) * (1.0 - metal * 0.7)
268 + spec_color[c] * spec;
269 img.rgba[o + c] = (v.clamp(0.0, 1.0) * 255.0) as u8;
270 }
271 img.rgba[o + 3] = 255;
272 }
273 }
274 img
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn quad(normal: [f32; 3]) -> (Vec<Vertex>, Vec<u16>) {
282 let v = |pos: [f32; 3]| Vertex {
283 pos,
284 normal,
285 tangent: [1.0, 0.0, 0.0],
286 color: [1.0; 3],
287 uv: [0.0; 2],
288 };
289 (
290 vec![
291 v([-1.0, -1.0, 0.0]),
292 v([1.0, -1.0, 0.0]),
293 v([1.0, 1.0, 0.0]),
294 v([-1.0, 1.0, 0.0]),
295 ],
296 vec![0, 1, 2, 0, 2, 3],
297 )
298 }
299
300 fn coverage(img: &RasterImage) -> usize {
301 img.rgba.chunks_exact(4).filter(|p| p[3] > 0).count()
302 }
303
304 #[test]
305 fn a_mesh_fills_pixels_and_the_background_stays_transparent() {
306 let (verts, indices) = quad([0.0, 0.0, 1.0]);
307 let img = shade_mesh(&verts, &indices, 64, [0.8, 0.8, 0.8]);
308 let covered = coverage(&img);
309 assert!(covered > 500, "the quad covers a real area: {covered}");
310 assert!(
311 covered < (64 * 64),
312 "the margin stays transparent: {covered}"
313 );
314 assert_eq!(img.rgba[3], 0);
316 }
317
318 #[test]
319 fn empty_or_degenerate_input_renders_transparent() {
320 assert_eq!(coverage(&shade_mesh(&[], &[], 32, [1.0; 3])), 0);
321 let (verts, _) = quad([0.0, 0.0, 1.0]);
322 assert_eq!(coverage(&shade_mesh(&verts, &[0, 1], 32, [1.0; 3])), 0);
323 let point = vec![verts[0], verts[0], verts[0]];
325 assert_eq!(coverage(&shade_mesh(&point, &[0, 1, 2], 32, [1.0; 3])), 0);
326 }
327
328 #[test]
329 fn nearer_geometry_wins_the_depth_test() {
330 let (mut near, mut idx) = quad([0.0, 0.0, 1.0]);
333 let (far, far_idx) = quad([0.0, 0.0, 1.0]);
334 for v in &mut near {
336 for (p, d) in v.pos.iter_mut().zip(VIEW_DIR) {
337 *p -= d * 2.0;
338 }
339 }
340 let base = near.len() as u16;
341 near.extend(far);
342 idx.extend(far_idx.iter().map(|i| i + base));
343 let both = shade_mesh(&near, &idx, 64, [1.0, 1.0, 1.0]);
346 let alone = shade_mesh(&near[..4], &[0, 1, 2, 0, 2, 3], 64, [1.0, 1.0, 1.0]);
347 let center = ((32 * 64 + 32) * 4) as usize;
348 assert_eq!(
349 both.rgba[center..center + 3],
350 alone.rgba[center..center + 3],
351 "the nearer quad's shade wins at the center"
352 );
353 }
354
355 #[test]
356 fn deterministic_output() {
357 let (verts, indices) = quad([0.0, 0.0, 1.0]);
358 let a = shade_mesh(&verts, &indices, 48, [0.5, 0.6, 0.7]);
359 let b = shade_mesh(&verts, &indices, 48, [0.5, 0.6, 0.7]);
360 assert_eq!(a.rgba, b.rgba);
361 }
362
363 #[test]
364 fn parts_share_one_frame_and_depth_buffer() {
365 let (mut far, far_idx) = quad([0.0, 0.0, 1.0]);
370 let (mut near, near_idx) = quad([0.0, 0.0, 1.0]);
371 for v in &mut far {
372 v.pos[0] *= 4.0;
373 v.pos[1] *= 4.0;
374 }
375 for v in &mut near {
376 v.pos[0] *= 0.5;
377 v.pos[1] *= 0.5;
378 for (p, d) in v.pos.iter_mut().zip(VIEW_DIR) {
379 *p -= d * 2.0;
380 }
381 }
382 let parts = |a: bool| {
383 let far_part = MeshPart {
384 verts: &far,
385 indices: &far_idx,
386 color: [0.0, 1.0, 0.0],
387 };
388 let near_part = MeshPart {
389 verts: &near,
390 indices: &near_idx,
391 color: [1.0, 0.0, 0.0],
392 };
393 if a {
394 [far_part, near_part]
395 } else {
396 [near_part, far_part]
397 }
398 };
399 let img = shade_parts(&parts(true), 64);
400 let count = |channel: usize| {
401 img.rgba
402 .chunks_exact(4)
403 .filter(|p| p[3] > 0 && p[channel] > p[(channel + 1) % 3].max(p[(channel + 2) % 3]))
404 .count()
405 };
406 assert!(count(0) > 0, "the near red part shows");
407 assert!(count(1) > 0, "the far green part shows");
408 let swapped = shade_parts(&parts(false), 64);
412 assert_eq!(img.rgba, swapped.rgba, "depth decides, not paint order");
413 }
414
415 #[test]
416 fn sphere_swatch_is_round_lit_and_material_sensitive() {
417 let rough = shade_sphere(64, [0.8, 0.2, 0.2], 1.0, 0.0);
418 let covered = coverage(&rough);
419 let full = 64 * 64;
420 assert!(covered > full / 2 && covered < full, "a disc: {covered}");
421 assert_eq!(rough.rgba[3], 0, "corners transparent");
422 let shiny = shade_sphere(64, [0.8, 0.2, 0.2], 0.1, 0.0);
423 assert_ne!(rough.rgba, shiny.rgba, "roughness changes the highlight");
424 let metal = shade_sphere(64, [0.8, 0.2, 0.2], 0.1, 1.0);
425 assert_ne!(shiny.rgba, metal.rgba, "metallic changes the shading");
426 }
427}