git.lucas.co / cce-model
3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git

src/trace.rs (7.7K)

  1 //! The path-traced view's scene, and how long to let it refine.
  2 //!
  3 //! cce-ui's tracer takes triangles with a material each (albedo and
  4 //! emission, linear) in the space the camera's inverse mvp unprojects into —
  5 //! the fitted space the raster pass draws in, so one camera serves both.
  6 //! The model goes in with a colour per triangle (its corner colours
  7 //! averaged, or its material's colour, times its texture sampled at the
  8 //! triangle's centre: the tracer has no textures, and at the density a
  9 //! texture is worth having, one texel a triangle reads well), deduplicated
 10 //! into materials, over a
 11 //! wide ground plane at the grid's height, so the traced model stands on
 12 //! something and casts a shadow; the grid's lines, which are not surfaces,
 13 //! have no place in it.
 14 
 15 use std::collections::HashMap;
 16 use std::path::Path;
 17 
 18 use cce_mesh_io::{srgb_to_linear, Mesh, Texture};
 19 use cce_ui::engine::{RtEnvironment, RtMaterial, RtTriangle};
 20 
 21 /// Samples to refine to: enough for the tracer's denoiser to settle, and
 22 /// then the GPU is left alone. Fewer on battery, where every frame of
 23 /// compute is drawn from it.
 24 pub const SAMPLES_ON_MAINS: u32 = 256;
 25 pub const SAMPLES_ON_BATTERY: u32 = 32;
 26 
 27 /// The ground's half-width, in the fitted space (the model is 2 across):
 28 /// far enough that its edge is past the horizon from any framing.
 29 const GROUND_HALF: f32 = 60.0;
 30 /// Dark, near the raster background, so switching views does not turn the
 31 /// whole window grey.
 32 const GROUND_ALBEDO: [f32; 3] = [0.09, 0.09, 0.095];
 33 
 34 /// Corner colours are quantized this finely before they are shared as
 35 /// materials, so a vertex-coloured scan does not become a material a
 36 /// triangle.
 37 const COLOUR_STEPS: f32 = 64.0;
 38 
 39 /// What a camera ray that misses everything shows: the middle of the
 40 /// raster view's background gradient, so the switch does not flash.
 41 pub const BACKGROUND: [f32; 3] = [0.06, 0.063, 0.072];
 42 
 43 /// The sky and sun, with the sun where the raster bake's key light is, and
 44 /// the sky grey rather than the tracer's default blue: the model's own
 45 /// colours should not shift when the view is switched.
 46 pub fn environment(key: [f32; 3]) -> RtEnvironment {
 47     RtEnvironment {
 48         sun_direction: key,
 49         sky_zenith: [0.62, 0.63, 0.66],
 50         sky_nadir: [0.2, 0.2, 0.21],
 51         ..RtEnvironment::default()
 52     }
 53 }
 54 
 55 /// The model's triangles and a ground plane under them at `floor_y`.
 56 pub fn scene(mesh: &Mesh, textures: &[Texture], floor_y: f32) -> (Vec<RtTriangle>, Vec<RtMaterial>) {
 57     let mut materials: Vec<RtMaterial> = Vec::new();
 58     let mut slot: HashMap<[u16; 3], u32> = HashMap::new();
 59     let mut material_for = |c: [f32; 3]| -> u32 {
 60         let key = c.map(|v| (v.clamp(0.0, 1.0) * COLOUR_STEPS).round() as u16);
 61         *slot.entry(key).or_insert_with(|| {
 62             materials.push(RtMaterial { albedo: key.map(|k| k as f32 / COLOUR_STEPS), emission: [0.0; 3] });
 63             (materials.len() - 1) as u32
 64         })
 65     };
 66     let mut triangles = Vec::with_capacity(mesh.triangles.len() + 2);
 67     for (t, tri) in mesh.triangles.iter().enumerate() {
 68         let mut colour = if mesh.corner_colors.is_some() {
 69             let [a, b, c] = [0, 1, 2].map(|k| mesh.corner_color(t, k));
 70             [0, 1, 2].map(|i| (a[i] + b[i] + c[i]) / 3.0)
 71         } else {
 72             mesh.corner_color(t, 0)
 73         };
 74         if let (Some(tex), Some(uvs)) = (mesh.material(t).texture.and_then(|i| textures.get(i)), &mesh.corner_uvs) {
 75             let [a, b, c] = [uvs[t * 3], uvs[t * 3 + 1], uvs[t * 3 + 2]];
 76             let texel = tex.sample([(a[0] + b[0] + c[0]) / 3.0, (a[1] + b[1] + c[1]) / 3.0]);
 77             colour = [0, 1, 2].map(|i| colour[i] * srgb_to_linear(texel[i]));
 78         }
 79         let [p0, p1, p2] = tri.map(|i| mesh.positions[i as usize].to_array());
 80         triangles.push(RtTriangle { p0, p1, p2, material: material_for(colour) });
 81     }
 82     let ground = material_for(GROUND_ALBEDO);
 83     let g = GROUND_HALF;
 84     // Counter-clockwise seen from above.
 85     let corner = |x: f32, z: f32| [x, floor_y, z];
 86     triangles.push(RtTriangle { p0: corner(-g, -g), p1: corner(-g, g), p2: corner(g, g), material: ground });
 87     triangles.push(RtTriangle { p0: corner(-g, -g), p1: corner(g, g), p2: corner(g, -g), material: ground });
 88     (triangles, materials)
 89 }
 90 
 91 /// True when the machine is running from its battery: a mains supply is
 92 /// listed under `root` (`/sys/class/power_supply`) and none is online. A
 93 /// machine with no mains supply listed (a desktop) is never on battery.
 94 pub fn on_battery(root: &Path) -> bool {
 95     let Ok(entries) = std::fs::read_dir(root) else { return false };
 96     let mut mains = false;
 97     for e in entries.flatten() {
 98         let read = |name: &str| std::fs::read_to_string(e.path().join(name)).unwrap_or_default();
 99         if read("type").trim() == "Mains" {
100             mains = true;
101             if read("online").trim() == "1" {
102                 return false;
103             }
104         }
105     }
106     mains
107 }
108 
109 #[cfg(test)]
110 mod tests {
111     use super::*;
112     use glam::Vec3;
113 
114     fn quad(corner_colors: Option<Vec<[f32; 3]>>) -> Mesh {
115         Mesh {
116             positions: vec![Vec3::ZERO, Vec3::X, Vec3::new(1.0, 1.0, 0.0), Vec3::Y],
117             triangles: vec![[0, 1, 2], [0, 2, 3]],
118             tri_material: vec![0, 0],
119             materials: vec![cce_mesh_io::Material::colour([0.5, 0.25, 0.0])],
120             corner_colors,
121             ..Mesh::default()
122         }
123     }
124 
125     #[test]
126     fn the_model_stands_on_a_ground_plane() {
127         let (tris, mats) = scene(&quad(None), &[], -1.0);
128         assert_eq!(tris.len(), 2 + 2);
129         assert_eq!(mats.len(), 2, "one model colour and the ground");
130         let ground = &tris[2];
131         assert!([ground.p0, ground.p1, ground.p2].iter().all(|p| p[1] == -1.0));
132         assert_eq!(mats[ground.material as usize].albedo, [0.09375, 0.09375, 0.09375], "the ground's colour, quantized");
133         // Its normal points up, so it is lit from above.
134         let [a, b, c] = [ground.p0, ground.p1, ground.p2].map(Vec3::from);
135         assert!((b - a).cross(c - a).y > 0.0);
136     }
137 
138     #[test]
139     fn corner_colours_are_averaged_and_shared() {
140         let red = [1.0, 0.0, 0.0];
141         let (tris, mats) = scene(&quad(Some(vec![red; 6])), &[], 0.0);
142         assert_eq!(tris[0].material, tris[1].material, "two red triangles share one material");
143         assert_eq!(mats[tris[0].material as usize].albedo, red);
144     }
145 
146     #[test]
147     fn a_textured_triangle_takes_its_texel() {
148         let mut m = quad(None);
149         m.materials[0] = cce_mesh_io::Material { color: [1.0; 3], texture: Some(0), ..Default::default() };
150         m.corner_uvs = Some(vec![[0.1, 0.5]; 6]);
151         let tex = Texture { width: 2, height: 1, rgba: vec![255, 0, 0, 255, 0, 0, 255, 255] };
152         let (tris, mats) = scene(&m, &[tex], 0.0);
153         assert_eq!(mats[tris[0].material as usize].albedo, [1.0, 0.0, 0.0], "the left texel, red");
154     }
155 
156     #[test]
157     fn mains_online_is_not_battery() {
158         let root = std::env::temp_dir().join(format!("cce-model-power-{}", std::process::id()));
159         let supply = |name: &str, ty: &str, online: Option<&str>| {
160             let d = root.join(name);
161             std::fs::create_dir_all(&d).unwrap();
162             std::fs::write(d.join("type"), format!("{ty}\n")).unwrap();
163             if let Some(o) = online {
164                 std::fs::write(d.join("online"), format!("{o}\n")).unwrap();
165             }
166         };
167         assert!(!on_battery(&root), "nothing listed: a desktop");
168         supply("BAT0", "Battery", None);
169         assert!(!on_battery(&root), "a battery and no mains supply listed");
170         supply("AC", "Mains", Some("0"));
171         assert!(on_battery(&root));
172         supply("AC", "Mains", Some("1"));
173         assert!(!on_battery(&root));
174         std::fs::remove_dir_all(&root).ok();
175     }
176 }