mirror of
https://codeberg.org/icewind/vmdl.git
synced 2026-06-03 08:34:23 +02:00
gltf: textures
This commit is contained in:
parent
150709864c
commit
9dc540db5c
5 changed files with 303 additions and 9 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2717,6 +2717,7 @@ dependencies = [
|
||||||
"gltf",
|
"gltf",
|
||||||
"gltf-json",
|
"gltf-json",
|
||||||
"iai",
|
"iai",
|
||||||
|
"image 0.23.14",
|
||||||
"itertools 0.12.0",
|
"itertools 0.12.0",
|
||||||
"miette",
|
"miette",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ tracing-subscriber = "0.3.18"
|
||||||
steamy-vdf = { version = "0.3.0", git = "https://github.com/icewind1991/steamy", branch = "nom7" }
|
steamy-vdf = { version = "0.3.0", git = "https://github.com/icewind1991/steamy", branch = "nom7" }
|
||||||
gltf-json = "1.3.0"
|
gltf-json = "1.3.0"
|
||||||
gltf = "1.3.0"
|
gltf = "1.3.0"
|
||||||
|
image = "0.23.14"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "parse"
|
name = "parse"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
use crate::material::MaterialData;
|
||||||
use bytemuck::{offset_of, Pod, Zeroable};
|
use bytemuck::{offset_of, Pod, Zeroable};
|
||||||
use gltf_json::accessor::{ComponentType, GenericComponentType, Type};
|
use gltf_json::accessor::{ComponentType, GenericComponentType, Type};
|
||||||
use gltf_json::buffer::{Target, View};
|
use gltf_json::buffer::{Target, View};
|
||||||
|
use gltf_json::image::MimeType;
|
||||||
|
use gltf_json::material::{AlphaCutoff, AlphaMode, PbrBaseColorFactor, PbrMetallicRoughness};
|
||||||
use gltf_json::mesh::{Mode, Primitive, Semantic};
|
use gltf_json::mesh::{Mode, Primitive, Semantic};
|
||||||
|
use gltf_json::texture::Info;
|
||||||
use gltf_json::validation::Checked::Valid;
|
use gltf_json::validation::Checked::Valid;
|
||||||
use gltf_json::{Accessor, Index, Mesh, Value};
|
use gltf_json::{Accessor, Extras, Image, Index, Material, Mesh, Texture, Value};
|
||||||
|
use image::png::PngEncoder;
|
||||||
|
use image::{DynamicImage, GenericImageView};
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use vmdl::Model;
|
use vmdl::{Model, SkinTable};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
|
#[derive(Copy, Clone, Debug, Default, Zeroable, Pod)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
|
@ -110,13 +116,14 @@ pub fn push_model(
|
||||||
views: &mut Vec<View>,
|
views: &mut Vec<View>,
|
||||||
accessors: &mut Vec<Accessor>,
|
accessors: &mut Vec<Accessor>,
|
||||||
model: &Model,
|
model: &Model,
|
||||||
|
skin: &SkinTable,
|
||||||
) -> Mesh {
|
) -> Mesh {
|
||||||
let accessor_start = accessors.len() as u32;
|
let accessor_start = accessors.len() as u32;
|
||||||
push_vertices(buffer, views, accessors, model);
|
push_vertices(buffer, views, accessors, model);
|
||||||
|
|
||||||
let primitives = model
|
let primitives = model
|
||||||
.meshes()
|
.meshes()
|
||||||
.map(|mesh| push_primitive(buffer, views, accessors, &mesh, accessor_start))
|
.map(|mesh| push_primitive(buffer, views, accessors, &mesh, accessor_start, skin))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Mesh {
|
Mesh {
|
||||||
|
|
@ -134,6 +141,7 @@ pub fn push_primitive(
|
||||||
accessors: &mut Vec<Accessor>,
|
accessors: &mut Vec<Accessor>,
|
||||||
mesh: &vmdl::Mesh,
|
mesh: &vmdl::Mesh,
|
||||||
vertex_accessor_start: u32,
|
vertex_accessor_start: u32,
|
||||||
|
skin: &SkinTable,
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
let buffer_start = buffer.len() as u32;
|
let buffer_start = buffer.len() as u32;
|
||||||
let view_start = views.len() as u32;
|
let view_start = views.len() as u32;
|
||||||
|
|
@ -195,8 +203,115 @@ pub fn push_primitive(
|
||||||
extensions: Default::default(),
|
extensions: Default::default(),
|
||||||
extras: Default::default(),
|
extras: Default::default(),
|
||||||
indices: Some(Index::new(accessor_start)),
|
indices: Some(Index::new(accessor_start)),
|
||||||
material: None,
|
material: Some(Index::new(
|
||||||
|
skin.texture_index(mesh.material_index())
|
||||||
|
.expect("skin out of bounds") as u32,
|
||||||
|
)),
|
||||||
mode: Valid(Mode::Triangles),
|
mode: Valid(Mode::Triangles),
|
||||||
targets: None,
|
targets: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_material(
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
views: &mut Vec<View>,
|
||||||
|
textures: &mut Vec<Texture>,
|
||||||
|
images: &mut Vec<Image>,
|
||||||
|
material: MaterialData,
|
||||||
|
) -> Material {
|
||||||
|
let textures_start = textures.len() as u32;
|
||||||
|
|
||||||
|
let texture = material
|
||||||
|
.texture
|
||||||
|
.map(|tex| push_texture(buffer, views, images, tex));
|
||||||
|
let texture_index = texture.map(|texture| {
|
||||||
|
textures.push(texture);
|
||||||
|
textures_start
|
||||||
|
});
|
||||||
|
|
||||||
|
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: Index::new(index),
|
||||||
|
tex_coord: 0,
|
||||||
|
extensions: None,
|
||||||
|
extras: Extras::default(),
|
||||||
|
}),
|
||||||
|
..PbrMetallicRoughness::default()
|
||||||
|
},
|
||||||
|
..Material::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_texture(
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
views: &mut Vec<View>,
|
||||||
|
images: &mut Vec<Image>,
|
||||||
|
image: DynamicImage,
|
||||||
|
) -> Texture {
|
||||||
|
let buffer_start = buffer.len() as u32;
|
||||||
|
let view_start = views.len() as u32;
|
||||||
|
let image_start = 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;
|
||||||
|
|
||||||
|
let view = View {
|
||||||
|
buffer: Index::new(0),
|
||||||
|
byte_length,
|
||||||
|
byte_offset: Some(buffer_start),
|
||||||
|
byte_stride: None,
|
||||||
|
extensions: Default::default(),
|
||||||
|
extras: Default::default(),
|
||||||
|
name: None,
|
||||||
|
target: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
views.push(view);
|
||||||
|
|
||||||
|
let image = Image {
|
||||||
|
buffer_view: Some(Index::new(view_start)),
|
||||||
|
mime_type: Some(MimeType("image/png".into())),
|
||||||
|
name: None,
|
||||||
|
uri: None,
|
||||||
|
extensions: None,
|
||||||
|
extras: Default::default(),
|
||||||
|
};
|
||||||
|
images.push(image);
|
||||||
|
|
||||||
|
Texture {
|
||||||
|
name: None,
|
||||||
|
sampler: None,
|
||||||
|
source: Index::new(image_start),
|
||||||
|
extensions: None,
|
||||||
|
extras: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
mod convert;
|
mod convert;
|
||||||
mod error;
|
mod error;
|
||||||
mod loader;
|
mod loader;
|
||||||
|
mod material;
|
||||||
|
|
||||||
use gltf_json as json;
|
use gltf_json as json;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use crate::convert::push_model;
|
use crate::convert::{push_material, push_model};
|
||||||
|
use crate::loader::Loader;
|
||||||
|
use crate::material::load_material_fallback;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use gltf_json::Index;
|
use gltf_json::Index;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
@ -35,12 +38,32 @@ fn pad_byte_vector(mut vec: Vec<u8>) -> Vec<u8> {
|
||||||
vec
|
vec
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export(model: Model, output: Output) {
|
fn export(model: Model, output: Output) -> Result<(), Error> {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
let mut views = Vec::new();
|
let mut views = Vec::new();
|
||||||
let mut accessors = Vec::new();
|
let mut accessors = Vec::new();
|
||||||
|
let mut textures = Vec::new();
|
||||||
|
let mut images = Vec::new();
|
||||||
|
let skin = model.skin_tables().next().unwrap();
|
||||||
|
|
||||||
let mesh = push_model(&mut buffer, &mut views, &mut accessors, &model);
|
let loader = Loader::new()?;
|
||||||
|
|
||||||
|
let mesh = push_model(&mut buffer, &mut views, &mut accessors, &model, &skin);
|
||||||
|
|
||||||
|
let materials = model
|
||||||
|
.textures()
|
||||||
|
.iter()
|
||||||
|
.map(|tex| load_material_fallback(&tex.name, &tex.search_paths, &loader))
|
||||||
|
.map(|material| {
|
||||||
|
push_material(
|
||||||
|
&mut buffer,
|
||||||
|
&mut views,
|
||||||
|
&mut textures,
|
||||||
|
&mut images,
|
||||||
|
material,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let node = json::Node {
|
let node = json::Node {
|
||||||
camera: None,
|
camera: None,
|
||||||
|
|
@ -81,6 +104,9 @@ fn export(model: Model, output: Output) {
|
||||||
name: None,
|
name: None,
|
||||||
nodes: vec![Index::new(0)],
|
nodes: vec![Index::new(0)],
|
||||||
}],
|
}],
|
||||||
|
materials,
|
||||||
|
images,
|
||||||
|
textures,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -112,6 +138,7 @@ fn export(model: Model, output: Output) {
|
||||||
glb.to_writer(writer).expect("glTF binary output error");
|
glb.to_writer(writer).expect("glTF binary output error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(path: &Path) -> Result<Model, vmdl::ModelError> {
|
fn load(path: &Path) -> Result<Model, vmdl::ModelError> {
|
||||||
|
|
@ -129,6 +156,6 @@ fn main() -> Result<(), Error> {
|
||||||
let path = PathBuf::from(args_os().nth(1).expect("No model file provided"));
|
let path = PathBuf::from(args_os().nth(1).expect("No model file provided"));
|
||||||
let source_model = load(&path)?;
|
let source_model = load(&path)?;
|
||||||
|
|
||||||
export(source_model, Output::Binary);
|
export(source_model, Output::Binary)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
150
examples/gltf/material.rs
Normal file
150
examples/gltf/material.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
use crate::loader::Loader;
|
||||||
|
use crate::Error;
|
||||||
|
use image::DynamicImage;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use steamy_vdf::{Entry, Table};
|
||||||
|
use tracing::error;
|
||||||
|
use vtf::vtf::VTF;
|
||||||
|
|
||||||
|
fn get_path(vmt: &Entry, name: &str) -> Option<String> {
|
||||||
|
Some(vmt.lookup(name)?.as_str()?.replace('\\', "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 color: [u8; 4],
|
||||||
|
pub texture: Option<DynamicImage>,
|
||||||
|
pub alpha_test: Option<f32>,
|
||||||
|
pub bump_map: Option<DynamicImage>,
|
||||||
|
pub translucent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 raw = loader.load_from_paths(&path, &dirs)?;
|
||||||
|
|
||||||
|
let vmt = parse_vdf(&raw)?;
|
||||||
|
let vmt = resolve_vmt_patch(vmt, loader)?;
|
||||||
|
|
||||||
|
let table = vmt
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(Error::Other("empty vmt"))?;
|
||||||
|
let base_texture = get_path(&table, "$basetexture").ok_or(Error::Other("no $basetexture"))?;
|
||||||
|
|
||||||
|
let translucent = table
|
||||||
|
.lookup("$translucent")
|
||||||
|
.map(|val| val.as_str() == Some("1"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let glass = table
|
||||||
|
.lookup("$surfaceprop")
|
||||||
|
.map(|val| val.as_str() == Some("glass"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let alpha_test = table
|
||||||
|
.lookup("$alphatest")
|
||||||
|
.map(|val| val.as_str() == Some("1"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let texture = load_texture(
|
||||||
|
base_texture.as_str(),
|
||||||
|
loader,
|
||||||
|
translucent | glass | alpha_test,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let alpha_cutout = table
|
||||||
|
.lookup("$alphatestreference")
|
||||||
|
.and_then(Entry::as_str)
|
||||||
|
.and_then(|val| f32::from_str(val).ok())
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
|
let bump_map = get_path(&table, "$bumpmap")
|
||||||
|
.map(|path| load_texture(&path, loader, true).ok())
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
Ok(MaterialData {
|
||||||
|
color: [255; 4],
|
||||||
|
name: name.into(),
|
||||||
|
texture: Some(texture),
|
||||||
|
bump_map,
|
||||||
|
alpha_test: alpha_test.then_some(alpha_cutout),
|
||||||
|
translucent: translucent | glass | alpha_test,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_vdf(bytes: &[u8]) -> Result<Table, Error> {
|
||||||
|
let bytes = bytes.to_ascii_lowercase();
|
||||||
|
let mut reader = steamy_vdf::Reader::from(bytes.as_slice());
|
||||||
|
Table::load(&mut reader).map_err(|e| {
|
||||||
|
println!("{}", String::from_utf8_lossy(&bytes));
|
||||||
|
error!(
|
||||||
|
source = String::from_utf8_lossy(&bytes).to_string(),
|
||||||
|
error = ?e,
|
||||||
|
"failed to parse vmt"
|
||||||
|
);
|
||||||
|
Error::Other("failed to parse vdf")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_texture(name: &str, loader: &Loader, _alpha: bool) -> Result<DynamicImage, Error> {
|
||||||
|
let path = format!(
|
||||||
|
"materials/{}.vtf",
|
||||||
|
name.trim_end_matches(".vtf").trim_start_matches('/')
|
||||||
|
);
|
||||||
|
let mut raw = loader.load(&path)?;
|
||||||
|
let vtf = VTF::read(&mut raw)?;
|
||||||
|
let image = vtf.highres_image.decode(0)?;
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_vmt_patch(vmt: Table, loader: &Loader) -> Result<Table, Error> {
|
||||||
|
if vmt.len() != 1 {
|
||||||
|
panic!("vmt with more than 1 item?");
|
||||||
|
}
|
||||||
|
if let Some(Entry::Table(patch)) = vmt.get("patch") {
|
||||||
|
let include = patch
|
||||||
|
.get("include")
|
||||||
|
.expect("no include in patch")
|
||||||
|
.as_value()
|
||||||
|
.expect("include is not a value")
|
||||||
|
.to_string();
|
||||||
|
let _replace = patch
|
||||||
|
.get("replace")
|
||||||
|
.expect("no replace in patch")
|
||||||
|
.as_table()
|
||||||
|
.expect("replace is not a table");
|
||||||
|
let included_raw = loader.load(&include.to_ascii_lowercase())?;
|
||||||
|
|
||||||
|
// todo actually patch
|
||||||
|
parse_vdf(&included_raw)
|
||||||
|
} else {
|
||||||
|
Ok(vmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue