use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Mesh {
pub vertices: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub indices: Vec<u32>,
pub tex_coords: Vec<[f32; 2]>, pub tangents: Vec<[f32; 4]>,
pub joint_indices: Vec<[u32; 4]>,
pub joint_weights: Vec<[f32; 4]>,
}
impl Mesh {
pub fn from_obj(data: &[u8]) -> anyhow::Result<Vec<Self>> {
let mut cursor = std::io::Cursor::new(data);
let (models, _) = tobj::load_obj_buf(&mut cursor, &tobj::LoadOptions::default(), |_| {
Ok((Vec::new(), Default::default()))
})?;
let mut meshes = Vec::new();
for m in models {
let mesh = m.mesh;
let vertices: Vec<[f32; 3]> = mesh
.positions
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
let normals = if mesh.normals.is_empty() {
vec![[0.0, 0.0, 1.0]; vertices.len()]
} else {
mesh.normals.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect()
};
let tex_coords = if mesh.texcoords.is_empty() {
vec![[0.0, 0.0]; vertices.len()]
} else {
mesh.texcoords.chunks_exact(2).map(|c| [c[0], c[1]]).collect()
};
let mut new_mesh = Mesh {
vertices,
normals,
indices: mesh.indices,
tex_coords,
tangents: Vec::new(),
joint_indices: Vec::new(),
joint_weights: Vec::new(),
};
let vertex_count = new_mesh.vertices.len();
for &idx in &new_mesh.indices {
if (idx as usize) >= vertex_count {
anyhow::bail!(
"Index {} out of bounds for {} vertices in OBJ mesh",
idx,
vertex_count
);
}
}
new_mesh.tangents = new_mesh.compute_tangents();
meshes.push(new_mesh);
}
for m in &meshes {
debug_assert_eq!(
m.vertices.len(),
m.normals.len(),
"Mesh vertex/normal count mismatch after normal generation"
);
debug_assert_eq!(
m.vertices.len(),
m.tex_coords.len(),
"Mesh vertex/tex_coord count mismatch"
);
}
Ok(meshes)
}
pub fn from_stl(data: &[u8]) -> anyhow::Result<Self> {
let stl =
cvkg_stl::parse_bytes(data).map_err(|e| anyhow::anyhow!("STL parse failed: {e}"))?;
let vertex_count = stl.vertices.len();
let mut m = Self {
vertices: stl.vertices,
normals: stl.normals,
indices: stl.indices,
tex_coords: vec![[0.0, 0.0]; vertex_count], tangents: Vec::new(),
joint_indices: Vec::new(),
joint_weights: Vec::new(),
};
m.tangents = vec![[0.0, 0.0, 1.0, 1.0]; vertex_count];
Ok(m)
}
pub fn aabb(&self) -> (glam::Vec3, glam::Vec3) {
if self.vertices.is_empty() {
return (glam::Vec3::ZERO, glam::Vec3::ZERO);
}
let mut min = glam::Vec3::new(f32::MAX, f32::MAX, f32::MAX);
let mut max = glam::Vec3::new(f32::MIN, f32::MIN, f32::MIN);
for v in &self.vertices {
let p = glam::Vec3::new(v[0], v[1], v[2]);
min = min.min(p);
max = max.max(p);
}
let center = (min + max) * 0.5;
let half_extents = (max - min) * 0.5;
(center, half_extents)
}
pub fn compute_tangents(&self) -> Vec<[f32; 4]> {
let vertex_count = self.vertices.len();
if vertex_count == 0 {
return Vec::new();
}
let mut tan1 = vec![glam::Vec3::ZERO; vertex_count];
let mut tan2 = vec![glam::Vec3::ZERO; vertex_count];
for chunk in self.indices.chunks_exact(3) {
let i1 = chunk[0] as usize;
let i2 = chunk[1] as usize;
let i3 = chunk[2] as usize;
let v1 = glam::Vec3::from(self.vertices[i1]);
let v2 = glam::Vec3::from(self.vertices[i2]);
let v3 = glam::Vec3::from(self.vertices[i3]);
let w1 = glam::Vec2::from(self.tex_coords[i1]);
let w2 = glam::Vec2::from(self.tex_coords[i2]);
let w3 = glam::Vec2::from(self.tex_coords[i3]);
let x1 = v2.x - v1.x;
let x2 = v3.x - v1.x;
let y1 = v2.y - v1.y;
let y2 = v3.y - v1.y;
let z1 = v2.z - v1.z;
let z2 = v3.z - v1.z;
let s1 = w2.x - w1.x;
let s2 = w3.x - w1.x;
let t1 = w2.y - w1.y;
let t2 = w3.y - w1.y;
let r = 1.0 / (s1 * t2 - s2 * t1 + 1e-10);
let sdir = glam::Vec3::new(
(t2 * x1 - t1 * x2) * r,
(t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r,
);
let tdir = glam::Vec3::new(
(s1 * x2 - s2 * x1) * r,
(s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r,
);
tan1[i1] += sdir;
tan1[i2] += sdir;
tan1[i3] += sdir;
tan2[i1] += tdir;
tan2[i2] += tdir;
tan2[i3] += tdir;
}
let mut tangents = vec![[0.0, 0.0, 0.0, 1.0]; vertex_count];
for i in 0..vertex_count {
let n = glam::Vec3::from(self.normals[i]);
let t = tan1[i];
let t_ortho = (t - n * n.dot(t)).normalize_or_zero();
let w = if n.cross(t).dot(tan2[i]) < 0.0 {
-1.0
} else {
1.0
};
tangents[i] = [t_ortho.x, t_ortho.y, t_ortho.z, w];
}
tangents
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Transform3D {
pub position: glam::Vec3,
pub rotation: glam::Quat,
pub scale: glam::Vec3,
}
impl Default for Transform3D {
fn default() -> Self {
Self {
position: glam::Vec3::ZERO,
rotation: glam::Quat::IDENTITY,
scale: glam::Vec3::ONE,
}
}
}
impl Transform3D {
pub fn to_matrix(&self) -> glam::Mat4 {
glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
}
pub fn to_mat4(&self) -> glam::Mat4 {
self.to_matrix()
}
pub fn from_2d(x: f32, y: f32, rotation: f32) -> Self {
Self {
position: glam::Vec3::new(x, y, 0.0),
rotation: glam::Quat::from_rotation_z(rotation),
scale: glam::Vec3::ONE,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Camera3D {
pub position: glam::Vec3,
pub target: glam::Vec3,
pub up: glam::Vec3,
pub fov_y: f32,
pub near: f32,
pub far: f32,
pub perspective: bool,
pub aspect: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Material3D {
pub base_color: [f32; 4],
pub base_color_texture: Option<String>,
pub normal_map_texture: Option<String>,
pub metallic_roughness_texture: Option<String>,
pub metallic: f32,
pub roughness: f32,
pub emissive: [f32; 3],
pub opacity: f32,
pub uv_scale: [f32; 2],
pub uv_offset: [f32; 2],
}
impl Default for Material3D {
fn default() -> Self {
Self {
base_color: [1.0, 1.0, 1.0, 1.0],
base_color_texture: None,
normal_map_texture: None,
metallic_roughness_texture: None,
metallic: 0.0,
roughness: 0.5,
emissive: [0.0, 0.0, 0.0],
opacity: 1.0,
uv_scale: [1.0, 1.0],
uv_offset: [0.0, 0.0],
}
}
}
impl Material3D {
pub fn unlit(color: [f32; 4]) -> Self {
Self {
base_color: color,
base_color_texture: None,
normal_map_texture: None,
metallic_roughness_texture: None,
metallic: 0.0,
roughness: 1.0,
emissive: [0.0, 0.0, 0.0],
opacity: color[3],
uv_scale: [1.0, 1.0],
uv_offset: [0.0, 0.0],
}
}
pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
Self {
base_color: color,
base_color_texture: None,
normal_map_texture: None,
metallic_roughness_texture: None,
metallic: 1.0,
roughness: roughness.clamp(0.0, 1.0),
emissive: [0.0, 0.0, 0.0],
opacity: color[3],
uv_scale: [1.0, 1.0],
uv_offset: [0.0, 0.0],
}
}
}
impl Mesh {
pub fn convex_hull(&self) -> Vec<usize> {
if self.vertices.len() < 3 {
return Vec::new();
}
let mut min_idx = 0;
let mut max_idx = 0;
let mut min_val = f32::MAX;
let mut max_val = f32::MIN;
for (i, v) in self.vertices.iter().enumerate() {
let x = v[0];
if x < min_val {
min_val = x;
min_idx = i;
}
if x > max_val {
max_val = x;
max_idx = i;
}
}
let p1 = glam::Vec3::from(self.vertices[min_idx]);
let p2 = glam::Vec3::from(self.vertices[max_idx]);
let axis = (p2 - p1).normalize();
let mut max_dist = 0.0;
let mut p3_idx = 0;
for (i, v) in self.vertices.iter().enumerate() {
let pt = glam::Vec3::from(*v);
let to_pt = pt - p1;
let proj = to_pt.dot(axis);
let closest = p1 + axis * proj;
let dist = (pt - closest).length();
if dist > max_dist {
max_dist = dist;
p3_idx = i;
}
}
let hull_indices: Vec<usize> = vec![min_idx, max_idx, p3_idx];
hull_indices
}
}