Skip to main content

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}