gltf exporter wip

This commit is contained in:
Robin Appelman 2023-12-16 18:31:21 +01:00
commit 30acc4b093
8 changed files with 475 additions and 7 deletions

14
examples/gltf/convert.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::Vertex;
use vmdl::Model;
pub fn model_to_vertices(model: &Model) -> Vec<Vertex> {
model
.meshes()
.flat_map(|mesh| mesh.vertices())
.map(|vertex| Vertex {
position: vertex.position.into(),
uv: vertex.texture_coordinates.into(),
normal: vertex.normal.into(),
})
.collect()
}

18
examples/gltf/error.rs Normal file
View file

@ -0,0 +1,18 @@
use crate::loader::LoadError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Three(#[from] Box<dyn std::error::Error>),
#[error(transparent)]
Mdl(#[from] vmdl::ModelError),
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
Loader(#[from] LoadError),
#[error(transparent)]
Vtf(#[from] vtf::Error),
#[error("{0}")]
Other(&'static str),
}

97
examples/gltf/loader.rs Normal file
View file

@ -0,0 +1,97 @@
use std::fmt::{Debug, Formatter};
use std::fs;
use std::path::PathBuf;
use steamlocate::SteamDir;
use thiserror::Error;
use tracing::{debug, error, info};
use vpk::VPK;
#[derive(Debug, Error)]
pub enum LoadError {
#[error("{0}")]
Other(&'static str),
#[error(transparent)]
IO(#[from] std::io::Error),
}
impl From<&'static str> for LoadError {
fn from(e: &'static str) -> Self {
LoadError::Other(e)
}
}
pub struct Loader {
tf_dir: PathBuf,
download: PathBuf,
vpks: Vec<VPK>,
}
impl Debug for Loader {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Loader")
.field("tf_dir", &self.tf_dir)
.finish_non_exhaustive()
}
}
impl Loader {
pub fn new() -> Result<Self, LoadError> {
let tf_dir = SteamDir::locate()
.ok_or("Can't find steam directory")?
.app(&440)
.ok_or("Can't find tf2 directory")?
.path
.join("tf");
let download = tf_dir.join("download");
let vpks = tf_dir
.read_dir()?
.filter_map(|item| item.ok())
.filter_map(|item| Some(item.path().to_str()?.to_string()))
.filter(|path| path.ends_with("dir.vpk"))
.map(|path| vpk::from_path(&path))
.filter_map(|res| res.ok())
.collect();
Ok(Loader {
tf_dir,
download,
vpks,
})
}
#[tracing::instrument]
pub fn load(&self, name: &str) -> Result<Vec<u8>, LoadError> {
debug!("loading {}", name);
if name.ends_with("bsp") {
let path = self.tf_dir.join(name);
if path.exists() {
debug!("found in tf2 dir");
return Ok(fs::read(path)?);
}
let path = self.download.join(name);
if path.exists() {
debug!("found in download dir");
return Ok(fs::read(path)?);
}
}
for vpk in self.vpks.iter() {
if let Some(entry) = vpk.tree.get(name) {
let data = entry.get()?.into_owned();
debug!("got {} bytes from vpk", data.len());
return Ok(data);
}
}
info!("Failed to find {} in vpk", name);
Err(LoadError::Other("Can't find file in vpks"))
}
pub fn load_from_paths(&self, name: &str, paths: &[String]) -> Result<Vec<u8>, LoadError> {
for path in paths {
if let Ok(data) = self.load(&format!("{}{}", path, name)) {
return Ok(data);
}
}
error!("Failed to find {} in vpk paths: {}", name, paths.join(", "));
Err(LoadError::Other("Can't find file in vpks"))
}
}

246
examples/gltf/main.rs Normal file
View file

@ -0,0 +1,246 @@
mod convert;
mod error;
mod loader;
use gltf_json as json;
use std::{fs, mem};
use crate::convert::model_to_vertices;
pub use error::Error;
use json::validation::Checked::Valid;
use std::borrow::Cow;
use std::env::args_os;
use std::io::Write;
use std::path::{Path, PathBuf};
use vmdl::{Mdl, Model, Vtx, Vvd};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Output {
/// Output standard glTF.
Standard,
/// Output binary glTF.
Binary,
}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct Vertex {
position: [f32; 3],
uv: [f32; 2],
normal: [f32; 3],
}
/// Calculate bounding coordinates of a list of vertices, used for the clipping distance of the model
fn bounding_coords(points: &[Vertex]) -> ([f32; 3], [f32; 3]) {
let mut min = [f32::MAX, f32::MAX, f32::MAX];
let mut max = [f32::MIN, f32::MIN, f32::MIN];
for point in points {
let p = point.position;
for i in 0..3 {
min[i] = f32::min(min[i], p[i]);
max[i] = f32::max(max[i], p[i]);
}
}
(min, max)
}
fn align_to_multiple_of_four(n: &mut u32) {
*n = (*n + 3) & !3;
}
fn to_padded_byte_vector<T>(vec: Vec<T>) -> Vec<u8> {
let byte_length = vec.len() * mem::size_of::<T>();
let byte_capacity = vec.capacity() * mem::size_of::<T>();
let alloc = vec.into_boxed_slice();
let ptr = Box::<[T]>::into_raw(alloc) as *mut u8;
let mut new_vec = unsafe { Vec::from_raw_parts(ptr, byte_length, byte_capacity) };
while new_vec.len() % 4 != 0 {
new_vec.push(0); // pad to multiple of four bytes
}
new_vec
}
fn export(model: Model, output: Output) {
let vertices = model_to_vertices(&model);
let (min, max) = bounding_coords(&vertices);
let buffer_length = (vertices.len() * mem::size_of::<Vertex>()) as u32;
let buffer = json::Buffer {
byte_length: buffer_length,
extensions: Default::default(),
extras: Default::default(),
name: None,
uri: if output == Output::Standard {
Some("buffer0.bin".into())
} else {
None
},
};
let buffer_view = json::buffer::View {
buffer: json::Index::new(0),
byte_length: buffer.byte_length,
byte_offset: None,
byte_stride: Some(mem::size_of::<Vertex>() as u32),
extensions: Default::default(),
extras: Default::default(),
name: None,
target: Some(Valid(json::buffer::Target::ArrayBuffer)),
};
let positions = json::Accessor {
buffer_view: Some(json::Index::new(0)),
byte_offset: Some(0),
count: vertices.len() as u32,
component_type: Valid(json::accessor::GenericComponentType(
json::accessor::ComponentType::F32,
)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(json::accessor::Type::Vec3),
min: Some(json::Value::from(Vec::from(min))),
max: Some(json::Value::from(Vec::from(max))),
name: None,
normalized: false,
sparse: None,
};
let uvs = json::Accessor {
buffer_view: Some(json::Index::new(0)),
byte_offset: Some((3 * mem::size_of::<f32>()) as u32),
count: vertices.len() as u32,
component_type: Valid(json::accessor::GenericComponentType(
json::accessor::ComponentType::F32,
)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(json::accessor::Type::Vec3),
min: None,
max: None,
name: None,
normalized: false,
sparse: None,
};
let normals = json::Accessor {
buffer_view: Some(json::Index::new(0)),
byte_offset: Some((5 * mem::size_of::<f32>()) as u32),
count: vertices.len() as u32,
component_type: Valid(json::accessor::GenericComponentType(
json::accessor::ComponentType::F32,
)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(json::accessor::Type::Vec3),
min: None,
max: None,
name: None,
normalized: false,
sparse: None,
};
let primitive = json::mesh::Primitive {
attributes: {
let mut map = std::collections::BTreeMap::new();
map.insert(Valid(json::mesh::Semantic::Positions), json::Index::new(0));
map.insert(
Valid(json::mesh::Semantic::TexCoords(0)),
json::Index::new(1),
);
map.insert(Valid(json::mesh::Semantic::Normals), json::Index::new(2));
map
},
extensions: Default::default(),
extras: Default::default(),
indices: None,
material: None,
mode: Valid(json::mesh::Mode::Triangles),
targets: None,
};
let mesh = json::Mesh {
extensions: Default::default(),
extras: Default::default(),
name: None,
primitives: vec![primitive],
weights: None,
};
let node = json::Node {
camera: None,
children: None,
extensions: Default::default(),
extras: Default::default(),
matrix: None,
mesh: Some(json::Index::new(0)),
name: None,
rotation: None,
scale: None,
translation: None,
skin: None,
weights: None,
};
let root = json::Root {
accessors: vec![positions, uvs, normals],
buffers: vec![buffer],
buffer_views: vec![buffer_view],
meshes: vec![mesh],
nodes: vec![node],
scenes: vec![json::Scene {
extensions: Default::default(),
extras: Default::default(),
name: None,
nodes: vec![json::Index::new(0)],
}],
..Default::default()
};
match output {
Output::Standard => {
let _ = fs::create_dir("triangle");
let writer = fs::File::create("triangle/triangle.gltf").expect("I/O error");
json::serialize::to_writer_pretty(writer, &root).expect("Serialization error");
let bin = to_padded_byte_vector(vertices);
let mut writer = fs::File::create("triangle/buffer0.bin").expect("I/O error");
writer.write_all(&bin).expect("I/O error");
}
Output::Binary => {
let json_string = json::serialize::to_string(&root).expect("Serialization error");
let mut json_offset = json_string.len() as u32;
align_to_multiple_of_four(&mut json_offset);
let glb = gltf::binary::Glb {
header: gltf::binary::Header {
magic: *b"glTF",
version: 2,
length: json_offset + buffer_length,
},
bin: Some(Cow::Owned(to_padded_byte_vector(vertices))),
json: Cow::Owned(json_string.into_bytes()),
};
let writer = std::fs::File::create("output.glb").expect("I/O error");
glb.to_writer(writer).expect("glTF binary output error");
}
}
}
fn load(path: &Path) -> Result<Model, vmdl::ModelError> {
let data = fs::read(path)?;
let mdl = Mdl::read(&data)?;
let data = fs::read(path.with_extension("dx90.vtx"))?;
let vtx = Vtx::read(&data)?;
let data = fs::read(path.with_extension("vvd"))?;
let vvd = Vvd::read(&data)?;
Ok(Model::from_parts(mdl, vtx, vvd))
}
fn main() -> Result<(), Error> {
let path = PathBuf::from(args_os().nth(1).expect("No model file provided"));
let source_model = load(&path)?;
export(source_model, Output::Binary);
Ok(())
}