3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git
src/light.rs (5.1K)
1 //! The light, and the lit meshes it falls on.
2 //!
3 //! The model is drawn through cce-ui's lit pipeline (`draw::lit`): a vertex
4 //! with a normal, a texture coordinate and a colour, shaded per fragment
5 //! with a metallic-roughness specular, so highlights move with the camera
6 //! and textures show. This module turns a mesh into those vertices, one lit
7 //! mesh per material (a draw binds one texture), and holds the studio rig
8 //! they are lit by: a key, a fill and a sky/ground ambient, fixed in world
9 //! space, with the traced view's sun on the same key.
10
11 use cce_mesh_io::{Material, Mesh};
12 use cce_ui::engine::{LitLight, LitVertex};
13 use glam::Vec3;
14
15 /// Faces meeting at a sharper angle than this keep separate normals, so a
16 /// cube's edges stay edges and a sphere's facets blend. Used when the file
17 /// brings no normals of its own.
18 pub const CREASE_DEGREES: f32 = 40.0;
19
20 /// Direction TOWARD the key light, world space (Y up): above and to the
21 /// left of the home three-quarter view, so a model opens with a lit side
22 /// and a shaded side rather than lit flat from the camera.
23 pub const KEY: Vec3 = Vec3::new(-0.35, 0.75, 0.55);
24 /// Toward the fill: the home view's right, low, so the shaded side is not black.
25 const FILL: Vec3 = Vec3::new(0.75, 0.1, -0.1);
26
27 /// The rig the lit pipeline shades by.
28 pub fn rig() -> LitLight {
29 LitLight {
30 key_toward: KEY.to_array(),
31 key_color: [0.95; 3],
32 fill_toward: FILL.to_array(),
33 fill_color: [0.25; 3],
34 sky: [0.20; 3],
35 ground: [0.06; 3],
36 }
37 }
38
39 /// A normal per triangle corner: the file's own when it has them, else
40 /// derived with the crease angle.
41 pub fn normals(mesh: &Mesh) -> Vec<Vec3> {
42 mesh.file_normals.clone().unwrap_or_else(|| mesh.corner_normals(CREASE_DEGREES))
43 }
44
45 /// The triangles of one material, as lit vertices.
46 #[derive(Debug)]
47 pub struct LitPart {
48 pub verts: Vec<LitVertex>,
49 pub material: Material,
50 }
51
52 /// The mesh as one lit part per material it uses. A mesh with corner colours
53 /// carries them on the vertices and draws its parts in white (the reader has
54 /// already folded the material's colour into them); one without draws white
55 /// vertices in its material's colour.
56 pub fn lit_parts(mesh: &Mesh) -> Vec<LitPart> {
57 let normals = normals(mesh);
58 let tinted = mesh.corner_colors.is_some();
59 let mut parts: Vec<Option<LitPart>> = (0..mesh.materials.len().max(1)).map(|_| None).collect();
60 for (t, tri) in mesh.triangles.iter().enumerate() {
61 let m = (mesh.tri_material[t] as usize).min(parts.len() - 1);
62 let part = parts[m].get_or_insert_with(|| {
63 let mut material = mesh.material(t);
64 if tinted {
65 material.color = [1.0; 3];
66 }
67 LitPart { verts: Vec::new(), material }
68 });
69 for (k, &v) in tri.iter().enumerate() {
70 let c = t * 3 + k;
71 part.verts.push(LitVertex {
72 position: mesh.positions[v as usize].to_array(),
73 normal: normals[c].to_array(),
74 uv: mesh.corner_uvs.as_ref().map_or([0.0; 2], |uv| uv[c]),
75 color: if tinted { mesh.corner_color(t, k) } else { [1.0; 3] },
76 });
77 }
78 }
79 parts.into_iter().flatten().collect()
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85
86 /// A unit cube, as twelve triangles over eight shared points.
87 pub(crate) fn cube() -> Mesh {
88 let p = |i: u32| Vec3::new((i & 1) as f32, ((i >> 1) & 1) as f32, ((i >> 2) & 1) as f32);
89 let quads = [[1, 3, 7, 5], [4, 6, 2, 0], [2, 6, 7, 3], [4, 0, 1, 5], [4, 5, 7, 6], [1, 0, 2, 3]];
90 Mesh {
91 positions: (0..8).map(p).collect(),
92 triangles: quads.iter().flat_map(|q| [[q[0], q[1], q[2]], [q[0], q[2], q[3]]]).collect(),
93 tri_material: vec![0; 12],
94 materials: vec![Material::default()],
95 ..Mesh::default()
96 }
97 }
98
99 #[test]
100 fn a_cube_is_one_part_with_its_edges_kept() {
101 let parts = lit_parts(&cube());
102 assert_eq!(parts.len(), 1);
103 assert_eq!(parts[0].verts.len(), 36);
104 assert_eq!(parts[0].material.color, cce_mesh_io::CLAY);
105 // Past the crease: each face's corners carry that face's normal, so
106 // six distinct normals in all.
107 let mut normals: Vec<[i32; 3]> = parts[0].verts.iter().map(|v| v.normal.map(|c| c.round() as i32)).collect();
108 normals.sort();
109 normals.dedup();
110 assert_eq!(normals.len(), 6);
111 }
112
113 #[test]
114 fn each_material_is_its_own_part_and_corner_colours_ride_the_vertices() {
115 let mut m = cube();
116 m.materials.push(Material::colour([0.0, 0.0, 1.0]));
117 m.tri_material[0] = 1;
118 assert_eq!(lit_parts(&m).len(), 2);
119 m.corner_colors = Some(vec![[0.0, 0.5, 0.0]; 36]);
120 let parts = lit_parts(&m);
121 assert!(parts.iter().all(|p| p.material.color == [1.0; 3]), "the colour is on the vertices");
122 assert_eq!(parts[0].verts[0].color, [0.0, 0.5, 0.0]);
123 }
124
125 #[test]
126 fn the_files_normals_win() {
127 let mut m = cube();
128 m.file_normals = Some(vec![Vec3::Y; 36]);
129 assert!(lit_parts(&m)[0].verts.iter().all(|v| v.normal == [0.0, 1.0, 0.0]));
130 }
131 }