This commit is contained in:
Robin Appelman 2023-12-21 00:12:19 +01:00
commit 04e6b909a3
4 changed files with 278 additions and 106 deletions

2
Cargo.lock generated
View file

@ -1982,7 +1982,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "vmdl" name = "vmdl"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/icewind1991/vmdl#585a4850319e67555c10f9fad8f158e9e3d18c15" source = "git+https://github.com/icewind1991/vmdl#5cd7c3d859996102c47f77a8f292570c55d8438c"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 1.3.2", "bitflags 1.3.2",

View file

@ -73,6 +73,7 @@
cargo-msrv cargo-msrv
cargo-semver-checks cargo-semver-checks
cargo-insta cargo-insta
meshoptimizer
(writeShellApplication { (writeShellApplication {
name = "cargo-fuzz"; name = "cargo-fuzz";
runtimeInputs = [cargo-fuzz toolchain]; runtimeInputs = [cargo-fuzz toolchain];

View file

@ -3,13 +3,15 @@ mod bsp;
mod error; mod error;
pub mod gltf_builder; pub mod gltf_builder;
mod materials; mod materials;
// mod prop; mod prop;
use crate::bsp::{bsp_models, push_bsp_model}; use crate::bsp::{bsp_models, push_bsp_model};
use crate::prop::push_or_get_model;
use clap::Parser; use clap::Parser;
pub use error::Error; pub use error::Error;
use gltf::Glb; use gltf::Glb;
use gltf_json::{Buffer, Index, Root, Scene}; use gltf_json::scene::UnitQuaternion;
use gltf_json::{Buffer, Index, Node, Root, Scene};
use miette::Context; use miette::Context;
use std::borrow::Cow; use std::borrow::Cow;
use std::fs::{read, File}; use std::fs::{read, File};
@ -67,7 +69,7 @@ fn main() -> miette::Result<()> {
Ok(()) Ok(())
} }
fn export(bsp: Bsp, loader: &mut Loader) -> Result<Glb<'static>, Error> { fn export(bsp: Bsp, loader: &Loader) -> Result<Glb<'static>, Error> {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let mut root = Root::default(); let mut root = Root::default();
@ -77,6 +79,32 @@ fn export(bsp: Bsp, loader: &mut Loader) -> Result<Glb<'static>, Error> {
root.nodes.push(node); root.nodes.push(node);
} }
for prop in bsp.static_props() {
let mesh = push_or_get_model(&mut buffer, &mut root, loader, &prop.model(), prop.skin);
let rotation = prop.rotation();
let node = Node {
camera: None,
children: None,
extensions: Default::default(),
extras: Default::default(),
matrix: None,
mesh: Some(mesh),
name: None,
rotation: Some(UnitQuaternion([
rotation.v.x,
rotation.v.y,
rotation.v.z,
rotation.s,
])),
scale: None,
translation: Some(map_coords(prop.origin)),
skin: None,
weights: None,
};
root.nodes.push(node);
}
let node_indices = 0..root.nodes.len(); let node_indices = 0..root.nodes.len();
root.scenes = vec![Scene { root.scenes = vec![Scene {
name: None, name: None,

View file

@ -1,15 +1,110 @@
use crate::bsp::map_coords; use crate::gltf_builder::push_or_get_material;
use crate::materials::{load_material_fallback}; use crate::{map_coords, Error};
use crate::Error; use bytemuck::{offset_of, Pod, Zeroable};
use gltf_json::accessor::{ComponentType, GenericComponentType, Type};
use gltf_json::buffer::{Target, View};
use gltf_json::mesh::{Mode, Primitive, Semantic};
use gltf_json::validation::Checked::Valid;
use gltf_json::{Accessor, Index, Mesh, Root, Value};
use std::mem::size_of;
use tf_asset_loader::Loader; use tf_asset_loader::Loader;
use tracing::{error, warn}; use vmdl::{Mdl, Model, SkinTable, Vtx, Vvd};
use vbsp::{Handle, StaticPropLump};
use vmdl::mdl::{Mdl, TextureInfo}; #[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
use vmdl::vtx::Vtx; #[repr(C)]
use vmdl::vvd::Vvd; pub struct ModelVertex {
position: [f32; 3],
normal: [f32; 3],
uv: [f32; 2],
}
impl From<&vmdl::vvd::Vertex> for ModelVertex {
fn from(vertex: &vmdl::vvd::Vertex) -> Self {
ModelVertex {
position: map_coords(vertex.position),
uv: vertex.texture_coordinates.into(),
normal: vertex.normal.into(),
}
}
}
fn push_vertices(buffer: &mut Vec<u8>, gltf: &mut Root, model: &Model) {
let start = buffer.len() as u32;
let view_start = gltf.buffer_views.len() as u32;
let vertex_count = model.vertices().len() as u32;
let (min, max) = model.bounding_box();
let min = map_coords(min);
let max = map_coords(max);
let vertex_data = model
.vertices()
.iter()
.map(ModelVertex::from)
.flat_map(bytemuck::cast::<_, [u8; size_of::<ModelVertex>()]>);
buffer.extend(vertex_data);
let vertex_buffer_view = View {
buffer: Index::new(0),
byte_length: buffer.len() as u32 - start,
byte_offset: Some(start),
byte_stride: Some(size_of::<ModelVertex>() as u32),
extensions: Default::default(),
extras: Default::default(),
name: None,
target: Some(Valid(Target::ArrayBuffer)),
};
gltf.buffer_views.push(vertex_buffer_view);
let positions = Accessor {
buffer_view: Some(Index::new(view_start)),
byte_offset: Some(offset_of!(ModelVertex, position) as u32),
count: vertex_count,
component_type: Valid(GenericComponentType(ComponentType::F32)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(Type::Vec3),
min: Some(Value::from(Vec::from(min))),
max: Some(Value::from(Vec::from(max))),
name: None,
normalized: false,
sparse: None,
};
let uvs = Accessor {
buffer_view: Some(Index::new(view_start)),
byte_offset: Some(offset_of!(ModelVertex, uv) as u32),
count: vertex_count,
component_type: Valid(GenericComponentType(ComponentType::F32)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(Type::Vec2),
min: None,
max: None,
name: None,
normalized: false,
sparse: None,
};
let normals = Accessor {
buffer_view: Some(Index::new(view_start)),
byte_offset: Some(offset_of!(ModelVertex, normal) as u32),
count: vertex_count,
component_type: Valid(GenericComponentType(ComponentType::F32)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(Type::Vec3),
min: None,
max: None,
name: None,
normalized: false,
sparse: None,
};
gltf.accessors.extend([positions, uvs, normals]);
}
#[tracing::instrument(skip(loader))] #[tracing::instrument(skip(loader))]
pub fn load_prop(loader: &Loader, name: &str) -> Result<vmdl::Model, Error> { pub fn load_prop(loader: &Loader, name: &str) -> Result<Model, Error> {
let load = |name: &str| -> Result<Vec<u8>, Error> { let load = |name: &str| -> Result<Vec<u8>, Error> {
loader loader
.load(name)? .load(name)?
@ -19,109 +114,157 @@ pub fn load_prop(loader: &Loader, name: &str) -> Result<vmdl::Model, Error> {
let vtx = Vtx::read(&load(&name.replace(".mdl", ".dx90.vtx"))?)?; let vtx = Vtx::read(&load(&name.replace(".mdl", ".dx90.vtx"))?)?;
let vvd = Vvd::read(&load(&name.replace(".mdl", ".vvd"))?)?; let vvd = Vvd::read(&load(&name.replace(".mdl", ".vvd"))?)?;
Ok(vmdl::Model::from_parts(mdl, vtx, vvd)) Ok(Model::from_parts(mdl, vtx, vvd))
} }
pub fn load_props<'a, I: Iterator<Item = Handle<'a, StaticPropLump>>>(
pub fn push_or_get_model(
buffer: &mut Vec<u8>,
gltf: &mut Root,
loader: &Loader, loader: &Loader,
props: I, model: &str,
) -> Result<Vec<CpuModel>, Error> {
let props = props
.filter_map(|prop| match load_prop(loader, prop.model()) {
Ok(model) => Some((prop, model)),
Err(e) => {
error!(error = ?e, prop = prop.model(), "Failed to load prop");
None
}
})
.map(|(prop, model)| {
let transform =
Mat4::from_translation(map_coords(prop.origin)) * Mat4::from(prop.rotation());
PropData {
name: prop.model(),
model,
transform,
skin: prop.skin,
}
});
props
.map(|prop| {
let geometries: Vec<_> = prop_to_meshes(&prop).collect();
let materials: Vec<_> = prop
.model
.textures()
.iter()
.map(|tex| prop_texture_to_material(tex, loader))
.collect();
Ok(CpuModel {
name: prop.name.into(),
geometries,
materials,
})
})
.collect()
}
struct PropData<'a> {
name: &'a str,
model: vmdl::Model,
transform: Mat4,
skin: i32, skin: i32,
) -> Index<Mesh> {
let skinned_name = format!("{model}_{skin}");
match get_mesh_index(&mut gltf.meshes, &skinned_name) {
Some(index) => index,
None => {
let prop = load_prop(loader, model).expect("failed to load prop");
let index = gltf.meshes.len() as u32;
let material = push_model(buffer, gltf, loader, &prop, skin);
gltf.meshes.push(material);
Index::new(index)
}
}
} }
fn prop_to_meshes<'a>(prop: &'a PropData) -> impl Iterator<Item = Primitive> + 'a { fn get_mesh_index(meshes: &[Mesh], name: &str) -> Option<Index<Mesh>> {
let transform = prop.transform; meshes
let model = &prop.model; .iter()
.enumerate()
.find_map(|(i, mat)| (mat.name.as_deref() == Some(name)).then_some(i))
.map(|i| Index::new(i as u32))
}
let skin = match model.skin_tables().nth(prop.skin as usize) { pub fn push_model(
Some(skin) => skin, buffer: &mut Vec<u8>,
None => { gltf: &mut Root,
warn!(index = prop.skin, prop = prop.name, "invalid skin index"); loader: &Loader,
model.skin_tables().next().unwrap() model: &Model,
skin: i32,
) -> Mesh {
let accessor_start = gltf.accessors.len() as u32;
push_vertices(buffer, gltf, model);
let skin_table = model
.skin_tables()
.nth(skin as usize)
.unwrap_or_else(|| model.skin_tables().next().unwrap());
let primitives = model
.meshes()
.map(|mesh| push_primitive(buffer, gltf, loader, &mesh, accessor_start, &skin_table))
.collect();
Mesh {
extensions: Default::default(),
extras: Default::default(),
name: Some(format!("{}_{skin}", model.name())),
primitives,
weights: None,
} }
}
pub fn push_primitive(
buffer: &mut Vec<u8>,
gltf: &mut Root,
loader: &Loader,
mesh: &vmdl::Mesh,
vertex_accessor_start: u32,
skin: &SkinTable,
) -> Primitive {
let buffer_start = buffer.len() as u32;
let view_start = gltf.buffer_views.len() as u32;
let accessor_start = gltf.accessors.len() as u32;
buffer.extend(
mesh.vertex_strip_indices()
.flatten()
.flat_map(|index| (index as u32).to_le_bytes()),
);
let byte_length = buffer.len() as u32 - buffer_start;
let view = View {
buffer: Index::new(0),
byte_length,
byte_offset: Some(buffer_start),
byte_stride: None,
extensions: Default::default(),
extras: Default::default(),
name: None,
target: Some(Valid(Target::ElementArrayBuffer)),
}; };
gltf.buffer_views.push(view);
model.meshes().map(move |mesh| { let accessor = Accessor {
let material_index = skin.texture_index(mesh.material_index()); buffer_view: Some(Index::new(view_start)),
byte_offset: Some(0),
count: byte_length / size_of::<u32>() as u32,
component_type: Valid(GenericComponentType(ComponentType::U32)),
extensions: Default::default(),
extras: Default::default(),
type_: Valid(Type::Scalar),
min: None,
max: None,
name: None,
normalized: false,
sparse: None,
};
gltf.accessors.push(accessor);
let positions: Vec<Vec3> = mesh let texture = skin
.vertices() .texture_info(mesh.material_index())
.map(|vertex| map_coords(vertex.position)) .expect("mat out of bounds");
.collect(); let texture_path = find_material(&texture.name, &texture.search_paths, loader)
let normals: Vec<Vec3> = mesh .expect("failed to find texture");
.vertices() let material_index = push_or_get_material(buffer, gltf, loader, &texture_path);
.map(|vertex| map_coords(vertex.normal))
.collect();
let uvs: Vec<Vec2> = mesh
.vertices()
.map(|vertex| vertex.texture_coordinates.into())
.collect();
let tangents: Vec<Vec4> = mesh.tangents().map(|tangent| tangent.into()).collect();
let geometry = Geometry::Triangles(TriMesh {
positions: Positions::F32(positions),
normals: Some(normals),
uvs: Some(uvs),
tangents: Some(tangents),
..TriMesh::default()
});
Primitive { Primitive {
name: mesh.model_name.into(), attributes: {
transformation: transform, let mut map = std::collections::BTreeMap::new();
animations: vec![], map.insert(
geometry, Valid(Semantic::Positions),
material_index, Index::new(vertex_accessor_start),
);
map.insert(
Valid(Semantic::TexCoords(0)),
Index::new(vertex_accessor_start + 1),
);
map.insert(
Valid(Semantic::Normals),
Index::new(vertex_accessor_start + 2),
);
map
},
extensions: Default::default(),
extras: Default::default(),
indices: Some(Index::new(accessor_start)),
material: Some(material_index),
mode: Valid(Mode::Triangles),
targets: None,
} }
})
} }
fn prop_texture_to_material(texture: &TextureInfo, loader: &Loader) -> CpuMaterial { fn find_material(name: &str, paths: &[String], loader: &Loader) -> Option<String> {
convert_material(load_material_fallback( for dir in paths {
&texture.name, let full_name = format!(
&texture.search_paths, "{}{}.vmt",
loader, dir.to_ascii_lowercase().trim_start_matches('/'),
)) name.to_ascii_lowercase().trim_end_matches(".vmt")
);
let path = format!("materials/{full_name}");
if loader.exists(&path).unwrap_or_default() {
return Some(full_name);
}
}
None
} }