gltf: textures

This commit is contained in:
Robin Appelman 2023-12-16 23:35:20 +01:00
commit 9dc540db5c
5 changed files with 303 additions and 9 deletions

1
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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(),
}
}

View file

@ -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
View 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)
}
}