1use crate::assets::Assets;
4use bytemuck::{Pod, Zeroable};
5use glam::{Mat4, Vec3};
6use std::collections::HashMap;
7use std::f32::consts::{PI, TAU};
8
9#[repr(C)]
11#[derive(Clone, Copy, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
12pub struct Vertex {
13 pub pos: [f32; 3],
15 pub normal: [f32; 3],
17 pub uv: [f32; 2],
19}
20
21#[derive(Clone)]
23pub struct Albedo {
24 pub width: u32,
26 pub height: u32,
28 pub rgba: Vec<u8>,
30}
31
32#[derive(Clone)]
34pub struct MeshPart {
35 pub first_index: u32,
37 pub index_count: u32,
39 pub albedo: Option<std::sync::Arc<Albedo>>,
41 pub normal: Option<std::sync::Arc<Albedo>>,
43 pub orm: Option<std::sync::Arc<Albedo>>,
45}
46
47#[derive(Clone)]
50pub struct MeshData {
51 pub vertices: Vec<Vertex>,
53 pub indices: Vec<u32>,
55 pub albedo: Option<std::sync::Arc<Albedo>>,
57 pub base_color: [f32; 4],
59 pub parts: Vec<MeshPart>,
61 pub dependencies: Vec<String>,
64}
65
66impl Default for MeshData {
67 fn default() -> Self {
68 Self { vertices: Vec::new(), indices: Vec::new(), albedo: None, base_color: [1.0; 4], parts: Vec::new(), dependencies: Vec::new() }
69 }
70}
71
72pub const PRIMITIVE_PREFIX: &str = "primitive:";
74#[derive(Clone, Debug, PartialEq)]
76pub struct ProcParams {
77 pub kind: String,
79 pub size: Vec3,
81 pub radius: f32,
83 pub radius2: f32,
85 pub height: f32,
87 pub segments: u32,
89 pub rings: u32,
91 pub seed: u32,
93}
94
95pub const PROC_PREFIX: &str = "proc:";
97
98impl ProcParams {
99 pub fn key(&self) -> String {
101 format!(
102 "{PROC_PREFIX}{}:{},{},{},{},{},{},{},{},{}",
103 self.kind, self.size.x, self.size.y, self.size.z, self.radius, self.radius2, self.height, self.segments, self.rings, self.seed
104 )
105 }
106
107 pub fn parse(key: &str) -> Option<ProcParams> {
109 let (kind, rest) = key.strip_prefix(PROC_PREFIX)?.split_once(':')?;
110 let n: Vec<&str> = rest.split(',').collect();
111 if n.len() != 9 {
112 return None;
113 }
114 let real = |i: usize| n[i].parse::<f32>().ok().filter(|v| v.is_finite());
115 Some(ProcParams {
116 kind: kind.to_string(),
117 size: Vec3::new(real(0)?, real(1)?, real(2)?),
118 radius: real(3)?,
119 radius2: real(4)?,
120 height: real(5)?,
121 segments: n[6].parse().ok()?,
122 rings: n[7].parse().ok()?,
123 seed: n[8].parse().ok()?,
124 })
125 }
126
127 pub fn build(&self) -> Option<MeshData> {
129 if !self.size.is_finite() || !self.radius.is_finite() || !self.radius2.is_finite() || !self.height.is_finite() {
130 return None;
131 }
132 let seg = self.segments.clamp(3, 256);
133 let rings = self.rings.clamp(2, 128);
134 let (r, h) = (self.radius.max(0.001), self.height.max(0.001));
135 Some(match self.kind.as_str() {
136 "cube" => scaled(cube(), self.size),
137 "plane" => scaled(plane(rings.min(128)), Vec3::new(self.size.x, 1.0, self.size.z)),
138 "sphere" => lathe(&arc_rows(0.0, 0.0, PI, rings, r), seg),
139 "capsule" => {
140 let straight = (h * 0.5 - r).max(0.0);
141 let mut rows = arc_rows(straight, 0.0, PI / 2.0, rings.div_ceil(2), r);
142 rows.extend(arc_rows(-straight, PI / 2.0, PI, rings.div_ceil(2), r));
143 lathe(&rows, seg)
144 }
145 "cylinder" | "cone" => {
146 let rt = if self.kind == "cone" { 0.0 } else { self.radius2.max(0.0) };
147 let (nr, ny) = (h, r - rt);
148 let len = (nr * nr + ny * ny).sqrt();
149 let (nr, ny) = (nr / len, ny / len);
150 let hh = h * 0.5;
151 let mut rows = vec![(0.0, hh, 0.0, 1.0)];
152 if rt > 0.0 {
153 rows.push((rt, hh, 0.0, 1.0));
154 }
155 rows.extend([(rt, hh, nr, ny), (r, -hh, nr, ny), (r, -hh, 0.0, -1.0), (0.0, -hh, 0.0, -1.0)]);
156 lathe(&rows, seg)
157 }
158 "torus" => torus(r, self.radius2.max(0.001), seg, rings),
159 "rock" => rock(self.size, self.radius2.clamp(0.0, 1.5), self.segments.clamp(1, 7), self.seed),
160 _ => return None,
161 })
162 }
163}
164
165fn rock(size: Vec3, ruggedness: f32, subdivisions: u32, seed: u32) -> MeshData {
168 use crate::noise::{Noise, NoiseKind};
169 let t = (1.0 + 5.0_f32.sqrt()) / 2.0;
170 let mut verts: Vec<Vec3> = [
171 (-1.0, t, 0.0),
172 (1.0, t, 0.0),
173 (-1.0, -t, 0.0),
174 (1.0, -t, 0.0),
175 (0.0, -1.0, t),
176 (0.0, 1.0, t),
177 (0.0, -1.0, -t),
178 (0.0, 1.0, -t),
179 (t, 0.0, -1.0),
180 (t, 0.0, 1.0),
181 (-t, 0.0, -1.0),
182 (-t, 0.0, 1.0),
183 ]
184 .iter()
185 .map(|&(x, y, z)| Vec3::new(x, y, z).normalize())
186 .collect();
187 let mut faces: Vec<[u32; 3]> = vec![
188 [0, 11, 5],
189 [0, 5, 1],
190 [0, 1, 7],
191 [0, 7, 10],
192 [0, 10, 11],
193 [1, 5, 9],
194 [5, 11, 4],
195 [11, 10, 2],
196 [10, 7, 6],
197 [7, 1, 8],
198 [3, 9, 4],
199 [3, 4, 2],
200 [3, 2, 6],
201 [3, 6, 8],
202 [3, 8, 9],
203 [4, 9, 5],
204 [2, 4, 11],
205 [6, 2, 10],
206 [8, 6, 7],
207 [9, 8, 1],
208 ];
209 for _ in 0..subdivisions {
210 let mut cache: HashMap<(u32, u32), u32> = HashMap::new();
211 let mut mid = |a: u32, b: u32, verts: &mut Vec<Vec3>| -> u32 {
212 let key = (a.min(b), a.max(b));
213 *cache.entry(key).or_insert_with(|| {
214 verts.push(((verts[a as usize] + verts[b as usize]) * 0.5).normalize());
215 verts.len() as u32 - 1
216 })
217 };
218 let mut next = Vec::with_capacity(faces.len() * 4);
219 for [a, b, c] in faces {
220 let (ab, bc, ca) = (mid(a, b, &mut verts), mid(b, c, &mut verts), mid(c, a, &mut verts));
221 next.extend([[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]]);
222 }
223 faces = next;
224 }
225 let seed = seed.wrapping_mul(2654435761);
226 let ridged = Noise::new(NoiseKind::Ridged).with_seed(seed).with_octaves(4);
227 let billow = Noise::new(NoiseKind::Billow).with_seed(seed ^ 0x5bd1).with_octaves(3);
228 let fine = Noise::new(NoiseKind::Perlin).with_seed(seed ^ 0x9e37).with_octaves(5);
229 let lumps = Noise::new(NoiseKind::Perlin).with_seed(seed ^ 0x1234).with_octaves(2);
230 let mut m = MeshData::default();
231 for v in &verts {
232 let d = *v;
233 let displacement = 0.16 * lumps.sample3(d.x * 1.1, d.y * 1.1, d.z * 1.1)
234 + ruggedness
235 * (0.22 * ridged.sample3(d.x * 1.8, d.y * 1.8, d.z * 1.8)
236 + 0.10 * billow.sample3(d.x * 3.6, d.y * 3.6, d.z * 3.6)
237 + 0.035 * fine.sample3(d.x * 11.0, d.y * 11.0, d.z * 11.0));
238 let mut p = d * (1.0 + displacement) * size * 0.5;
239 let floor = -size.y * 0.32;
241 if p.y < floor {
242 p.y = floor + (p.y - floor) * 0.15;
243 }
244 let a = d.abs();
246 let uv = if a.y >= a.x && a.y >= a.z {
247 [p.x * 0.5 + 0.5, p.z * 0.5 + 0.5]
248 } else if a.x >= a.z {
249 [p.z * 0.5 + 0.5, p.y * 0.5 + 0.5]
250 } else {
251 [p.x * 0.5 + 0.5, p.y * 0.5 + 0.5]
252 };
253 m.vertices.push(Vertex { pos: p.into(), normal: d.into(), uv });
254 }
255 for [a, b, c] in faces {
256 m.indices.extend([a, b, c]);
257 }
258 compute_normals(&mut m);
259 m
260}
261
262fn scaled(mut m: MeshData, s: Vec3) -> MeshData {
263 for v in &mut m.vertices {
264 v.pos = (Vec3::from(v.pos) * s).into();
265 }
266 m
267}
268
269fn torus(major: f32, minor: f32, segments: u32, rings: u32) -> MeshData {
270 let mut m = MeshData::default();
271 for i in 0..=segments {
272 let u = i as f32 / segments as f32 * TAU;
273 for j in 0..=rings {
274 let v = j as f32 / rings as f32 * TAU;
275 let n = Vec3::new(v.cos() * u.cos(), v.sin(), v.cos() * u.sin());
276 let c = Vec3::new(u.cos(), 0.0, u.sin()) * major;
277 m.vertices.push(Vertex {
278 pos: (c + n * minor).into(),
279 normal: n.into(),
280 uv: [i as f32 / segments as f32, j as f32 / rings as f32],
281 });
282 }
283 }
284 let stride = rings + 1;
285 for i in 0..segments {
286 for j in 0..rings {
287 let a = i * stride + j;
288 let b = a + stride;
289 m.indices.extend([a, a + 1, b, a + 1, b + 1, b]);
290 }
291 }
292 m
293}
294
295pub const PRIMITIVES: &[&str] = &["cube", "sphere", "plane", "cylinder", "capsule", "cone"];
297
298impl MeshData {
299 pub fn triangle_count(&self) -> usize {
301 self.indices.len() / 3
302 }
303
304 pub fn is_empty(&self) -> bool {
306 self.indices.len() < 3 || self.vertices.is_empty()
307 }
308
309 pub fn validate(&self) -> Result<(), String> {
312 self.validate_geometry()?;
313 for (i, vertex) in self.vertices.iter().enumerate() {
314 if !Vec3::from(vertex.normal).is_finite() || vertex.uv.iter().any(|v| !v.is_finite()) {
315 return Err(format!("vertex {i} has non-finite normal or UV"));
316 }
317 }
318 if self.base_color.iter().any(|v| !v.is_finite()) {
319 return Err("base colour is non-finite".into());
320 }
321 if let Some(image) = &self.albedo {
322 let expected = (image.width as usize).checked_mul(image.height as usize).and_then(|n| n.checked_mul(4));
323 if image.width == 0 || image.height == 0 || expected != Some(image.rgba.len()) {
324 return Err("embedded albedo dimensions do not match RGBA bytes".into());
325 }
326 }
327 Ok(())
328 }
329
330 fn validate_geometry(&self) -> Result<(), String> {
331 if !self.indices.len().is_multiple_of(3) {
332 return Err("triangle index count is not divisible by three".into());
333 }
334 if let Some(&index) = self.indices.iter().find(|&&i| i as usize >= self.vertices.len()) {
335 return Err(format!("mesh index {index} is outside {} vertices", self.vertices.len()));
336 }
337 if let Some((i, _)) = self.vertices.iter().enumerate().find(|(_, v)| !Vec3::from(v.pos).is_finite()) {
338 return Err(format!("vertex {i} position is non-finite"));
339 }
340 Ok(())
341 }
342
343 pub fn recalculate_normals(&mut self) -> Result<(), String> {
346 self.validate_geometry()?;
347 compute_normals(self);
348 Ok(())
349 }
350
351 pub fn transformed(&self, matrix: Mat4) -> Result<Self, String> {
355 self.validate()?;
356 if !matrix.is_finite() || matrix.determinant() == 0.0 || !matrix.row(3).abs_diff_eq(glam::Vec4::W, 1e-6) {
357 return Err("mesh transform must be finite, affine and nonsingular".into());
358 }
359 let normals = matrix.inverse().transpose();
360 let mut result = self.clone();
361 for vertex in &mut result.vertices {
362 vertex.pos = matrix.transform_point3(Vec3::from(vertex.pos)).into();
363 vertex.normal = normals.transform_vector3(Vec3::from(vertex.normal)).normalize_or_zero().into();
364 }
365 if matrix.determinant() < 0.0 {
366 for triangle in result.indices.as_chunks_mut::<3>().0.iter_mut() {
367 triangle.swap(1, 2);
368 }
369 }
370 result.validate()?;
371 Ok(result)
372 }
373
374 pub fn effective_parts(&self) -> Vec<MeshPart> {
376 if self.parts.is_empty() {
377 vec![MeshPart { first_index: 0, index_count: self.indices.len() as u32, albedo: self.albedo.clone(), normal: None, orm: None }]
378 } else {
379 self.parts.clone()
380 }
381 }
382
383 pub fn append(&mut self, other: &Self) -> Result<(), String> {
387 self.validate()?;
388 other.validate()?;
389 let total = self.vertices.len().checked_add(other.vertices.len()).ok_or("vertex count overflow")?;
390 if total > u32::MAX as usize {
391 return Err("merged mesh exceeds u32 index space".into());
392 }
393 let offset = self.vertices.len() as u32;
394 let index_offset = self.indices.len() as u32;
395 if !self.parts.is_empty() || !other.parts.is_empty() {
396 let mut parts = self.effective_parts();
397 parts.extend(other.effective_parts().into_iter().map(|p| MeshPart { first_index: p.first_index + index_offset, ..p }));
398 self.parts = merge_parts(parts);
399 }
400 self.vertices.extend_from_slice(&other.vertices);
401 self.indices.extend(other.indices.iter().map(|i| i + offset));
402 self.dependencies.extend(other.dependencies.iter().cloned());
403 self.dependencies.sort();
404 self.dependencies.dedup();
405 Ok(())
406 }
407
408 pub fn surface_area(&self) -> Result<f32, String> {
411 self.validate_geometry()?;
412 Ok(self
413 .indices
414 .as_chunks::<3>()
415 .0
416 .iter()
417 .map(|t| {
418 let a = Vec3::from(self.vertices[t[0] as usize].pos);
419 let b = Vec3::from(self.vertices[t[1] as usize].pos);
420 let c = Vec3::from(self.vertices[t[2] as usize].pos);
421 (b - a).cross(c - a).length() * 0.5
422 })
423 .sum())
424 }
425
426 pub fn bounds(&self) -> (Vec3, Vec3) {
428 let mut min = Vec3::splat(f32::MAX);
429 let mut max = Vec3::splat(f32::MIN);
430 for v in &self.vertices {
431 min = min.min(Vec3::from(v.pos));
432 max = max.max(Vec3::from(v.pos));
433 }
434 if self.vertices.is_empty() {
435 (Vec3::ZERO, Vec3::ZERO)
436 } else {
437 (min, max)
438 }
439 }
440
441 pub fn primitive(name: &str) -> Option<MeshData> {
443 Some(match name {
444 "cube" => cube(),
445 "plane" => plane(32),
446 "sphere" => sphere(),
447 "cylinder" => lathe(
448 &[
449 (0.0, 0.5, 0.0, 1.0),
450 (0.5, 0.5, 0.0, 1.0),
451 (0.5, 0.5, 1.0, 0.0),
452 (0.5, -0.5, 1.0, 0.0),
453 (0.5, -0.5, 0.0, -1.0),
454 (0.0, -0.5, 0.0, -1.0),
455 ],
456 32,
457 ),
458 "cone" => {
459 let (nr, ny) = (0.894_427_2, 0.447_213_6);
460 lathe(&[(0.0, 0.5, nr, ny), (0.5, -0.5, nr, ny), (0.5, -0.5, 0.0, -1.0), (0.0, -0.5, 0.0, -1.0)], 32)
461 }
462 "capsule" => capsule(),
463 _ => return None,
464 })
465 }
466
467 pub fn load(assets: &Assets, path: &str) -> Result<MeshData, String> {
469 if path.starts_with(PROC_PREFIX) {
470 return ProcParams::parse(path).and_then(|p| p.build()).ok_or_else(|| format!("bad procedural mesh '{path}'"));
471 }
472 if let Some(name) = path.strip_prefix(PRIMITIVE_PREFIX) {
473 return Self::primitive(name).ok_or_else(|| format!("unknown primitive '{name}'"));
474 }
475 let bytes = assets.read(path).map_err(|e| format!("{path}: {e}"))?;
476 let lower = path.to_ascii_lowercase();
477 let mesh = if lower.ends_with(".obj") {
478 parse_obj(&String::from_utf8_lossy(&bytes))
479 } else if lower.ends_with(".fbx") {
480 parse_fbx(assets, path, &bytes)
481 } else if lower.ends_with(".gltf") || lower.ends_with(".glb") {
482 parse_gltf(assets, path, &bytes)
483 } else {
484 Err("unsupported model format (use .obj, .fbx, .gltf or .glb)".into())
485 };
486 mesh.and_then(|mesh| {
487 mesh.validate()?;
488 Ok(mesh)
489 })
490 .map_err(|e| format!("{path}: {e}"))
491 }
492}
493
494fn merge_parts(parts: Vec<MeshPart>) -> Vec<MeshPart> {
496 let mut out: Vec<MeshPart> = Vec::new();
497 for p in parts {
498 let same = |a: &Option<std::sync::Arc<Albedo>>, b: &Option<std::sync::Arc<Albedo>>| match (a, b) {
499 (Some(x), Some(y)) => std::sync::Arc::ptr_eq(x, y),
500 (None, None) => true,
501 _ => false,
502 };
503 match out.last_mut() {
504 Some(last) if last.first_index + last.index_count == p.first_index && same(&last.albedo, &p.albedo) && same(&last.normal, &p.normal) && same(&last.orm, &p.orm) => last.index_count += p.index_count,
505 _ => out.push(p),
506 }
507 }
508 out
509}
510
511pub(crate) fn material_albedo(images: &[Option<std::sync::Arc<Albedo>>], pbr: &gltf::material::PbrMetallicRoughness) -> Option<std::sync::Arc<Albedo>> {
513 let factor = pbr.base_color_factor();
514 let white = factor.iter().take(3).all(|v| (v - 1.0).abs() < 0.01);
515 let texture = pbr.base_color_texture().and_then(|info| images.get(info.texture().source().index()).cloned().flatten());
516 let to_byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
517 match texture {
518 Some(t) if white => Some(t),
519 Some(t) => {
520 let mut copy = (*t).clone();
521 for px in copy.rgba.chunks_exact_mut(4) {
522 for c in 0..3 {
523 px[c] = (px[c] as f32 * factor[c]).clamp(0.0, 255.0) as u8;
524 }
525 }
526 Some(std::sync::Arc::new(copy))
527 }
528 None if white => None,
529 None => Some(std::sync::Arc::new(Albedo { width: 1, height: 1, rgba: vec![to_byte(factor[0]), to_byte(factor[1]), to_byte(factor[2]), 255] })),
530 }
531}
532
533fn material_maps(images: &[Option<std::sync::Arc<Albedo>>], material: &gltf::Material) -> (Option<std::sync::Arc<Albedo>>, Option<std::sync::Arc<Albedo>>) {
537 let image = |index: usize| images.get(index).cloned().flatten();
538 let normal = material.normal_texture().and_then(|t| image(t.texture().source().index()));
539 let orm = material.pbr_metallic_roughness().metallic_roughness_texture().and_then(|info| {
540 let source = info.texture().source().index();
541 let keep_occlusion = material.occlusion_texture().is_some_and(|o| o.texture().source().index() == source);
542 image(source).map(|img| {
543 let mut copy = (*img).clone();
544 for px in copy.rgba.chunks_exact_mut(4) {
545 if !keep_occlusion {
546 px[0] = 255;
547 }
548 px[2] = 0;
549 }
550 std::sync::Arc::new(copy)
551 })
552 });
553 (normal, orm)
554}
555
556fn quad_indices(base: u32) -> [u32; 6] {
557 [base, base + 1, base + 2, base, base + 2, base + 3]
558}
559
560fn cube() -> MeshData {
561 let (x, y, z) = (Vec3::X, Vec3::Y, Vec3::Z);
562 let faces = [(x, y, z), (y, z, x), (z, x, y), (y, x, -z), (z, y, -x), (x, z, -y)];
564 let mut m = MeshData::default();
565 for (u, v, n) in faces {
566 let base = m.vertices.len() as u32;
567 for (su, sv, uv) in [(-1.0, -1.0, [0.0, 1.0]), (1.0, -1.0, [1.0, 1.0]), (1.0, 1.0, [1.0, 0.0]), (-1.0, 1.0, [0.0, 0.0])] {
568 let p = n * 0.5 + u * (0.5 * su) + v * (0.5 * sv);
569 m.vertices.push(Vertex { pos: p.into(), normal: n.into(), uv });
570 }
571 m.indices.extend(quad_indices(base));
572 }
573 m
574}
575
576fn plane(n: u32) -> MeshData {
578 let mut m = MeshData::default();
579 for iz in 0..=n {
580 for ix in 0..=n {
581 let (u, v) = (ix as f32 / n as f32, iz as f32 / n as f32);
582 m.vertices.push(Vertex { pos: [u - 0.5, 0.0, v - 0.5], normal: [0.0, 1.0, 0.0], uv: [u, v] });
583 }
584 }
585 for iz in 0..n {
586 for ix in 0..n {
587 let a = iz * (n + 1) + ix;
588 let b = a + n + 1;
589 m.indices.extend([a, b, a + 1, a + 1, b, b + 1]);
590 }
591 }
592 m
593}
594
595fn lathe(rows: &[(f32, f32, f32, f32)], segments: u32) -> MeshData {
597 let mut m = MeshData::default();
598 let stride = segments + 1;
599 for (i, &(r, y, nr, ny)) in rows.iter().enumerate() {
600 for j in 0..=segments {
601 let t = j as f32 / segments as f32;
602 let (s, c) = (t * TAU).sin_cos();
603 m.vertices.push(Vertex { pos: [r * c, y, r * s], normal: [nr * c, ny, nr * s], uv: [t, i as f32 / (rows.len() - 1) as f32] });
604 }
605 }
606 for i in 0..rows.len() as u32 - 1 {
607 for j in 0..segments {
608 let a = i * stride + j;
609 let b = a + stride;
610 m.indices.extend([a, a + 1, b, a + 1, b + 1, b]);
611 }
612 }
613 m
614}
615
616fn arc_rows(center_y: f32, from: f32, to: f32, steps: u32, radius: f32) -> Vec<(f32, f32, f32, f32)> {
617 (0..=steps)
618 .map(|k| {
619 let a = from + (to - from) * k as f32 / steps as f32;
620 (radius * a.sin(), center_y + radius * a.cos(), a.sin(), a.cos())
621 })
622 .collect()
623}
624
625fn sphere() -> MeshData {
626 lathe(&arc_rows(0.0, 0.0, PI, 16, 0.5), 32)
627}
628
629fn capsule() -> MeshData {
631 let mut rows = arc_rows(0.5, 0.0, PI / 2.0, 8, 0.5);
632 rows.extend(arc_rows(-0.5, PI / 2.0, PI, 8, 0.5));
633 lathe(&rows, 32)
634}
635
636fn parse_obj(text: &str) -> Result<MeshData, String> {
637 let (mut pos, mut uvs, mut norms) = (Vec::<[f32; 3]>::new(), Vec::<[f32; 2]>::new(), Vec::<[f32; 3]>::new());
638 let mut m = MeshData::default();
639 let mut dedupe: HashMap<(usize, usize, usize), u32> = HashMap::new();
640 let mut has_normals = true;
641 let nums = |s: std::str::SplitWhitespace| -> Vec<f32> { s.filter_map(|t| t.parse().ok()).collect() };
642 for line in text.lines() {
643 let mut it = line.split_whitespace();
644 match it.next() {
645 Some("v") => {
646 let n = nums(it);
647 if n.len() >= 3 {
648 pos.push([n[0], n[1], n[2]]);
649 }
650 }
651 Some("vt") => {
652 let n = nums(it);
653 if n.len() >= 2 {
654 uvs.push([n[0], 1.0 - n[1]]);
655 }
656 }
657 Some("vn") => {
658 let n = nums(it);
659 if n.len() >= 3 {
660 norms.push([n[0], n[1], n[2]]);
661 }
662 }
663 Some("f") => {
664 let mut face = Vec::new();
665 for tok in it {
666 let mut parts = tok.split('/');
667 let mut idx = |len: usize| -> usize {
668 match parts.next().and_then(|s| s.parse::<i64>().ok()) {
669 Some(i) if i > 0 => i as usize,
670 Some(i) if i < 0 => (len as i64 + i + 1).max(0) as usize,
671 _ => 0,
672 }
673 };
674 let key = (idx(pos.len()), idx(uvs.len()), idx(norms.len()));
675 if key.0 == 0 || key.0 > pos.len() {
676 return Err(format!("bad face index in '{line}'"));
677 }
678 let id = *dedupe.entry(key).or_insert_with(|| {
679 has_normals &= key.2 != 0 && key.2 <= norms.len();
680 m.vertices.push(Vertex {
681 pos: pos[key.0 - 1],
682 normal: if key.2 != 0 && key.2 <= norms.len() { norms[key.2 - 1] } else { [0.0; 3] },
683 uv: if key.1 != 0 && key.1 <= uvs.len() { uvs[key.1 - 1] } else { [0.0; 2] },
684 });
685 m.vertices.len() as u32 - 1
686 });
687 face.push(id);
688 }
689 for k in 1..face.len().saturating_sub(1) {
690 m.indices.extend([face[0], face[k], face[k + 1]]);
691 }
692 }
693 _ => {}
694 }
695 }
696 if m.indices.is_empty() {
697 return Err("no faces".into());
698 }
699 if !has_normals {
700 compute_normals(&mut m);
701 }
702 Ok(m)
703}
704
705fn parse_fbx(assets: &Assets, path: &str, bytes: &[u8]) -> Result<MeshData, String> {
709 use ufbx::{CoordinateAxes, CoordinateAxis};
710 let opts = ufbx::LoadOpts {
711 target_axes: CoordinateAxes { right: CoordinateAxis::PositiveX, up: CoordinateAxis::PositiveY, front: CoordinateAxis::PositiveZ },
712 target_unit_meters: 1.0,
713 generate_missing_normals: true,
714 ..Default::default()
715 };
716 let scene = ufbx::load_memory(bytes, opts).map_err(|e| format!("{e:?}"))?;
717 let dir = path.rsplit_once('/').map_or("", |(d, _)| d);
718 let mut m = MeshData::default();
719 let mut tri = Vec::new();
720 for node in scene.nodes.iter() {
721 let Some(mesh) = &node.mesh else { continue };
722 let g = &node.geometry_to_world;
723 let world = Mat4::from_cols_array(&[
724 g.m00 as f32,
725 g.m10 as f32,
726 g.m20 as f32,
727 0.0,
728 g.m01 as f32,
729 g.m11 as f32,
730 g.m21 as f32,
731 0.0,
732 g.m02 as f32,
733 g.m12 as f32,
734 g.m22 as f32,
735 0.0,
736 g.m03 as f32,
737 g.m13 as f32,
738 g.m23 as f32,
739 1.0,
740 ]);
741 let normal_m = world.inverse().transpose();
742 let parent = dir.rsplit_once('/').map_or("", |(d, _)| d);
744 let join = |d: &str, f: &str| if d.is_empty() { f.to_string() } else { format!("{d}/{f}") };
745 let stem = path.rsplit('/').next().unwrap_or(path).rsplit_once('.').map_or(path, |(s, _)| s).to_string();
746 let mut material_images: Vec<Option<std::sync::Arc<Albedo>>> = Vec::new();
747 for mat in mesh.materials.iter() {
748 let tex = mat.pbr.base_color.texture.as_ref().or(mat.fbx.diffuse_color.texture.as_ref());
749 let mut image = None;
750 if let Some(t) = tex {
751 let name = t.filename.rsplit(['/', '\\']).next().unwrap_or("").to_string();
752 let candidates = [join(dir, &name), join(&join(dir, "textures"), &name), join(&join(parent, "textures"), &name), join(&join(parent, "Textures"), &name)];
754 m.dependencies.extend(candidates.iter().cloned());
757 image = candidates.iter().find_map(|c| load_albedo(assets, c));
758 }
759 if image.is_none() {
760 image = guess_texture(assets, dir, parent, &stem, &mat.element.name.to_string());
761 }
762 material_images.push(image);
763 }
764 if m.albedo.is_none() {
765 m.albedo = material_images.iter().flatten().next().cloned();
766 }
767 let base = m.vertices.len() as u32;
768 for i in 0..mesh.num_indices {
769 let p = mesh.vertex_position[i];
770 let n = if mesh.vertex_normal.exists { mesh.vertex_normal[i] } else { ufbx::Vec3 { x: 0.0, y: 1.0, z: 0.0 } };
771 let uv = if mesh.vertex_uv.exists {
772 let uv = mesh.vertex_uv[i];
773 [uv.x as f32, 1.0 - uv.y as f32]
774 } else {
775 [0.0; 2]
776 };
777 m.vertices.push(Vertex {
778 pos: world.transform_point3(Vec3::new(p.x as f32, p.y as f32, p.z as f32)).into(),
779 normal: normal_m.transform_vector3(Vec3::new(n.x as f32, n.y as f32, n.z as f32)).normalize_or_zero().into(),
780 uv,
781 });
782 }
783 let groups = mesh.materials.len().max(1);
785 let mut per_material: Vec<Vec<u32>> = vec![Vec::new(); groups];
786 for (face_index, face) in mesh.faces.iter().enumerate() {
787 ufbx::triangulate_face_vec(&mut tri, mesh, *face);
788 let slot = if mesh.face_material.is_empty() { 0 } else { (mesh.face_material[face_index] as usize).min(groups - 1) };
789 per_material[slot].extend(tri.iter().map(|&i| base + i));
790 }
791 for (slot, list) in per_material.into_iter().enumerate() {
792 if list.is_empty() {
793 continue;
794 }
795 let first_index = m.indices.len() as u32;
796 let index_count = list.len() as u32;
797 m.indices.extend(list);
798 let albedo = material_images.get(slot).cloned().flatten();
799 m.parts.push(MeshPart { first_index, index_count, albedo, normal: None, orm: None });
800 }
801 }
802 if m.indices.is_empty() {
803 return Err("no triangles".into());
804 }
805 m.parts = merge_parts(std::mem::take(&mut m.parts));
806 if m.parts.len() <= 1 {
807 m.parts.clear();
808 }
809 Ok(m)
810}
811
812fn load_albedo(assets: &Assets, path: &str) -> Option<std::sync::Arc<Albedo>> {
813 let img = image::load_from_memory(&assets.read(path).ok()?).ok()?;
814 let rgba = img.to_rgba8();
815 Some(std::sync::Arc::new(Albedo { width: rgba.width(), height: rgba.height(), rgba: rgba.into_raw() }))
816}
817
818fn guess_texture(assets: &Assets, dir: &str, parent: &str, stem: &str, material: &str) -> Option<std::sync::Arc<Albedo>> {
822 let material = material.to_ascii_lowercase();
823 let kind = if material.contains("trunk") || material.contains("bark") {
824 "trunk"
825 } else if material.contains("leaf") || material.contains("leaves") {
826 "leaf"
827 } else {
828 return None;
829 };
830 let family: String = stem.chars().filter(|c| c.is_ascii_alphabetic()).collect::<String>().to_ascii_lowercase();
831 let sub = |d: &str| if d.is_empty() { "textures".to_string() } else { format!("{}/textures", d.to_ascii_lowercase()) };
832 let roots = [dir.to_ascii_lowercase(), sub(dir), sub(parent)];
833 let mut best: Option<(i32, String)> = None;
834 for file in assets.list() {
835 let lower = file.to_ascii_lowercase();
836 if !(lower.ends_with(".png") || lower.ends_with(".jpg") || lower.ends_with(".jpeg")) {
837 continue;
838 }
839 let (folder, name) = lower.rsplit_once('/').map_or(("", lower.as_str()), |(f, n)| (f, n));
840 if !roots.iter().any(|r| r.as_str() == folder) || !name.contains(kind) {
841 continue;
842 }
843 let score = if name.contains(&family) { 2 } else { 0 } - (name.contains("alt") as i32);
844 if best.as_ref().is_none_or(|(s, _)| score > *s) {
845 best = Some((score, file));
846 }
847 }
848 best.and_then(|(_, file)| load_albedo(assets, &file))
849}
850
851fn compute_normals(m: &mut MeshData) {
852 for v in &mut m.vertices {
853 v.normal = [0.0; 3];
854 }
855 for tri in m.indices.as_chunks::<3>().0.iter() {
856 let p = |i: u32| Vec3::from(m.vertices[i as usize].pos);
857 let n = (p(tri[1]) - p(tri[0])).cross(p(tri[2]) - p(tri[0]));
858 for &i in tri {
859 let v = &mut m.vertices[i as usize];
860 v.normal = (Vec3::from(v.normal) + n).into();
861 }
862 }
863 for v in &mut m.vertices {
864 v.normal = Vec3::from(v.normal).normalize_or_zero().into();
865 }
866}
867
868fn to_albedo(img: &gltf::image::Data) -> Option<Albedo> {
869 use gltf::image::Format;
870 let n = (img.width * img.height) as usize;
871 let rgba = match img.format {
872 Format::R8G8B8A8 => img.pixels.clone(),
873 Format::R8G8B8 => img.pixels.as_chunks::<3>().0.iter().flat_map(|p| [p[0], p[1], p[2], 255]).collect(),
874 Format::R8 => img.pixels.iter().flat_map(|&v| [v, v, v, 255]).collect(),
875 Format::R8G8 => img.pixels.as_chunks::<2>().0.iter().flat_map(|p| [p[0], p[0], p[0], p[1]]).collect(),
876 _ => return None,
877 };
878 (rgba.len() == n * 4).then_some(Albedo { width: img.width, height: img.height, rgba })
879}
880
881fn decode_uri_path(uri: &str) -> Result<String, String> {
882 let mut decoded = Vec::with_capacity(uri.len());
883 let mut bytes = uri.bytes();
884 while let Some(byte) = bytes.next() {
885 if byte == b'%' {
886 let high = bytes.next().and_then(|b| (b as char).to_digit(16)).ok_or("invalid URI percent escape")?;
887 let low = bytes.next().and_then(|b| (b as char).to_digit(16)).ok_or("invalid URI percent escape")?;
888 decoded.push(((high << 4) | low) as u8);
889 } else {
890 decoded.push(byte);
891 }
892 }
893 String::from_utf8(decoded).map_err(|e| format!("asset URI is not UTF-8: {e}"))
894}
895
896pub(crate) struct ImportedGltf {
899 pub(crate) document: gltf::Document,
900 pub(crate) buffers: Vec<gltf::buffer::Data>,
901 pub(crate) images: Vec<Option<std::sync::Arc<Albedo>>>,
902 pub(crate) dependencies: Vec<String>,
903}
904
905pub(crate) fn import_gltf(assets: &Assets, path: &str, bytes: &[u8]) -> Result<ImportedGltf, String> {
907 match gltf::import_slice(bytes) {
908 Ok((document, buffers, images)) => Ok(ImportedGltf {
909 document,
910 buffers,
911 images: images.iter().map(|i| to_albedo(i).map(std::sync::Arc::new)).collect(),
912 dependencies: Vec::new(),
913 }),
914 Err(_) => load_external(assets, path, bytes),
915 }
916}
917
918fn load_external(assets: &Assets, path: &str, bytes: &[u8]) -> Result<ImportedGltf, String> {
919 let gltf::Gltf { document, blob } = gltf::Gltf::from_slice(bytes).map_err(|e| e.to_string())?;
920 let dir = path.rsplit_once('/').map_or("", |(d, _)| d);
921 let dependencies = std::cell::RefCell::new(Vec::new());
922 let read = |uri: &str| -> Result<Vec<u8>, String> {
923 if uri.starts_with("data:") {
924 return gltf::buffer::Data::from_source(gltf::buffer::Source::Uri(uri), None).map(|data| data.0).map_err(|e| e.to_string());
925 }
926 let decoded = decode_uri_path(uri)?;
927 let full = if dir.is_empty() { decoded } else { format!("{dir}/{decoded}") };
928 let full = crate::assets::normalize_path(&full).map_err(|e| e.to_string())?;
929 dependencies.borrow_mut().push(full.clone());
930 assets.read(&full).map_err(|e| format!("{full}: {e}"))
931 };
932 let mut buffers = Vec::new();
933 for b in document.buffers() {
934 let data = match b.source() {
935 gltf::buffer::Source::Bin => blob.clone().ok_or("GLB without a binary chunk")?,
936 gltf::buffer::Source::Uri(uri) => read(uri)?,
937 };
938 if data.len() < b.length() {
939 return Err(format!("glTF buffer {} is shorter than its declared byteLength", b.index()));
940 }
941 buffers.push(gltf::buffer::Data(data));
942 }
943 let mut albedos = Vec::new();
944 for img in document.images() {
945 let encoded = match img.source() {
946 gltf::image::Source::Uri { uri, .. } => read(uri).ok(),
947 gltf::image::Source::View { view, .. } => buffers
948 .get(view.buffer().index())
949 .and_then(|buffer| view.offset().checked_add(view.length()).and_then(|end| buffer.0.get(view.offset()..end)))
950 .map(<[u8]>::to_vec),
951 };
952 albedos.push(encoded.and_then(|e| image::load_from_memory(&e).ok()).map(|i| {
953 let rgba = i.to_rgba8();
954 std::sync::Arc::new(Albedo { width: rgba.width(), height: rgba.height(), rgba: rgba.into_raw() })
955 }));
956 }
957 Ok(ImportedGltf { document, buffers, images: albedos, dependencies: dependencies.into_inner() })
958}
959
960fn parse_gltf(assets: &Assets, path: &str, bytes: &[u8]) -> Result<MeshData, String> {
961 let ImportedGltf { document: doc, buffers, images, dependencies } = match gltf::import_slice(bytes) {
962 Ok((document, buffers, images)) => ImportedGltf {
963 document,
964 buffers,
965 images: images.iter().map(|i| to_albedo(i).map(std::sync::Arc::new)).collect(),
966 dependencies: Vec::new(),
967 },
968 Err(_) => load_external(assets, path, bytes)?,
969 };
970 let scene = doc.default_scene().or_else(|| doc.scenes().next()).ok_or("no scene")?;
971 let mut mesh = MeshData { dependencies, ..Default::default() };
972 enum Work<'a> {
975 Enter(gltf::Node<'a>, Mat4),
976 Exit(usize),
977 }
978 let mut stack: Vec<_> = scene.nodes().collect::<Vec<_>>().into_iter().rev().map(|node| Work::Enter(node, Mat4::IDENTITY)).collect();
979 let mut active = std::collections::HashSet::new();
980 while let Some(work) = stack.pop() {
981 let (node, parent) = match work {
982 Work::Exit(index) => {
983 active.remove(&index);
984 continue;
985 }
986 Work::Enter(node, parent) => (node, parent),
987 };
988 if !active.insert(node.index()) {
989 return Err(format!("cyclic glTF node hierarchy at node {}", node.index()));
990 }
991 let world = parent * Mat4::from_cols_array_2d(&node.transform().matrix());
992 if !world.is_finite() {
993 return Err(format!("non-finite glTF node transform {}", node.index()));
994 }
995 let normal_matrix = if world.determinant() == 0.0 { Mat4::IDENTITY } else { world.inverse().transpose() };
996 if let Some(source) = node.mesh() {
997 for primitive in source.primitives() {
998 use gltf::mesh::Mode;
999 if !matches!(primitive.mode(), Mode::Triangles | Mode::TriangleStrip | Mode::TriangleFan) {
1000 continue;
1001 }
1002 let pbr = primitive.material().pbr_metallic_roughness();
1003 if mesh.albedo.is_none() && mesh.indices.is_empty() {
1004 mesh.base_color = pbr.base_color_factor();
1005 }
1006 if mesh.albedo.is_none() {
1007 if let Some(info) = pbr.base_color_texture() {
1008 mesh.albedo = images.get(info.texture().source().index()).cloned().flatten();
1009 }
1010 }
1011 let part_albedo = material_albedo(&images, &pbr);
1012 let (part_normal, part_orm) = material_maps(&images, &primitive.material());
1013 let first_index = mesh.indices.len() as u32;
1014 let reader = primitive.reader(|buffer| buffers.get(buffer.index()).map(|data| data.0.as_slice()));
1015 let Some(positions) = reader.read_positions() else {
1016 continue;
1017 };
1018 let base = u32::try_from(mesh.vertices.len()).map_err(|_| "glTF exceeds u32 vertex space")?;
1019 let normals: Vec<_> = reader.read_normals().map(|values| values.collect()).unwrap_or_default();
1020 let uvs: Vec<_> = reader.read_tex_coords(0).map(|values| values.into_f32().collect()).unwrap_or_default();
1021 for (index, position) in positions.enumerate() {
1022 mesh.vertices.push(Vertex {
1023 pos: world.transform_point3(Vec3::from(position)).into(),
1024 normal: normals
1025 .get(index)
1026 .map_or([0.0; 3], |&normal| normal_matrix.transform_vector3(Vec3::from(normal)).normalize_or_zero().into()),
1027 uv: uvs.get(index).copied().unwrap_or([0.0; 2]),
1028 });
1029 }
1030 let count = mesh.vertices.len() - base as usize;
1031 let indices: Vec<u32> =
1032 reader.read_indices().map(|indices| indices.into_u32().collect()).unwrap_or_else(|| (0..count as u32).collect());
1033 if indices.iter().any(|&index| index as usize >= count) {
1034 return Err("glTF primitive index exceeds its POSITION count".into());
1035 }
1036 let mut triangles = Vec::new();
1037 match primitive.mode() {
1038 Mode::Triangles => {
1039 if !indices.len().is_multiple_of(3) {
1040 return Err("glTF triangle index count is not divisible by three".into());
1041 }
1042 triangles.extend(indices.as_chunks::<3>().0.iter().map(|tri| [tri[0], tri[1], tri[2]]));
1043 }
1044 Mode::TriangleStrip => {
1045 for (index, tri) in indices.windows(3).enumerate() {
1046 triangles.push(if index % 2 == 0 { [tri[0], tri[1], tri[2]] } else { [tri[1], tri[0], tri[2]] });
1047 }
1048 }
1049 Mode::TriangleFan => {
1050 for tri in indices.get(1..).unwrap_or_default().windows(2) {
1051 triangles.push([indices[0], tri[0], tri[1]]);
1052 }
1053 }
1054 _ => unreachable!(),
1055 }
1056 for mut triangle in triangles {
1057 if triangle[0] == triangle[1] || triangle[1] == triangle[2] || triangle[0] == triangle[2] {
1058 continue;
1059 }
1060 if world.determinant() < 0.0 {
1061 triangle.swap(1, 2);
1062 }
1063 for index in triangle {
1064 mesh.indices.push(base.checked_add(index).ok_or("glTF index overflow")?);
1065 }
1066 }
1067 let index_count = mesh.indices.len() as u32 - first_index;
1068 if index_count > 0 {
1069 mesh.parts.push(MeshPart { first_index, index_count, albedo: part_albedo, normal: part_normal, orm: part_orm });
1070 }
1071 }
1072 }
1073 stack.push(Work::Exit(node.index()));
1074 for child in node.children().collect::<Vec<_>>().into_iter().rev() {
1075 stack.push(Work::Enter(child, world));
1076 }
1077 }
1078 if mesh.indices.is_empty() {
1079 return Err("no triangles".into());
1080 }
1081 mesh.parts = merge_parts(std::mem::take(&mut mesh.parts));
1082 if mesh.parts.len() <= 1 && mesh.parts.iter().all(|p| p.normal.is_none() && p.orm.is_none()) {
1084 if let Some(albedo) = mesh.parts.first().and_then(|p| p.albedo.clone()) {
1087 mesh.albedo = Some(albedo);
1088 }
1089 mesh.parts.clear();
1090 }
1091 mesh.validate_geometry()?;
1092 if mesh.vertices.iter().any(|vertex| vertex.normal == [0.0; 3]) {
1093 compute_normals(&mut mesh);
1096 }
1097 mesh.validate()?;
1098 Ok(mesh)
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use super::*;
1104
1105 #[test]
1106 fn primitives_face_outward() {
1107 for name in PRIMITIVES {
1108 let m = MeshData::primitive(name).unwrap();
1109 let mut checked = 0;
1110 for tri in m.indices.as_chunks::<3>().0.iter() {
1111 let v = |i: u32| m.vertices[i as usize];
1112 let (a, b, c) = (v(tri[0]), v(tri[1]), v(tri[2]));
1113 let geo = (Vec3::from(b.pos) - Vec3::from(a.pos)).cross(Vec3::from(c.pos) - Vec3::from(a.pos));
1114 if geo.length() < 1e-6 {
1115 continue; }
1117 let n = Vec3::from(a.normal) + Vec3::from(b.normal) + Vec3::from(c.normal);
1118 assert!(geo.dot(n) > 0.0, "{name}: triangle winds against its normals");
1119 checked += 1;
1120 }
1121 assert!(checked > 0, "{name}: no triangles");
1122 }
1123 }
1124
1125 #[test]
1126 fn procedural_shapes_face_outward() {
1127 for kind in ["cube", "plane", "sphere", "capsule", "cylinder", "cone", "torus", "rock"] {
1128 let p = ProcParams {
1129 kind: kind.into(),
1130 size: Vec3::new(2.0, 1.0, 3.0),
1131 radius: 0.7,
1132 radius2: 0.25,
1133 height: 2.0,
1134 segments: 20,
1135 rings: 10,
1136 seed: 3,
1137 };
1138 assert_eq!(ProcParams::parse(&p.key()), Some(p.clone()), "{kind} key roundtrip");
1139 let m = p.build().unwrap();
1140 let mut checked = 0;
1141 for tri in m.indices.as_chunks::<3>().0.iter() {
1142 let v = |i: u32| m.vertices[i as usize];
1143 let (a, b, c) = (v(tri[0]), v(tri[1]), v(tri[2]));
1144 let geo = (Vec3::from(b.pos) - Vec3::from(a.pos)).cross(Vec3::from(c.pos) - Vec3::from(a.pos));
1145 if geo.length() < 1e-6 {
1146 continue;
1147 }
1148 let n = Vec3::from(a.normal) + Vec3::from(b.normal) + Vec3::from(c.normal);
1149 assert!(geo.dot(n) > 0.0, "{kind}: wrong winding");
1150 checked += 1;
1151 }
1152 assert!(checked > 0, "{kind}");
1153 }
1154 }
1155
1156 #[test]
1157 fn obj_quad_is_triangulated() {
1158 let m = parse_obj("v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n").unwrap();
1159 assert_eq!(m.indices.len(), 6);
1160 assert!(m.vertices[0].normal[2] > 0.99);
1161 }
1162}