#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
mod chunk;
mod convert;
mod geometry;
use chunk::{walk_chunks, walk_chunks_from, Chunk};
use geometry::*;
pub use convert::MeshBuffers;
pub type Mat4x3 = [[f32; 3]; 4];
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error3ds {
#[error("data too short (need >= 6 bytes, got {0})")]
Truncated(usize),
#[error("not a 3DS file — expected MAIN3DS (0x4D4D), got 0x{0:04X}")]
NotA3ds(u16),
#[error("chunk 0x{id:04X} at offset {offset} length {length} exceeds parent bounds")]
ChunkOverflow {
id: u16,
offset: usize,
length: u32,
},
#[error("offset {start} is past the end of chunk 0x{id:04X} (end {end})")]
BadOffset {
id: u16,
start: usize,
end: usize,
},
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Scene3ds {
pub meshes: Vec<Mesh3ds>,
pub materials: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mesh3ds {
pub name: String,
pub vertices: Vec<[f32; 3]>,
pub faces: Vec<[u16; 3]>,
pub uvs: Vec<[f32; 2]>,
pub smooth_groups: Vec<u32>,
pub transform: Mat4x3,
pub material: Option<String>,
}
impl Default for Mesh3ds {
fn default() -> Self {
Self {
name: String::new(),
vertices: Vec::new(),
faces: Vec::new(),
uvs: Vec::new(),
smooth_groups: Vec::new(),
transform: [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 0.0],
],
material: None,
}
}
}
impl Mesh3ds {
pub fn to_flat_buffers(&self) -> MeshBuffers {
convert::to_flat(self)
}
pub fn to_smooth_buffers(&self) -> MeshBuffers {
convert::to_smooth(self)
}
}
pub fn parse(data: &[u8]) -> Result<Scene3ds, Error3ds> {
if data.len() < 6 {
return Err(Error3ds::Truncated(data.len()));
}
let root = Chunk::read_at(data, 0)?;
if root.id != 0x4D4D {
return Err(Error3ds::NotA3ds(root.id));
}
let mut scene = Scene3ds::default();
parse_main(data, &root, &mut scene)?;
Ok(scene)
}
fn parse_main(data: &[u8], chunk: &Chunk, scene: &mut Scene3ds) -> Result<(), Error3ds> {
for child in walk_chunks(data, chunk)? {
let child = child?;
if child.id == 0x3D3D {
parse_edit(data, &child, scene)?;
}
}
Ok(())
}
fn parse_edit(data: &[u8], chunk: &Chunk, scene: &mut Scene3ds) -> Result<(), Error3ds> {
for child in walk_chunks(data, chunk)? {
let child = child?;
match child.id {
0x4000 => {
if let Some(mesh) = parse_named_object(data, &child)? {
scene.meshes.push(mesh);
}
}
0xAFFF => {
if let Some(name) = parse_mat_entry(data, &child)? {
scene.materials.push(name);
}
}
_ => {}
}
}
Ok(())
}
fn parse_named_object(data: &[u8], chunk: &Chunk) -> Result<Option<Mesh3ds>, Error3ds> {
let name = read_cstring(data, chunk.data_start)?;
let name_end = chunk.data_start + name.len() + 1;
for child in walk_chunks_from(data, chunk, name_end)? {
let child = child?;
if child.id == 0x4100 {
let mut mesh = parse_tri_object(data, &child)?;
mesh.name = name;
return Ok(Some(mesh));
}
}
Ok(None)
}
fn parse_tri_object(data: &[u8], chunk: &Chunk) -> Result<Mesh3ds, Error3ds> {
let mut mesh = Mesh3ds::default();
for child in walk_chunks(data, chunk)? {
let child = child?;
match child.id {
0x4110 => mesh.vertices = read_point_array(data, &child)?,
0x4120 => {
mesh.faces = read_face_array(data, &child)?;
}
0x4130 if mesh.material.is_none() => {
mesh.material = read_msh_mat_group_name(data, &child)?;
}
0x4140 => mesh.uvs = read_tex_verts(data, &child)?,
0x4150 => mesh.smooth_groups = read_smooth_group(data, &child)?,
0x4160 => mesh.transform = read_mesh_matrix(data, &child)?,
_ => {}
}
}
Ok(mesh)
}
fn parse_mat_entry(data: &[u8], chunk: &Chunk) -> Result<Option<String>, Error3ds> {
for child in walk_chunks(data, chunk)? {
let child = child?;
if child.id == 0xA000 {
return Ok(Some(read_cstring(data, child.data_start)?));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
fn write_chunk(id: u16, data: &[u8], buf: &mut Vec<u8>) {
buf.extend_from_slice(&id.to_le_bytes());
let len = (6 + data.len()) as u32;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(data);
}
fn write_point_array(verts: &[[f32; 3]], buf: &mut Vec<u8>) {
let mut data = Vec::new();
data.extend_from_slice(&(verts.len() as u16).to_le_bytes());
for v in verts {
for c in v {
data.extend_from_slice(&c.to_le_bytes());
}
}
write_chunk(0x4110, &data, buf);
}
fn write_face_array(faces: &[[u16; 3]], buf: &mut Vec<u8>) {
let mut data = Vec::new();
data.extend_from_slice(&(faces.len() as u16).to_le_bytes());
for f in faces {
for &idx in f {
data.extend_from_slice(&idx.to_le_bytes());
}
data.extend_from_slice(&0u16.to_le_bytes());
}
write_chunk(0x4120, &data, buf);
}
fn write_tex_verts(uvs: &[[f32; 2]], buf: &mut Vec<u8>) {
let mut data = Vec::new();
data.extend_from_slice(&(uvs.len() as u16).to_le_bytes());
for uv in uvs {
for c in uv {
data.extend_from_slice(&c.to_le_bytes());
}
}
write_chunk(0x4140, &data, buf);
}
fn write_smooth_groups(groups: &[u32], buf: &mut Vec<u8>) {
let mut data = Vec::new();
for g in groups {
data.extend_from_slice(&g.to_le_bytes());
}
write_chunk(0x4150, &data, buf);
}
#[allow(dead_code)]
fn write_mesh_matrix(mat: &[[f32; 3]; 4], buf: &mut Vec<u8>) {
let mut data = Vec::new();
for row in mat {
for c in row {
data.extend_from_slice(&c.to_le_bytes());
}
}
write_chunk(0x4160, &data, buf);
}
#[allow(dead_code)]
fn write_msh_mat_group(name: &str, buf: &mut Vec<u8>) {
let mut data = Vec::new();
data.extend_from_slice(name.as_bytes());
data.push(0);
data.extend_from_slice(&0u16.to_le_bytes()); write_chunk(0x4130, &data, buf);
}
fn write_mat_entry(name: &str, buf: &mut Vec<u8>) {
let mut data = Vec::new();
data.extend_from_slice(name.as_bytes());
data.push(0);
let mut entry = Vec::new();
write_chunk(0xA000, &data, &mut entry);
write_chunk(0xAFFF, &entry, buf);
}
fn build_cube_3ds(smooth: bool) -> Vec<u8> {
let verts: Vec<[f32; 3]> = vec![
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0], ];
let faces: Vec<[u16; 3]> = vec![
[0, 1, 2], [0, 2, 3],
[5, 4, 7], [5, 7, 6],
[4, 5, 1], [4, 1, 0],
[2, 6, 7], [2, 7, 3],
[4, 0, 3], [4, 3, 7],
[1, 5, 6], [1, 6, 2],
];
let uvs: Vec<[f32; 2]> = vec![
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
];
let mut tri = Vec::new();
write_point_array(&verts, &mut tri);
write_face_array(&faces, &mut tri);
write_tex_verts(&uvs, &mut tri);
if smooth {
let groups = vec![1u32; faces.len()];
write_smooth_groups(&groups, &mut tri);
}
let mut named = Vec::new();
named.extend_from_slice(b"Cube\0");
write_chunk(0x4100, &tri, &mut named);
let mut edit = Vec::new();
write_chunk(0x4000, &named, &mut edit);
write_mat_entry("Default", &mut edit);
let mut main = Vec::new();
write_chunk(0x3D3D, &edit, &mut main);
let mut file = Vec::new();
write_chunk(0x4D4D, &main, &mut file);
file
}
#[test]
fn test_truncated() {
assert!(matches!(parse(&[]), Err(Error3ds::Truncated(0))));
assert!(matches!(parse(&[0x4D, 0x4D]), Err(Error3ds::Truncated(2))));
}
#[test]
fn test_not_a_3ds() {
let mut buf = Vec::new();
write_chunk(0x1234, &[], &mut buf);
assert!(matches!(parse(&buf), Err(Error3ds::NotA3ds(0x1234))));
}
#[test]
fn test_parse_cube() {
let data = build_cube_3ds(false);
let scene = parse(&data).unwrap();
assert_eq!(scene.meshes.len(), 1);
let mesh = &scene.meshes[0];
assert_eq!(mesh.name, "Cube");
assert_eq!(mesh.vertices.len(), 8);
assert_eq!(mesh.faces.len(), 12);
assert_eq!(mesh.uvs.len(), 8);
assert!(mesh.smooth_groups.is_empty());
assert_eq!(scene.materials, vec!["Default"]);
}
#[test]
fn test_flat_buffers() {
let data = build_cube_3ds(false);
let scene = parse(&data).unwrap();
let mesh = &scene.meshes[0];
let buf = mesh.to_flat_buffers();
assert_eq!(buf.positions.len(), mesh.faces.len() * 3);
assert_eq!(buf.normals.len(), mesh.faces.len() * 3);
assert_eq!(buf.uvs.len(), mesh.faces.len() * 3);
assert_eq!(buf.indices.len(), mesh.faces.len() * 3);
for n in &buf.normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-5, "normal length = {}", len);
}
}
#[test]
fn test_smooth_buffers() {
let data = build_cube_3ds(true);
let scene = parse(&data).unwrap();
let mesh = &scene.meshes[0];
let buf = mesh.to_smooth_buffers();
assert!(buf.positions.len() < 36, "smooth should share vertices, got {}", buf.positions.len());
assert_eq!(buf.indices.len(), 36);
for n in &buf.normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-5, "normal length = {}", len);
}
}
#[test]
fn test_smooth_fallback_when_empty() {
let data = build_cube_3ds(false);
let scene = parse(&data).unwrap();
let mesh = &scene.meshes[0];
let flat = mesh.to_flat_buffers();
let smooth = mesh.to_smooth_buffers();
assert_eq!(flat.positions.len(), smooth.positions.len());
}
#[test]
fn test_transform_default_identity() {
let data = build_cube_3ds(false);
let scene = parse(&data).unwrap();
let mesh = &scene.meshes[0];
assert_eq!(mesh.transform[0], [1.0, 0.0, 0.0]);
assert_eq!(mesh.transform[1], [0.0, 1.0, 0.0]);
assert_eq!(mesh.transform[2], [0.0, 0.0, 1.0]);
assert_eq!(mesh.transform[3], [0.0, 0.0, 0.0]);
}
}