bsp geometry

This commit is contained in:
Robin Appelman 2023-12-20 23:28:48 +01:00
commit 304dc2d031
13 changed files with 3380 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

64
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,64 @@
name: "CI"
on:
pull_request:
push:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: icewind1991/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: '${{ secrets.ATTIC_TOKEN }}'
- run: nix build .#check
clippy:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: icewind1991/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: '${{ secrets.ATTIC_TOKEN }}'
- run: nix build .#clippy
test:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: icewind1991/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: '${{ secrets.ATTIC_TOKEN }}'
- run: nix build .#test
semver:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v3
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2
msrv:
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: icewind1991/attic-action@v1
with:
name: ci
instance: https://cache.icewind.me
authToken: '${{ secrets.ATTIC_TOKEN }}'
- run: nix build .#msrv

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
target
*.bench
*.obj
result
.direnv
*.snap.new
*.bsp
*.glb

2286
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "vbsp-to-gltf"
version = "0.1.0"
edition = "2021"
[dependencies]
miette = { version = "5.5.0", features = ["fancy"] }
vbsp = "0.4.0"
thiserror = "1.0.37"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-tree = "0.2.2"
vtf = "0.1.6"
vmt-parser = { version = "0.1", git = "https://github.com/icewind1991/vmt-parser" }
image = "0.23.14"
tf-asset-loader = { version = "0.1", features = ["bsp"] }
vmdl = { version = "*", git = "https://github.com/icewind1991/vmdl" }
clap = { version = "4.0.29", features = ["derive"] }
gltf-json = "=1.3.0"
gltf = "=1.3.0"
bytemuck = { version = "1.14.0", features = ["derive"] }
[profile.dev.package."*"]
opt-level = 2

105
flake.lock generated Normal file
View file

@ -0,0 +1,105 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1702346276,
"narHash": "sha256-eAQgwIWApFQ40ipeOjVSoK4TEHVd6nbSd9fApiHIw5A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cf28ee258fd5f9a52de6b9865cdb93a1f96d09b7",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"utils": "utils"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1702520151,
"narHash": "sha256-jxJWosN7hgcW+dFT8V3EBDCYUOjv5tpjEBRmlakS7tU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d6a1d8f80dbcda4c13993b859a3574c3dde61072",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

102
flake.nix Normal file
View file

@ -0,0 +1,102 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11";
utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
rust-overlay.inputs.flake-utils.follows = "utils";
};
outputs = {
self,
nixpkgs,
utils,
naersk,
rust-overlay,
}:
utils.lib.eachDefaultSystem (system: let
overlays = [(import rust-overlay)];
pkgs = (import nixpkgs) {
inherit system overlays;
};
inherit (pkgs) lib callPackage rust-bin mkShell;
inherit (lib.sources) sourceByRegex;
msrv = (fromTOML (readFile ./Cargo.toml)).package.rust-version;
inherit (builtins) fromTOML readFile;
toolchain = rust-bin.stable.latest.default;
msrvToolchain = rust-bin.stable."${msrv}".default;
naersk' = callPackage naersk {
rustc = toolchain;
cargo = toolchain;
};
msrvNaersk = callPackage naersk {
rustc = msrvToolchain;
cargo = msrvToolchain;
};
src = sourceByRegex ./. ["Cargo.*" "(src|derive|benches|tests|examples|koth_bagel.*)(/.*)?"];
nearskOpt = {
pname = "vbsp";
root = src;
};
in rec {
packages = {
check = naersk'.buildPackage (nearskOpt
// {
mode = "check";
});
clippy = naersk'.buildPackage (nearskOpt
// {
mode = "clippy";
});
test = naersk'.buildPackage (nearskOpt
// {
release = false;
mode = "test";
});
msrv = msrvNaersk.buildPackage (nearskOpt
// {
mode = "check";
});
};
devShells = let
tools = with pkgs; [
bacon
cargo-edit
cargo-outdated
cargo-audit
cargo-msrv
cargo-semver-checks
cargo-insta
(writeShellApplication {
name = "cargo-fuzz";
runtimeInputs = [cargo-fuzz toolchain];
text = ''
# shellcheck disable=SC2068
RUSTC_BOOTSTRAP=1 cargo-fuzz $@
'';
})
(writeShellApplication {
name = "cargo-expand";
runtimeInputs = [cargo-expand toolchain];
text = ''
# shellcheck disable=SC2068
RUSTC_BOOTSTRAP=1 cargo-expand $@
'';
})
];
in {
default = mkShell {
nativeBuildInputs = [toolchain] ++ tools;
};
msrv = mkShell {
nativeBuildInputs = [msrvToolchain] ++ tools;
};
};
});
}

195
src/bsp.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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,
))
}