use std::collections::HashMap;
use oxideav_mesh3d::{Error, Indices, Mesh, Primitive, Result, Scene3D, Topology};
use crate::mtl::parse_mtl;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
struct FaceVert {
v: u32,
vt: u32,
vn: u32,
}
#[derive(Debug)]
enum Element {
Face(Vec<FaceVert>),
Line(Vec<FaceVert>),
Point(Vec<FaceVert>),
}
#[derive(Debug, Default)]
struct PrimAccum {
elements: Vec<Element>,
material: Option<String>,
smoothing_group: Option<String>,
groups: Vec<String>,
merging_group: Option<String>,
bevel: Option<String>,
c_interp: Option<String>,
d_interp: Option<String>,
lod: Option<String>,
}
#[derive(Debug, Default)]
struct MeshAccum {
name: Option<String>,
primitives: Vec<PrimAccum>,
}
impl MeshAccum {
fn current_or_new(&mut self) -> &mut PrimAccum {
if self.primitives.is_empty() {
self.primitives.push(PrimAccum::default());
}
self.primitives.last_mut().unwrap()
}
}
#[derive(Debug, Default)]
struct ObjDoc {
positions: Vec<[f32; 3]>,
position_weights: Vec<Option<f32>>,
position_colors: Vec<Option<[f32; 4]>>,
texcoords: Vec<[f32; 2]>,
normals: Vec<[f32; 3]>,
vp: Vec<[f32; 3]>,
mtllibs: Vec<String>,
resolved_materials: HashMap<String, oxideav_mesh3d::Material>,
meshes: Vec<MeshAccum>,
freeform_directives: Vec<Vec<String>>,
}
fn preprocess_lines(text: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut acc = String::new();
for raw_line in text.split('\n') {
let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
let no_comment = match line.find('#') {
Some(idx) => &line[..idx],
None => line,
};
let trimmed = no_comment.trim_end();
if let Some(stripped) = trimmed.strip_suffix('\\') {
acc.push_str(stripped);
acc.push(' ');
} else {
acc.push_str(trimmed);
out.push(std::mem::take(&mut acc));
}
}
if !acc.is_empty() {
out.push(acc);
}
out
}
fn parse_face_vertex(tok: &str, n_pos: i64, n_tex: i64, n_norm: i64) -> Result<FaceVert> {
let mut parts = tok.split('/');
let v = parts
.next()
.ok_or_else(|| Error::invalid(format!("face vertex missing position: {tok:?}")))?;
if v.is_empty() {
return Err(Error::invalid(format!(
"face vertex missing position index: {tok:?}"
)));
}
let vt = parts.next().unwrap_or("");
let vn = parts.next().unwrap_or("");
let resolve = |s: &str, n: i64, kind: &str| -> Result<u32> {
if s.is_empty() {
return Ok(0);
}
let raw: i64 = s.parse().map_err(|_| {
Error::invalid(format!(
"invalid {kind} index in face vertex {tok:?}: {s:?}"
))
})?;
let resolved = if raw < 0 { n + 1 + raw } else { raw };
if resolved <= 0 || resolved > n {
return Err(Error::invalid(format!(
"{kind} index out of range in face vertex {tok:?}: {raw} (have {n})"
)));
}
Ok(resolved as u32)
};
Ok(FaceVert {
v: resolve(v, n_pos, "position")?,
vt: resolve(vt, n_tex, "texcoord")?,
vn: resolve(vn, n_norm, "normal")?,
})
}
fn parse_obj_doc(text: &str) -> Result<ObjDoc> {
let mut doc = ObjDoc::default();
doc.meshes.push(MeshAccum::default());
let lines = preprocess_lines(text);
for line in &lines {
let mut tokens = line.split_whitespace();
let Some(keyword) = tokens.next() else {
continue;
};
match keyword {
"v" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("v: bad float ({e})")))?;
let (w, rgb) = match coords.len() {
3 => (None, None),
4 => (Some(coords[3]), None),
6 => (None, Some([coords[3], coords[4], coords[5], 1.0])),
7 => (
Some(coords[3]),
Some([coords[4], coords[5], coords[6], 1.0]),
),
n => {
return Err(Error::invalid(format!(
"v: expected 3, 4, 6, or 7 floats (xyz, xyzw, xyzrgb, or \
xyzwrgb per spec + MeshLab vertex-colour extension), got {n}"
)));
}
};
doc.positions.push([coords[0], coords[1], coords[2]]);
doc.position_weights.push(w);
doc.position_colors.push(rgb);
}
"vt" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vt: bad float ({e})")))?;
if coords.is_empty() {
return Err(Error::invalid("vt: expected ≥1 coord"));
}
let u = coords[0];
let v = coords.get(1).copied().unwrap_or(0.0);
doc.texcoords.push([u, v]);
}
"vn" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vn: bad float ({e})")))?;
if coords.len() != 3 {
return Err(Error::invalid(format!(
"vn: expected 3 coords, got {}",
coords.len()
)));
}
doc.normals.push([coords[0], coords[1], coords[2]]);
}
"vp" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vp: bad float ({e})")))?;
if coords.is_empty() {
return Err(Error::invalid("vp: expected ≥1 coord"));
}
let u = coords[0];
let v = coords.get(1).copied().unwrap_or(0.0);
let w = coords.get(2).copied().unwrap_or(0.0);
doc.vp.push([u, v, w]);
}
"cstype" | "deg" | "curv" | "curv2" | "surf" | "parm" | "trim" | "hole" | "scrv"
| "sp" | "end" | "bzp" | "bsp" | "bmat" | "step" => {
let mut entry: Vec<String> = Vec::new();
entry.push(keyword.to_string());
for tok in tokens {
entry.push(tok.to_string());
}
doc.freeform_directives.push(entry);
}
"f" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.len() < 3 {
return Err(Error::invalid(format!(
"f: face needs ≥3 vertices, got {}",
verts.len()
)));
}
let mesh = doc.meshes.last_mut().unwrap();
mesh.current_or_new().elements.push(Element::Face(verts));
}
"l" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.len() < 2 {
return Err(Error::invalid(format!(
"l: line needs ≥2 vertices, got {}",
verts.len()
)));
}
let mesh = doc.meshes.last_mut().unwrap();
mesh.current_or_new().elements.push(Element::Line(verts));
}
"p" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.is_empty() {
return Err(Error::invalid("p: needs ≥1 vertex"));
}
let mesh = doc.meshes.last_mut().unwrap();
let prim = mesh.current_or_new();
if prim
.elements
.iter()
.any(|e| !matches!(e, Element::Point(_)))
{
let mat = prim.material.clone();
let groups = prim.groups.clone();
let smoothing = prim.smoothing_group.clone();
let merging = prim.merging_group.clone();
let bevel = prim.bevel.clone();
let c_interp = prim.c_interp.clone();
let d_interp = prim.d_interp.clone();
let lod = prim.lod.clone();
mesh.primitives.push(PrimAccum {
material: mat,
groups,
smoothing_group: smoothing,
merging_group: merging,
bevel,
c_interp,
d_interp,
lod,
elements: vec![Element::Point(verts)],
});
} else {
prim.elements.push(Element::Point(verts));
}
}
"bevel" | "c_interp" | "d_interp" | "lod" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
let current: Option<&str> = match keyword {
"bevel" => last.bevel.as_deref(),
"c_interp" => last.c_interp.as_deref(),
"d_interp" => last.d_interp.as_deref(),
"lod" => last.lod.as_deref(),
_ => unreachable!(),
};
if last.elements.is_empty() {
match keyword {
"bevel" => last.bevel = Some(v),
"c_interp" => last.c_interp = Some(v),
"d_interp" => last.d_interp = Some(v),
"lod" => last.lod = Some(v),
_ => unreachable!(),
}
} else if current != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let merging = last.merging_group.clone();
let mut bevel = last.bevel.clone();
let mut c_interp = last.c_interp.clone();
let mut d_interp = last.d_interp.clone();
let mut lod = last.lod.clone();
match keyword {
"bevel" => bevel = Some(v),
"c_interp" => c_interp = Some(v),
"d_interp" => d_interp = Some(v),
"lod" => lod = Some(v),
_ => unreachable!(),
}
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: smoothing,
merging_group: merging,
groups,
bevel,
c_interp,
d_interp,
lod,
elements: Vec::new(),
});
}
}
"mg" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() {
last.merging_group = Some(v);
} else if last.merging_group.as_deref() != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: smoothing,
groups,
merging_group: Some(v),
bevel,
c_interp,
d_interp,
lod,
elements: Vec::new(),
});
}
}
"o" => {
let name: String = tokens.collect::<Vec<_>>().join(" ");
let last = doc.meshes.last_mut().unwrap();
if last.name.is_none() && last.primitives.is_empty() {
last.name = if name.is_empty() { None } else { Some(name) };
} else {
doc.meshes.push(MeshAccum {
name: if name.is_empty() { None } else { Some(name) },
primitives: Vec::new(),
});
}
}
"g" => {
let names: Vec<String> = tokens.map(|t| t.to_string()).collect();
if names.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let prim = mesh.current_or_new();
for name in names {
if !prim.groups.iter().any(|g| g == &name) {
prim.groups.push(name);
}
}
}
"s" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() {
last.smoothing_group = Some(v);
} else if last.smoothing_group.as_deref() != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let merging = last.merging_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: Some(v),
groups,
merging_group: merging,
bevel,
c_interp,
d_interp,
lod,
elements: Vec::new(),
});
}
}
"usemtl" => {
let name: String = tokens.collect::<Vec<_>>().join(" ");
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() && last.material.is_none() {
last.material = if name.is_empty() { None } else { Some(name) };
} else {
mesh.primitives.push(PrimAccum {
material: if name.is_empty() { None } else { Some(name) },
..PrimAccum::default()
});
}
}
"mtllib" => {
for tok in tokens {
if !doc.mtllibs.iter().any(|m| m == tok) {
doc.mtllibs.push(tok.to_string());
}
}
}
_ => {}
}
}
Ok(doc)
}
fn build_scene(doc: ObjDoc) -> Result<Scene3D> {
use oxideav_mesh3d::{Axis, Material, Unit};
let mut scene = Scene3D::new();
scene.up_axis = Axis::PosY;
scene.unit = Unit::Metres;
let mut material_ids: HashMap<String, oxideav_mesh3d::MaterialId> = HashMap::new();
let mut material_names: Vec<String> = doc.resolved_materials.keys().cloned().collect();
material_names.sort();
for name in &material_names {
let mut mat = doc
.resolved_materials
.get(name)
.cloned()
.unwrap_or_else(Material::new);
if mat.name.is_none() {
mat.name = Some(name.clone());
}
let id = scene.add_material(mat);
material_ids.insert(name.clone(), id);
}
for mesh_acc in doc.meshes {
let has_anything = mesh_acc.primitives.iter().any(|p| !p.elements.is_empty());
if !has_anything {
continue;
}
let mut mesh = Mesh::new(mesh_acc.name.clone());
for prim_acc in mesh_acc.primitives {
let (mut primitive, arities) = build_primitive(
&prim_acc,
&doc.positions,
&doc.position_weights,
&doc.position_colors,
&doc.texcoords,
&doc.normals,
&material_ids,
)?;
if primitive.positions.is_empty() {
continue;
}
if arities.iter().any(|&a| a != 3) {
primitive.extras.insert(
"obj:original_face_arities".to_string(),
serde_json::to_value(&arities).unwrap(),
);
}
mesh.primitives.push(primitive);
}
scene.add_mesh(mesh);
}
if !doc.mtllibs.is_empty() {
scene.extras.insert(
"obj:mtllibs".to_string(),
serde_json::to_value(&doc.mtllibs).unwrap(),
);
}
if !doc.positions.is_empty()
&& (doc.freeform_directives.iter().any(|d| {
matches!(
d.first().map(String::as_str),
Some("curv" | "curv2" | "surf" | "bzp" | "bsp")
)
}))
{
scene.extras.insert(
"obj:positions".to_string(),
serde_json::to_value(&doc.positions).unwrap(),
);
if doc.position_weights.iter().any(Option::is_some) {
scene.extras.insert(
"obj:position_weights".to_string(),
serde_json::to_value(&doc.position_weights).unwrap(),
);
}
if doc.position_colors.iter().any(Option::is_some) {
scene.extras.insert(
"obj:position_colors".to_string(),
serde_json::to_value(&doc.position_colors).unwrap(),
);
}
}
if !doc.vp.is_empty() {
scene
.extras
.insert("obj:vp".to_string(), serde_json::to_value(&doc.vp).unwrap());
}
if !doc.freeform_directives.is_empty() {
scene.extras.insert(
"obj:freeform_directives".to_string(),
serde_json::to_value(&doc.freeform_directives).unwrap(),
);
}
Ok(scene)
}
fn tessellate_curves(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
let mut active_kind: Option<&'static str> = None;
let mut active_degree: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut pending_curves: Vec<&Vec<String>> = Vec::new();
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
pending_curves.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_degree = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
if let Some(d) = entry.get(1).and_then(|t| t.parse::<u32>().ok()) {
active_degree = Some(d);
}
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
}
"curv" => {
pending_curves.push(entry);
}
"end" => {
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
pending_curves.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_kind = None;
active_degree = None;
}
_ => {}
}
}
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
out
}
#[allow(clippy::too_many_arguments)]
fn flush_block(
out: &mut Vec<Primitive>,
doc: &ObjDoc,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending_curves: &[&Vec<String>],
samples: u32,
) {
let Some(kind) = active_kind else {
return;
};
for entry in pending_curves {
if entry.len() < 5 {
continue;
}
let Ok(u_min) = entry[1].parse::<f32>() else {
continue;
};
let Ok(u_max) = entry[2].parse::<f32>() else {
continue;
};
let n_pos = doc.positions.len() as i64;
let mut control_points: Vec<[f32; 3]> = Vec::new();
let mut control_weights: Vec<f32> = Vec::new();
let mut bad = false;
for tok in &entry[3..] {
let Ok(raw) = tok.parse::<i64>() else {
bad = true;
break;
};
let resolved = if raw < 0 { n_pos + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_pos {
bad = true;
break;
}
let pos = doc.positions[(resolved as usize) - 1];
control_points.push(pos);
let w = doc.position_weights[(resolved as usize) - 1].unwrap_or(1.0);
control_weights.push(w);
}
if bad || control_points.len() < 2 {
continue;
}
let curve_points = match kind {
"bezier" | "rat_bezier" => sample_bezier(
&control_points,
&control_weights,
kind,
u_min,
u_max,
samples,
),
"bspline" | "rat_bspline" => {
let Some(degree) = active_degree else {
continue;
};
if parm_u.len() != control_points.len() + degree as usize + 1 {
continue;
}
sample_bspline(
&control_points,
&control_weights,
kind,
degree,
parm_u,
u_min,
u_max,
samples,
)
}
"cardinal" => {
if active_degree.is_some_and(|d| d != 3) {
continue;
}
if control_points.len() < 4 {
continue;
}
sample_cardinal(&control_points, samples)
}
"taylor" => {
let degree = match active_degree {
Some(d) => d as usize,
None => control_points.len().saturating_sub(1),
};
if control_points.len() != degree + 1 {
continue;
}
sample_taylor(&control_points, u_min, u_max, samples)
}
"bmatrix" => {
let Some(degree) = active_degree else {
continue;
};
let Some(step) = step_u else {
continue;
};
let Some(n_plus_1) = (degree as usize).checked_add(1) else {
continue;
};
let Some(expected_bmat) = n_plus_1.checked_mul(n_plus_1) else {
continue;
};
if bmat_u.len() != expected_bmat {
continue;
}
if step == 0 {
continue;
}
if control_points.len() < n_plus_1 {
continue;
}
sample_bmatrix(&control_points, bmat_u, degree, step, samples)
}
_ => continue,
};
if curve_points.len() < 2 {
continue;
}
let mut prim = Primitive::new(Topology::LineStrip);
let n = curve_points.len() as u32;
prim.positions = curve_points;
if n > u16::MAX as u32 {
prim.indices = Some(Indices::U32((0..n).collect()));
} else {
prim.indices = Some(Indices::U16((0..n).map(|i| i as u16).collect()));
}
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:curve_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
let reported_degree = match kind {
"bezier" | "rat_bezier" => (control_points.len() - 1) as u64,
"bspline" | "rat_bspline" => active_degree.unwrap_or(0) as u64,
"cardinal" => 3,
"taylor" => active_degree
.map(u64::from)
.unwrap_or_else(|| (control_points.len() - 1) as u64),
"bmatrix" => active_degree.map(u64::from).unwrap_or(0),
_ => 0,
};
prim.extras.insert(
"obj:curve_degree".to_string(),
serde_json::Value::Number(serde_json::Number::from(reported_degree)),
);
let range_arr = serde_json::Value::Array(vec![
serde_json::Value::from(u_min as f64),
serde_json::Value::from(u_max as f64),
]);
prim.extras
.insert("obj:curve_u_range".to_string(), range_arr);
prim.extras.insert(
"obj:curve_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
out.push(prim);
}
}
fn tessellate_curve2(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
let mut active_kind: Option<&'static str> = None;
let mut active_degree: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut pending: Vec<&Vec<String>> = Vec::new();
let flush = |out: &mut Vec<Primitive>,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending: &[&Vec<String>]| {
flush_curve2_block(
out,
doc,
active_kind,
active_degree,
parm_u,
bmat_u,
step_u,
pending,
samples,
);
};
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_degree = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
if let Some(d) = entry.get(1).and_then(|t| t.parse::<u32>().ok()) {
active_degree = Some(d);
}
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
}
"curv2" => {
pending.push(entry);
}
"end" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_kind = None;
active_degree = None;
}
_ => {}
}
}
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
out
}
#[allow(clippy::too_many_arguments)]
fn flush_curve2_block(
out: &mut Vec<Primitive>,
doc: &ObjDoc,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending: &[&Vec<String>],
samples: u32,
) {
let Some(kind) = active_kind else {
return;
};
let n_vp = doc.vp.len() as i64;
for entry in pending {
if entry.len() < 3 {
continue;
}
let mut control_points: Vec<[f32; 3]> = Vec::new();
let mut control_weights: Vec<f32> = Vec::new();
let mut bad = false;
for tok in &entry[1..] {
let Ok(raw) = tok.parse::<i64>() else {
bad = true;
break;
};
let resolved = if raw < 0 { n_vp + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_vp {
bad = true;
break;
}
let vp = doc.vp[(resolved as usize) - 1];
control_points.push([vp[0], vp[1], 0.0]);
let w = if vp[2] == 0.0 { 1.0 } else { vp[2] };
control_weights.push(w);
}
if bad || control_points.len() < 2 {
continue;
}
let (u_min, u_max) = match (parm_u.first(), parm_u.last()) {
(Some(&a), Some(&b)) if parm_u.len() >= 2 => (a, b),
_ => (0.0, 1.0),
};
let curve_points = match kind {
"bezier" | "rat_bezier" => sample_bezier(
&control_points,
&control_weights,
kind,
u_min,
u_max,
samples,
),
"bspline" | "rat_bspline" => {
let Some(degree) = active_degree else {
continue;
};
if parm_u.len() != control_points.len() + degree as usize + 1 {
continue;
}
sample_bspline(
&control_points,
&control_weights,
kind,
degree,
parm_u,
u_min,
u_max,
samples,
)
}
"cardinal" => {
if active_degree.is_some_and(|d| d != 3) {
continue;
}
if control_points.len() < 4 {
continue;
}
sample_cardinal(&control_points, samples)
}
"taylor" => {
let degree = match active_degree {
Some(d) => d as usize,
None => control_points.len().saturating_sub(1),
};
if control_points.len() != degree + 1 {
continue;
}
sample_taylor(&control_points, u_min, u_max, samples)
}
"bmatrix" => {
let Some(degree) = active_degree else {
continue;
};
let Some(step) = step_u else {
continue;
};
let Some(n_plus_1) = (degree as usize).checked_add(1) else {
continue;
};
let Some(expected_bmat) = n_plus_1.checked_mul(n_plus_1) else {
continue;
};
if bmat_u.len() != expected_bmat {
continue;
}
if step == 0 {
continue;
}
if control_points.len() < n_plus_1 {
continue;
}
sample_bmatrix(&control_points, bmat_u, degree, step, samples)
}
_ => continue,
};
if curve_points.len() < 2 {
continue;
}
let mut prim = Primitive::new(Topology::LineStrip);
let n = curve_points.len() as u32;
prim.positions = curve_points;
if n > u16::MAX as u32 {
prim.indices = Some(Indices::U32((0..n).collect()));
} else {
prim.indices = Some(Indices::U16((0..n).map(|i| i as u16).collect()));
}
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras
.insert("obj:curve2".to_string(), serde_json::Value::Bool(true));
prim.extras.insert(
"obj:curve_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
let reported_degree = match kind {
"bezier" | "rat_bezier" => (control_points.len() - 1) as u64,
"bspline" | "rat_bspline" => active_degree.unwrap_or(0) as u64,
"cardinal" => 3,
"taylor" => active_degree
.map(u64::from)
.unwrap_or_else(|| (control_points.len() - 1) as u64),
"bmatrix" => active_degree.map(u64::from).unwrap_or(0),
_ => 0,
};
prim.extras.insert(
"obj:curve_degree".to_string(),
serde_json::Value::Number(serde_json::Number::from(reported_degree)),
);
prim.extras.insert(
"obj:curve_u_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(u_min as f64),
serde_json::Value::from(u_max as f64),
]),
);
prim.extras.insert(
"obj:curve_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
out.push(prim);
}
}
fn tessellate_surfaces(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
if samples == 0 {
return out;
}
let mut active_kind: Option<&'static str> = None;
let mut deg_u: Option<u32> = None;
let mut deg_v: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut parm_v: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut bmat_v: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut step_v: Option<u32> = None;
let mut pending_surfs: Vec<&Vec<String>> = Vec::new();
#[allow(clippy::too_many_arguments)]
let flush = |out: &mut Vec<Primitive>,
kind: Option<&'static str>,
deg_u: Option<u32>,
deg_v: Option<u32>,
parm_u: &[f32],
parm_v: &[f32],
bmat_u: &[f32],
bmat_v: &[f32],
step_u: Option<u32>,
step_v: Option<u32>,
surfs: &[&Vec<String>]| {
let Some(kind) = kind else {
return;
};
for entry in surfs {
if let Some(prim) = flush_surface(
doc, kind, deg_u, deg_v, parm_u, parm_v, bmat_u, bmat_v, step_u, step_v, entry,
samples,
) {
out.push(prim);
}
}
};
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
);
pending_surfs.clear();
deg_u = None;
deg_v = None;
parm_u.clear();
parm_v.clear();
bmat_u.clear();
bmat_v.clear();
step_u = None;
step_v = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
deg_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
deg_v = entry.get(2).and_then(|t| t.parse::<u32>().ok()).or(deg_u);
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"parm" if entry.get(1).map(String::as_str) == Some("v") => {
parm_v = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("v") => {
bmat_v = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
step_v = entry.get(2).and_then(|t| t.parse::<u32>().ok());
}
"surf" => pending_surfs.push(entry),
"end" => {
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
);
pending_surfs.clear();
active_kind = None;
deg_u = None;
deg_v = None;
parm_u.clear();
parm_v.clear();
bmat_u.clear();
bmat_v.clear();
step_u = None;
step_v = None;
}
_ => {}
}
}
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
);
out
}
#[allow(clippy::too_many_arguments)]
fn flush_surface(
doc: &ObjDoc,
kind: &'static str,
deg_u: Option<u32>,
deg_v: Option<u32>,
parm_u: &[f32],
parm_v: &[f32],
bmat_u: &[f32],
bmat_v: &[f32],
step_u: Option<u32>,
step_v: Option<u32>,
entry: &[String],
samples: u32,
) -> Option<Primitive> {
if entry.len() < 6 {
return None;
}
let s0 = entry[1].parse::<f32>().ok()?;
let s1 = entry[2].parse::<f32>().ok()?;
let t0 = entry[3].parse::<f32>().ok()?;
let t1 = entry[4].parse::<f32>().ok()?;
let du = deg_u? as usize;
let dv = deg_v? as usize;
let bspline = matches!(kind, "bspline" | "rat_bspline");
let cardinal = kind == "cardinal";
let taylor = kind == "taylor";
let bmatrix = kind == "bmatrix";
let (cols, rows) = if bspline {
let need_u = du.checked_add(2)?;
let need_v = dv.checked_add(2)?;
if parm_u.len() < need_u || parm_v.len() < need_v {
return None;
}
(parm_u.len() - du - 1, parm_v.len() - dv - 1) } else if bmatrix {
let su = step_u? as usize;
let sv = step_v? as usize;
if su == 0 || sv == 0 || parm_u.len() < 2 || parm_v.len() < 2 {
return None;
}
let cols = (parm_u.len() - 2)
.checked_mul(su)?
.checked_add(du)?
.checked_add(1)?;
let rows = (parm_v.len() - 2)
.checked_mul(sv)?
.checked_add(dv)?
.checked_add(1)?;
(cols, rows)
} else if cardinal {
if du != 3 || dv != 3 {
return None;
}
let total = entry.len() - 5; let cols = if parm_u.len() > 2 {
parm_u.len() + 1
} else {
isqrt_exact(total)?
};
let rows = if parm_v.len() > 2 {
parm_v.len() + 1
} else if cols != 0 && total % cols == 0 {
total / cols
} else {
return None;
};
(cols, rows)
} else {
(du.checked_add(1)?, dv.checked_add(1)?)
};
let expected = cols.checked_mul(rows)?;
if expected != entry.len().saturating_sub(5) {
return None;
}
let n_pos = doc.positions.len() as i64;
let mut grid: Vec<[f32; 3]> = Vec::with_capacity(expected);
let mut weights: Vec<f32> = Vec::with_capacity(expected);
for tok in &entry[5..] {
let first_field = tok.split('/').next().unwrap_or(tok);
let raw = first_field.parse::<i64>().ok()?;
let resolved = if raw < 0 { n_pos + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_pos {
return None;
}
grid.push(doc.positions[(resolved as usize) - 1]);
let w = doc.position_weights[(resolved as usize) - 1].unwrap_or(1.0);
weights.push(w);
}
if grid.len() != expected {
return None;
}
let positions = if bspline {
sample_bspline_surface(
&grid, &weights, kind, du as u32, dv as u32, parm_u, parm_v, s0, s1, t0, t1, cols,
rows, samples,
)
} else if cardinal {
sample_cardinal_surface(&grid, cols, rows, samples)
} else if taylor {
sample_taylor_surface(&grid, cols, rows, s0, s1, t0, t1, samples)
} else if bmatrix {
let need_u = du.checked_add(1)?.checked_mul(du.checked_add(1)?)?;
let need_v = dv.checked_add(1)?.checked_mul(dv.checked_add(1)?)?;
if bmat_u.len() != need_u || bmat_v.len() != need_v {
return None;
}
let su = step_u?;
let sv = step_v?;
sample_bmatrix_surface(
&grid, bmat_u, bmat_v, du as u32, dv as u32, su, sv, cols, rows, samples,
)
} else {
sample_bezier_surface(&grid, &weights, kind, cols, rows, samples)
};
if positions.is_empty() {
return None;
}
let stride = samples as usize + 1;
let mut indices: Vec<u32> = Vec::with_capacity((samples as usize) * (samples as usize) * 6);
for sv in 0..samples as usize {
for su in 0..samples as usize {
let i00 = (sv * stride + su) as u32;
let i10 = (sv * stride + su + 1) as u32;
let i01 = ((sv + 1) * stride + su) as u32;
let i11 = ((sv + 1) * stride + su + 1) as u32;
indices.push(i00);
indices.push(i10);
indices.push(i11);
indices.push(i00);
indices.push(i11);
indices.push(i01);
}
}
let mut prim = Primitive::new(Topology::Triangles);
let n_verts = positions.len() as u32;
prim.positions = positions;
prim.indices = if n_verts > u16::MAX as u32 {
Some(Indices::U32(indices))
} else {
Some(Indices::U16(indices.iter().map(|&i| i as u16).collect()))
};
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:tessellated_surface".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:surface_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
prim.extras.insert(
"obj:surface_degree".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(du as u64),
serde_json::Value::from(dv as u64),
]),
);
prim.extras.insert(
"obj:surface_u_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(s0 as f64),
serde_json::Value::from(s1 as f64),
]),
);
prim.extras.insert(
"obj:surface_v_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(t0 as f64),
serde_json::Value::from(t1 as f64),
]),
);
prim.extras.insert(
"obj:surface_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
Some(prim)
}
fn sample_bezier_surface(
grid: &[[f32; 3]],
weights: &[f32],
kind: &str,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let rational = kind == "rat_bezier";
let homo: Vec<[f32; 4]> = grid
.iter()
.zip(weights.iter())
.map(|(p, w)| {
let weight = if rational { *w } else { 1.0 };
[p[0] * weight, p[1] * weight, p[2] * weight, weight]
})
.collect();
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
sv as f32 / (n - 1) as f32
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
su as f32 / (n - 1) as f32
};
let mut col_pts: Vec<[f32; 4]> = Vec::with_capacity(rows);
for r in 0..rows {
let row = &homo[r * cols..r * cols + cols];
col_pts.push(de_casteljau_4d(row, u));
}
let pt = de_casteljau_4d(&col_pts, v);
let [x, y, z, w] = pt;
if rational && w.abs() > f32::EPSILON {
out.push([x / w, y / w, z / w]);
} else {
out.push([x, y, z]);
}
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_bmatrix_surface(
grid: &[[f32; 3]],
bmat_u: &[f32],
bmat_v: &[f32],
deg_u: u32,
deg_v: u32,
step_u: u32,
step_v: u32,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
let n_plus_1 = match (deg_u as usize).checked_add(1) {
Some(v) => v,
None => return Vec::new(),
};
let m_plus_1 = match (deg_v as usize).checked_add(1) {
Some(v) => v,
None => return Vec::new(),
};
let need_bmat_u = match n_plus_1.checked_mul(n_plus_1) {
Some(v) => v,
None => return Vec::new(),
};
let need_bmat_v = match m_plus_1.checked_mul(m_plus_1) {
Some(v) => v,
None => return Vec::new(),
};
if samples == 0
|| cols == 0
|| rows == 0
|| step_u == 0
|| step_v == 0
|| grid.len() != cols * rows
|| bmat_u.len() != need_bmat_u
|| bmat_v.len() != need_bmat_v
|| cols < n_plus_1
|| rows < m_plus_1
{
return Vec::new();
}
let su_stride = step_u as usize;
let sv_stride = step_v as usize;
let n_seg_u = (cols - n_plus_1) / su_stride + 1;
let n_seg_v = (rows - m_plus_1) / sv_stride + 1;
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv_i in 0..n {
let gv = if sv_i == n - 1 {
n_seg_v as f32
} else {
sv_i as f32 * n_seg_v as f32 / (n - 1) as f32
};
let mut seg_v = gv.floor() as usize;
let mut tv = gv - seg_v as f32;
if seg_v >= n_seg_v {
seg_v = n_seg_v - 1;
tv = 1.0;
}
let base_v = seg_v * sv_stride;
let mut tv_pow: Vec<f32> = Vec::with_capacity(m_plus_1);
let mut pv = 1.0_f32;
for _ in 0..m_plus_1 {
tv_pow.push(pv);
pv *= tv;
}
let mut v_coef: Vec<f32> = Vec::with_capacity(m_plus_1);
for b in 0..m_plus_1 {
let mut c = 0.0_f32;
for q in 0..m_plus_1 {
c += bmat_v[b * m_plus_1 + q] * tv_pow[q];
}
v_coef.push(c);
}
for su_i in 0..n {
let gu = if su_i == n - 1 {
n_seg_u as f32
} else {
su_i as f32 * n_seg_u as f32 / (n - 1) as f32
};
let mut seg_u = gu.floor() as usize;
let mut tu = gu - seg_u as f32;
if seg_u >= n_seg_u {
seg_u = n_seg_u - 1;
tu = 1.0;
}
let base_u = seg_u * su_stride;
let mut tu_pow: Vec<f32> = Vec::with_capacity(n_plus_1);
let mut pu = 1.0_f32;
for _ in 0..n_plus_1 {
tu_pow.push(pu);
pu *= tu;
}
let mut u_coef: Vec<f32> = Vec::with_capacity(n_plus_1);
for a in 0..n_plus_1 {
let mut c = 0.0_f32;
for p in 0..n_plus_1 {
c += bmat_u[a * n_plus_1 + p] * tu_pow[p];
}
u_coef.push(c);
}
let mut accum = [0.0_f32; 3];
for (b, vc) in v_coef.iter().enumerate() {
let row = (base_v + b) * cols;
for (a, uc) in u_coef.iter().enumerate() {
let cp = grid[row + base_u + a];
let w = uc * vc;
accum[0] += w * cp[0];
accum[1] += w * cp[1];
accum[2] += w * cp[2];
}
}
out.push(accum);
}
}
out
}
fn de_casteljau_4d(points: &[[f32; 4]], t: f32) -> [f32; 4] {
if points.is_empty() {
return [0.0, 0.0, 0.0, 1.0];
}
let mut buf: Vec<[f32; 4]> = points.to_vec();
let n = buf.len();
for level in 1..n {
for j in 0..(n - level) {
buf[j] = [
(1.0 - t) * buf[j][0] + t * buf[j + 1][0],
(1.0 - t) * buf[j][1] + t * buf[j + 1][1],
(1.0 - t) * buf[j][2] + t * buf[j + 1][2],
(1.0 - t) * buf[j][3] + t * buf[j + 1][3],
];
}
}
buf[0]
}
#[allow(clippy::too_many_arguments)]
fn sample_bspline_surface(
grid: &[[f32; 3]],
weights: &[f32],
kind: &str,
deg_u: u32,
deg_v: u32,
knots_u: &[f32],
knots_v: &[f32],
s0: f32,
s1: f32,
t0: f32,
t1: f32,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let nu = deg_u as usize;
let nv = deg_v as usize;
if knots_u.len() != cols + nu + 1 || knots_v.len() != rows + nv + 1 {
return Vec::new();
}
let u_lo_bound = knots_u[nu];
let u_hi_bound = knots_u[cols]; let v_lo_bound = knots_v[nv];
let v_hi_bound = knots_v[rows]; let u_min = s0.max(u_lo_bound);
let u_max = s1.min(u_hi_bound);
let v_min = t0.max(v_lo_bound);
let v_max = t1.min(v_hi_bound);
if u_min > u_max || v_min > v_max {
return Vec::new();
}
let rational = kind == "rat_bspline";
let n = samples as usize + 1;
let nudge = |t: f32, lo: f32, hi: f32| -> f32 {
if t >= hi {
let biased = hi - (hi - lo).abs() * 1e-7 - f32::EPSILON;
if biased < lo { lo } else { biased }
} else {
t
}
};
let u_basis_rows: Vec<Vec<f32>> = (0..n)
.map(|i| {
let t01 = if n == 1 {
0.0
} else {
i as f32 / (n - 1) as f32
};
let u = nudge(u_min + t01 * (u_max - u_min), u_lo_bound, u_hi_bound);
bspline_basis(u, knots_u, nu)
})
.collect();
let v_basis_rows: Vec<Vec<f32>> = (0..n)
.map(|j| {
let t01 = if n == 1 {
0.0
} else {
j as f32 / (n - 1) as f32
};
let v = nudge(v_min + t01 * (v_max - v_min), v_lo_bound, v_hi_bound);
bspline_basis(v, knots_v, nv)
})
.collect();
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for vb in v_basis_rows.iter() {
for ub in u_basis_rows.iter() {
let mut acc = [0.0f32; 3];
let mut wsum = 0.0f32;
for (j, &bv) in vb.iter().enumerate().take(rows) {
if bv == 0.0 {
continue;
}
for (i, &bu) in ub.iter().enumerate().take(cols) {
if bu == 0.0 {
continue;
}
let idx = j * cols + i;
let w = if rational { weights[idx] } else { 1.0 };
let coeff = bu * bv * w;
if coeff == 0.0 {
continue;
}
wsum += coeff;
acc[0] += coeff * grid[idx][0];
acc[1] += coeff * grid[idx][1];
acc[2] += coeff * grid[idx][2];
}
}
if wsum.abs() > f32::EPSILON {
out.push([acc[0] / wsum, acc[1] / wsum, acc[2] / wsum]);
} else {
out.push(acc);
}
}
}
out
}
fn sample_bezier(
control_points: &[[f32; 3]],
control_weights: &[f32],
kind: &str,
_u_min: f32,
_u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let rational = kind == "rat_bezier";
let homogeneous: Vec<[f32; 4]> = control_points
.iter()
.zip(control_weights.iter())
.map(|(p, w)| {
let weight = if rational { *w } else { 1.0 };
[p[0] * weight, p[1] * weight, p[2] * weight, weight]
})
.collect();
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let t01 = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let t = t01;
let mut buf: Vec<[f32; 4]> = homogeneous.clone();
let n = buf.len();
for level in 1..n {
for j in 0..(n - level) {
buf[j] = [
(1.0 - t) * buf[j][0] + t * buf[j + 1][0],
(1.0 - t) * buf[j][1] + t * buf[j + 1][1],
(1.0 - t) * buf[j][2] + t * buf[j + 1][2],
(1.0 - t) * buf[j][3] + t * buf[j + 1][3],
];
}
}
let [x, y, z, w] = buf[0];
if rational && w.abs() > f32::EPSILON {
out.push([x / w, y / w, z / w]);
} else {
out.push([x, y, z]);
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_bspline(
control_points: &[[f32; 3]],
control_weights: &[f32],
kind: &str,
degree: u32,
knots: &[f32],
u_min: f32,
u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let n = degree as usize;
let k_plus_1 = control_points.len(); if knots.len() != k_plus_1 + n + 1 {
return Vec::new();
}
let t_lo_bound = knots[n];
let t_hi_bound = knots[k_plus_1]; let t_min = u_min.max(t_lo_bound);
let t_max = u_max.min(t_hi_bound);
if t_min > t_max {
return Vec::new();
}
let rational = kind == "rat_bspline";
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let t01 = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let mut t = t_min + t01 * (t_max - t_min);
if t >= t_hi_bound {
t = t_hi_bound - (t_hi_bound - t_lo_bound).abs() * 1e-7 - f32::EPSILON;
if t < t_lo_bound {
t = t_lo_bound;
}
}
let basis = bspline_basis(t, knots, n);
let mut acc = [0.0f32; 3];
let mut wsum = 0.0f32;
for j in 0..k_plus_1 {
let bj = basis[j];
if bj == 0.0 {
continue;
}
let w = if rational { control_weights[j] } else { 1.0 };
let bw = bj * w;
wsum += bw;
acc[0] += bw * control_points[j][0];
acc[1] += bw * control_points[j][1];
acc[2] += bw * control_points[j][2];
}
if rational && wsum.abs() > f32::EPSILON {
out.push([acc[0] / wsum, acc[1] / wsum, acc[2] / wsum]);
} else if !rational && wsum.abs() > f32::EPSILON {
out.push(acc);
} else {
out.push(acc);
}
}
out
}
fn bspline_basis(t: f32, knots: &[f32], degree: usize) -> Vec<f32> {
let m = knots.len();
if m <= degree + 1 {
return Vec::new();
}
let k_plus_1 = m - degree - 1;
let mut basis: Vec<f32> = Vec::with_capacity(m - 1);
for i in 0..(m - 1) {
let inside = if i + 1 == m - 1 {
knots[i] <= t && t <= knots[i + 1]
} else {
knots[i] <= t && t < knots[i + 1]
};
basis.push(if inside { 1.0 } else { 0.0 });
}
for k in 1..=degree {
let new_len = m - 1 - k;
for j in 0..new_len {
let denom_left = knots[j + k] - knots[j];
let denom_right = knots[j + k + 1] - knots[j + 1];
let left = if denom_left.abs() < f32::EPSILON {
0.0
} else {
(t - knots[j]) / denom_left * basis[j]
};
let right = if denom_right.abs() < f32::EPSILON {
0.0
} else {
(knots[j + k + 1] - t) / denom_right * basis[j + 1]
};
basis[j] = left + right;
}
basis.truncate(new_len);
}
debug_assert_eq!(basis.len(), k_plus_1);
basis
}
fn sample_cardinal(control_points: &[[f32; 3]], samples: u32) -> Vec<[f32; 3]> {
if control_points.len() < 4 || samples == 0 {
return Vec::new();
}
let n_segments = control_points.len() - 3;
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let s = if i == n_samples - 1 {
n_segments as f32
} else {
i as f32 * n_segments as f32 / (n_samples - 1) as f32
};
let mut seg = s.floor() as usize;
let mut t = s - seg as f32;
if seg >= n_segments {
seg = n_segments - 1;
t = 1.0;
}
let c0 = control_points[seg];
let c1 = control_points[seg + 1];
let c2 = control_points[seg + 2];
let c3 = control_points[seg + 3];
let mut b: [[f32; 3]; 4] = [[0.0; 3]; 4];
for a in 0..3 {
b[0][a] = c1[a];
b[1][a] = c1[a] + (c2[a] - c0[a]) / 6.0;
b[2][a] = c2[a] - (c3[a] - c1[a]) / 6.0;
b[3][a] = c2[a];
}
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
let p = [
w0 * b[0][0] + w1 * b[1][0] + w2 * b[2][0] + w3 * b[3][0],
w0 * b[0][1] + w1 * b[1][1] + w2 * b[2][1] + w3 * b[3][1],
w0 * b[0][2] + w1 * b[1][2] + w2 * b[2][2] + w3 * b[3][2],
];
out.push(p);
}
out
}
fn cardinal_eval_1d(points: &[[f32; 3]], s: f32) -> [f32; 3] {
let n_segments = points.len() - 3;
let mut seg = s.floor() as isize;
let mut t = s - seg as f32;
if seg < 0 {
seg = 0;
t = 0.0;
} else if seg as usize >= n_segments {
seg = n_segments as isize - 1;
t = 1.0;
}
let seg = seg as usize;
let c0 = points[seg];
let c1 = points[seg + 1];
let c2 = points[seg + 2];
let c3 = points[seg + 3];
let mut b: [[f32; 3]; 4] = [[0.0; 3]; 4];
for a in 0..3 {
b[0][a] = c1[a];
b[1][a] = c1[a] + (c2[a] - c0[a]) / 6.0;
b[2][a] = c2[a] - (c3[a] - c1[a]) / 6.0;
b[3][a] = c2[a];
}
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
[
w0 * b[0][0] + w1 * b[1][0] + w2 * b[2][0] + w3 * b[3][0],
w0 * b[0][1] + w1 * b[1][1] + w2 * b[2][1] + w3 * b[3][1],
w0 * b[0][2] + w1 * b[1][2] + w2 * b[2][2] + w3 * b[3][2],
]
}
fn sample_cardinal_surface(
grid: &[[f32; 3]],
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols < 4 || rows < 4 || grid.len() != cols * rows {
return Vec::new();
}
let n = samples as usize + 1;
let u_span = (cols - 3) as f32; let v_span = (rows - 3) as f32;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
sv as f32 / (n - 1) as f32 * v_span
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
su as f32 / (n - 1) as f32 * u_span
};
let mut col_pts: Vec<[f32; 3]> = Vec::with_capacity(rows);
for r in 0..rows {
let row = &grid[r * cols..r * cols + cols];
col_pts.push(cardinal_eval_1d(row, u));
}
out.push(cardinal_eval_1d(&col_pts, v));
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_taylor_surface(
grid: &[[f32; 3]],
cols: usize,
rows: usize,
s0: f32,
s1: f32,
t0: f32,
t1: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
let mut col_pts: Vec<[f32; 3]> = Vec::with_capacity(rows);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
t0 + (sv as f32 / (n - 1) as f32) * (t1 - t0)
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
s0 + (su as f32 / (n - 1) as f32) * (s1 - s0)
};
col_pts.clear();
for r in 0..rows {
let row_start = r * cols;
let mut acc = grid[row_start + cols - 1];
for i in (0..cols - 1).rev() {
let cij = grid[row_start + i];
acc[0] = acc[0] * u + cij[0];
acc[1] = acc[1] * u + cij[1];
acc[2] = acc[2] * u + cij[2];
}
col_pts.push(acc);
}
let mut acc = col_pts[rows - 1];
for j in (0..rows - 1).rev() {
let cj = col_pts[j];
acc[0] = acc[0] * v + cj[0];
acc[1] = acc[1] * v + cj[1];
acc[2] = acc[2] * v + cj[2];
}
out.push(acc);
}
}
out
}
fn isqrt_exact(n: usize) -> Option<usize> {
if n == 0 {
return None;
}
let mut r = (n as f64).sqrt() as usize;
while r * r > n {
r -= 1;
}
while (r + 1) * (r + 1) <= n {
r += 1;
}
if r * r == n { Some(r) } else { None }
}
fn sample_taylor(
control_points: &[[f32; 3]],
u_min: f32,
u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
let k = control_points.len();
for i in 0..n_samples {
let frac = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let t = u_min + frac * (u_max - u_min);
let mut acc = control_points[k - 1];
for j in (0..(k - 1)).rev() {
acc[0] = acc[0] * t + control_points[j][0];
acc[1] = acc[1] * t + control_points[j][1];
acc[2] = acc[2] * t + control_points[j][2];
}
out.push(acc);
}
out
}
fn sample_bmatrix(
control_points: &[[f32; 3]],
bmat_u: &[f32],
degree: u32,
step: u32,
samples: u32,
) -> Vec<[f32; 3]> {
let Some(n_plus_1) = (degree as usize).checked_add(1) else {
return Vec::new();
};
let Some(expected_bmat) = n_plus_1.checked_mul(n_plus_1) else {
return Vec::new();
};
if control_points.len() < n_plus_1 || bmat_u.len() != expected_bmat || step == 0 || samples == 0
{
return Vec::new();
}
let s = step as usize;
let n_segments = (control_points.len() - n_plus_1) / s + 1;
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let g = if i == n_samples - 1 {
n_segments as f32
} else {
i as f32 * n_segments as f32 / (n_samples - 1) as f32
};
let mut seg = g.floor() as usize;
let mut t = g - seg as f32;
if seg >= n_segments {
seg = n_segments - 1;
t = 1.0;
}
let base = seg * s;
let mut t_pow: Vec<f32> = Vec::with_capacity(n_plus_1);
let mut p = 1.0_f32;
for _ in 0..n_plus_1 {
t_pow.push(p);
p *= t;
}
let mut accum = [0.0_f32; 3];
for ii in 0..n_plus_1 {
let mut coef = 0.0_f32;
for jj in 0..n_plus_1 {
coef += bmat_u[ii * n_plus_1 + jj] * t_pow[jj];
}
let cp = control_points[base + ii];
accum[0] += coef * cp[0];
accum[1] += coef * cp[1];
accum[2] += coef * cp[2];
}
out.push(accum);
}
out
}
fn is_tessellated_curve(prim: &Primitive) -> bool {
prim.extras
.get("obj:tessellated_curve")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn single_line_topology(elements: &[Element]) -> Topology {
if elements.len() != 1 {
return Topology::Lines;
}
let Element::Line(verts) = &elements[0] else {
return Topology::Lines;
};
if verts.len() < 2 {
return Topology::Lines;
}
if verts.len() == 2 {
return Topology::Lines;
}
let same_start_end = verts.first().map(|fv| fv.v) == verts.last().map(|fv| fv.v);
if same_start_end {
Topology::LineLoop
} else {
Topology::LineStrip
}
}
fn build_primitive(
prim_acc: &PrimAccum,
positions: &[[f32; 3]],
position_weights: &[Option<f32>],
position_colors: &[Option<[f32; 4]>],
texcoords: &[[f32; 2]],
normals: &[[f32; 3]],
material_ids: &HashMap<String, oxideav_mesh3d::MaterialId>,
) -> Result<(Primitive, Vec<u32>)> {
let first = prim_acc.elements.first();
let topology = match first {
Some(Element::Face(_)) => Topology::Triangles,
Some(Element::Line(_)) => single_line_topology(&prim_acc.elements),
Some(Element::Point(_)) => Topology::Points,
None => Topology::Triangles,
};
for elt in &prim_acc.elements {
let ok = matches!(
(&topology, elt),
(Topology::Triangles, Element::Face(_))
| (Topology::Lines, Element::Line(_))
| (Topology::LineStrip, Element::Line(_))
| (Topology::LineLoop, Element::Line(_))
| (Topology::Points, Element::Point(_))
);
if !ok {
return Err(Error::unsupported(
"OBJ primitive mixes face / line / point elements under one usemtl",
));
}
}
let has_uv = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| fv.vt != 0)
}
});
let has_normal = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| fv.vn != 0)
}
});
let has_color = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| {
position_colors
.get((fv.v - 1) as usize)
.is_some_and(Option::is_some)
})
}
});
let mut prim = Primitive::new(topology);
if has_uv {
prim.uvs.push(Vec::new());
}
if has_normal {
prim.normals = Some(Vec::new());
}
if has_color {
prim.colors.push(Vec::new());
}
let mut color_present: Vec<bool> = Vec::new();
let mut weights_seen: Vec<Option<f32>> = Vec::new();
let mut indexer: HashMap<FaceVert, u32> = HashMap::new();
let mut arities: Vec<u32> = Vec::new();
let mut local_indices: Vec<u32> = Vec::new();
let intern = |fv: FaceVert,
prim: &mut Primitive,
indexer: &mut HashMap<FaceVert, u32>,
color_present: &mut Vec<bool>,
weights_seen: &mut Vec<Option<f32>>|
-> Result<u32> {
if let Some(&idx) = indexer.get(&fv) {
return Ok(idx);
}
let pos = positions
.get((fv.v - 1) as usize)
.ok_or_else(|| Error::invalid(format!("face references missing position {}", fv.v)))?;
prim.positions.push(*pos);
if has_uv {
let uv = if fv.vt == 0 {
[0.0, 0.0]
} else {
*texcoords.get((fv.vt - 1) as usize).ok_or_else(|| {
Error::invalid(format!("face references missing texcoord {}", fv.vt))
})?
};
prim.uvs[0].push(uv);
}
if has_normal {
let n = if fv.vn == 0 {
[0.0, 0.0, 0.0]
} else {
*normals.get((fv.vn - 1) as usize).ok_or_else(|| {
Error::invalid(format!("face references missing normal {}", fv.vn))
})?
};
prim.normals.as_mut().unwrap().push(n);
}
if has_color {
let rgba = position_colors
.get((fv.v - 1) as usize)
.copied()
.flatten()
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
prim.colors[0].push(rgba);
color_present.push(
position_colors
.get((fv.v - 1) as usize)
.is_some_and(Option::is_some),
);
}
weights_seen.push(position_weights.get((fv.v - 1) as usize).copied().flatten());
let new_idx = (prim.positions.len() - 1) as u32;
indexer.insert(fv, new_idx);
Ok(new_idx)
};
for elt in &prim_acc.elements {
match elt {
Element::Face(verts) => {
let arity = verts.len() as u32;
arities.push(arity);
let resolved: Vec<u32> = verts
.iter()
.map(|&fv| {
intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)
})
.collect::<Result<Vec<_>>>()?;
for i in 1..(resolved.len() - 1) {
local_indices.push(resolved[0]);
local_indices.push(resolved[i]);
local_indices.push(resolved[i + 1]);
}
}
Element::Line(verts) => {
let resolved: Vec<u32> = verts
.iter()
.map(|&fv| {
intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)
})
.collect::<Result<Vec<_>>>()?;
match topology {
Topology::LineStrip => {
local_indices.extend_from_slice(&resolved);
}
Topology::LineLoop => {
let n = resolved.len().saturating_sub(1);
local_indices.extend_from_slice(&resolved[..n]);
}
_ => {
for w in resolved.windows(2) {
local_indices.push(w[0]);
local_indices.push(w[1]);
}
}
}
}
Element::Point(verts) => {
for &fv in verts {
let idx = intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)?;
local_indices.push(idx);
}
}
}
}
if local_indices.iter().any(|&i| i >= u16::MAX as u32) {
prim.indices = Some(Indices::U32(local_indices));
} else {
prim.indices = Some(Indices::U16(
local_indices.into_iter().map(|i| i as u16).collect(),
));
}
if has_color && color_present.iter().any(|&b| b) {
prim.extras.insert(
"obj:vertex_color_present".to_string(),
serde_json::to_value(&color_present).unwrap(),
);
}
if weights_seen.iter().any(Option::is_some) {
prim.extras.insert(
"obj:vertex_weight".to_string(),
serde_json::to_value(&weights_seen).unwrap(),
);
}
if let Some(name) = &prim_acc.material {
if let Some(id) = material_ids.get(name) {
prim.material = Some(*id);
}
prim.extras.insert(
"obj:usemtl".to_string(),
serde_json::Value::String(name.clone()),
);
}
if let Some(s) = &prim_acc.smoothing_group {
prim.extras.insert(
"obj:smoothing_group".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.merging_group {
prim.extras.insert(
"obj:merging_group".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.bevel {
prim.extras.insert(
"obj:bevel".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.c_interp {
prim.extras.insert(
"obj:c_interp".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.d_interp {
prim.extras.insert(
"obj:d_interp".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.lod {
prim.extras
.insert("obj:lod".to_string(), serde_json::Value::String(s.clone()));
}
if !prim_acc.groups.is_empty() {
prim.extras.insert(
"obj:groups".to_string(),
serde_json::to_value(&prim_acc.groups).unwrap(),
);
}
Ok((prim, arities))
}
#[derive(Clone, Debug, Default)]
pub struct ParseOptions {
pub curve_tessellation_samples: u32,
}
pub fn parse_obj(text: &str) -> Result<Scene3D> {
parse_obj_with_resolver(text, |_path| Ok(Vec::new()))
}
pub fn parse_obj_from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Scene3D> {
let path = path.as_ref();
let bytes =
std::fs::read(path).map_err(|e| Error::invalid(format!("OBJ read {path:?}: {e}")))?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| Error::invalid(format!("OBJ {path:?} contained non-UTF-8 bytes")))?;
let parent = path.parent().map(std::path::Path::to_path_buf);
parse_obj_with_resolver(text, |libname| {
let lib_path = match &parent {
Some(dir) => dir.join(libname),
None => std::path::PathBuf::from(libname),
};
std::fs::read(&lib_path)
.map_err(|e| Error::invalid(format!("mtllib read {lib_path:?}: {e}")))
})
}
pub fn parse_obj_with_resolver<R>(text: &str, resolve: R) -> Result<Scene3D>
where
R: FnMut(&str) -> Result<Vec<u8>>,
{
parse_obj_with_options(text, &ParseOptions::default(), resolve)
}
pub fn parse_obj_with_options<R>(
text: &str,
options: &ParseOptions,
mut resolve: R,
) -> Result<Scene3D>
where
R: FnMut(&str) -> Result<Vec<u8>>,
{
let mut doc = parse_obj_doc(text)?;
for lib in doc.mtllibs.clone() {
let bytes = resolve(&lib)?;
if bytes.is_empty() {
continue;
}
let lib_text = std::str::from_utf8(&bytes)
.map_err(|_| Error::invalid(format!("mtllib {lib:?} contained non-UTF-8 bytes")))?;
let materials = parse_mtl(lib_text)?;
for mat in materials {
if let Some(name) = mat.name.clone() {
doc.resolved_materials.insert(name, mat);
}
}
}
let tessellated = if options.curve_tessellation_samples > 0 {
tessellate_curves(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_curve2 = if options.curve_tessellation_samples > 0 {
tessellate_curve2(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_surfaces = if options.curve_tessellation_samples > 0 {
tessellate_surfaces(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let mut scene = build_scene(doc)?;
if !tessellated.is_empty() {
let mut mesh = Mesh::new(Some("obj:curves".to_string()));
for prim in tessellated {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_curve2.is_empty() {
let mut mesh = Mesh::new(Some("obj:curves2".to_string()));
for prim in tessellated_curve2 {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_surfaces.is_empty() {
let mut mesh = Mesh::new(Some("obj:surfaces".to_string()));
for prim in tessellated_surfaces {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
Ok(scene)
}
#[derive(Clone, Debug, Default)]
pub struct SerializeOptions<'a> {
pub mtl_basename: Option<&'a str>,
pub negative_indices: bool,
}
pub fn serialize_obj(scene: &Scene3D, mtl_basename: Option<&str>) -> Result<Vec<u8>> {
serialize_obj_with_options(
scene,
&SerializeOptions {
mtl_basename,
..SerializeOptions::default()
},
)
}
pub fn serialize_obj_with_options(
scene: &Scene3D,
options: &SerializeOptions<'_>,
) -> Result<Vec<u8>> {
let mtl_basename = options.mtl_basename;
let negative = options.negative_indices;
use std::fmt::Write;
let mut out = String::new();
writeln!(out, "# OBJ generated by oxideav-obj").unwrap();
if let Some(base) = mtl_basename {
writeln!(out, "mtllib {base}.mtl").unwrap();
}
if mtl_basename.is_none() {
if let Some(serde_json::Value::Array(list)) = scene.extras.get("obj:mtllibs") {
for entry in list {
if let Some(s) = entry.as_str() {
writeln!(out, "mtllib {s}").unwrap();
}
}
}
}
let mut positions: Vec<[f32; 3]> = Vec::new();
let mut position_colors: Vec<Option<[f32; 4]>> = Vec::new();
let mut position_weights: Vec<Option<f32>> = Vec::new();
let mut texcoords: Vec<[f32; 2]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut pos_map: HashMap<KeyVec3, u32> = HashMap::new();
let mut tex_map: HashMap<KeyVec2, u32> = HashMap::new();
let mut nor_map: HashMap<KeyVec3, u32> = HashMap::new();
let intern_pos = |p: [f32; 3],
colour: Option<[f32; 4]>,
weight: Option<f32>,
positions: &mut Vec<[f32; 3]>,
colours: &mut Vec<Option<[f32; 4]>>,
weights: &mut Vec<Option<f32>>,
map: &mut HashMap<KeyVec3, u32>|
-> u32 {
let key = KeyVec3::from(p);
if let Some(&i) = map.get(&key) {
let slot = (i - 1) as usize;
if colours[slot].is_none() {
colours[slot] = colour;
}
if weights[slot].is_none() {
weights[slot] = weight;
}
return i;
}
positions.push(p);
colours.push(colour);
weights.push(weight);
let idx = positions.len() as u32;
map.insert(key, idx);
idx
};
let intern_tex =
|p: [f32; 2], texcoords: &mut Vec<[f32; 2]>, map: &mut HashMap<KeyVec2, u32>| -> u32 {
let key = KeyVec2::from(p);
if let Some(&i) = map.get(&key) {
return i;
}
texcoords.push(p);
let idx = texcoords.len() as u32;
map.insert(key, idx);
idx
};
let intern_nor =
|p: [f32; 3], normals: &mut Vec<[f32; 3]>, map: &mut HashMap<KeyVec3, u32>| -> u32 {
let key = KeyVec3::from(p);
if let Some(&i) = map.get(&key) {
return i;
}
normals.push(p);
let idx = normals.len() as u32;
map.insert(key, idx);
idx
};
if let Some(serde_json::Value::Array(src_positions)) = scene.extras.get("obj:positions") {
let src_weights: Vec<Option<f32>> = scene
.extras
.get("obj:position_weights")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect())
.unwrap_or_default();
let src_colors: Vec<Option<[f32; 4]>> = scene
.extras
.get("obj:position_colors")
.and_then(serde_json::Value::as_array)
.map(|arr| {
arr.iter()
.map(|v| {
v.as_array().map(|c| {
let mut rgba = [1.0; 4];
for (i, x) in c.iter().enumerate().take(4) {
rgba[i] = x.as_f64().map(|f| f as f32).unwrap_or(0.0);
}
rgba
})
})
.collect()
})
.unwrap_or_default();
for (i, pv) in src_positions.iter().enumerate() {
let serde_json::Value::Array(coords) = pv else {
continue;
};
let mut p = [0.0_f32; 3];
for (j, c) in coords.iter().enumerate().take(3) {
p[j] = c.as_f64().map(|f| f as f32).unwrap_or(0.0);
}
let weight = src_weights.get(i).copied().flatten();
let colour = src_colors.get(i).copied().flatten();
intern_pos(
p,
colour,
weight,
&mut positions,
&mut position_colors,
&mut position_weights,
&mut pos_map,
);
}
}
type GlobalTriple = (u32, u32, u32); let mut global_indices: Vec<Vec<Vec<GlobalTriple>>> = Vec::new();
for mesh in &scene.meshes {
let mut mesh_globals: Vec<Vec<GlobalTriple>> = Vec::new();
for prim in &mesh.primitives {
if is_tessellated_curve(prim) {
mesh_globals.push(Vec::new());
continue;
}
let has_uv = !prim.uvs.is_empty();
let has_normal = prim.normals.is_some();
let has_color = !prim.colors.is_empty();
let color_present: Vec<bool> = prim
.extras
.get("obj:vertex_color_present")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_bool().unwrap_or(false)).collect())
.unwrap_or_else(|| vec![has_color; prim.positions.len()]);
let weight_overrides: Vec<Option<f32>> = prim
.extras
.get("obj:vertex_weight")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect())
.unwrap_or_default();
let mut prim_globals: Vec<GlobalTriple> = Vec::with_capacity(prim.positions.len());
for vi in 0..prim.positions.len() {
let colour = if has_color && color_present.get(vi).copied().unwrap_or(false) {
Some(prim.colors[0][vi])
} else {
None
};
let weight = weight_overrides.get(vi).copied().flatten();
let v_idx = intern_pos(
prim.positions[vi],
colour,
weight,
&mut positions,
&mut position_colors,
&mut position_weights,
&mut pos_map,
);
let vt_idx = if has_uv {
intern_tex(prim.uvs[0][vi], &mut texcoords, &mut tex_map)
} else {
0
};
let vn_idx = if has_normal {
intern_nor(
prim.normals.as_ref().unwrap()[vi],
&mut normals,
&mut nor_map,
)
} else {
0
};
prim_globals.push((v_idx, vt_idx, vn_idx));
}
mesh_globals.push(prim_globals);
}
global_indices.push(mesh_globals);
}
for (i, p) in positions.iter().enumerate() {
let weight = position_weights[i];
let colour = position_colors[i];
let mut s = String::with_capacity(40);
s.push_str("v ");
s.push_str(&fmt_float(p[0]));
s.push(' ');
s.push_str(&fmt_float(p[1]));
s.push(' ');
s.push_str(&fmt_float(p[2]));
if let Some(w) = weight {
s.push(' ');
s.push_str(&fmt_float(w));
}
if let Some(rgb) = colour {
s.push(' ');
s.push_str(&fmt_float(rgb[0]));
s.push(' ');
s.push_str(&fmt_float(rgb[1]));
s.push(' ');
s.push_str(&fmt_float(rgb[2]));
}
writeln!(out, "{s}").unwrap();
}
if let Some(serde_json::Value::Array(vps)) = scene.extras.get("obj:vp") {
for entry in vps {
if let serde_json::Value::Array(coords) = entry {
let parts: Vec<f32> = coords
.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect();
if parts.is_empty() {
continue;
}
let trim = if parts.len() >= 3 && parts[2] != 0.0 {
3
} else if parts.len() >= 2 && parts[1] != 0.0 {
2
} else {
1
};
let mut s = String::from("vp");
for coord in parts.iter().take(trim) {
s.push(' ');
s.push_str(&fmt_float(*coord));
}
writeln!(out, "{s}").unwrap();
}
}
}
for t in &texcoords {
writeln!(out, "vt {} {}", fmt_float(t[0]), fmt_float(t[1])).unwrap();
}
for n in &normals {
writeln!(
out,
"vn {} {} {}",
fmt_float(n[0]),
fmt_float(n[1]),
fmt_float(n[2])
)
.unwrap();
}
for (mi, mesh) in scene.meshes.iter().enumerate() {
if mesh.primitives.iter().all(is_tessellated_curve) && !mesh.primitives.is_empty() {
continue;
}
if let Some(name) = &mesh.name {
writeln!(out, "o {name}").unwrap();
}
for (pi, prim) in mesh.primitives.iter().enumerate() {
if is_tessellated_curve(prim) {
continue;
}
let arities: Option<Vec<u32>> = prim
.extras
.get("obj:original_face_arities")
.and_then(|v| serde_json::from_value(v.clone()).ok());
if let Some(serde_json::Value::Array(gs)) = prim.extras.get("obj:groups") {
let names: Vec<&str> = gs.iter().filter_map(|v| v.as_str()).collect();
if !names.is_empty() {
writeln!(out, "g {}", names.join(" ")).unwrap();
}
}
if let Some(s) = prim
.extras
.get("obj:smoothing_group")
.and_then(|v| v.as_str())
{
writeln!(out, "s {s}").unwrap();
}
if let Some(s) = prim
.extras
.get("obj:merging_group")
.and_then(|v| v.as_str())
{
writeln!(out, "mg {s}").unwrap();
}
for keyword in ["bevel", "c_interp", "d_interp", "lod"] {
let key = format!("obj:{keyword}");
if let Some(s) = prim.extras.get(&key).and_then(|v| v.as_str()) {
writeln!(out, "{keyword} {s}").unwrap();
}
}
let mtl_name: Option<String> = prim
.extras
.get("obj:usemtl")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
prim.material.and_then(|id| {
scene
.materials
.get(id.0 as usize)
.and_then(|m| m.name.clone())
})
});
if let Some(name) = &mtl_name {
writeln!(out, "usemtl {name}").unwrap();
}
let prim_globals = &global_indices[mi][pi];
let has_uv = !prim.uvs.is_empty();
let has_normal = prim.normals.is_some();
match prim.topology {
Topology::Triangles => {
let face_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => {
(0..prim.positions.len() as u32).collect()
}
};
if let Some(per_prim_arities) = arities.as_ref() {
let mut tri_pos: usize = 0;
for &arity in per_prim_arities {
let mut verts: Vec<u32> = Vec::with_capacity(arity as usize);
let n_tris = (arity as usize).saturating_sub(2);
verts.push(face_indices[tri_pos * 3]);
verts.push(face_indices[tri_pos * 3 + 1]);
verts.push(face_indices[tri_pos * 3 + 2]);
for k in 1..n_tris {
verts.push(face_indices[(tri_pos + k) * 3 + 2]);
}
tri_pos += n_tris;
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
let consumed = per_prim_arities
.iter()
.map(|&a| (a as usize).saturating_sub(2))
.sum::<usize>();
for tri in consumed..(face_indices.len() / 3) {
let verts = [
face_indices[tri * 3],
face_indices[tri * 3 + 1],
face_indices[tri * 3 + 2],
];
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
} else {
for tri in 0..(face_indices.len() / 3) {
let verts = [
face_indices[tri * 3],
face_indices[tri * 3 + 1],
face_indices[tri * 3 + 2],
];
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
}
}
Topology::Lines => {
let line_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
let total_v = positions.len() as u32;
let mut chain: Vec<u32> = Vec::new();
let flush = |chain: &mut Vec<u32>, out: &mut String| {
if chain.len() < 2 {
chain.clear();
return;
}
let parts: Vec<String> = chain
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "l {}", parts.join(" ")).unwrap();
chain.clear();
};
for w in line_indices.chunks_exact(2) {
let (a, b) = (w[0], w[1]);
if chain.is_empty() {
chain.push(a);
chain.push(b);
} else if *chain.last().unwrap() == a {
chain.push(b);
} else {
flush(&mut chain, &mut out);
chain.push(a);
chain.push(b);
}
}
flush(&mut chain, &mut out);
}
Topology::LineStrip | Topology::LineLoop => {
let mut strip_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
if matches!(prim.topology, Topology::LineLoop)
&& let Some(&first) = strip_indices.first()
{
strip_indices.push(first);
}
if strip_indices.len() >= 2 {
let total_v = positions.len() as u32;
let parts: Vec<String> = strip_indices
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "l {}", parts.join(" ")).unwrap();
}
}
Topology::Points => {
let pt_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
let total_v = positions.len() as u32;
if !pt_indices.is_empty() {
let parts: Vec<String> = pt_indices
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "p {}", parts.join(" ")).unwrap();
}
}
other => {
return Err(Error::unsupported(format!(
"OBJ encoder: topology {other:?} not representable"
)));
}
}
}
}
if let Some(serde_json::Value::Array(directives)) = scene.extras.get("obj:freeform_directives")
{
for entry in directives {
if let serde_json::Value::Array(toks) = entry {
let parts: Vec<&str> = toks.iter().filter_map(|v| v.as_str()).collect();
if parts.is_empty() {
continue;
}
writeln!(out, "{}", parts.join(" ")).unwrap();
}
}
}
Ok(out.into_bytes())
}
#[allow(clippy::too_many_arguments)]
fn write_face(
out: &mut String,
verts: &[u32],
prim_globals: &[(u32, u32, u32)],
has_uv: bool,
has_normal: bool,
negative: bool,
total_v: u32,
total_vt: u32,
total_vn: u32,
) {
use std::fmt::Write;
out.push('f');
for &local in verts {
let (v, vt, vn) = prim_globals[local as usize];
let v_s = fmt_index(v, total_v, negative);
let vt_s = fmt_index(vt, total_vt, negative);
let vn_s = fmt_index(vn, total_vn, negative);
match (has_uv, has_normal) {
(true, true) => write!(out, " {v_s}/{vt_s}/{vn_s}").unwrap(),
(true, false) => write!(out, " {v_s}/{vt_s}").unwrap(),
(false, true) => write!(out, " {v_s}//{vn_s}").unwrap(),
(false, false) => write!(out, " {v_s}").unwrap(),
}
}
out.push('\n');
}
fn fmt_index(idx: u32, total: u32, negative: bool) -> String {
if idx == 0 || !negative {
idx.to_string()
} else {
let raw = (idx as i64) - (total as i64) - 1;
raw.to_string()
}
}
fn fmt_float(x: f32) -> String {
if x == 0.0 {
return "0".to_string();
}
let s = format!("{x:.6}");
let trimmed = s.trim_end_matches('0').trim_end_matches('.').to_string();
if trimmed.is_empty() || trimmed == "-" {
"0".to_string()
} else {
trimmed
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct KeyVec2 {
a: u32,
b: u32,
}
impl From<[f32; 2]> for KeyVec2 {
fn from(v: [f32; 2]) -> Self {
Self {
a: v[0].to_bits(),
b: v[1].to_bits(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct KeyVec3 {
a: u32,
b: u32,
c: u32,
}
impl From<[f32; 3]> for KeyVec3 {
fn from(v: [f32; 3]) -> Self {
Self {
a: v[0].to_bits(),
b: v[1].to_bits(),
c: v[2].to_bits(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preprocess_strips_comments_and_glues_continuations() {
let lines =
preprocess_lines("v 1.0 2.0 \\\n3.0 # comment\nv 4 5 6\n# pure comment\nf 1 2 3");
assert_eq!(lines[0].trim(), "v 1.0 2.0 3.0");
assert_eq!(lines[1].trim(), "v 4 5 6");
assert_eq!(lines[2].trim(), "");
assert_eq!(lines[3].trim(), "f 1 2 3");
}
#[test]
fn fmt_float_is_diff_friendly() {
assert_eq!(fmt_float(1.0), "1");
assert_eq!(fmt_float(0.0), "0");
assert_eq!(fmt_float(-0.5), "-0.5");
assert_eq!(fmt_float(1.0 / 3.0), "0.333333");
}
}