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]]
name = "vmdl"
version = "0.1.0"
source = "git+https://github.com/icewind1991/vmdl#585a4850319e67555c10f9fad8f158e9e3d18c15"
source = "git+https://github.com/icewind1991/vmdl#5cd7c3d859996102c47f77a8f292570c55d8438c"
dependencies = [
"arrayvec",
"bitflags 1.3.2",

View file

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

View file

@ -3,13 +3,15 @@ mod bsp;
mod error;
pub mod gltf_builder;
mod materials;
// mod prop;
mod prop;
use crate::bsp::{bsp_models, push_bsp_model};
use crate::prop::push_or_get_model;
use clap::Parser;
pub use error::Error;
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 std::borrow::Cow;
use std::fs::{read, File};
@ -67,7 +69,7 @@ fn main() -> miette::Result<()> {
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 root = Root::default();
@ -77,6 +79,32 @@ fn export(bsp: Bsp, loader: &mut Loader) -> Result<Glb<'static>, Error> {
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();
root.scenes = vec![Scene {
name: None,

View file

@ -1,15 +1,110 @@
use crate::bsp::map_coords;
use crate::materials::{load_material_fallback};
use crate::Error;
use crate::gltf_builder::push_or_get_material;
use crate::{map_coords, 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 tracing::{error, warn};
use vbsp::{Handle, StaticPropLump};
use vmdl::mdl::{Mdl, TextureInfo};
use vmdl::vtx::Vtx;
use vmdl::vvd::Vvd;
use vmdl::{Mdl, Model, SkinTable, Vtx, Vvd};
#[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
#[repr(C)]
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))]
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> {
loader
.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 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,
props: I,
) -> 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,
model: &str,
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 {
let transform = prop.transform;
let model = &prop.model;
fn get_mesh_index(meshes: &[Mesh], name: &str) -> Option<Index<Mesh>> {
meshes
.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) {
Some(skin) => skin,
None => {
warn!(index = prop.skin, prop = prop.name, "invalid skin index");
model.skin_tables().next().unwrap()
pub fn push_model(
buffer: &mut Vec<u8>,
gltf: &mut Root,
loader: &Loader,
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 material_index = skin.texture_index(mesh.material_index());
let accessor = Accessor {
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
.vertices()
.map(|vertex| map_coords(vertex.position))
.collect();
let normals: Vec<Vec3> = mesh
.vertices()
.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()
});
let texture = skin
.texture_info(mesh.material_index())
.expect("mat out of bounds");
let texture_path = find_material(&texture.name, &texture.search_paths, loader)
.expect("failed to find texture");
let material_index = push_or_get_material(buffer, gltf, loader, &texture_path);
Primitive {
name: mesh.model_name.into(),
transformation: transform,
animations: vec![],
geometry,
material_index,
attributes: {
let mut map = std::collections::BTreeMap::new();
map.insert(
Valid(Semantic::Positions),
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 {
convert_material(load_material_fallback(
&texture.name,
&texture.search_paths,
loader,
))
fn find_material(name: &str, paths: &[String], loader: &Loader) -> Option<String> {
for dir in paths {
let full_name = format!(
"{}{}.vmt",
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
}