git.lucas.co / cce-model
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 }