mirror of
https://codeberg.org/icewind/vbsp-to-gltf.git
synced 2026-06-03 18:24:07 +02:00
bsp geometry
This commit is contained in:
commit
304dc2d031
13 changed files with 3380 additions and 0 deletions
195
src/bsp.rs
Normal file
195
src/bsp.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
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, Node, Root, Value};
|
||||
use std::mem::size_of;
|
||||
use tf_asset_loader::Loader;
|
||||
use vbsp::{Bsp, Entity, Face, Handle, Model, Vector};
|
||||
|
||||
pub fn bsp_models(bsp: &Bsp) -> Result<Vec<(Handle<Model>, Vector)>, Error> {
|
||||
let world_model = bsp
|
||||
.models()
|
||||
.next()
|
||||
.ok_or(Error::Other("No world model".into()))?;
|
||||
|
||||
let mut models: Vec<_> = bsp
|
||||
.entities
|
||||
.iter()
|
||||
.flat_map(|ent| ent.parse())
|
||||
.filter_map(|ent| match ent {
|
||||
Entity::Brush(ent)
|
||||
| Entity::BrushIllusionary(ent)
|
||||
| Entity::BrushWall(ent)
|
||||
| Entity::BrushWallToggle(ent) => Some(ent),
|
||||
_ => None,
|
||||
})
|
||||
.flat_map(|brush| Some((brush.model[1..].parse::<usize>().ok()?, brush.origin)))
|
||||
.flat_map(|(index, origin)| Some((bsp.models().nth(index)?, origin)))
|
||||
.collect();
|
||||
models.push((
|
||||
world_model,
|
||||
Vector {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
fn bounding_box<'a>(vertices: impl IntoIterator<Item = Vector>) -> ([f32; 3], [f32; 3]) {
|
||||
let mut min = Vector::from([f32::MAX, f32::MAX, f32::MAX]);
|
||||
let mut max = Vector::from([f32::MIN, f32::MIN, f32::MIN]);
|
||||
|
||||
for point in vertices {
|
||||
min.x = f32::min(min.x, point.x);
|
||||
min.y = f32::min(min.y, point.y);
|
||||
min.z = f32::min(min.z, point.z);
|
||||
|
||||
max.x = f32::max(max.x, point.x);
|
||||
max.y = f32::max(max.y, point.y);
|
||||
max.z = f32::max(max.z, point.z);
|
||||
}
|
||||
(min.into(), max.into())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct BspVertexData {
|
||||
position: [f32; 3],
|
||||
uv: [f32; 2],
|
||||
}
|
||||
|
||||
pub fn push_bsp_model(
|
||||
buffer: &mut Vec<u8>,
|
||||
gltf: &mut Root,
|
||||
loader: &Loader,
|
||||
model: &Handle<Model>,
|
||||
offset: Vector,
|
||||
) -> Node {
|
||||
let primitives = model
|
||||
.faces()
|
||||
.filter(|face| face.is_visible())
|
||||
.map(|face| push_bsp_face(buffer, gltf, loader, &face))
|
||||
.collect();
|
||||
|
||||
let mesh = Mesh {
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
name: None,
|
||||
primitives,
|
||||
weights: None,
|
||||
};
|
||||
|
||||
let mesh_index = gltf.meshes.len() as u32;
|
||||
gltf.meshes.push(mesh);
|
||||
|
||||
Node {
|
||||
camera: None,
|
||||
children: None,
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
matrix: None,
|
||||
mesh: Some(Index::new(mesh_index)),
|
||||
name: None,
|
||||
rotation: None,
|
||||
scale: None,
|
||||
translation: Some(map_coords(offset)),
|
||||
skin: None,
|
||||
weights: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_bsp_face(
|
||||
buffer: &mut Vec<u8>,
|
||||
gltf: &mut Root,
|
||||
loader: &Loader,
|
||||
face: &Handle<Face>,
|
||||
) -> Primitive {
|
||||
let vertex_count = face.vertex_positions().count() as u32;
|
||||
|
||||
let buffer_start = buffer.len() as u32;
|
||||
|
||||
let (min, max) = bounding_box(face.vertex_positions());
|
||||
|
||||
let texture = face.texture();
|
||||
let vertices = face.vertex_positions().map(move |pos| BspVertexData {
|
||||
position: map_coords(pos),
|
||||
uv: texture.uv(pos),
|
||||
});
|
||||
|
||||
let vertex_data = vertices.flat_map(bytemuck::cast::<_, [u8; size_of::<BspVertexData>()]>);
|
||||
buffer.extend(vertex_data);
|
||||
|
||||
let vertex_buffer_view = View {
|
||||
buffer: Index::new(0),
|
||||
byte_length: buffer.len() as u32 - buffer_start,
|
||||
byte_offset: Some(buffer_start),
|
||||
byte_stride: Some(size_of::<BspVertexData>() as u32),
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
name: None,
|
||||
target: Some(Valid(Target::ArrayBuffer)),
|
||||
};
|
||||
|
||||
let vertex_view = Index::new(gltf.buffer_views.len() as u32);
|
||||
gltf.buffer_views.push(vertex_buffer_view);
|
||||
|
||||
let positions = Accessor {
|
||||
buffer_view: Some(vertex_view),
|
||||
byte_offset: Some(offset_of!(BspVertexData, 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(map_coords(min).to_vec())),
|
||||
max: Some(Value::from(map_coords(max).to_vec())),
|
||||
name: None,
|
||||
normalized: false,
|
||||
sparse: None,
|
||||
};
|
||||
let uvs = Accessor {
|
||||
buffer_view: Some(vertex_view),
|
||||
byte_offset: Some(offset_of!(BspVertexData, 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 accessor_start = gltf.accessors.len() as u32;
|
||||
gltf.accessors.push(positions);
|
||||
gltf.accessors.push(uvs);
|
||||
|
||||
let material_index = push_or_get_material(buffer, gltf, loader, face.texture().name());
|
||||
|
||||
Primitive {
|
||||
attributes: {
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert(Valid(Semantic::Positions), Index::new(accessor_start));
|
||||
map.insert(
|
||||
Valid(Semantic::TexCoords(0)),
|
||||
Index::new(accessor_start + 1),
|
||||
);
|
||||
map
|
||||
},
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
indices: None,
|
||||
material: Some(material_index),
|
||||
mode: Valid(Mode::Triangles),
|
||||
targets: None,
|
||||
}
|
||||
}
|
||||
29
src/error.rs
Normal file
29
src/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use miette::Diagnostic;
|
||||
use std::string::FromUtf8Error;
|
||||
use tf_asset_loader::LoaderError;
|
||||
use thiserror::Error;
|
||||
use vmt_parser::VdfError;
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Bsp(#[from] vbsp::BspError),
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Vtf(#[from] vtf::Error),
|
||||
#[error(transparent)]
|
||||
Vdf(#[from] VdfError),
|
||||
#[error(transparent)]
|
||||
Mdl(#[from] vmdl::ModelError),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
#[error(transparent)]
|
||||
String(#[from] FromUtf8Error),
|
||||
#[error(transparent)]
|
||||
Loader(#[from] LoaderError),
|
||||
#[error(transparent)]
|
||||
Gltf(#[from] gltf::Error),
|
||||
#[error("resource {0} not found in vpks or pack")]
|
||||
ResourceNotFound(String),
|
||||
}
|
||||
177
src/gltf_builder.rs
Normal file
177
src/gltf_builder.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use crate::materials::{load_material_fallback, MaterialData, TextureData};
|
||||
use crate::pad_byte_vector;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use gltf_json::buffer::View;
|
||||
use gltf_json::image::MimeType;
|
||||
use gltf_json::material::{AlphaCutoff, AlphaMode, PbrBaseColorFactor, PbrMetallicRoughness};
|
||||
use gltf_json::texture::Info;
|
||||
use gltf_json::validation::Checked::Valid;
|
||||
use gltf_json::{Extras, Image, Index, Material, Root, Texture};
|
||||
use image::png::PngEncoder;
|
||||
use image::{ColorType, DynamicImage, GenericImageView};
|
||||
use tf_asset_loader::Loader;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct Vertex {
|
||||
position: [f32; 3],
|
||||
normal: [f32; 3],
|
||||
uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl From<&vmdl::vvd::Vertex> for Vertex {
|
||||
fn from(vertex: &vmdl::vvd::Vertex) -> Self {
|
||||
Vertex {
|
||||
position: vertex.position.into(),
|
||||
uv: vertex.texture_coordinates.into(),
|
||||
normal: vertex.normal.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_or_get_material(
|
||||
buffer: &mut Vec<u8>,
|
||||
gltf: &mut Root,
|
||||
loader: &Loader,
|
||||
material: &str,
|
||||
) -> Index<Material> {
|
||||
let material = material.to_ascii_lowercase();
|
||||
match get_material_index(&mut gltf.materials, &material) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let material = load_material_fallback(&material, &[String::new()], loader);
|
||||
let index = gltf.materials.len() as u32;
|
||||
let material = push_material(buffer, gltf, material);
|
||||
gltf.materials.push(material);
|
||||
Index::new(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_material_index(materials: &[Material], path: &str) -> Option<Index<Material>> {
|
||||
materials
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, mat)| (mat.name.as_deref() == Some(path)).then_some(i))
|
||||
.map(|i| Index::new(i as u32))
|
||||
}
|
||||
|
||||
pub fn push_material(buffer: &mut Vec<u8>, gltf: &mut Root, material: MaterialData) -> Material {
|
||||
let texture_index = material
|
||||
.texture
|
||||
.map(|tex| push_or_get_texture(buffer, gltf, tex));
|
||||
|
||||
let alpha_mode = match (material.translucent, material.alpha_test.is_some()) {
|
||||
(true, _) => AlphaMode::Blend,
|
||||
(false, true) => AlphaMode::Mask,
|
||||
_ => AlphaMode::Opaque,
|
||||
};
|
||||
|
||||
Material {
|
||||
name: Some(material.name),
|
||||
alpha_cutoff: material
|
||||
.alpha_test
|
||||
.map(AlphaCutoff)
|
||||
.filter(|_| alpha_mode == AlphaMode::Mask),
|
||||
double_sided: true,
|
||||
alpha_mode: Valid(alpha_mode),
|
||||
pbr_metallic_roughness: PbrMetallicRoughness {
|
||||
base_color_factor: PbrBaseColorFactor(
|
||||
material.color.map(|channel| channel as f32 / 255.0),
|
||||
),
|
||||
base_color_texture: texture_index.map(|index| Info {
|
||||
index,
|
||||
tex_coord: 0,
|
||||
extensions: None,
|
||||
extras: Extras::default(),
|
||||
}),
|
||||
..PbrMetallicRoughness::default()
|
||||
},
|
||||
..Material::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn push_or_get_texture(
|
||||
buffer: &mut Vec<u8>,
|
||||
gltf: &mut Root,
|
||||
texture: TextureData,
|
||||
) -> Index<Texture> {
|
||||
match get_texture_index(&mut gltf.textures, &texture.name) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let index = gltf.textures.len() as u32;
|
||||
let texture = push_texture(buffer, gltf, texture);
|
||||
gltf.textures.push(texture);
|
||||
Index::new(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_texture_index(textures: &[Texture], name: &str) -> Option<Index<Texture>> {
|
||||
textures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, tex)| (tex.name.as_deref() == Some(name)).then_some(i))
|
||||
.map(|i| Index::new(i as u32))
|
||||
}
|
||||
|
||||
fn push_texture(buffer: &mut Vec<u8>, gltf: &mut Root, texture: TextureData) -> Texture {
|
||||
let mut image = texture.image;
|
||||
if image.color() != ColorType::Rgba8 && image.color() != ColorType::Rgb8 {
|
||||
if image.color().has_alpha() {
|
||||
image = DynamicImage::ImageRgba8(image.into_rgba8());
|
||||
} else {
|
||||
image = DynamicImage::ImageRgb8(image.into_rgb8());
|
||||
}
|
||||
}
|
||||
let buffer_start = buffer.len() as u32;
|
||||
let view_start = gltf.buffer_views.len() as u32;
|
||||
let image_start = gltf.images.len() as u32;
|
||||
|
||||
let mut png_buffer = Vec::new();
|
||||
let encoder = PngEncoder::new(&mut png_buffer);
|
||||
encoder
|
||||
.encode(
|
||||
image.as_bytes(),
|
||||
image.width(),
|
||||
image.height(),
|
||||
image.color(),
|
||||
)
|
||||
.expect("failed to encode");
|
||||
|
||||
buffer.extend_from_slice(&png_buffer);
|
||||
|
||||
let byte_length = buffer.len() as u32 - buffer_start;
|
||||
pad_byte_vector(buffer);
|
||||
|
||||
let view = View {
|
||||
buffer: Index::new(0),
|
||||
byte_length,
|
||||
byte_offset: Some(buffer_start),
|
||||
byte_stride: None,
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
name: Some(texture.name.clone()),
|
||||
target: None,
|
||||
};
|
||||
|
||||
gltf.buffer_views.push(view);
|
||||
|
||||
let image = Image {
|
||||
buffer_view: Some(Index::new(view_start)),
|
||||
mime_type: Some(MimeType("image/png".into())),
|
||||
name: Some(texture.name.clone()),
|
||||
uri: None,
|
||||
extensions: None,
|
||||
extras: Default::default(),
|
||||
};
|
||||
gltf.images.push(image);
|
||||
|
||||
Texture {
|
||||
name: Some(texture.name),
|
||||
sampler: None,
|
||||
source: Index::new(image_start),
|
||||
extensions: None,
|
||||
extras: Default::default(),
|
||||
}
|
||||
}
|
||||
132
src/main.rs
Normal file
132
src/main.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use gltf_json as json;
|
||||
mod bsp;
|
||||
mod error;
|
||||
pub mod gltf_builder;
|
||||
mod materials;
|
||||
// mod prop;
|
||||
|
||||
use crate::bsp::{bsp_models, push_bsp_model};
|
||||
use clap::Parser;
|
||||
pub use error::Error;
|
||||
use gltf::Glb;
|
||||
use gltf_json::{Buffer, Index, Root, Scene};
|
||||
use miette::Context;
|
||||
use std::borrow::Cow;
|
||||
use std::fs::{read, File};
|
||||
use std::path::PathBuf;
|
||||
use tf_asset_loader::Loader;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_tree::HierarchicalLayer;
|
||||
use vbsp::Bsp;
|
||||
|
||||
fn setup() {
|
||||
miette::set_panic_hook();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::from_default_env())
|
||||
.with(
|
||||
HierarchicalLayer::new(2)
|
||||
.with_targets(true)
|
||||
.with_bracketed_fields(true),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
/// View a demo file
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Path of the map file
|
||||
source: PathBuf,
|
||||
/// Path to save the glb to
|
||||
target: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> miette::Result<()> {
|
||||
setup();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let mut loader = Loader::new().map_err(Error::from)?;
|
||||
let data = read(args.source).map_err(Error::from)?;
|
||||
let map = Bsp::read(&data).map_err(Error::from)?;
|
||||
loader.add_source(map.pack.clone());
|
||||
|
||||
let glb = export(map, &mut loader)?;
|
||||
|
||||
let writer = File::create(&args.target)
|
||||
.map_err(Error::from)
|
||||
.wrap_err("Failed to open target")?;
|
||||
|
||||
glb.to_writer(writer)
|
||||
.map_err(Error::from)
|
||||
.wrap_err("glTF binary output error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export(bsp: Bsp, loader: &mut Loader) -> Result<Glb<'static>, Error> {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
let mut root = Root::default();
|
||||
|
||||
for (model, offset) in bsp_models(&bsp)? {
|
||||
let node = push_bsp_model(&mut buffer, &mut root, loader, &model, offset);
|
||||
root.nodes.push(node);
|
||||
}
|
||||
|
||||
let node_indices = 0..root.nodes.len();
|
||||
root.scenes = vec![Scene {
|
||||
name: None,
|
||||
extensions: None,
|
||||
extras: Default::default(),
|
||||
nodes: node_indices.map(|index| Index::new(index as u32)).collect(),
|
||||
}];
|
||||
|
||||
root.buffers.push(Buffer {
|
||||
byte_length: buffer.len() as u32,
|
||||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
name: None,
|
||||
uri: None,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
pad_byte_vector(&mut buffer);
|
||||
Ok(Glb {
|
||||
header: gltf::binary::Header {
|
||||
magic: *b"glTF",
|
||||
version: 2,
|
||||
length: json_offset + buffer.len() as u32,
|
||||
},
|
||||
bin: Some(Cow::Owned(buffer)),
|
||||
json: Cow::Owned(json_string.into_bytes()),
|
||||
})
|
||||
}
|
||||
|
||||
fn align_to_multiple_of_four(n: &mut u32) {
|
||||
*n = (*n + 3) & !3;
|
||||
}
|
||||
|
||||
fn pad_byte_vector(vec: &mut Vec<u8>) {
|
||||
while vec.len() % 4 != 0 {
|
||||
vec.push(0); // pad to multiple of four bytes
|
||||
}
|
||||
}
|
||||
|
||||
// 1 hammer unit is ~1.905cm
|
||||
pub const UNIT_SCALE: f32 = 1.0 / (1.905 * 100.0);
|
||||
|
||||
pub fn map_coords<C: Into<[f32; 3]>>(vec: C) -> [f32; 3] {
|
||||
let vec = vec.into();
|
||||
[
|
||||
vec[1] * UNIT_SCALE,
|
||||
vec[2] * UNIT_SCALE,
|
||||
vec[0] * UNIT_SCALE,
|
||||
]
|
||||
}
|
||||
129
src/materials.rs
Normal file
129
src/materials.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use crate::Error;
|
||||
use image::DynamicImage;
|
||||
use tf_asset_loader::Loader;
|
||||
use tracing::{error, instrument};
|
||||
use vmt_parser::from_str;
|
||||
use vmt_parser::material::{Material, WaterMaterial};
|
||||
use vtf::vtf::VTF;
|
||||
|
||||
pub fn load_material_fallback(name: &str, search_dirs: &[String], loader: &Loader) -> MaterialData {
|
||||
match load_material(name, search_dirs, loader) {
|
||||
Ok(mat) => mat,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "failed to load material");
|
||||
MaterialData {
|
||||
name: name.into(),
|
||||
color: [255, 0, 255, 255],
|
||||
..MaterialData::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MaterialData {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub color: [u8; 4],
|
||||
pub texture: Option<TextureData>,
|
||||
pub alpha_test: Option<f32>,
|
||||
pub bump_map: Option<TextureData>,
|
||||
pub translucent: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextureData {
|
||||
pub name: String,
|
||||
pub image: DynamicImage,
|
||||
}
|
||||
|
||||
#[instrument(skip(loader))]
|
||||
pub fn load_material(
|
||||
name: &str,
|
||||
search_dirs: &[String],
|
||||
loader: &Loader,
|
||||
) -> Result<MaterialData, Error> {
|
||||
let dirs = search_dirs
|
||||
.iter()
|
||||
.map(|dir| {
|
||||
format!(
|
||||
"materials/{}",
|
||||
dir.to_ascii_lowercase().trim_start_matches("/")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let path = format!("{}.vmt", name.to_ascii_lowercase().trim_end_matches(".vmt"));
|
||||
let path = loader
|
||||
.find_in_paths(&path, &dirs)
|
||||
.ok_or(Error::Other(format!("Can't find file {}", path)))?;
|
||||
let raw = loader.load(&path)?.expect("didn't find foudn path?");
|
||||
let vdf = String::from_utf8(raw)?;
|
||||
|
||||
let material = from_str(&vdf).map_err(|e| {
|
||||
let report = miette::ErrReport::new(e);
|
||||
println!("{:?}", report);
|
||||
Error::Other(format!("Failed to load material {}", path))
|
||||
})?;
|
||||
let material = material.resolve(|path| {
|
||||
let data = loader
|
||||
.load(path)?
|
||||
.ok_or(Error::Other(format!("Can't find file {}", path)))?;
|
||||
let vdf = String::from_utf8(data)?;
|
||||
Ok::<_, Error>(vdf)
|
||||
})?;
|
||||
|
||||
if let Material::Water(WaterMaterial {
|
||||
base_texture: None, ..
|
||||
}) = &material
|
||||
{
|
||||
return Ok(MaterialData {
|
||||
color: [82, 180, 217, 128],
|
||||
name: name.into(),
|
||||
path,
|
||||
texture: None,
|
||||
bump_map: None,
|
||||
alpha_test: None,
|
||||
translucent: true,
|
||||
});
|
||||
}
|
||||
|
||||
let base_texture = material.base_texture();
|
||||
|
||||
let translucent = material.translucent();
|
||||
let glass = material.surface_prop() == Some("glass");
|
||||
let alpha_test = material.alpha_test();
|
||||
let texture = load_texture(base_texture, loader)?;
|
||||
|
||||
let bump_map = material.bump_map().and_then(|path| {
|
||||
Some(TextureData {
|
||||
image: load_texture(&path, loader).ok()?,
|
||||
name: path.into(),
|
||||
})
|
||||
});
|
||||
|
||||
Ok(MaterialData {
|
||||
color: [255; 4],
|
||||
name: name.into(),
|
||||
path,
|
||||
texture: Some(TextureData {
|
||||
name: base_texture.into(),
|
||||
image: texture,
|
||||
}),
|
||||
bump_map,
|
||||
alpha_test,
|
||||
translucent: translucent | glass,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_texture(name: &str, loader: &Loader) -> Result<DynamicImage, Error> {
|
||||
let path = format!(
|
||||
"materials/{}.vtf",
|
||||
name.trim_end_matches(".vtf").trim_start_matches('/')
|
||||
);
|
||||
let mut raw = loader
|
||||
.load(&path)?
|
||||
.ok_or(Error::Other(format!("Can't find file {}", path)))?;
|
||||
let vtf = VTF::read(&mut raw)?;
|
||||
let image = vtf.highres_image.decode(0)?;
|
||||
Ok(image)
|
||||
}
|
||||
127
src/prop.rs
Normal file
127
src/prop.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use crate::bsp::map_coords;
|
||||
use crate::materials::{load_material_fallback};
|
||||
use crate::Error;
|
||||
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;
|
||||
|
||||
#[tracing::instrument(skip(loader))]
|
||||
pub fn load_prop(loader: &Loader, name: &str) -> Result<vmdl::Model, Error> {
|
||||
let load = |name: &str| -> Result<Vec<u8>, Error> {
|
||||
loader
|
||||
.load(name)?
|
||||
.ok_or(Error::ResourceNotFound(name.into()))
|
||||
};
|
||||
let mdl = Mdl::read(&load(name)?)?;
|
||||
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))
|
||||
}
|
||||
pub fn load_props<'a, I: Iterator<Item = Handle<'a, StaticPropLump>>>(
|
||||
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,
|
||||
skin: i32,
|
||||
}
|
||||
|
||||
fn prop_to_meshes<'a>(prop: &'a PropData) -> impl Iterator<Item = Primitive> + 'a {
|
||||
let transform = prop.transform;
|
||||
let model = &prop.model;
|
||||
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
model.meshes().map(move |mesh| {
|
||||
let material_index = skin.texture_index(mesh.material_index());
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
Primitive {
|
||||
name: mesh.model_name.into(),
|
||||
transformation: transform,
|
||||
animations: vec![],
|
||||
geometry,
|
||||
material_index,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn prop_texture_to_material(texture: &TextureInfo, loader: &Loader) -> CpuMaterial {
|
||||
convert_material(load_material_fallback(
|
||||
&texture.name,
|
||||
&texture.search_paths,
|
||||
loader,
|
||||
))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue