pub mod parser;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelVersion {
V26,
V27,
V40,
V50,
Unknown,
}
#[derive(Debug, Clone)]
pub enum TextureData {
SolidColor(u8),
Texture { texture_id: u16, image_id: u8 },
}
#[derive(Debug, Clone)]
pub struct Model3DHeader {
pub version: [u8; 4],
pub num_vertices: u32,
pub num_faces: u32,
pub radius: u32,
pub num_frames: u32,
pub offset_frame_data: u32,
pub total_face_vertices: u32,
pub offset_section4: u32,
pub section4_count: u32,
#[allow(
clippy::pub_underscore_fields,
reason = "field name mirrors reverse-engineered binary layout"
)]
pub _unused_24: u32,
pub offset_normal_indices: u32,
pub offset_vertex_normals: u32,
pub offset_vertex_coords: u32,
pub offset_face_normals: u32,
pub total_face_vertices_dup: u32,
pub offset_face_data: u32,
}
#[derive(Debug, Clone)]
pub struct FaceVertex {
pub vertex_index: u32,
pub u: i16,
pub v: i16,
}
#[derive(Debug, Clone)]
pub struct FaceData {
pub vertex_count: u8,
pub tex_hi: u8,
pub texture_data: TextureData,
pub face_vertices: Vec<FaceVertex>,
}
impl FaceData {
#[must_use]
pub const fn size_in_bytes(&self, version: &ModelVersion) -> usize {
let texture_header_size = if matches!(version, ModelVersion::V40 | ModelVersion::V50) {
1 + 4 } else {
1 + 2 };
let vertex_data_size = self.face_vertices.len() * (4 + 2 + 2);
1 + texture_header_size + 4 + vertex_data_size
}
}
#[derive(Debug, Clone, Copy)]
pub struct VertexCoord {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone)]
pub struct FaceNormal {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone)]
pub struct VertexNormal {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameType {
Static3D,
AnimatedI16,
AnimatedI32,
Static3DC,
Unknown(u32),
}
#[derive(Debug, Clone)]
pub struct FrameDataEntry {
pub vertex_offset: u32,
pub normal_offset: u32,
pub reserved: u32,
pub frame_type: FrameType,
}
#[derive(Debug, Clone)]
pub struct FrameVertexData {
pub coords: Vec<VertexCoord>,
}
#[derive(Debug, Clone)]
pub struct FrameNormalData {
pub face_normals: Vec<FaceNormal>,
}
#[derive(Debug, Clone)]
pub struct Model3DFile {
pub header: Model3DHeader,
pub version: ModelVersion,
pub frame_data: Vec<FrameDataEntry>,
pub face_data: Vec<FaceData>,
pub vertex_coords: Vec<VertexCoord>,
pub face_normals: Vec<FaceNormal>,
pub normal_indices: Vec<u32>,
pub vertex_normals: Vec<VertexNormal>,
pub frame_vertex_data: Vec<FrameVertexData>,
pub frame_normal_data: Vec<FrameNormalData>,
}
impl Model3DHeader {
#[must_use]
pub fn version_string(&self) -> String {
String::from_utf8_lossy(&self.version)
.trim_matches('\0')
.to_string()
}
#[must_use]
pub fn parse_version(&self) -> ModelVersion {
let version_str = self.version_string();
match version_str.as_str() {
"v2.6" => ModelVersion::V26,
"v2.7" => ModelVersion::V27,
"v4.0" => ModelVersion::V40,
"v5.0" => ModelVersion::V50,
_ => ModelVersion::Unknown,
}
}
#[must_use]
pub fn is_v27_or_earlier(&self) -> bool {
matches!(self.parse_version(), ModelVersion::V26 | ModelVersion::V27)
}
#[must_use]
pub fn is_v40_or_later(&self) -> bool {
matches!(self.parse_version(), ModelVersion::V40 | ModelVersion::V50)
}
}
impl Model3DFile {
#[must_use]
pub fn total_face_vertices(&self) -> usize {
self.face_data
.iter()
.map(|face| usize::from(face.vertex_count))
.sum()
}
#[must_use]
pub fn bounding_box(&self) -> Option<(VertexCoord, VertexCoord)> {
if self.vertex_coords.is_empty() {
return None;
}
let mut min = VertexCoord {
x: f32::INFINITY,
y: f32::INFINITY,
z: f32::INFINITY,
};
let mut max = VertexCoord {
x: f32::NEG_INFINITY,
y: f32::NEG_INFINITY,
z: f32::NEG_INFINITY,
};
for vertex in &self.vertex_coords {
min.x = min.x.min(vertex.x);
min.y = min.y.min(vertex.y);
min.z = min.z.min(vertex.z);
max.x = max.x.max(vertex.x);
max.y = max.y.max(vertex.y);
max.z = max.z.max(vertex.z);
}
Some((min, max))
}
}
use crate::{Result, error::Error};
use log::warn;
fn parse_data_internal(input: &[u8], kind: &str) -> Result<Model3DFile> {
match parser::parse_3d_file(input) {
Ok((remaining, model)) => {
if !remaining.is_empty() {
warn!("{} bytes remaining in {} data", remaining.len(), kind);
}
Ok(model)
}
Err(e) => Err(Error::Parse(format!("Failed to parse {kind}: {e:?}"))),
}
}
pub fn parse_3d_file(input: &[u8]) -> Result<Model3DFile> {
parse_data_internal(input, "3D model file")
}