mirror of
https://codeberg.org/icewind/vbsp-to-gltf.git
synced 2026-06-03 10:14:08 +02:00
bsp geometry
This commit is contained in:
commit
304dc2d031
13 changed files with 3380 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal 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
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
target
|
||||||
|
*.bench
|
||||||
|
*.obj
|
||||||
|
result
|
||||||
|
.direnv
|
||||||
|
*.snap.new
|
||||||
|
*.bsp
|
||||||
|
*.glb
|
||||||
2286
Cargo.lock
generated
Normal file
2286
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal 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
105
flake.lock
generated
Normal 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
102
flake.nix
Normal 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
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