1use crate::gfx::light::{Light, cel_quantize};
32
33#[derive(Debug, Clone)]
36pub struct LingMaterial {
37 pub albedo: u32, pub roughness: f32, pub metallic: f32, pub emission: u32,
44 pub emission_strength: f32,
45
46 pub specular: f32, pub specular_tint: f32, pub subsurface: f32,
52 pub subsurface_color: u32,
53
54 pub clearcoat: f32,
56 pub clearcoat_roughness:f32,
57
58 pub transmission: f32, pub ior: f32, pub iridescence: f32, pub sheen: f32, pub anisotropy: f32, pub anisotropy_angle: f32, pub toon_bands: u32, pub shadow_softness: f32, pub outline_px: f32, pub outline_color: u32,
73 pub highlight_color: u32,
74}
75
76impl Default for LingMaterial {
77 fn default() -> Self {
78 Self {
79 albedo: 0x00FF_FFFF,
80 roughness: 0.8,
81 metallic: 0.0,
82 emission: 0,
83 emission_strength: 0.0,
84 specular: 0.04,
85 specular_tint: 0.0,
86 subsurface: 0.0,
87 subsurface_color: 0x00FF_C8A0,
88 clearcoat: 0.0,
89 clearcoat_roughness: 0.03,
90 transmission: 0.0,
91 ior: 1.5,
92 iridescence: 0.0,
93 sheen: 0.0,
94 anisotropy: 0.0,
95 anisotropy_angle: 0.0,
96 toon_bands: 3,
97 shadow_softness: 0.04,
98 outline_px: 0.0,
99 outline_color: 0x00_00_00,
100 highlight_color: 0x00FF_FFFF,
101 }
102 }
103}
104
105#[inline]
108fn unpack(c: u32) -> (f32, f32, f32) {
109 (
110 ((c >> 16) & 0xFF) as f32 / 255.0,
111 ((c >> 8) & 0xFF) as f32 / 255.0,
112 (c & 0xFF) as f32 / 255.0,
113 )
114}
115
116#[inline]
117fn pack01(r: f32, g: f32, b: f32) -> u32 {
118 let r = (r.clamp(0.0, 1.0) * 255.0) as u32;
119 let g = (g.clamp(0.0, 1.0) * 255.0) as u32;
120 let b = (b.clamp(0.0, 1.0) * 255.0) as u32;
121 (r << 16) | (g << 8) | b
122}
123
124#[inline]
126fn schlick(cos_theta: f32, f0: f32) -> f32 {
127 let c = (1.0 - cos_theta).clamp(0.0, 1.0);
128 let c2 = c * c;
129 f0 + (1.0 - f0) * c2 * c2 * c
130}
131
132#[inline]
135fn ggx_toon(n_dot_h: f32, roughness: f32) -> f32 {
136 let a2 = roughness * roughness * roughness * roughness; let d = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
138 let ggx = a2 / (std::f32::consts::PI * d * d + 1e-6);
139 let t = (ggx * a2 * 3.0).clamp(0.0, 1.0);
141 if t > 0.5 { 1.0 } else { 0.0 }
143}
144
145#[inline]
148fn ggx_smooth(n_dot_h: f32, roughness: f32) -> f32 {
149 let a2 = roughness * roughness * roughness * roughness; let d = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
151 let ggx = a2 / (std::f32::consts::PI * d * d + 1e-6);
152 (ggx * a2 * 3.0).clamp(0.0, 1.0)
153}
154
155#[inline]
164pub fn toon_diffuse(n_dot_l: f32, bands: u32, softness: f32) -> f32 {
165 let t = n_dot_l.clamp(0.0, 1.0);
166 if softness < 1e-3 {
167 return cel_quantize(t);
168 }
169 let n = bands.max(2) as f32;
172 let scaled = t * n;
173 let frac = scaled.fract();
174 let edge_width = softness.clamp(0.0, 0.5);
175
176 if frac > 1.0 - edge_width {
177 let lo = cel_quantize((scaled.floor() / n).clamp(0.0, 1.0));
179 let hi = cel_quantize(((scaled.floor() + 1.0) / n).clamp(0.0, 1.0));
180 let s = ((frac - (1.0 - edge_width)) / edge_width).clamp(0.0, 1.0);
181 let blend = s * s * (3.0 - 2.0 * s); lo + (hi - lo) * blend
183 } else {
184 cel_quantize((scaled.floor() / n).clamp(0.0, 1.0))
186 }
187}
188
189#[inline]
190fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
191 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
192}
193
194#[inline]
195fn norm3(v: [f32; 3]) -> [f32; 3] {
196 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
197 if len < 1e-7 { [0.0, 0.0, 1.0] } else { [v[0] / len, v[1] / len, v[2] / len] }
198}
199
200pub fn shade(
211 mat: &LingMaterial,
212 normal: [f32; 3],
213 view_dir: [f32; 3],
214 centroid: [f32; 3],
215 lights: &[Light],
216 ambient: f32,
217) -> u32 {
218 let (ar, ag, ab) = unpack(mat.albedo);
219 let n = norm3(normal);
220 let v = norm3(view_dir);
221 let n_dot_v = dot3(n, v).abs().clamp(1e-4, 1.0);
222
223 let mut acc_r = ar * ambient;
225 let mut acc_g = ag * ambient;
226 let mut acc_b = ab * ambient;
227 if mat.emission_strength > 0.0 {
228 let (er, eg, eb) = unpack(mat.emission);
229 acc_r += er * mat.emission_strength;
230 acc_g += eg * mat.emission_strength;
231 acc_b += eb * mat.emission_strength;
232 }
233
234 for l in lights {
236 let dx = l.x - centroid[0];
237 let dy = l.y - centroid[1];
238 let dz = l.z - centroid[2];
239 let dist = (dx * dx + dy * dy + dz * dz).sqrt().max(1e-6);
240 let atten = if l.radius > 0.0 { (1.0 - dist / l.radius).max(0.0) } else { 1.0 };
241 if atten <= 0.0 { continue; }
242
243 let ld = [dx / dist, dy / dist, dz / dist];
244 let n_dot_l = dot3(n, ld).abs(); let h = norm3([ld[0] + v[0], ld[1] + v[1], ld[2] + v[2]]);
246 let n_dot_h = dot3(n, h).clamp(0.0, 1.0);
247
248 let smooth_mode = mat.toon_bands == 0;
250 let diff = if smooth_mode {
251 n_dot_l } else {
253 toon_diffuse(n_dot_l, mat.toon_bands, mat.shadow_softness)
254 };
255
256 let (eff_r, eff_g, eff_b) = if mat.subsurface > 0.0 {
258 let (sr, sg, sb) = unpack(mat.subsurface_color);
259 let zone = ((0.3 - n_dot_l) * 3.33).clamp(0.0, 1.0) * mat.subsurface;
260 (ar + (sr - ar) * zone, ag + (sg - ag) * zone, ab + (sb - ab) * zone)
261 } else {
262 (ar, ag, ab)
263 };
264
265 let dr = eff_r * diff;
266 let dg = eff_g * diff;
267 let db = eff_b * diff;
268
269 let f0_dielectric = mat.specular * 0.08; let f0 = f0_dielectric + mat.metallic * (ar - f0_dielectric);
272 let fresnel = schlick(n_dot_v, f0.clamp(0.0, 1.0));
273 let spec = if smooth_mode {
274 ggx_smooth(n_dot_h, mat.roughness.max(0.01)) * fresnel
275 } else {
276 ggx_toon(n_dot_h, mat.roughness.max(0.01)) * fresnel
277 };
278
279 let spec_white = spec * (1.0 - mat.specular_tint);
280 let sr = spec_white + spec * ar * mat.specular_tint;
281 let sg = spec_white + spec * ag * mat.specular_tint;
282 let sb = spec_white + spec * ab * mat.specular_tint;
283
284 let coat = ggx_toon(n_dot_h, mat.clearcoat_roughness.max(0.01)) * mat.clearcoat;
286
287 let (ir, ig, ib) = if mat.iridescence > 0.0 {
290 let p = n_dot_v * std::f32::consts::TAU * 2.0;
291 let tau3 = std::f32::consts::FRAC_PI_3 * 2.0;
292 let ir = (p.cos() * 0.5 + 0.5) * mat.iridescence;
293 let ig = ((p + tau3).cos() * 0.5 + 0.5) * mat.iridescence;
294 let ib = ((p + tau3 * 2.0).cos() * 0.5 + 0.5) * mat.iridescence;
295 (ir, ig, ib)
296 } else {
297 (0.0, 0.0, 0.0)
298 };
299
300 let sheen = if mat.sheen > 0.0 {
302 let t = (1.0 - n_dot_l).powi(3) * mat.sheen;
303 t
304 } else {
305 0.0
306 };
307
308 let intensity = l.intensity * atten;
309 acc_r += (dr + sr + coat + ir + sheen * ar) * l.r * intensity;
310 acc_g += (dg + sg + coat + ig + sheen * ag) * l.g * intensity;
311 acc_b += (db + sb + coat + ib + sheen * ab) * l.b * intensity;
312 }
313
314 pack01(acc_r, acc_g, acc_b)
315}
316
317pub fn shade_vertices(
320 mat: &LingMaterial,
321 normal: [f32; 3],
322 va: [f32; 3],
323 vb: [f32; 3],
324 vc: [f32; 3],
325 camera_pos: [f32; 3],
326 lights: &[Light],
327 ambient: f32,
328) -> (u32, u32, u32) {
329 let view = |v: [f32; 3]| [camera_pos[0]-v[0], camera_pos[1]-v[1], camera_pos[2]-v[2]];
330 (
331 shade(mat, normal, view(va), va, lights, ambient),
332 shade(mat, normal, view(vb), vb, lights, ambient),
333 shade(mat, normal, view(vc), vc, lights, ambient),
334 )
335}
336
337pub fn shade_polygon(
340 mat: &LingMaterial,
341 normal: [f32; 3],
342 verts: &[[f32; 3]],
343 n: usize,
344 camera_pos: [f32; 3],
345 lights: &[Light],
346 ambient: f32,
347 out: &mut [u32],
348) {
349 let view = |v: [f32; 3]| [camera_pos[0]-v[0], camera_pos[1]-v[1], camera_pos[2]-v[2]];
350 for i in 0..n.min(verts.len()).min(out.len()) {
351 out[i] = shade(mat, normal, view(verts[i]), verts[i], lights, ambient);
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn default_material_no_crash_no_lights() {
361 let mat = LingMaterial::default();
362 let c = shade(&mat, [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0], &[], 0.5);
363 let lum = ((c >> 16 & 0xFF) as f32 * 0.299
364 + (c >> 8 & 0xFF) as f32 * 0.587
365 + (c & 0xFF) as f32 * 0.114) / 255.0;
366 assert!(lum > 0.1, "expected visible output, got lum={lum:.3}");
368 }
369
370 #[test]
371 fn metallic_tints_specular() {
372 let mut mat = LingMaterial::default();
373 mat.albedo = 0x00FF_0000; mat.metallic = 1.0;
375 mat.specular_tint = 1.0;
376 mat.roughness = 0.1;
377
378 let light = crate::gfx::light::Light {
379 x: 0.0, y: 0.0, z: 10.0,
380 r: 1.0, g: 1.0, b: 1.0,
381 intensity: 2.0, radius: 0.0,
382 };
383 let c = shade(
384 &mat,
385 [0.0, 0.0, 1.0],
386 [0.0, 0.0, 1.0], [0.0, 0.0, 0.0],
388 &[light],
389 0.05,
390 );
391 let r = (c >> 16) & 0xFF;
392 let g = (c >> 8) & 0xFF;
393 assert!(r >= g, "metallic red should be reddish: r={r} g={g}");
395 }
396}