mirror of
https://codeberg.org/icewind/vbsp-to-gltf.git
synced 2026-06-03 18:24:07 +02:00
add convert options
This commit is contained in:
parent
1161fcf2ad
commit
16d20a5faf
11 changed files with 171 additions and 41 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -2181,6 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8277e703c934b9693d0773d5749faacc6366b3d81d012da556a4cfd4ab87f336"
|
||||
dependencies = [
|
||||
"libm",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2610,6 +2611,7 @@ dependencies = [
|
|||
name = "vbsp-to-gltf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"async-tempfile",
|
||||
"axum",
|
||||
"bytemuck",
|
||||
|
|
@ -2622,6 +2624,7 @@ dependencies = [
|
|||
"miette",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"texpresso",
|
||||
"tf-asset-loader",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
|
|||
12
Cargo.toml
12
Cargo.toml
|
|
@ -34,9 +34,10 @@ gltf-json = { version = "1.4.0", features = ["KHR_texture_transform"] }
|
|||
gltf = "1.4.0"
|
||||
cgmath = "0.18.0"
|
||||
bytemuck = { version = "1.14.0", features = ["derive"] }
|
||||
texpresso = { version = "2.0.1", features = ["rayon"] }
|
||||
serde = "1.0.193"
|
||||
|
||||
url = { version = "2.5.0", optional = true, features = ["serde"] }
|
||||
serde = { version = "1.0.193", optional = true }
|
||||
toml = { version = "0.8.8", optional = true }
|
||||
axum = { version = "0.7.2", optional = true, features = ["macros"] }
|
||||
tokio = { version = "1.35.1", features = ["full"], optional = true }
|
||||
|
|
@ -44,9 +45,14 @@ reqwest = { version = "0.11.23", optional = true, default-features = false, feat
|
|||
async-tempfile = { version = "0.5.0", optional = true }
|
||||
tower-http = { version = "0.5.0", optional = true, features = ["cors"] }
|
||||
http = { version = "1.0.0", optional = true }
|
||||
ahash = { version = "0.8.6", optional = true }
|
||||
|
||||
[features]
|
||||
server = ["url", "serde", "toml", "axum", "tokio", "reqwest", "async-tempfile", "tower-http", "http"]
|
||||
server = ["url", "toml", "axum", "tokio", "reqwest", "async-tempfile", "tower-http", "http", "ahash"]
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
opt-level = 2
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
19
src/bsp.rs
19
src/bsp.rs
|
|
@ -1,6 +1,7 @@
|
|||
use crate::convert::map_coords;
|
||||
use crate::error::Error;
|
||||
use crate::gltf_builder::push_or_get_material;
|
||||
use crate::ConvertOptions;
|
||||
use bytemuck::{offset_of, Pod, Zeroable};
|
||||
use gltf_json::accessor::{ComponentType, GenericComponentType, Type};
|
||||
use gltf_json::buffer::{Stride, Target, View};
|
||||
|
|
@ -73,11 +74,12 @@ pub fn push_bsp_model(
|
|||
loader: &Loader,
|
||||
model: &Handle<Model>,
|
||||
offset: Vector,
|
||||
options: &ConvertOptions,
|
||||
) -> Node {
|
||||
let primitives = model
|
||||
.faces()
|
||||
.filter(|face| face.is_visible())
|
||||
.map(|face| push_bsp_face(buffer, gltf, loader, &face))
|
||||
.map(|face| push_bsp_face(buffer, gltf, loader, &face, options))
|
||||
.collect();
|
||||
|
||||
let mesh = Mesh {
|
||||
|
|
@ -112,6 +114,7 @@ pub fn push_bsp_face(
|
|||
gltf: &mut Root,
|
||||
loader: &Loader,
|
||||
face: &Handle<Face>,
|
||||
options: &ConvertOptions,
|
||||
) -> Primitive {
|
||||
let vertex_count = face.vertex_positions().count() as u64;
|
||||
|
||||
|
|
@ -175,7 +178,17 @@ pub fn push_bsp_face(
|
|||
gltf.accessors.push(positions);
|
||||
gltf.accessors.push(uvs);
|
||||
|
||||
let material_index = push_or_get_material(buffer, gltf, loader, face.texture().name());
|
||||
let material_index = if options.textures {
|
||||
Some(push_or_get_material(
|
||||
buffer,
|
||||
gltf,
|
||||
loader,
|
||||
face.texture().name(),
|
||||
options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Primitive {
|
||||
attributes: {
|
||||
|
|
@ -190,7 +203,7 @@ pub fn push_bsp_face(
|
|||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
indices: None,
|
||||
material: Some(material_index),
|
||||
material: material_index,
|
||||
mode: Valid(Mode::Triangles),
|
||||
targets: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
|||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_tree::HierarchicalLayer;
|
||||
use vbsp::Bsp;
|
||||
use vbsp_to_gltf::{export, Error};
|
||||
use vbsp_to_gltf::{export, ConvertOptions, Error};
|
||||
|
||||
fn setup() {
|
||||
miette::set_panic_hook();
|
||||
|
|
@ -43,7 +43,7 @@ fn main() -> miette::Result<()> {
|
|||
let map = Bsp::read(&data).map_err(Error::from)?;
|
||||
loader.add_source(map.pack.clone().into_zip());
|
||||
|
||||
let glb = export(map, &loader)?;
|
||||
let glb = export(map, &loader, ConvertOptions::default())?;
|
||||
|
||||
let writer = File::create(&args.target)
|
||||
.map_err(Error::from)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use gltf_json as json;
|
|||
|
||||
use crate::bsp::{bsp_models, push_bsp_model};
|
||||
use crate::prop::push_or_get_model;
|
||||
use crate::Error;
|
||||
use crate::{ConvertOptions, Error};
|
||||
use cgmath::{Deg, Quaternion, Rotation3};
|
||||
use gltf::Glb;
|
||||
use gltf_json::scene::UnitQuaternion;
|
||||
|
|
@ -12,18 +12,25 @@ use std::borrow::Cow;
|
|||
use tf_asset_loader::Loader;
|
||||
use vbsp::Bsp;
|
||||
|
||||
pub fn export(bsp: Bsp, loader: &Loader) -> Result<Glb<'static>, Error> {
|
||||
pub fn export(bsp: Bsp, loader: &Loader, options: ConvertOptions) -> 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);
|
||||
let node = push_bsp_model(&mut buffer, &mut root, loader, &model, offset, &options);
|
||||
root.nodes.push(node);
|
||||
}
|
||||
|
||||
for prop in bsp.static_props() {
|
||||
let mesh = push_or_get_model(&mut buffer, &mut root, loader, prop.model(), prop.skin);
|
||||
let mesh = push_or_get_model(
|
||||
&mut buffer,
|
||||
&mut root,
|
||||
loader,
|
||||
prop.model(),
|
||||
prop.skin,
|
||||
&options,
|
||||
);
|
||||
let rotation = prop.rotation();
|
||||
|
||||
let node = Node {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::convert::pad_byte_vector;
|
||||
use crate::materials::{load_material_fallback, MaterialData, TextureData};
|
||||
use crate::ConvertOptions;
|
||||
use gltf_json::buffer::View;
|
||||
use gltf_json::extensions::texture::{
|
||||
TextureTransform, TextureTransformOffset, TextureTransformRotation, TextureTransformScale,
|
||||
|
|
@ -20,12 +21,13 @@ pub fn push_or_get_material(
|
|||
gltf: &mut Root,
|
||||
loader: &Loader,
|
||||
material: &str,
|
||||
options: &ConvertOptions,
|
||||
) -> Index<Material> {
|
||||
let material = material.to_ascii_lowercase();
|
||||
match get_material_index(&gltf.materials, &material) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
let material = load_material_fallback(&material, &[String::new()], loader);
|
||||
let material = load_material_fallback(&material, &[String::new()], loader, options);
|
||||
let index = gltf.materials.len() as u32;
|
||||
let material = push_material(buffer, gltf, material);
|
||||
gltf.materials.push(material);
|
||||
|
|
|
|||
37
src/lib.rs
37
src/lib.rs
|
|
@ -5,5 +5,42 @@ pub mod gltf_builder;
|
|||
mod materials;
|
||||
mod prop;
|
||||
|
||||
use ahash::RandomState;
|
||||
pub use convert::export;
|
||||
pub use error::Error;
|
||||
use serde::Deserialize;
|
||||
use std::hash::{BuildHasher, Hash, Hasher};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ConvertOptions {
|
||||
#[serde(default = "default_enable")]
|
||||
pub textures: bool,
|
||||
#[serde(default = "default_scale")]
|
||||
pub texture_scale: f32,
|
||||
}
|
||||
|
||||
impl ConvertOptions {
|
||||
pub fn key(&self) -> u64 {
|
||||
let mut hasher = RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||
self.textures.hash(&mut hasher);
|
||||
self.texture_scale.to_le_bytes().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConvertOptions {
|
||||
fn default() -> Self {
|
||||
ConvertOptions {
|
||||
textures: true,
|
||||
texture_scale: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_enable() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_scale() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
use crate::Error;
|
||||
use image::DynamicImage;
|
||||
use crate::{ConvertOptions, Error};
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use tf_asset_loader::Loader;
|
||||
use tracing::{error, instrument};
|
||||
use vmt_parser::material::{Material, WaterMaterial};
|
||||
use vmt_parser::{from_str, TextureTransform};
|
||||
use vtf::vtf::VTF;
|
||||
|
||||
pub fn load_material_fallback(name: &str, search_dirs: &[String], loader: &Loader) -> MaterialData {
|
||||
match load_material(name, search_dirs, loader) {
|
||||
pub fn load_material_fallback(
|
||||
name: &str,
|
||||
search_dirs: &[String],
|
||||
loader: &Loader,
|
||||
options: &ConvertOptions,
|
||||
) -> MaterialData {
|
||||
match load_material(name, search_dirs, loader, options) {
|
||||
Ok(mat) => mat,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "failed to load material");
|
||||
|
|
@ -44,6 +50,7 @@ pub fn load_material(
|
|||
name: &str,
|
||||
search_dirs: &[String],
|
||||
loader: &Loader,
|
||||
options: &ConvertOptions,
|
||||
) -> Result<MaterialData, Error> {
|
||||
let dirs = search_dirs
|
||||
.iter()
|
||||
|
|
@ -94,11 +101,11 @@ pub fn load_material(
|
|||
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 texture = load_texture(base_texture, loader, options)?;
|
||||
|
||||
let bump_map = material.bump_map().and_then(|path| {
|
||||
Some(TextureData {
|
||||
image: load_texture(path, loader).ok()?,
|
||||
image: load_texture(path, loader, options).ok()?,
|
||||
name: path.into(),
|
||||
})
|
||||
});
|
||||
|
|
@ -124,7 +131,11 @@ pub fn load_material(
|
|||
})
|
||||
}
|
||||
|
||||
fn load_texture(name: &str, loader: &Loader) -> Result<DynamicImage, Error> {
|
||||
fn load_texture(
|
||||
name: &str,
|
||||
loader: &Loader,
|
||||
options: &ConvertOptions,
|
||||
) -> Result<DynamicImage, Error> {
|
||||
let path = format!(
|
||||
"materials/{}.vtf",
|
||||
name.trim_end_matches(".vtf").trim_start_matches('/')
|
||||
|
|
@ -134,5 +145,13 @@ fn load_texture(name: &str, loader: &Loader) -> Result<DynamicImage, Error> {
|
|||
.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)
|
||||
if options.texture_scale != 1.0 {
|
||||
Ok(image.resize(
|
||||
(image.width() as f32 * options.texture_scale) as u32,
|
||||
(image.height() as f32 * options.texture_scale) as u32,
|
||||
FilterType::CatmullRom,
|
||||
))
|
||||
} else {
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
src/prop.rs
43
src/prop.rs
|
|
@ -1,6 +1,6 @@
|
|||
use crate::convert::map_coords;
|
||||
use crate::gltf_builder::push_or_get_material;
|
||||
use crate::Error;
|
||||
use crate::{ConvertOptions, Error};
|
||||
use bytemuck::{offset_of, Pod, Zeroable};
|
||||
use gltf_json::accessor::{ComponentType, GenericComponentType, Type};
|
||||
use gltf_json::buffer::{Stride, Target, View};
|
||||
|
|
@ -125,6 +125,7 @@ pub fn push_or_get_model(
|
|||
loader: &Loader,
|
||||
model: &str,
|
||||
skin: i32,
|
||||
options: &ConvertOptions,
|
||||
) -> Index<Mesh> {
|
||||
let skinned_name = format!("{model}_{skin}");
|
||||
match get_mesh_index(&gltf.meshes, &skinned_name) {
|
||||
|
|
@ -132,7 +133,7 @@ pub fn push_or_get_model(
|
|||
None => {
|
||||
let prop = load_prop(loader, model).expect("failed to load prop");
|
||||
let index = gltf.meshes.len() as u32;
|
||||
let material = push_model(buffer, gltf, loader, &prop, skin, skinned_name);
|
||||
let material = push_model(buffer, gltf, loader, &prop, skin, skinned_name, options);
|
||||
gltf.meshes.push(material);
|
||||
Index::new(index)
|
||||
}
|
||||
|
|
@ -154,6 +155,7 @@ pub fn push_model(
|
|||
model: &Model,
|
||||
skin: i32,
|
||||
skinned_name: String,
|
||||
options: &ConvertOptions,
|
||||
) -> Mesh {
|
||||
let accessor_start = gltf.accessors.len() as u32;
|
||||
push_vertices(buffer, gltf, model);
|
||||
|
|
@ -164,7 +166,17 @@ pub fn push_model(
|
|||
|
||||
let primitives = model
|
||||
.meshes()
|
||||
.map(|mesh| push_primitive(buffer, gltf, loader, &mesh, accessor_start, &skin_table))
|
||||
.map(|mesh| {
|
||||
push_primitive(
|
||||
buffer,
|
||||
gltf,
|
||||
loader,
|
||||
&mesh,
|
||||
accessor_start,
|
||||
&skin_table,
|
||||
options,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Mesh {
|
||||
|
|
@ -183,6 +195,7 @@ pub fn push_primitive(
|
|||
mesh: &vmdl::Mesh,
|
||||
vertex_accessor_start: u32,
|
||||
skin: &SkinTable,
|
||||
options: &ConvertOptions,
|
||||
) -> Primitive {
|
||||
let buffer_start = buffer.len() as u64;
|
||||
let view_start = gltf.buffer_views.len() as u32;
|
||||
|
|
@ -224,12 +237,22 @@ pub fn push_primitive(
|
|||
};
|
||||
gltf.accessors.push(accessor);
|
||||
|
||||
let texture = skin
|
||||
.texture_info(mesh.material_index())
|
||||
.expect("mat out of bounds");
|
||||
let texture_path = find_material(&texture.name, &texture.search_paths, loader)
|
||||
.expect("failed to find texture");
|
||||
let material_index = push_or_get_material(buffer, gltf, loader, &texture_path);
|
||||
let material = if options.textures {
|
||||
let texture = skin
|
||||
.texture_info(mesh.material_index())
|
||||
.expect("mat out of bounds");
|
||||
let texture_path = find_material(&texture.name, &texture.search_paths, loader)
|
||||
.expect("failed to find texture");
|
||||
Some(push_or_get_material(
|
||||
buffer,
|
||||
gltf,
|
||||
loader,
|
||||
&texture_path,
|
||||
options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Primitive {
|
||||
attributes: {
|
||||
|
|
@ -251,7 +274,7 @@ pub fn push_primitive(
|
|||
extensions: Default::default(),
|
||||
extras: Default::default(),
|
||||
indices: Some(Index::new(accessor_start)),
|
||||
material: Some(material_index),
|
||||
material,
|
||||
mode: Valid(Mode::Triangles),
|
||||
targets: None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,13 @@
|
|||
<aside>
|
||||
Note that map conversion can take 10 to 30 seconds, once a map has been converted once it is cached for subsequent requests.
|
||||
</aside>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
The following options can be set as query parameters
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>textures=false</code>: disable textures</li>
|
||||
<li><code>texture_scale=<scale></code>: scale textures, lower scale for significantly smaller filesizes</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
mod pack;
|
||||
|
||||
use crate::pack::pack;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::routing::get;
|
||||
|
|
@ -27,7 +27,7 @@ use tracing_subscriber::EnvFilter;
|
|||
use tracing_tree::HierarchicalLayer;
|
||||
use url::Url;
|
||||
use vbsp::{Bsp, BspError};
|
||||
use vbsp_to_gltf::{export, Error};
|
||||
use vbsp_to_gltf::{export, ConvertOptions, Error};
|
||||
|
||||
type Result<T, E = ServerError> = std::result::Result<T, E>;
|
||||
|
||||
|
|
@ -163,8 +163,12 @@ struct App {
|
|||
}
|
||||
|
||||
impl App {
|
||||
fn cached(&self, map: &str) -> Result<Option<Vec<u8>>> {
|
||||
let path = self.cache_dir.join(map);
|
||||
fn cache_path(&self, map: &str, options_key: u64) -> PathBuf {
|
||||
self.cache_dir.join(format!("{options_key:016x}_{map}"))
|
||||
}
|
||||
|
||||
fn cached(&self, map: &str, options_key: u64) -> Result<Option<Vec<u8>>> {
|
||||
let path = self.cache_path(map, options_key);
|
||||
if path.exists() {
|
||||
Ok(Some(read(path)?))
|
||||
} else {
|
||||
|
|
@ -172,8 +176,8 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn cache(&self, map: &str, data: &[u8]) -> Result<()> {
|
||||
let path = self.cache_dir.join(map);
|
||||
fn cache(&self, map: &str, data: &[u8], options_key: u64) -> Result<()> {
|
||||
let path = self.cache_path(map, options_key);
|
||||
Ok(write(path, data)?)
|
||||
}
|
||||
|
||||
|
|
@ -198,11 +202,19 @@ async fn index() -> impl IntoResponse {
|
|||
Html(include_str!("./index.html"))
|
||||
}
|
||||
|
||||
async fn convert(State(app): State<Arc<App>>, Path(map): Path<String>) -> impl IntoResponse {
|
||||
async fn convert(
|
||||
State(app): State<Arc<App>>,
|
||||
Path(map): Path<String>,
|
||||
Query(mut options): Query<ConvertOptions>,
|
||||
) -> impl IntoResponse {
|
||||
if options.texture_scale > 1.0 {
|
||||
options.texture_scale = 1.0;
|
||||
}
|
||||
let options_key = options.key();
|
||||
if !map.is_ascii() || map.contains('/') || !map.ends_with(".glb") {
|
||||
return Err(ServerError::InvalidMapName(map));
|
||||
}
|
||||
if let Some(cached) = app.cached(&map)? {
|
||||
if let Some(cached) = app.cached(&map, options_key)? {
|
||||
info!(map = map, "serving cached model");
|
||||
return Ok(cached);
|
||||
}
|
||||
|
|
@ -215,7 +227,7 @@ async fn convert(State(app): State<Arc<App>>, Path(map): Path<String>) -> impl I
|
|||
let bsp = Bsp::read(&bsp_data).map_err(Error::from)?;
|
||||
loader.add_source(bsp.pack.clone().into_zip());
|
||||
|
||||
let glb = export(bsp, &loader)?;
|
||||
let glb = export(bsp, &loader, options)?;
|
||||
let glb = glb.to_vec().map_err(Error::from)?;
|
||||
let packed = pack(&map, &glb).await?;
|
||||
|
||||
|
|
@ -226,7 +238,7 @@ async fn convert(State(app): State<Arc<App>>, Path(map): Path<String>) -> impl I
|
|||
"optimized model"
|
||||
);
|
||||
|
||||
app.cache(&map, &packed)?;
|
||||
app.cache(&map, &packed, options_key)?;
|
||||
|
||||
Ok(packed)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue