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 }