merge props and optimizate material handling

This commit is contained in:
Robin Appelman 2023-12-26 16:14:25 +01:00
commit 77bcd106f9
4 changed files with 102 additions and 58 deletions

View file

@ -8,8 +8,6 @@ tf2 map viewer based on [vbsp](https://github.com/icewind1991/vbsp)
cargo run --release -- /path/to/map.bsp cargo run --release -- /path/to/map.bsp
``` ```
Note that asset loading isn't very well optimized so loading maps can take a while.
In order to load the assets referenced by the map, TF2 needs to be installed locally. In order to load the assets referenced by the map, TF2 needs to be installed locally.
![pl_badwater as rendered by the viewer](screenshots/badwater.png) ![pl_badwater as rendered by the viewer](screenshots/badwater.png)

View file

@ -105,7 +105,7 @@ fn model_to_model(
let materials: Vec<_> = textures let materials: Vec<_> = textures
.iter() .iter()
.map(|texture| load_material_fallback(texture, &["".into()], loader)) .map(|texture| load_material_fallback(texture, loader))
.map(convert_material) .map(convert_material)
.collect(); .collect();

View file

@ -1,20 +1,22 @@
use crate::Error; use crate::Error;
use image::{DynamicImage, GenericImageView}; use image::{DynamicImage, GenericImageView};
use std::cell::RefCell;
use tf_asset_loader::Loader; use tf_asset_loader::Loader;
use three_d::{CpuMaterial, CpuTexture}; use three_d::{CpuMaterial, CpuTexture};
use three_d_asset::Srgba; use three_d_asset::Srgba;
use tracing::{error, instrument}; use tracing::{error, instrument};
use vmdl::mdl::TextureInfo;
use vmt_parser::from_str; use vmt_parser::from_str;
use vmt_parser::material::{Material, WaterMaterial}; use vmt_parser::material::{Material, WaterMaterial};
use vtf::vtf::VTF; use vtf::vtf::VTF;
pub fn load_material_fallback(name: &str, search_dirs: &[String], loader: &Loader) -> MaterialData { pub fn load_material_fallback(name: &str, loader: &Loader) -> MaterialData {
match load_material(name, search_dirs, loader) { match load_material(name, loader) {
Ok(mat) => mat, Ok(mat) => mat,
Err(e) => { Err(e) => {
error!(error = ?e, material = name, "failed to load material"); error!(error = ?e, material = name, "failed to load material");
MaterialData { MaterialData {
name: name.into(), path: name.into(),
color: [255, 0, 255, 255], color: [255, 0, 255, 255],
..MaterialData::default() ..MaterialData::default()
} }
@ -24,7 +26,6 @@ pub fn load_material_fallback(name: &str, search_dirs: &[String], loader: &Loade
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct MaterialData { pub struct MaterialData {
pub name: String,
pub path: String, pub path: String,
pub color: [u8; 4], pub color: [u8; 4],
pub texture: Option<TextureData>, pub texture: Option<TextureData>,
@ -40,24 +41,15 @@ pub struct TextureData {
} }
#[instrument(skip(loader))] #[instrument(skip(loader))]
pub fn load_material( pub fn load_material(path: &str, loader: &Loader) -> Result<MaterialData, Error> {
name: &str, let path = if path.starts_with("materials/") {
search_dirs: &[String], path.to_string()
loader: &Loader, } else {
) -> Result<MaterialData, Error> { format!(
let dirs = search_dirs "materials/{}.vmt",
.iter() path.to_ascii_lowercase().trim_end_matches(".vmt")
.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::ResourceNotFound(path))?;
let raw = loader let raw = loader
.load(&path)? .load(&path)?
.ok_or_else(|| Error::ResourceNotFound(path.clone()))?; .ok_or_else(|| Error::ResourceNotFound(path.clone()))?;
@ -82,7 +74,6 @@ pub fn load_material(
{ {
return Ok(MaterialData { return Ok(MaterialData {
color: [82, 180, 217, 128], color: [82, 180, 217, 128],
name: name.into(),
path, path,
texture: None, texture: None,
bump_map: None, bump_map: None,
@ -107,7 +98,6 @@ pub fn load_material(
Ok(MaterialData { Ok(MaterialData {
color: [255; 4], color: [255; 4],
name: name.into(),
path, path,
texture: Some(TextureData { texture: Some(TextureData {
name: base_texture.into(), name: base_texture.into(),
@ -138,7 +128,7 @@ pub fn convert_material(material: MaterialData) -> CpuMaterial {
material.color[2], material.color[2],
material.color[3], material.color[3],
), ),
name: material.name, name: material.path,
albedo_texture: material albedo_texture: material
.texture .texture
.map(|tex| convert_texture(tex, material.translucent | material.alpha_test.is_some())), .map(|tex| convert_texture(tex, material.translucent | material.alpha_test.is_some())),
@ -177,3 +167,63 @@ pub fn convert_texture(texture: TextureData, keep_alpha: bool) -> CpuTexture {
..CpuTexture::default() ..CpuTexture::default()
} }
} }
#[derive(Debug)]
pub struct MaterialSet<'a> {
loader: &'a Loader,
materials: RefCell<Vec<String>>,
}
impl<'s> MaterialSet<'s> {
pub fn new(loader: &'s Loader) -> Self {
MaterialSet {
loader,
materials: RefCell::default(),
}
}
pub fn get_index(&self, material: &TextureInfo) -> usize {
let search_path = material
.search_paths
.iter()
.map(|dir| {
format!(
"materials/{}",
dir.to_ascii_lowercase().trim_start_matches('/')
)
})
.collect::<Vec<_>>();
let material = format!(
"{}.vmt",
material.name.to_ascii_lowercase().trim_end_matches(".vmt")
);
let material = if search_path.is_empty() {
material.to_string()
} else {
self.loader
.find_in_paths(&material, &search_path)
.unwrap_or(material.into())
};
let mut materials = self.materials.borrow_mut();
match materials
.iter()
.enumerate()
.find_map(|(i, name)| (*name == material).then_some(i))
{
Some(i) => i,
None => {
let i = materials.len();
materials.push(material);
i
}
}
}
pub fn into_iter(self) -> impl Iterator<Item = String> {
self.materials.into_inner().into_iter()
}
}

View file

@ -1,12 +1,12 @@
use crate::bsp::map_coords; use crate::bsp::map_coords;
use crate::material::{convert_material, load_material_fallback}; use crate::material::{convert_material, load_material_fallback, MaterialSet};
use crate::Error; use crate::Error;
use tf_asset_loader::Loader; use tf_asset_loader::Loader;
use three_d::{CpuMaterial, CpuModel, Mat4, Positions, Vec2, Vec3, Vec4}; use three_d::{CpuMaterial, CpuModel, Mat4, Positions, Vec2, Vec3, Vec4};
use three_d_asset::{Geometry, Primitive, TriMesh}; use three_d_asset::{Geometry, Primitive, TriMesh};
use tracing::{error, warn}; use tracing::{error, warn};
use vbsp::{Handle, StaticPropLump}; use vbsp::{Handle, StaticPropLump};
use vmdl::mdl::{Mdl, TextureInfo}; use vmdl::mdl::Mdl;
use vmdl::vtx::Vtx; use vmdl::vtx::Vtx;
use vmdl::vvd::Vvd; use vmdl::vvd::Vvd;
@ -28,7 +28,7 @@ pub fn load_props<'a, I: Iterator<Item = Handle<'a, StaticPropLump>>>(
props: I, props: I,
show_textures: bool, show_textures: bool,
) -> Result<Vec<CpuModel>, Error> { ) -> Result<Vec<CpuModel>, Error> {
let props = props let props: Vec<_> = props
.filter_map(|prop| match load_prop(loader, prop.model()) { .filter_map(|prop| match load_prop(loader, prop.model()) {
Ok(model) => Some((prop, model)), Ok(model) => Some((prop, model)),
Err(e) => { Err(e) => {
@ -45,28 +45,26 @@ pub fn load_props<'a, I: Iterator<Item = Handle<'a, StaticPropLump>>>(
transform, transform,
skin: prop.skin, skin: prop.skin,
} }
});
props
.map(|prop| {
let geometries: Vec<_> = prop_to_meshes(&prop, show_textures).collect();
let materials: Vec<_> = if show_textures {
prop.model
.textures()
.iter()
.map(|tex| prop_texture_to_material(tex, loader))
.collect()
} else {
Vec::new()
};
Ok(CpuModel {
name: prop.name.into(),
geometries,
materials,
})
}) })
.collect() .collect();
let used_materials = MaterialSet::new(loader);
let geometries = props
.iter()
.flat_map(|prop| prop_to_meshes(prop, &used_materials, show_textures))
.collect();
let materials = used_materials
.into_iter()
.map(|mat| prop_texture_to_material(&mat, loader))
.collect();
Ok(vec![CpuModel {
name: "props".into(),
geometries,
materials,
}])
} }
struct PropData<'a> { struct PropData<'a> {
@ -78,6 +76,7 @@ struct PropData<'a> {
fn prop_to_meshes<'a>( fn prop_to_meshes<'a>(
prop: &'a PropData, prop: &'a PropData,
used_materials: &'a MaterialSet<'a>,
show_textures: bool, show_textures: bool,
) -> impl Iterator<Item = Primitive> + 'a { ) -> impl Iterator<Item = Primitive> + 'a {
let transform = prop.transform; let transform = prop.transform;
@ -93,7 +92,8 @@ fn prop_to_meshes<'a>(
model.meshes().map(move |mesh| { model.meshes().map(move |mesh| {
let material_index = if show_textures { let material_index = if show_textures {
skin.texture_index(mesh.material_index()) skin.texture_info(mesh.material_index())
.map(|mat| used_materials.get_index(mat))
} else { } else {
None None
}; };
@ -131,10 +131,6 @@ fn prop_to_meshes<'a>(
}) })
} }
fn prop_texture_to_material(texture: &TextureInfo, loader: &Loader) -> CpuMaterial { fn prop_texture_to_material(texture: &str, loader: &Loader) -> CpuMaterial {
convert_material(load_material_fallback( convert_material(load_material_fallback(&texture, loader))
&texture.name,
&texture.search_paths,
loader,
))
} }