1#[derive(Clone, Copy, Debug)]
27pub struct LightS {
28 pub pos: [f32; 3],
29 pub color: [f32; 3],
30 pub intensity: f32,
31 pub radius: f32, }
33
34#[derive(Clone, Copy, Debug)]
36pub struct ShadeParams {
37 pub bands: u32,
39 pub ambient: f32,
41 pub shadow: [f32; 3],
43 pub rim: f32,
45 pub rim_color: [f32; 3],
47 pub holo: bool,
49}
50
51impl Default for ShadeParams {
52 fn default() -> Self {
53 Self {
54 bands: 4,
55 ambient: 0.22,
56 shadow: [0.10, 0.13, 0.30], rim: 0.6,
58 rim_color: [0.45, 0.85, 1.0], holo: true,
60 }
61 }
62}
63
64impl ShadeParams {
65 pub fn windwaker() -> Self {
67 Self {
68 bands: 3,
69 ambient: 0.35,
70 shadow: [0.22, 0.32, 0.52], rim: 0.85,
72 rim_color: [0.95, 0.98, 1.0], holo: false,
74 }
75 }
76}
77
78#[inline]
79fn norm3(v: [f32; 3]) -> [f32; 3] {
80 let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
81 if l < 1e-8 {
82 [0.0, 0.0, 0.0]
83 } else {
84 [v[0] / l, v[1] / l, v[2] / l]
85 }
86}
87#[inline]
88fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
89 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
90}
91
92#[inline]
96pub fn cel_ramp(d: f32) -> f32 {
97 let lo = 0.30;
99 let hi = 0.55;
100 if d < lo {
101 0.20
102 } else if d > hi {
103 1.0
104 } else {
105 let t = (d - lo) / (hi - lo);
107 let s = t * t * (3.0 - 2.0 * t);
108 0.20 + s * 0.80
109 }
110}
111
112#[inline]
114pub fn windwaker_cel_ramp(d: f32) -> f32 {
115 let lo = 0.28;
116 let hi = 0.38;
117 if d < lo {
118 0.25
119 } else if d > hi {
120 1.0
121 } else {
122 let t = (d - lo) / (hi - lo);
123 let s = t * t * (3.0 - 2.0 * t);
124 0.25 + s * 0.75
125 }
126}
127
128pub fn lit_vertex(
131 base: [f32; 3],
132 n: [f32; 3],
133 pos: [f32; 3],
134 eye: [f32; 3],
135 lights: &[LightS],
136 p: &ShadeParams,
137) -> [f32; 3] {
138 let n = norm3(n);
139 let mut acc = [
141 base[0] * p.ambient + p.shadow[0] * (1.0 - p.ambient),
142 base[1] * p.ambient + p.shadow[1] * (1.0 - p.ambient),
143 base[2] * p.ambient + p.shadow[2] * (1.0 - p.ambient),
144 ];
145
146 for l in lights {
147 let d = [l.pos[0] - pos[0], l.pos[1] - pos[1], l.pos[2] - pos[2]];
148 let dist = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt().max(1e-6);
149 let atten = if l.radius > 0.0 {
150 (1.0 - dist / l.radius).max(0.0)
151 } else {
152 1.0
153 };
154 if atten <= 0.0 {
155 continue;
156 }
157 let ldir = [d[0] / dist, d[1] / dist, d[2] / dist];
158 let diff = dot3(n, ldir).max(0.0); let shaded = cel_ramp(diff) * l.intensity * atten;
160 acc[0] += base[0] * shaded * l.color[0];
161 acc[1] += base[1] * shaded * l.color[1];
162 acc[2] += base[2] * shaded * l.color[2];
163 }
164
165 if p.rim > 0.0 {
167 let vd = norm3([eye[0] - pos[0], eye[1] - pos[1], eye[2] - pos[2]]);
168 let f = (1.0 - dot3(n, vd).max(0.0)).clamp(0.0, 1.0);
169 let rim = f * f * f * p.rim; acc[0] += p.rim_color[0] * rim;
171 acc[1] += p.rim_color[1] * rim;
172 acc[2] += p.rim_color[2] * rim;
173 }
174
175 if p.holo {
178 let s = 0.07;
179 acc[0] += (0.5 + 0.5 * n[0]) * s * (0.4 + 0.6 * base[0]);
180 acc[1] += (0.5 + 0.5 * n[1]) * s * (0.4 + 0.6 * base[1]);
181 acc[2] += (0.5 + 0.5 * n[2]) * s * (0.4 + 0.6 * base[2]);
182 }
183
184 [acc[0].min(1.0), acc[1].min(1.0), acc[2].min(1.0)]
185}
186
187#[inline]
190pub fn posterize(c: [f32; 3], bands: u32) -> [f32; 3] {
191 let bands = bands.max(2) as f32;
192 let lum = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
193 if lum < 1e-5 {
194 return c;
195 }
196 let band = (lum * bands).floor().min(bands - 1.0);
200 let q = (band + 0.5) / bands;
201 let k = (q / lum).clamp(0.0, 4.0);
202 [
203 (c[0] * k).min(1.0),
204 (c[1] * k).min(1.0),
205 (c[2] * k).min(1.0),
206 ]
207}
208
209#[inline]
214pub fn posterize_soft(c: [f32; 3], bands: u32, softness: f32) -> [f32; 3] {
215 if bands < 2 {
216 return c;
217 }
218 let s = softness.clamp(0.0, 1.0);
219 if s <= 0.0 {
220 return posterize(c, bands);
221 }
222 let hard = posterize(c, bands);
223 [
224 hard[0] + (c[0] - hard[0]) * s,
225 hard[1] + (c[1] - hard[1]) * s,
226 hard[2] + (c[2] - hard[2]) * s,
227 ]
228}
229
230#[inline]
232pub fn pack(c: [f32; 3]) -> u32 {
233 let r = (c[0].clamp(0.0, 1.0) * 255.0) as u32;
234 let g = (c[1].clamp(0.0, 1.0) * 255.0) as u32;
235 let b = (c[2].clamp(0.0, 1.0) * 255.0) as u32;
236 (r << 16) | (g << 8) | b
237}
238
239#[inline]
241pub fn unpack(rgb: u32) -> [f32; 3] {
242 [
243 ((rgb >> 16) & 0xFF) as f32 / 255.0,
244 ((rgb >> 8) & 0xFF) as f32 / 255.0,
245 (rgb & 0xFF) as f32 / 255.0,
246 ]
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn posterize_preserves_hue_ratio() {
255 let c = [0.9, 0.5, 0.1];
258 let q = posterize(c, 4);
259 assert!(q[0] > q[1] && q[1] > q[2], "hue order must survive posterisation: {q:?}");
260 }
261
262 #[test]
263 fn posterize_four_bands_has_four_or_fewer_levels() {
264 let mut levels = std::collections::HashSet::new();
270 let mut t: f32 = 0.1;
271 while t <= 1.0 {
272 let q = posterize([t, t, t], 4);
273 levels.insert((q[0] * 1000.0).round() as i32);
274 t += 0.01;
275 }
276 assert!(levels.len() <= 4, "expected at most 4 distinct levels, got {}", levels.len());
277 }
278
279 #[test]
280 fn posterize_soft_zero_matches_hard_posterize() {
281 let c = [0.9, 0.5, 0.1];
282 assert_eq!(posterize_soft(c, 4, 0.0), posterize(c, 4));
283 }
284
285 #[test]
286 fn posterize_soft_one_matches_raw_color() {
287 let c = [0.9, 0.5, 0.1];
288 assert_eq!(posterize_soft(c, 4, 1.0), c);
289 }
290
291 #[test]
292 fn posterize_soft_bands_below_two_is_passthrough() {
293 let c = [0.31, 0.62, 0.83];
294 assert_eq!(posterize_soft(c, 1, 0.0), c);
295 }
296}