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]>, }
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(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(2).map(|c| [c[0], c[1]]).collect()
};
meshes.push(Mesh {
vertices,
normals,
indices: mesh.indices,
tex_coords,
});
}
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();
Ok(Self {
vertices: stl.vertices,
normals: stl.normals,
indices: stl.indices,
tex_coords: vec![[0.0, 0.0]; vertex_count], })
}
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)
}
}
#[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],
}
}
}