ling/gfx/light.rs
1// src/gfx/light.rs — Point lights with cel-shading quantisation.
2//
3// Design:
4// • Each Light lives in 3-D world space (same space as geometry before
5// camera rotation). For 4-D scenes, the Ling program projects lights
6// from 4-D → 3-D the same way it projects geometry, then registers them
7// with `add_light`.
8//
9// • Shading is cel / toon: the diffuse dot-product is quantised into
10// discrete bands so every face is either shadowed, mid-tone, or fully lit.
11// This gives the crisp, vector-graphic look of flat-shaded cel animation.
12//
13// • Distance attenuation is linear within `radius`; beyond `radius` the
14// contribution is zero. `radius == 0` means infinite (no attenuation).
15//
16// • Both sides of every face are illuminated (|dot|) — avoids harsh black
17// patches from back-facing geometry.
18
19/// A coloured point light in 3-D world space.
20#[derive(Debug, Clone)]
21pub struct Light {
22 /// World-space position.
23 pub x: f32, pub y: f32, pub z: f32,
24 /// Linear RGB colour components in [0..1].
25 pub r: f32, pub g: f32, pub b: f32,
26 /// Peak intensity multiplier (1.0 = full pen colour at contact).
27 pub intensity: f32,
28 /// Influence radius. 0 = no distance attenuation.
29 pub radius: f32,
30}
31
32/// Cel / toon quantisation — maps a continuous diffuse value → 3 discrete bands.
33///
34/// ```text
35/// raw dot band visual
36/// ───────────────────────────
37/// 0.00–0.25 shadow (dim)
38/// 0.25–0.60 mid (flat)
39/// 0.60–1.00 lit (bright)
40/// ```
41#[inline]
42pub fn cel_quantize(v: f32) -> f32 {
43 if v < 0.25 { 0.08 }
44 else if v < 0.60 { 0.50 }
45 else { 1.00 }
46}
47
48/// Compute the final lit colour for one triangle face.
49///
50/// Parameters
51/// - `base` : 0x00RRGGBB base colour set by `สีดินสอ`
52/// - `normal` : un-normalised world-space face normal (B−A) × (C−A)
53/// - `centroid` : average of the three vertices in world space
54/// - `lights` : active lights for this frame
55/// - `ambient` : ambient fill in [0..1]
56///
57/// Returns 0x00RRGGBB with lighting applied.
58pub fn compute_lit_color(
59 base: u32,
60 normal: [f32; 3],
61 centroid: [f32; 3],
62 lights: &[Light],
63 ambient: f32,
64) -> u32 {
65 let br = ((base >> 16) & 0xFF) as f32;
66 let bg = ((base >> 8) & 0xFF) as f32;
67 let bb = ( base & 0xFF) as f32;
68
69 // Accumulate light contributions starting from ambient fill
70 let mut acc_r = br * ambient;
71 let mut acc_g = bg * ambient;
72 let mut acc_b = bb * ambient;
73
74 // Normalise the face normal once
75 let [nx, ny, nz] = normal;
76 let nlen = (nx*nx + ny*ny + nz*nz).sqrt();
77 if nlen < 1e-6 {
78 // Degenerate triangle — return ambient only
79 return pack(acc_r.min(255.0), acc_g.min(255.0), acc_b.min(255.0));
80 }
81 let nx = nx / nlen;
82 let ny = ny / nlen;
83 let nz = nz / nlen;
84
85 for l in lights {
86 // Direction from face centroid → light
87 let dx = l.x - centroid[0];
88 let dy = l.y - centroid[1];
89 let dz = l.z - centroid[2];
90 let dist = (dx*dx + dy*dy + dz*dz).sqrt().max(1e-6);
91
92 // Linear attenuation within radius
93 let atten = if l.radius > 0.0 {
94 (1.0 - dist / l.radius).max(0.0)
95 } else {
96 1.0
97 };
98 if atten <= 0.0 { continue; }
99
100 let lx = dx / dist;
101 let ly = dy / dist;
102 let lz = dz / dist;
103
104 // |dot|: illuminate both sides (back-lit face looks mid-tone, not black)
105 let raw = (nx*lx + ny*ly + nz*lz).abs();
106 let shaded = cel_quantize(raw) * l.intensity * atten;
107
108 acc_r += br * shaded * l.r;
109 acc_g += bg * shaded * l.g;
110 acc_b += bb * shaded * l.b;
111 }
112
113 pack(acc_r.min(255.0), acc_g.min(255.0), acc_b.min(255.0))
114}
115
116#[inline]
117fn pack(r: f32, g: f32, b: f32) -> u32 {
118 ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
119}