mirror of
https://codeberg.org/icewind/vbsp-to-gltf.git
synced 2026-06-03 10:14:08 +02:00
props
This commit is contained in:
parent
304dc2d031
commit
04e6b909a3
4 changed files with 278 additions and 106 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
34
src/main.rs
34
src/main.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
345
src/prop.rs
345
src/prop.rs
|
|
@ -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}");
|
||||||
fn prop_to_meshes<'a>(prop: &'a PropData) -> impl Iterator<Item = Primitive> + 'a {
|
match get_mesh_index(&mut gltf.meshes, &skinned_name) {
|
||||||
let transform = prop.transform;
|
Some(index) => index,
|
||||||
let model = &prop.model;
|
|
||||||
|
|
||||||
let skin = match model.skin_tables().nth(prop.skin as usize) {
|
|
||||||
Some(skin) => skin,
|
|
||||||
None => {
|
None => {
|
||||||
warn!(index = prop.skin, prop = prop.name, "invalid skin index");
|
let prop = load_prop(loader, model).expect("failed to load prop");
|
||||||
model.skin_tables().next().unwrap()
|
let index = gltf.meshes.len() as u32;
|
||||||
|
let material = push_model(buffer, gltf, loader, &prop, skin);
|
||||||
|
gltf.meshes.push(material);
|
||||||
|
Index::new(index)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue