From dc8304afefa71037bea99722bee29f7645753836 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 12 Mar 2025 17:54:43 +0100 Subject: rename crates and binaries --- Cargo.lock | 46 +++-- Cargo.toml | 2 +- cli/Cargo.toml | 14 ++ cli/src/main.rs | 3 + client/Cargo.toml | 4 + import/Cargo.toml | 32 ++++ import/src/animation.rs | 133 +++++++++++++++ import/src/main.rs | 304 +++++++++++++++++++++++++++++++++ import/src/mesh.rs | 431 ++++++++++++++++++++++++++++++++++++++++++++++ import/src/physics.rs | 89 ++++++++++ import/src/prefab.rs | 442 ++++++++++++++++++++++++++++++++++++++++++++++++ import/src/vrm.rs | 135 +++++++++++++++ server/Cargo.toml | 4 + world/Cargo.toml | 28 --- world/src/animation.rs | 133 --------------- world/src/main.rs | 304 --------------------------------- world/src/mesh.rs | 431 ---------------------------------------------- world/src/physics.rs | 89 ---------- world/src/prefab.rs | 426 ---------------------------------------------- world/src/vrm.rs | 135 --------------- 20 files changed, 1620 insertions(+), 1565 deletions(-) create mode 100644 cli/Cargo.toml create mode 100644 cli/src/main.rs create mode 100644 import/Cargo.toml create mode 100644 import/src/animation.rs create mode 100644 import/src/main.rs create mode 100644 import/src/mesh.rs create mode 100644 import/src/physics.rs create mode 100644 import/src/prefab.rs create mode 100644 import/src/vrm.rs delete mode 100644 world/Cargo.toml delete mode 100644 world/src/animation.rs delete mode 100644 world/src/main.rs delete mode 100644 world/src/mesh.rs delete mode 100644 world/src/physics.rs delete mode 100644 world/src/prefab.rs delete mode 100644 world/src/vrm.rs diff --git a/Cargo.lock b/Cargo.lock index 33b9453..85123c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3410,6 +3410,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wearecli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.23", + "env_logger", + "weareshared", +] + [[package]] name = "weareclient" version = "0.1.0" @@ -3438,48 +3448,48 @@ dependencies = [ ] [[package]] -name = "weareserver" +name = "weareimport" version = "0.1.0" dependencies = [ "anyhow", "clap 4.5.23", "env_logger", + "glam", + "gltf", + "humansize", + "image", "log", + "rand 0.9.0-beta.1", + "rayon", + "serde", + "serde_json", "weareshared", - "xdg", ] [[package]] -name = "weareshared" +name = "weareserver" version = "0.1.0" dependencies = [ "anyhow", - "bincode", - "blake3", - "glam", + "clap 4.5.23", + "env_logger", "log", - "rand 0.9.0-beta.1", - "redb", + "weareshared", "xdg", ] [[package]] -name = "weareworld" +name = "weareshared" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.23", - "env_logger", + "bincode", + "blake3", "glam", - "gltf", - "humansize", - "image", "log", "rand 0.9.0-beta.1", - "rayon", - "serde", - "serde_json", - "weareshared", + "redb", + "xdg", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53b56f8..722342f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["shared", "server", "client", "world"] +members = ["shared", "server", "client", "import", "cli"] resolver = "3" diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..84587ba --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "wearecli" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "wearecontrolling" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.95" +clap = { version = "4.5.23", features = ["derive"] } +env_logger = "0.11.6" +weareshared = { path = "../shared" } diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/client/Cargo.toml b/client/Cargo.toml index d00335c..d73c282 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -3,6 +3,10 @@ name = "weareclient" version = "0.1.0" edition = "2024" +[[bin]] +name = "weareplaying" +path = "src/main.rs" + [dependencies] anyhow = "1.0.95" audiopus = "0.2.0" diff --git a/import/Cargo.toml b/import/Cargo.toml new file mode 100644 index 0000000..b8b3a10 --- /dev/null +++ b/import/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "weareimport" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "weareimporting" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.95" +clap = { version = "4.5.23", features = ["derive"] } +env_logger = "0.11.6" +serde = { version = "1.0.217", features = ["derive"] } +gltf = { version = "1.4.1", features = [ + "extras", + "names", + "extensions", + "KHR_lights_punctual", + "KHR_materials_transmission", + "KHR_materials_ior", + "KHR_materials_volume", + "KHR_materials_emissive_strength", +] } +glam = "0.29.2" +humansize = "2.1.3" +image = "0.25.5" +log = "0.4.22" +rand = "0.9.0-beta.1" +rayon = "1.10.0" +serde_json = "1.0.138" +weareshared = { path = "../shared" } diff --git a/import/src/animation.rs b/import/src/animation.rs new file mode 100644 index 0000000..ca049e5 --- /dev/null +++ b/import/src/animation.rs @@ -0,0 +1,133 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use anyhow::Result; +use glam::{Quat, vec3a}; +use gltf::{ + Animation, + animation::{Property, util::ReadOutputs}, + buffer::Data, +}; +use log::{debug, info}; +use std::collections::BTreeMap; +use weareshared::{ + Affine3A, + packets::Resource, + resources::{AnimationChannel, AnimationPart}, + store::ResourceStore, +}; + +pub fn import_animation( + a: Animation<'_>, + store: &ResourceStore, + transform: Affine3A, + joint_index_to_ibm: &BTreeMap, + joint_index_to_arm_index: &BTreeMap, + node_to_meshes: &BTreeMap>, + buffers: &[Data], +) -> Result> { + let mut max_time = 0f32; + let mut channels = Vec::new(); + for c in a.channels() { + let node = c.target().node().index(); + let reader = c.reader(|i| Some(&buffers[i.index()].0)); + let inputs: Vec = reader.read_inputs().unwrap().collect::>(); + let outputs: Vec = + if joint_index_to_ibm.contains_key(&node) || transform != Affine3A::IDENTITY { + let t = transform + * joint_index_to_ibm + .get(&node) + .copied() + .unwrap_or_default() + .inverse(); + let (_, rot, _) = t.to_scale_rotation_translation(); + match reader.read_outputs().unwrap() { + ReadOutputs::Translations(iter) => iter + .flat_map(|[x, y, z]| (t.matrix3 * vec3a(x, y, z)).to_array()) + .collect(), + ReadOutputs::Rotations(iter) => iter + .into_f32() + .map(Quat::from_array) + .map(|q| q * rot) + .flat_map(|q| q.to_array()) + .collect(), + // ReadOutputs::Scales(iter) => iter + // .flat_map(|[x, y, z]| (t.matrix3 * vec3a(x, y, z)).to_array()) + // .collect(), + ReadOutputs::MorphTargetWeights(iter) => iter.into_f32().collect(), + _ => continue, + } + } else { + match reader.read_outputs().unwrap() { + ReadOutputs::Translations(iter) => iter.flatten().collect(), + ReadOutputs::Rotations(iter) => iter.into_f32().flatten().collect(), + ReadOutputs::Scales(iter) => iter.flatten().collect(), + ReadOutputs::MorphTargetWeights(iter) => iter.into_f32().collect(), + } + }; + for x in &inputs { + max_time = max_time.max(*x) + } + let time = store.set(&inputs)?; + let value = store.set(&outputs)?; + + if let Some(&m) = joint_index_to_arm_index.get(&node) { + let a = 0; // TODO + let mut ch = AnimationChannel::default(); + match c.target().property() { + Property::Translation => ch.t_joint_translation = Some((a, m as u32)), + Property::Rotation => ch.t_joint_rotation = Some((a, m as u32)), + Property::Scale => ch.t_joint_scale = Some((a, m as u32)), + Property::MorphTargetWeights => continue, + } + ch.time = Some(time.clone()); + ch.value = Some(value.clone()); + debug!( + "animation channel {:?} of joint {m} of armature {a} with {} time and {} component values", + c.target().property(), + inputs.len(), + outputs.len() + ); + channels.push(ch); + } + if let Some(meshes) = node_to_meshes.get(&node) { + for &m in meshes { + let mut ch = AnimationChannel::default(); + match c.target().property() { + Property::Translation => ch.t_mesh_translation = Some(m as u32), + Property::Rotation => ch.t_mesh_rotation = Some(m as u32), + Property::Scale => ch.t_mesh_scale = Some(m as u32), + Property::MorphTargetWeights => continue, + } + ch.time = Some(time.clone()); + ch.value = Some(value.clone()); + debug!( + "animation channel {:?} of mesh {m} with {} time and {} component values", + c.target().property(), + inputs.len(), + outputs.len() + ); + channels.push(ch); + } + } + } + info!("adding animation with {} channels", channels.len()); + store.set(&AnimationPart { + name: a.name().map(|n| n.to_string()), + channel: channels, + duration: Some(max_time), + }) +} diff --git a/import/src/main.rs b/import/src/main.rs new file mode 100644 index 0000000..554a2bd --- /dev/null +++ b/import/src/main.rs @@ -0,0 +1,304 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +#![feature(iter_array_chunks)] +#![allow(clippy::too_many_arguments, clippy::type_complexity)] +pub mod animation; +pub mod mesh; +pub mod physics; +pub mod prefab; +pub mod vrm; + +use anyhow::{Result, bail}; +use clap::Parser; +use gltf::{image::Source, scene::Transform}; +use humansize::BINARY; +use image::{ImageReader, codecs::webp::WebPEncoder}; +use log::{debug, info}; +use prefab::import_prefab; +use rand::random; +use std::{ + borrow::Cow, + collections::HashMap, + fs::File, + io::{BufWriter, Cursor, Read, Write}, + marker::PhantomData, + net::{SocketAddr, TcpStream}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + thread::{self, sleep}, + time::Duration, +}; +use weareshared::{ + Affine3A, Vec3A, + helper::ReadWrite, + packets::{Data, Object, Packet, Resource}, + resources::{Image, RespackEntry}, + respack::save_respack, + store::ResourceStore, + vec3a, +}; + +#[derive(Parser)] +pub struct Args { + #[arg(short, long)] + address: Option, + + /// Output converted prefab as resource package + #[arg(short = 'o', long)] + pack: Option, + + /// Path(s) to a glTF file, binary or json format + scene: Vec, + /// Send all resources to the server then quit + #[arg(short, long)] + push: bool, + + /// Remove all other object from the world + #[arg(short, long)] + clear: bool, + /// Add the object to the world + #[arg(short, long)] + add: bool, + /// Transcode all textures to WebP + #[arg(short, long)] + webp: bool, + /// Add skybox + #[arg(long)] + skybox: Option, + /// Override prefab name + #[arg(short, long)] + name: Option, + #[arg(short, long)] + scale: Option, + #[arg(short, long)] + dry_run: bool, + #[arg(short, long)] + line_up: bool, + + #[arg(long)] + use_cache: bool, + + #[arg(short = 'S', long)] + with_default_sun: bool, + + #[arg(long)] + animation: Option, + #[arg(long)] + animation_bone_map: Option, + #[arg(long)] + animation_rotation_y: Option, + #[arg(long)] + animation_scale: Option, + #[arg(long)] + animation_apply_ibm: bool, + + /// Spins the object + #[arg(long)] + debug_spin: bool, + /// Adds a light + #[arg(long)] + debug_light: bool, + #[arg(long)] + no_particles: bool, + #[arg(long)] + no_animations: bool, +} + +fn main() -> Result<()> { + env_logger::init_from_env("LOG"); + let args = Args::parse(); + + let store = if args.use_cache && !args.pack.is_some() { + ResourceStore::new_env()? + } else { + ResourceStore::new_memory() + }; + + let mut prefabs = Vec::new(); + let texture_cache = Arc::new(Mutex::new(HashMap::new())); + + for scenepath in &args.scene { + prefabs.push(import_prefab(&store, &texture_cache, scenepath, &args)?); + } + + let mut size = 0; + store.iter(|_k, len| size += len).unwrap(); + info!( + "prefab has network size of {}", + humansize::format_size(size, BINARY) + ); + + if args.dry_run { + return Ok(()); + } + if let Some(outpath) = args.pack { + let entry = store.set(&RespackEntry { name: None })?; + let mut resources = Vec::new(); + store.iter(|r, _| resources.push(r))?; + save_respack( + BufWriter::new(File::create(outpath)?), + &store, + &resources, + Some(entry), + )?; + } else if let Some(address) = args.address { + let mut sock = TcpStream::connect(address)?; + Packet::Connect(random()).write(&mut sock)?; + for p in &prefabs { + Packet::AnnouncePrefab(p.clone()).write(&mut sock)?; + } + sock.flush()?; + + let mut obs = Vec::new(); + if args.add { + for (i, p) in prefabs.iter().enumerate() { + let ob = Object::new(); + info!("adding object {ob}"); + Packet::Add(ob, p.clone()).write(&mut sock)?; + if args.line_up { + Packet::Position(ob, vec3a(i as f32 * 1.2, 0., i as f32 * 0.3), Vec3A::ZERO) + .write(&mut sock)?; + } + obs.push(ob); + } + sock.flush()?; + } + + if args.debug_spin { + let ob = obs[0]; + let mut sock2 = sock.try_clone().unwrap(); + thread::spawn(move || { + let mut x = 0.; + loop { + Packet::Position(ob, Vec3A::ZERO, vec3a(0., x * 0.1, 0.)) + .write(&mut sock2) + .unwrap(); + sock2.flush().unwrap(); + x += 0.1; + sleep(Duration::from_millis(50)); + } + }); + } + if args.push { + if args.use_cache { + return Ok(()); + } + store.iter(|k, _| { + Packet::RespondResource(k, Data(store.get_raw(k).unwrap().unwrap())) + .write(&mut sock) + .unwrap(); + })?; + sock.flush()?; + } else { + loop { + let packet = Packet::read(&mut sock)?; + match packet { + Packet::RequestResource(hash) => { + if let Some(d) = store.get_raw(hash)? { + Packet::RespondResource(hash, Data(d)).write(&mut sock)?; + sock.flush()?; + } + } + Packet::Add(ob_a, _) => { + if args.clear && !obs.contains(&ob_a) { + info!("removing object {ob_a}"); + Packet::Remove(ob_a).write(&mut sock)?; + sock.flush()?; + } + } + _ => (), + } + } + } + } else { + bail!("no output option specified. either provide an address to a server or use --pack") + } + Ok(()) +} + +pub type TextureCache = Arc>>>>; +fn load_texture( + name: &str, + store: &ResourceStore, + path: &Path, + buffers: &[gltf::buffer::Data], + source: &Source, + webp: bool, + texture_cache: &TextureCache, +) -> Result>> { + let (mut image, uri) = match source { + gltf::image::Source::View { view, mime_type } => { + debug!("{name} texture is embedded and of type {mime_type:?}"); + let buf = + &buffers[view.buffer().index()].0[view.offset()..view.offset() + view.length()]; + (Image(Cow::Borrowed(buf)), None) + } + gltf::image::Source::Uri { + uri, + mime_type: Some(mime_type), + } => { + debug!("{name} texture is {uri:?} and of type {mime_type:?}"); + if let Some(res) = texture_cache.lock().unwrap().get(*uri) { + return Ok(res.to_owned()); + } + let path = path.join(uri); + let mut buf = Vec::new(); + File::open(path)?.read_to_end(&mut buf)?; + (Image(buf.into()), Some(uri.to_string())) + } + gltf::image::Source::Uri { + uri, + mime_type: None, + } => { + debug!("{name} texture is {uri:?} and has no type"); + if let Some(res) = texture_cache.lock().unwrap().get(*uri) { + return Ok(res.to_owned()); + } + let path = path.join(uri); + let mut buf = Vec::new(); + File::open(path)?.read_to_end(&mut buf)?; + (Image(buf.into()), Some(uri.to_string())) + } + }; + + if webp { + let mut image_out = Vec::new(); + + let len = image.0.len(); + ImageReader::new(Cursor::new(image.0)) + .with_guessed_format()? + .decode()? + .write_with_encoder(WebPEncoder::new_lossless(&mut image_out))?; + debug!("webp encode: {len} -> {}", image_out.len()); + image = Image(Cow::Owned(image_out)); + } + let res = Resource(store.set(&image)?.0, PhantomData); + if let Some(uri) = uri { + texture_cache.lock().unwrap().insert(uri, res.clone()); + } + Ok(res) +} + +pub fn transform_to_affine(trans: Transform) -> Affine3A { + let mat = trans.matrix(); + Affine3A::from_cols_array_2d(&[ + [mat[0][0], mat[0][1], mat[0][2]], + [mat[1][0], mat[1][1], mat[1][2]], + [mat[2][0], mat[2][1], mat[2][2]], + [mat[3][0], mat[3][1], mat[3][2]], + ]) +} diff --git a/import/src/mesh.rs b/import/src/mesh.rs new file mode 100644 index 0000000..ffc0f2f --- /dev/null +++ b/import/src/mesh.rs @@ -0,0 +1,431 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use crate::{Args, TextureCache, load_texture, vrm::VrmInfo}; +use anyhow::Result; +use gltf::{Mesh, Node, buffer::Data}; +use log::{debug, info, warn}; +use std::{collections::BTreeMap, path::Path}; +use weareshared::{ + Affine3A, Vec3A, packets::Resource, resources::MeshPart, store::ResourceStore, vec2, vec3a, + vec4, +}; + +pub fn import_mesh( + mesh: Mesh, + trans: Affine3A, + buffers: &[Data], + store: &ResourceStore, + path_base: &Path, + node: &Node, + args: &Args, + texture_cache: &TextureCache, + joint_index_map: &BTreeMap<(usize, u16), u32>, + vrm: &VrmInfo, + head_bones: &[u32], +) -> Result)>> { + let mut meshes = Vec::new(); + for p in mesh.primitives() { + let name = mesh.name().or(node.name()).map(|e| e.to_owned()); + if let Some(name) = &name { + info!("adding mesh {name:?}"); + } else { + info!("adding mesh"); + } + let reader = p.reader(|buf| Some(&buffers[buf.index()])); + + let mut num_vertex = 0; + let va_position = reader + .read_positions() + .map(|iter| { + let a = iter.map(|[x, y, z]| vec3a(x, y, z)).collect::>(); + debug!("{} vertex positions", a.len()); + num_vertex = a.len(); + store.set(&a) + }) + .transpose()?; + + let va_normal = reader + .read_normals() + .map(|iter| { + let a = iter.map(|[x, y, z]| vec3a(x, y, z)).collect::>(); + debug!("{} vertex normals", a.len()); + store.set(&a) + }) + .transpose()?; + + let va_tangent = reader + .read_tangents() + .map(|iter| { + // TODO dont ignore handedness + let a = iter + .map(|[x, y, z, h]| vec4(x, y, z, h)) + .collect::>(); + debug!("{} vertex tangents", a.len()); + store.set(&a) + }) + .transpose()?; + + let mut many_head_bones = false; + let va_joint_index = reader + .read_joints(0) + .map(|iter| { + let si = node.skin().unwrap().index(); + let a = iter + .into_u16() + .map(|x| x.map(|x| joint_index_map[&(si, x)])) + .collect::>(); + + let head_bone_count = a + .iter() + .flatten() + .filter(|b| head_bones.contains(*b)) + .count(); + many_head_bones |= head_bone_count > a.len() / 2; + debug!("{} vertex joint indecies", a.len()); + if many_head_bones { + debug!("many joints are head bones"); + } + if a.len() != num_vertex { + warn!("joint index count does not vertex count") + } + store.set(&a) + }) + .transpose()?; + + let va_joint_weight = reader + .read_weights(0) + .map(|iter| { + let a = iter.into_f32().collect::>(); + debug!("{} vertex joint weights", a.len()); + if a.len() != num_vertex { + warn!("joint weight count does not vertex count") + } + store.set(&a) + }) + .transpose()?; + + let va_texcoord = reader + .read_tex_coords(0) + .map(|iter| { + let a = iter.into_f32().map(|[x, y]| vec2(x, y)).collect::>(); + debug!("{} vertex texture coordinates", a.len()); + store.set(&a) + }) + .transpose()?; + + let va_albedo = reader + .read_colors(0) + .map(|iter| { + let a = iter + .into_rgb_f32() + .map(|[x, y, z]| vec3a(x, y, z)) + .collect::>(); + debug!("{} vertex colors", a.len()); + store.set(&a) + }) + .transpose()?; + + let va_alpha = reader + .read_colors(0) + .map(|iter| { + let mut color_a = vec![]; + for p in iter.into_rgba_f32() { + color_a.push(p[3]); + } + let o = if color_a.iter().any(|x| *x != 1.) { + debug!("{} vertex transmissions", color_a.len()); + Some(store.set(&color_a)?) + } else { + debug!("vertex transmission pruned"); + None + }; + Ok::<_, anyhow::Error>(o) + }) + .transpose()? + .flatten(); + + let index = reader + .read_indices() + .unwrap() + .into_u32() + .array_chunks::<3>() + .collect::>(); + + debug!("{} indecies", index.len() * 3); + let index = Some(store.set(&index)?); + + let mut tex_albedo = None; + let mut tex_alpha = None; + if let Some(tex) = p.material().pbr_metallic_roughness().base_color_texture() { + let r = load_texture( + "albedo", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?; + tex_albedo = Some(r.clone()); + tex_alpha = Some(r.clone()); + } + let mut tex_normal = None; + if let Some(tex) = p.material().normal_texture() { + tex_normal = Some(load_texture( + "normal", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?); + } + let mut tex_emission = None; + if let Some(tex) = p.material().emissive_texture() { + tex_emission = Some(load_texture( + "emission", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?); + } + let mut tex_transmission = None; + if let Some(tex) = p + .material() + .transmission() + .and_then(|t| t.transmission_texture()) + { + tex_transmission = Some(load_texture( + "transmission", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?); + } + let mut tex_thickness = None; + if let Some(tex) = p.material().volume().and_then(|t| t.thickness_texture()) { + tex_thickness = Some(load_texture( + "thickness", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?); + } + let mut tex_occlusion = None; + if let Some(tex) = p.material().occlusion_texture() { + tex_occlusion = Some(load_texture( + "occlusion", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?); + } + let mut tex_roughness = None; + let mut tex_metallic = None; + if let Some(tex) = p + .material() + .pbr_metallic_roughness() + .metallic_roughness_texture() + { + let r = load_texture( + "metallic+roughness", + store, + path_base, + buffers, + &tex.texture().source().source(), + args.webp, + texture_cache, + )?; + tex_roughness = Some(r.clone()); + tex_metallic = Some(r.clone()); + } + + let g_metallic = Some(p.material().pbr_metallic_roughness().metallic_factor()); + let g_roughness = Some(p.material().pbr_metallic_roughness().roughness_factor()); + + let base_color = p.material().pbr_metallic_roughness().base_color_factor(); + + let g_albedo = if base_color[0] != 1. || base_color[1] != 1. || base_color[2] != 1. { + debug!( + "albedo is r={},g={},b={}", + base_color[0], base_color[1], base_color[2] + ); + Some(Vec3A::new(base_color[0], base_color[1], base_color[2])) + } else { + debug!("albedo pruned"); + None + }; + let g_alpha = if base_color[3] != 1. { + debug!("alpha is {}", base_color[3]); + Some(base_color[3]) + } else { + debug!("alpha pruned"); + None + }; + + let emission = p.material().emissive_factor(); + let g_emission = if emission[0] != 0. || emission[1] != 0. || emission[2] != 0. { + debug!( + "emission is r={},g={},b={}", + base_color[0], base_color[1], base_color[2] + ); + Some(Vec3A::new(emission[0], emission[1], emission[2])) + } else { + debug!("emission pruned"); + None + }; + + let transmission = p + .material() + .transmission() + .map(|t| t.transmission_factor()) + .unwrap_or(0.); + + let g_transmission = if transmission != 0. { + debug!("transmission is {transmission}"); + Some(transmission) + } else { + debug!("transmission pruned"); + None + }; + + let g_dispersion = p + .material() + .extension_value("KHR_materials_dispersion") + .and_then(|e| e.get("dispersion")) + .and_then(|e| e.as_f64()) + .map(|e| e as f32); + if let Some(d) = g_dispersion { + debug!("dispersion is {d}"); + } + + // if node.name() == Some("fog") { + // eprintln!("{:#?}", p.material().volume().is_some()); + // eprintln!("{:#?}", p.material().ior()); + // eprintln!("{:#?}", p.material().transmission().is_some()); + // } + + let g_attenuation = p.material().volume().map(|v| { + let ref_dist = v.attenuation_distance(); + let att = Vec3A::from_array(v.attenuation_color().map( + // manually derived from attenuation coefficient formula. i hope this is correct. + |factor| -(factor.powf(1. / ref_dist)).ln(), + )); + debug!("attenuation is {att}"); + att + }); + let g_refractive_index = p.material().ior(); + if let Some(i) = g_refractive_index { + debug!("refractive index is {i}"); + } + let g_thickness = p.material().volume().map(|v| v.thickness_factor()); + + let g_unlit = bool_to_opt( + p.material() + .extension_value("KHR_materials_unlit") + .is_some(), + "unlit", + ); + + let g_double_sided = bool_to_opt(p.material().double_sided(), "double sided"); + + let hint_volume = bool_to_opt( + g_attenuation.is_some_and(|a| a.length() > 0.01), + "volume hint", + ); + + let hint_hide_first_person = bool_to_opt( + many_head_bones | vrm.hide_first_person.contains(&node.index()), + "hide first person hint", + ); + + let hint_mirror = bool_to_opt( + node.name().unwrap_or_default().ends_with("-mirror"), + "mirror hint", + ); + + let armature = node.skin().map(|_| 0); + + let mesh = MeshPart { + name, + index, + armature, + g_albedo, + g_alpha, + g_metallic, + g_roughness, + g_emission, + g_transmission, + g_attenuation, + g_thickness, + g_refractive_index, + g_dispersion, + g_unlit, + g_double_sided, + va_position, + va_normal, + va_tangent, + va_texcoord, + va_albedo, + va_alpha, + va_joint_index, + va_joint_weight, + tex_albedo, + tex_normal, + tex_roughness, + tex_metallic, + tex_alpha, + tex_emission, + tex_transmission, + tex_thickness, + tex_occlusion, + hint_hide_first_person, + hint_mirror, + hint_volume, + // not supported by gltf + hint_static: None, // TODO Set when instancing + va_transmission: None, + va_emission: None, + va_metallic: None, + va_roughness: None, + }; + meshes.push((node.index(), trans, store.set(&mesh)?)) + } + Ok(meshes) +} + +fn bool_to_opt(b: bool, log: &str) -> Option<()> { + if b { + debug!("{log}"); + Some(()) + } else { + None + } +} diff --git a/import/src/physics.rs b/import/src/physics.rs new file mode 100644 index 0000000..3beb49b --- /dev/null +++ b/import/src/physics.rs @@ -0,0 +1,89 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use anyhow::{Result, anyhow}; +use gltf::{Gltf, Node, buffer::Data, json::Value}; +use log::{debug, info}; +use weareshared::{ + Affine3A, packets::Resource, resources::CollisionPart, store::ResourceStore, vec3a, +}; + +pub fn import_physics( + gltf: &Gltf, + trans: Affine3A, + node: &Node, + store: &ResourceStore, + buffers: &[Data], +) -> Result)>> { + let mut collision = Vec::new(); + if let Some(physics) = node + .extensions() + .and_then(|e| e.get("KHR_physics_rigid_bodies")) + { + debug!("--- COLLISION ---"); + if let Some(collider) = physics.get("collider") { + if let Some(geometry) = collider.get("geometry") { + if let Some(&Value::Bool(chull)) = geometry.get("convexHull") { + let node = geometry + .get("node") + .and_then(|n| n.as_u64()) + .ok_or(anyhow!("coll geom node missing"))?; + let node = gltf + .nodes() + .nth(node as usize) + .ok_or(anyhow!("coll geom node reference invalid"))?; + let mesh = node.mesh().ok_or(anyhow!("coll geom node has no mesh"))?; + for p in mesh.primitives() { + let reader = p.reader(|buf| Some(&buffers[buf.index()])); + + let index = reader + .read_indices() + .ok_or(anyhow!("coll geom no index buffer"))? + .into_u32() + .array_chunks::<3>() + .collect::>(); + let position = reader + .read_positions() + .ok_or(anyhow!("coll geom no positions"))? + .map(|[x, y, z]| vec3a(x, y, z)) + .collect::>(); + + let mut collpart = CollisionPart { + name: node.name().map(|s| s.to_string()), + ..Default::default() + }; + + if chull { + debug!("convex hull has {} positions", position.len()); + collpart.sh_convex_hull = Some(store.set(&position)?); + } else { + debug!( + "mesh has {} indecies and {} positions", + index.len(), + position.len() + ); + collpart.sh_mesh = Some((store.set(&index)?, store.set(&position)?)); + } + + info!("added collision {:?}", node.name().unwrap_or_default()); + collision.push((trans, store.set(&collpart)?)); + } + } + } + } + } + Ok(collision) +} diff --git a/import/src/prefab.rs b/import/src/prefab.rs new file mode 100644 index 0000000..62690f7 --- /dev/null +++ b/import/src/prefab.rs @@ -0,0 +1,442 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use crate::{ + Args, TextureCache, animation::import_animation, mesh::import_mesh, physics::import_physics, + transform_to_affine, vrm::extract_vrm_data, +}; +use anyhow::{Context, Result, anyhow}; +use glam::{Affine3A, Vec3, Vec3A, vec3a}; +use gltf::{Gltf, Node, import_buffers, scene::Transform}; +use log::{debug, info}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use serde::Deserialize; +use serde_json::Value; +use std::{ + borrow::Cow, + collections::BTreeMap, + fs::{File, read_to_string}, + io::Read, + path::Path, + sync::Mutex, +}; +use weareshared::{ + packets::Resource, + resources::{ + ArmaturePart, AvatarInfoPart, EnvironmentPart, Image, LightPart, ParticlesPart, Prefab, + }, + store::ResourceStore, +}; + +pub fn import_prefab( + store: &ResourceStore, + texture_cache: &TextureCache, + scenepath: &Path, + args: &Args, +) -> Result> { + let path_base = scenepath.parent().unwrap(); + let mut gltf = + Gltf::from_reader_without_validation(File::open(scenepath)?).context("gltf parsing")?; + let blob = gltf.blob.take(); + let buffers = import_buffers(&gltf, Some(path_base), blob).context("importing buffers")?; + + let root = gltf.default_scene().ok_or(anyhow!("no default scene"))?; + + // gltf.as_json().asset.copyright + // eprintln!("{:?}", gltf.extensions_used()); + // eprintln!("{:?}", gltf.extensions()); + // eprintln!("{:?}", root.extensions()); + // eprintln!("{:?}", root.extras()); + + let mut nodes = Vec::new(); + fn traverse<'a>(out: &mut Vec<(Affine3A, Node<'a>)>, node: Node<'a>, trans: Affine3A) { + let trans = trans * transform_to_affine(node.transform()); + for c in node.children() { + traverse(out, c, trans); + } + out.push((trans, node)); + } + + let mut root_affine = Affine3A::IDENTITY; + root_affine.matrix3 *= args.scale.unwrap_or(1.); + root_affine.translation *= args.scale.unwrap_or(1.); + + for node in root.nodes() { + traverse(&mut nodes, node, root_affine); + } + + let vrm = extract_vrm_data(&gltf)?; + + let mut skin_index_to_arm_index = BTreeMap::new(); + let mut joint_index_to_arm_index = BTreeMap::new(); + let joint_name_to_arm_index: BTreeMap; + let armature = { + let mut name = Vec::new(); + let mut parent_pre_map = Vec::new(); + let mut transform = Vec::new(); + let mut inverse_bind_transform = Vec::new(); + + for skin in gltf.skins() { + let mut inverse_bind_mat = skin + .reader(|buf| Some(&buffers[buf.index()])) + .read_inverse_bind_matrices(); + for (j_ind, j) in skin.joints().enumerate() { + let ibm = inverse_bind_mat.as_mut().map(|x| x.next().unwrap()); + let a_ind = match joint_index_to_arm_index.get(&j.index()) { + Some(i) => *i, + None => { + let a_ind = name.len(); + // name.push(j.name().unwrap_or("").to_string()); + name.push(format!("bone{}", a_ind)); + transform.push(transform_to_affine(j.transform())); + parent_pre_map.push( + gltf.nodes() + .find(|n| n.children().any(|c| c.index() == j.index())) + .map(|n| n.index()), + ); + inverse_bind_transform.push( + ibm.map(|a| transform_to_affine(Transform::Matrix { matrix: a })) + .unwrap_or(Affine3A::IDENTITY), + ); + + joint_index_to_arm_index.insert(j.index(), a_ind); + a_ind + } + }; + skin_index_to_arm_index.insert((skin.index(), j_ind as u16), a_ind as u32); + } + } + + let parent = parent_pre_map + .into_iter() + .enumerate() + .map(|(i, p)| { + p.and_then(|i| joint_index_to_arm_index.get(&i).copied()) + .unwrap_or(i) as u16 + }) + .collect::>(); + + for (node, bname) in &vrm.bone_node_names { + let ind = joint_index_to_arm_index[node]; + name[ind] = bname.to_owned(); + } + + joint_name_to_arm_index = name + .iter() + .cloned() + .enumerate() + .map(|(a, b)| (b, a)) + .collect(); + + ArmaturePart { + name: Some(name), + parent: Some(parent), + transform: Some(transform), + inverse_bind_transform: Some(inverse_bind_transform), + } + }; + + let head_bones = { + let pa = armature.parent.clone().unwrap_or_default(); + let na = armature.name.clone().unwrap_or_default(); + (0..pa.len()) + .filter(|&(mut i)| { + let mut f = false; + while pa[i] as usize != i { + f |= na[i] == "head"; + i = pa[i] as usize; + } + f + }) + .map(|e| e as u32) + .collect::>() + }; + + let prefab = Mutex::new(Prefab::default()); + let node_to_meshes = Mutex::new(BTreeMap::>::new()); + + nodes + .par_iter() + .map(|(trans, node)| { + // if node.name().unwrap_or_default() == "particles" { + // eprintln!("{:?}", node.transform()); + // eprintln!("{:?}", node.extensions()); + // eprintln!("{:?}", node.extras()); + // } + let name = node.name().unwrap_or_default(); + let extras: Value = node + .extras() + .to_owned() + .map(|v| serde_json::from_str(v.get()).unwrap()) + .unwrap_or(Value::Object(Default::default())); + + if !name.ends_with("-collider") { + if let Some(mesh) = node.mesh() { + let meshes = import_mesh( + mesh, + *trans, + &buffers, + &store, + path_base, + node, + &args, + &texture_cache, + &skin_index_to_arm_index, + &vrm, + &head_bones, + )?; + for (node, trans, mesh) in meshes { + let mut k = prefab.lock().unwrap(); + let i = k.mesh.len(); + k.mesh.push((trans, mesh)); + node_to_meshes + .lock() + .unwrap() + .entry(node) + .or_default() + .push(i) + } + } + } + if extras.get("particles") == Some(&Value::Bool(true)) && !args.no_particles { + #[derive(Deserialize)] + struct ParticlesAttr { + density: Option, + lifetime: Option, + lifetime_spread: Option, + velocity: Option, + velocity_spread: Option, + } + // let sprite = extras + // .get("sprite") + // .ok_or(anyhow!("particle volume is missing sprite"))?; + + let attr: ParticlesAttr = + serde_json::from_value(extras).context("particles attributes")?; + + info!("adding particles part"); + + let part = store.set(&ParticlesPart { + sprite: None, + density: attr.density, + lifetime: attr.lifetime, + lifetime_spread: attr.lifetime_spread, + velocity: attr.velocity, + velocity_spread: attr.velocity_spread, + })?; + + prefab + .lock() + .unwrap() + .particles + .push((transform_to_affine(node.transform()), part)); + } + + if let Some(light) = node.light() { + let name = node.name().map(|e| e.to_owned()); + if let Some(name) = &name { + info!("adding light {name:?}"); + } else { + info!("adding light"); + } + let emission = Some(Vec3A::from_array(light.color()) * light.intensity()); + if let Some(e) = emission { + debug!("emission is {e}"); + } + let (position, _, _) = node.transform().decomposed(); + let part = store.set(&LightPart { + emission, + name, + radius: None, + })?; + prefab + .lock() + .unwrap() + .light + .push((Vec3A::from_array(position), part)); + } + { + let collider = import_physics(&gltf, *trans, node, &store, &buffers)?; + prefab.lock().unwrap().collision.extend(collider); + } + + Ok::<_, anyhow::Error>(()) + }) + .reduce( + || Ok(()), + |a, b| match (a, b) { + (Ok(()), Ok(())) => Ok(()), + (Ok(()), a) => a, + (a, _) => a, + }, + )?; + + let mut prefab = prefab.into_inner().unwrap(); + let node_to_meshes = node_to_meshes.into_inner().unwrap(); + + if let Some(apath) = args.animation.clone() { + let path_base = apath.parent().unwrap(); + let mut gltf = + Gltf::from_reader_without_validation(File::open(&apath)?).context("gltf parsing")?; + let blob = gltf.blob.take(); + let buffers = import_buffers(&gltf, Some(path_base), blob).context("importing buffers")?; + + let anim_name_map = if let Some(ref map_path) = args.animation_bone_map { + let mut map = BTreeMap::new(); + for l in read_to_string(map_path)?.lines() { + if !l.trim().is_empty() && !l.starts_with(";") { + let (a, b) = l.split_once("=").unwrap(); + map.insert(a.to_string(), b.to_string()); + } + } + Some(map) + } else { + None + }; + + let mut anim_joint_index_to_arm_index = BTreeMap::new(); + let mut joint_index_to_ibm = BTreeMap::new(); + for n in gltf.nodes() { + if let Some(name) = n.name() { + let Some(vrm_name) = (if let Some(map) = &anim_name_map { + map.get(name).cloned() + } else { + Some(name.to_string()) + }) else { + continue; + }; + let anim_node_index = n.index(); + if let Some(scene_arm_index) = joint_name_to_arm_index.get(&vrm_name) { + debug!( + "mapping {name:?} (node={}) -> {vrm_name:?} (bone={})", + anim_node_index, scene_arm_index + ); + anim_joint_index_to_arm_index.insert(anim_node_index, *scene_arm_index); + } + } + } + if args.animation_apply_ibm { + for s in gltf.skins() { + let reader = s.reader(|buf| Some(&buffers[buf.index()])); + if let Some(ibms) = reader.read_inverse_bind_matrices() { + for (jn, ibm) in s.joints().zip(ibms) { + joint_index_to_ibm.insert( + jn.index(), + transform_to_affine(Transform::Matrix { matrix: ibm }), + ); + } + } + } + debug!("{} joint IBMs found", joint_index_to_ibm.len()); + } + let transform = args + .animation_rotation_y + .map(Affine3A::from_rotation_y) + .unwrap_or_default() + * args + .animation_scale + .map(Vec3::splat) + .map(Affine3A::from_scale) + .unwrap_or_default(); + for a in gltf.animations() { + prefab.animation.push(import_animation( + a, + &store, + transform, + &joint_index_to_ibm, + &anim_joint_index_to_arm_index, + &BTreeMap::new(), + &buffers, + )?); + } + } + + for a in gltf.animations() { + if !args.no_animations { + prefab.animation.push(import_animation( + a, + &store, + Affine3A::IDENTITY, + &BTreeMap::new(), + &joint_index_to_arm_index, + &node_to_meshes, + &buffers, + )?); + }; + } + + if vrm.camera_mount.is_some() + || vrm.camera_mount_offset.is_some() + || !vrm.bone_node_names.is_empty() + { + info!("avatar info enabled"); + prefab.avatar_info = Some( + store.set(&AvatarInfoPart { + armature: Some(0), // TODO + camera_mount: vrm + .camera_mount + .map(|e| joint_index_to_arm_index[&e] as u32), + camera_mount_offset: vrm.camera_mount_offset, + ..Default::default() + })?, + ); + } + + prefab.armature = if armature.parent.as_ref().is_some_and(|a| !a.is_empty()) { + vec![store.set(&armature)?] + } else { + vec![] + }; + + let skybox = if let Some(skybox) = &args.skybox { + let mut buf = Vec::new(); + File::open(skybox)?.read_to_end(&mut buf)?; + Some(store.set(&Image(Cow::Owned(buf)))?) + } else { + None + }; + let sun = if args.with_default_sun { + Some(( + vec3a(1., -5., 1.).normalize(), + vec3a(1., 1., 1.).normalize() * 100_000., + )) + } else { + None + }; + prefab.environment = if skybox.is_some() || sun.is_some() { + Some(store.set(&EnvironmentPart { skybox, sun })?) + } else { + None + }; + + prefab.name = args.name.clone().or(gltf + .default_scene() + .and_then(|n| n.name()) + .map(|n| n.to_owned())); + + if args.debug_light { + prefab.light.push(( + vec3a(5., 5., 5.), + store.set(&LightPart { + name: Some("debug light".to_owned()), + emission: Some(vec3a(10., 5., 15.)), + radius: Some(0.3), + })?, + )); + } + + Ok(store.set(&prefab)?) +} diff --git a/import/src/vrm.rs b/import/src/vrm.rs new file mode 100644 index 0000000..f014e66 --- /dev/null +++ b/import/src/vrm.rs @@ -0,0 +1,135 @@ +/* + wearechat - generic multiplayer game with voip + Copyright (C) 2025 metamuffin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +use anyhow::Result; +use gltf::Gltf; +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet}; +use weareshared::Vec3A; + +#[derive(Debug, Default)] +pub struct VrmInfo { + pub bone_node_names: Vec<(usize, String)>, + pub hide_first_person: BTreeSet, + pub camera_mount: Option, + pub camera_mount_offset: Option, +} + +pub fn extract_vrm_data(gltf: &Gltf) -> Result { + let mut o = VrmInfo::default(); + let mut fp = None; + if let Some(vrm) = gltf.extension_value("VRM") { + // serde_json::to_writer(std::fs::File::create("/tmp/vrm").unwrap(), vrm).unwrap(); + let vrm: Vrm = serde_json::from_value(vrm.clone())?; + for bone in vrm.humanoid.human_bones { + o.bone_node_names.push((bone.node, bone.bone)) + } + fp = vrm.first_person; + } + if let Some(vrm) = gltf.extension_value("VRMC_vrm") { + // serde_json::to_writer(std::fs::File::create("/tmp/vrmc").unwrap(), vrm).unwrap(); + let vrm: Vrmc = serde_json::from_value(vrm.clone())?; + for (name, bone) in vrm.humanoid.human_bones { + o.bone_node_names.push((bone.node, name)) + } + fp = vrm.first_person; + } + if let Some(fp) = fp { + o.camera_mount = fp.first_person_bone; + o.camera_mount_offset = fp.first_person_bone_offset.map(convert_vrm_vec); + for ann in fp.mesh_annotations { + if let FirstPersonFlag::ThirdPersonOnly = ann.first_person_flag { + o.hide_first_person.insert(ann.node); + } + } + } + Ok(o) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Vrm { + humanoid: VrmHumanoid, + first_person: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmHumanoid { + human_bones: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmHumanBone { + bone: String, + node: usize, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Vrmc { + humanoid: VrmcHumanoid, + first_person: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmcFirstPerson { + first_person_bone: Option, + first_person_bone_offset: Option, + mesh_annotations: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmcFirstPersonMeshAnnotation { + node: usize, + #[serde(alias = "type")] // spec says something but their sample uses "type" instead + first_person_flag: FirstPersonFlag, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +enum FirstPersonFlag { + ThirdPersonOnly, + FirstPersonOnly, + Both, + #[default] + Auto, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmcHumanoid { + human_bones: BTreeMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VrmcHumanBone { + node: usize, +} + +#[derive(Debug, Deserialize)] +struct VrmVec3 { + x: f32, + y: f32, + z: f32, +} +fn convert_vrm_vec(VrmVec3 { x, y, z }: VrmVec3) -> Vec3A { + Vec3A::new(x, y, z) +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 9f7f37a..50440fb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -3,6 +3,10 @@ name = "weareserver" version = "0.1.0" edition = "2024" +[[bin]] +name = "wearehosting" +path = "src/main.rs" + [dependencies] anyhow = "1.0.95" clap = { version = "4.5.23", features = ["derive"] } diff --git a/world/Cargo.toml b/world/Cargo.toml deleted file mode 100644 index 8ce9a77..0000000 --- a/world/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "weareworld" -version = "0.1.0" -edition = "2024" - -[dependencies] -anyhow = "1.0.95" -clap = { version = "4.5.23", features = ["derive"] } -env_logger = "0.11.6" -serde = { version = "1.0.217", features = ["derive"] } -gltf = { version = "1.4.1", features = [ - "extras", - "names", - "extensions", - "KHR_lights_punctual", - "KHR_materials_transmission", - "KHR_materials_ior", - "KHR_materials_volume", - "KHR_materials_emissive_strength", -] } -log = "0.4.22" -weareshared = { path = "../shared" } -rand = "0.9.0-beta.1" -image = "0.25.5" -rayon = "1.10.0" -humansize = "2.1.3" -serde_json = "1.0.138" -glam = "0.29.2" diff --git a/world/src/animation.rs b/world/src/animation.rs deleted file mode 100644 index ca049e5..0000000 --- a/world/src/animation.rs +++ /dev/null @@ -1,133 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use anyhow::Result; -use glam::{Quat, vec3a}; -use gltf::{ - Animation, - animation::{Property, util::ReadOutputs}, - buffer::Data, -}; -use log::{debug, info}; -use std::collections::BTreeMap; -use weareshared::{ - Affine3A, - packets::Resource, - resources::{AnimationChannel, AnimationPart}, - store::ResourceStore, -}; - -pub fn import_animation( - a: Animation<'_>, - store: &ResourceStore, - transform: Affine3A, - joint_index_to_ibm: &BTreeMap, - joint_index_to_arm_index: &BTreeMap, - node_to_meshes: &BTreeMap>, - buffers: &[Data], -) -> Result> { - let mut max_time = 0f32; - let mut channels = Vec::new(); - for c in a.channels() { - let node = c.target().node().index(); - let reader = c.reader(|i| Some(&buffers[i.index()].0)); - let inputs: Vec = reader.read_inputs().unwrap().collect::>(); - let outputs: Vec = - if joint_index_to_ibm.contains_key(&node) || transform != Affine3A::IDENTITY { - let t = transform - * joint_index_to_ibm - .get(&node) - .copied() - .unwrap_or_default() - .inverse(); - let (_, rot, _) = t.to_scale_rotation_translation(); - match reader.read_outputs().unwrap() { - ReadOutputs::Translations(iter) => iter - .flat_map(|[x, y, z]| (t.matrix3 * vec3a(x, y, z)).to_array()) - .collect(), - ReadOutputs::Rotations(iter) => iter - .into_f32() - .map(Quat::from_array) - .map(|q| q * rot) - .flat_map(|q| q.to_array()) - .collect(), - // ReadOutputs::Scales(iter) => iter - // .flat_map(|[x, y, z]| (t.matrix3 * vec3a(x, y, z)).to_array()) - // .collect(), - ReadOutputs::MorphTargetWeights(iter) => iter.into_f32().collect(), - _ => continue, - } - } else { - match reader.read_outputs().unwrap() { - ReadOutputs::Translations(iter) => iter.flatten().collect(), - ReadOutputs::Rotations(iter) => iter.into_f32().flatten().collect(), - ReadOutputs::Scales(iter) => iter.flatten().collect(), - ReadOutputs::MorphTargetWeights(iter) => iter.into_f32().collect(), - } - }; - for x in &inputs { - max_time = max_time.max(*x) - } - let time = store.set(&inputs)?; - let value = store.set(&outputs)?; - - if let Some(&m) = joint_index_to_arm_index.get(&node) { - let a = 0; // TODO - let mut ch = AnimationChannel::default(); - match c.target().property() { - Property::Translation => ch.t_joint_translation = Some((a, m as u32)), - Property::Rotation => ch.t_joint_rotation = Some((a, m as u32)), - Property::Scale => ch.t_joint_scale = Some((a, m as u32)), - Property::MorphTargetWeights => continue, - } - ch.time = Some(time.clone()); - ch.value = Some(value.clone()); - debug!( - "animation channel {:?} of joint {m} of armature {a} with {} time and {} component values", - c.target().property(), - inputs.len(), - outputs.len() - ); - channels.push(ch); - } - if let Some(meshes) = node_to_meshes.get(&node) { - for &m in meshes { - let mut ch = AnimationChannel::default(); - match c.target().property() { - Property::Translation => ch.t_mesh_translation = Some(m as u32), - Property::Rotation => ch.t_mesh_rotation = Some(m as u32), - Property::Scale => ch.t_mesh_scale = Some(m as u32), - Property::MorphTargetWeights => continue, - } - ch.time = Some(time.clone()); - ch.value = Some(value.clone()); - debug!( - "animation channel {:?} of mesh {m} with {} time and {} component values", - c.target().property(), - inputs.len(), - outputs.len() - ); - channels.push(ch); - } - } - } - info!("adding animation with {} channels", channels.len()); - store.set(&AnimationPart { - name: a.name().map(|n| n.to_string()), - channel: channels, - duration: Some(max_time), - }) -} diff --git a/world/src/main.rs b/world/src/main.rs deleted file mode 100644 index 554a2bd..0000000 --- a/world/src/main.rs +++ /dev/null @@ -1,304 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -#![feature(iter_array_chunks)] -#![allow(clippy::too_many_arguments, clippy::type_complexity)] -pub mod animation; -pub mod mesh; -pub mod physics; -pub mod prefab; -pub mod vrm; - -use anyhow::{Result, bail}; -use clap::Parser; -use gltf::{image::Source, scene::Transform}; -use humansize::BINARY; -use image::{ImageReader, codecs::webp::WebPEncoder}; -use log::{debug, info}; -use prefab::import_prefab; -use rand::random; -use std::{ - borrow::Cow, - collections::HashMap, - fs::File, - io::{BufWriter, Cursor, Read, Write}, - marker::PhantomData, - net::{SocketAddr, TcpStream}, - path::{Path, PathBuf}, - sync::{Arc, Mutex}, - thread::{self, sleep}, - time::Duration, -}; -use weareshared::{ - Affine3A, Vec3A, - helper::ReadWrite, - packets::{Data, Object, Packet, Resource}, - resources::{Image, RespackEntry}, - respack::save_respack, - store::ResourceStore, - vec3a, -}; - -#[derive(Parser)] -pub struct Args { - #[arg(short, long)] - address: Option, - - /// Output converted prefab as resource package - #[arg(short = 'o', long)] - pack: Option, - - /// Path(s) to a glTF file, binary or json format - scene: Vec, - /// Send all resources to the server then quit - #[arg(short, long)] - push: bool, - - /// Remove all other object from the world - #[arg(short, long)] - clear: bool, - /// Add the object to the world - #[arg(short, long)] - add: bool, - /// Transcode all textures to WebP - #[arg(short, long)] - webp: bool, - /// Add skybox - #[arg(long)] - skybox: Option, - /// Override prefab name - #[arg(short, long)] - name: Option, - #[arg(short, long)] - scale: Option, - #[arg(short, long)] - dry_run: bool, - #[arg(short, long)] - line_up: bool, - - #[arg(long)] - use_cache: bool, - - #[arg(short = 'S', long)] - with_default_sun: bool, - - #[arg(long)] - animation: Option, - #[arg(long)] - animation_bone_map: Option, - #[arg(long)] - animation_rotation_y: Option, - #[arg(long)] - animation_scale: Option, - #[arg(long)] - animation_apply_ibm: bool, - - /// Spins the object - #[arg(long)] - debug_spin: bool, - /// Adds a light - #[arg(long)] - debug_light: bool, - #[arg(long)] - no_particles: bool, - #[arg(long)] - no_animations: bool, -} - -fn main() -> Result<()> { - env_logger::init_from_env("LOG"); - let args = Args::parse(); - - let store = if args.use_cache && !args.pack.is_some() { - ResourceStore::new_env()? - } else { - ResourceStore::new_memory() - }; - - let mut prefabs = Vec::new(); - let texture_cache = Arc::new(Mutex::new(HashMap::new())); - - for scenepath in &args.scene { - prefabs.push(import_prefab(&store, &texture_cache, scenepath, &args)?); - } - - let mut size = 0; - store.iter(|_k, len| size += len).unwrap(); - info!( - "prefab has network size of {}", - humansize::format_size(size, BINARY) - ); - - if args.dry_run { - return Ok(()); - } - if let Some(outpath) = args.pack { - let entry = store.set(&RespackEntry { name: None })?; - let mut resources = Vec::new(); - store.iter(|r, _| resources.push(r))?; - save_respack( - BufWriter::new(File::create(outpath)?), - &store, - &resources, - Some(entry), - )?; - } else if let Some(address) = args.address { - let mut sock = TcpStream::connect(address)?; - Packet::Connect(random()).write(&mut sock)?; - for p in &prefabs { - Packet::AnnouncePrefab(p.clone()).write(&mut sock)?; - } - sock.flush()?; - - let mut obs = Vec::new(); - if args.add { - for (i, p) in prefabs.iter().enumerate() { - let ob = Object::new(); - info!("adding object {ob}"); - Packet::Add(ob, p.clone()).write(&mut sock)?; - if args.line_up { - Packet::Position(ob, vec3a(i as f32 * 1.2, 0., i as f32 * 0.3), Vec3A::ZERO) - .write(&mut sock)?; - } - obs.push(ob); - } - sock.flush()?; - } - - if args.debug_spin { - let ob = obs[0]; - let mut sock2 = sock.try_clone().unwrap(); - thread::spawn(move || { - let mut x = 0.; - loop { - Packet::Position(ob, Vec3A::ZERO, vec3a(0., x * 0.1, 0.)) - .write(&mut sock2) - .unwrap(); - sock2.flush().unwrap(); - x += 0.1; - sleep(Duration::from_millis(50)); - } - }); - } - if args.push { - if args.use_cache { - return Ok(()); - } - store.iter(|k, _| { - Packet::RespondResource(k, Data(store.get_raw(k).unwrap().unwrap())) - .write(&mut sock) - .unwrap(); - })?; - sock.flush()?; - } else { - loop { - let packet = Packet::read(&mut sock)?; - match packet { - Packet::RequestResource(hash) => { - if let Some(d) = store.get_raw(hash)? { - Packet::RespondResource(hash, Data(d)).write(&mut sock)?; - sock.flush()?; - } - } - Packet::Add(ob_a, _) => { - if args.clear && !obs.contains(&ob_a) { - info!("removing object {ob_a}"); - Packet::Remove(ob_a).write(&mut sock)?; - sock.flush()?; - } - } - _ => (), - } - } - } - } else { - bail!("no output option specified. either provide an address to a server or use --pack") - } - Ok(()) -} - -pub type TextureCache = Arc>>>>; -fn load_texture( - name: &str, - store: &ResourceStore, - path: &Path, - buffers: &[gltf::buffer::Data], - source: &Source, - webp: bool, - texture_cache: &TextureCache, -) -> Result>> { - let (mut image, uri) = match source { - gltf::image::Source::View { view, mime_type } => { - debug!("{name} texture is embedded and of type {mime_type:?}"); - let buf = - &buffers[view.buffer().index()].0[view.offset()..view.offset() + view.length()]; - (Image(Cow::Borrowed(buf)), None) - } - gltf::image::Source::Uri { - uri, - mime_type: Some(mime_type), - } => { - debug!("{name} texture is {uri:?} and of type {mime_type:?}"); - if let Some(res) = texture_cache.lock().unwrap().get(*uri) { - return Ok(res.to_owned()); - } - let path = path.join(uri); - let mut buf = Vec::new(); - File::open(path)?.read_to_end(&mut buf)?; - (Image(buf.into()), Some(uri.to_string())) - } - gltf::image::Source::Uri { - uri, - mime_type: None, - } => { - debug!("{name} texture is {uri:?} and has no type"); - if let Some(res) = texture_cache.lock().unwrap().get(*uri) { - return Ok(res.to_owned()); - } - let path = path.join(uri); - let mut buf = Vec::new(); - File::open(path)?.read_to_end(&mut buf)?; - (Image(buf.into()), Some(uri.to_string())) - } - }; - - if webp { - let mut image_out = Vec::new(); - - let len = image.0.len(); - ImageReader::new(Cursor::new(image.0)) - .with_guessed_format()? - .decode()? - .write_with_encoder(WebPEncoder::new_lossless(&mut image_out))?; - debug!("webp encode: {len} -> {}", image_out.len()); - image = Image(Cow::Owned(image_out)); - } - let res = Resource(store.set(&image)?.0, PhantomData); - if let Some(uri) = uri { - texture_cache.lock().unwrap().insert(uri, res.clone()); - } - Ok(res) -} - -pub fn transform_to_affine(trans: Transform) -> Affine3A { - let mat = trans.matrix(); - Affine3A::from_cols_array_2d(&[ - [mat[0][0], mat[0][1], mat[0][2]], - [mat[1][0], mat[1][1], mat[1][2]], - [mat[2][0], mat[2][1], mat[2][2]], - [mat[3][0], mat[3][1], mat[3][2]], - ]) -} diff --git a/world/src/mesh.rs b/world/src/mesh.rs deleted file mode 100644 index ffc0f2f..0000000 --- a/world/src/mesh.rs +++ /dev/null @@ -1,431 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use crate::{Args, TextureCache, load_texture, vrm::VrmInfo}; -use anyhow::Result; -use gltf::{Mesh, Node, buffer::Data}; -use log::{debug, info, warn}; -use std::{collections::BTreeMap, path::Path}; -use weareshared::{ - Affine3A, Vec3A, packets::Resource, resources::MeshPart, store::ResourceStore, vec2, vec3a, - vec4, -}; - -pub fn import_mesh( - mesh: Mesh, - trans: Affine3A, - buffers: &[Data], - store: &ResourceStore, - path_base: &Path, - node: &Node, - args: &Args, - texture_cache: &TextureCache, - joint_index_map: &BTreeMap<(usize, u16), u32>, - vrm: &VrmInfo, - head_bones: &[u32], -) -> Result)>> { - let mut meshes = Vec::new(); - for p in mesh.primitives() { - let name = mesh.name().or(node.name()).map(|e| e.to_owned()); - if let Some(name) = &name { - info!("adding mesh {name:?}"); - } else { - info!("adding mesh"); - } - let reader = p.reader(|buf| Some(&buffers[buf.index()])); - - let mut num_vertex = 0; - let va_position = reader - .read_positions() - .map(|iter| { - let a = iter.map(|[x, y, z]| vec3a(x, y, z)).collect::>(); - debug!("{} vertex positions", a.len()); - num_vertex = a.len(); - store.set(&a) - }) - .transpose()?; - - let va_normal = reader - .read_normals() - .map(|iter| { - let a = iter.map(|[x, y, z]| vec3a(x, y, z)).collect::>(); - debug!("{} vertex normals", a.len()); - store.set(&a) - }) - .transpose()?; - - let va_tangent = reader - .read_tangents() - .map(|iter| { - // TODO dont ignore handedness - let a = iter - .map(|[x, y, z, h]| vec4(x, y, z, h)) - .collect::>(); - debug!("{} vertex tangents", a.len()); - store.set(&a) - }) - .transpose()?; - - let mut many_head_bones = false; - let va_joint_index = reader - .read_joints(0) - .map(|iter| { - let si = node.skin().unwrap().index(); - let a = iter - .into_u16() - .map(|x| x.map(|x| joint_index_map[&(si, x)])) - .collect::>(); - - let head_bone_count = a - .iter() - .flatten() - .filter(|b| head_bones.contains(*b)) - .count(); - many_head_bones |= head_bone_count > a.len() / 2; - debug!("{} vertex joint indecies", a.len()); - if many_head_bones { - debug!("many joints are head bones"); - } - if a.len() != num_vertex { - warn!("joint index count does not vertex count") - } - store.set(&a) - }) - .transpose()?; - - let va_joint_weight = reader - .read_weights(0) - .map(|iter| { - let a = iter.into_f32().collect::>(); - debug!("{} vertex joint weights", a.len()); - if a.len() != num_vertex { - warn!("joint weight count does not vertex count") - } - store.set(&a) - }) - .transpose()?; - - let va_texcoord = reader - .read_tex_coords(0) - .map(|iter| { - let a = iter.into_f32().map(|[x, y]| vec2(x, y)).collect::>(); - debug!("{} vertex texture coordinates", a.len()); - store.set(&a) - }) - .transpose()?; - - let va_albedo = reader - .read_colors(0) - .map(|iter| { - let a = iter - .into_rgb_f32() - .map(|[x, y, z]| vec3a(x, y, z)) - .collect::>(); - debug!("{} vertex colors", a.len()); - store.set(&a) - }) - .transpose()?; - - let va_alpha = reader - .read_colors(0) - .map(|iter| { - let mut color_a = vec![]; - for p in iter.into_rgba_f32() { - color_a.push(p[3]); - } - let o = if color_a.iter().any(|x| *x != 1.) { - debug!("{} vertex transmissions", color_a.len()); - Some(store.set(&color_a)?) - } else { - debug!("vertex transmission pruned"); - None - }; - Ok::<_, anyhow::Error>(o) - }) - .transpose()? - .flatten(); - - let index = reader - .read_indices() - .unwrap() - .into_u32() - .array_chunks::<3>() - .collect::>(); - - debug!("{} indecies", index.len() * 3); - let index = Some(store.set(&index)?); - - let mut tex_albedo = None; - let mut tex_alpha = None; - if let Some(tex) = p.material().pbr_metallic_roughness().base_color_texture() { - let r = load_texture( - "albedo", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?; - tex_albedo = Some(r.clone()); - tex_alpha = Some(r.clone()); - } - let mut tex_normal = None; - if let Some(tex) = p.material().normal_texture() { - tex_normal = Some(load_texture( - "normal", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?); - } - let mut tex_emission = None; - if let Some(tex) = p.material().emissive_texture() { - tex_emission = Some(load_texture( - "emission", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?); - } - let mut tex_transmission = None; - if let Some(tex) = p - .material() - .transmission() - .and_then(|t| t.transmission_texture()) - { - tex_transmission = Some(load_texture( - "transmission", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?); - } - let mut tex_thickness = None; - if let Some(tex) = p.material().volume().and_then(|t| t.thickness_texture()) { - tex_thickness = Some(load_texture( - "thickness", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?); - } - let mut tex_occlusion = None; - if let Some(tex) = p.material().occlusion_texture() { - tex_occlusion = Some(load_texture( - "occlusion", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?); - } - let mut tex_roughness = None; - let mut tex_metallic = None; - if let Some(tex) = p - .material() - .pbr_metallic_roughness() - .metallic_roughness_texture() - { - let r = load_texture( - "metallic+roughness", - store, - path_base, - buffers, - &tex.texture().source().source(), - args.webp, - texture_cache, - )?; - tex_roughness = Some(r.clone()); - tex_metallic = Some(r.clone()); - } - - let g_metallic = Some(p.material().pbr_metallic_roughness().metallic_factor()); - let g_roughness = Some(p.material().pbr_metallic_roughness().roughness_factor()); - - let base_color = p.material().pbr_metallic_roughness().base_color_factor(); - - let g_albedo = if base_color[0] != 1. || base_color[1] != 1. || base_color[2] != 1. { - debug!( - "albedo is r={},g={},b={}", - base_color[0], base_color[1], base_color[2] - ); - Some(Vec3A::new(base_color[0], base_color[1], base_color[2])) - } else { - debug!("albedo pruned"); - None - }; - let g_alpha = if base_color[3] != 1. { - debug!("alpha is {}", base_color[3]); - Some(base_color[3]) - } else { - debug!("alpha pruned"); - None - }; - - let emission = p.material().emissive_factor(); - let g_emission = if emission[0] != 0. || emission[1] != 0. || emission[2] != 0. { - debug!( - "emission is r={},g={},b={}", - base_color[0], base_color[1], base_color[2] - ); - Some(Vec3A::new(emission[0], emission[1], emission[2])) - } else { - debug!("emission pruned"); - None - }; - - let transmission = p - .material() - .transmission() - .map(|t| t.transmission_factor()) - .unwrap_or(0.); - - let g_transmission = if transmission != 0. { - debug!("transmission is {transmission}"); - Some(transmission) - } else { - debug!("transmission pruned"); - None - }; - - let g_dispersion = p - .material() - .extension_value("KHR_materials_dispersion") - .and_then(|e| e.get("dispersion")) - .and_then(|e| e.as_f64()) - .map(|e| e as f32); - if let Some(d) = g_dispersion { - debug!("dispersion is {d}"); - } - - // if node.name() == Some("fog") { - // eprintln!("{:#?}", p.material().volume().is_some()); - // eprintln!("{:#?}", p.material().ior()); - // eprintln!("{:#?}", p.material().transmission().is_some()); - // } - - let g_attenuation = p.material().volume().map(|v| { - let ref_dist = v.attenuation_distance(); - let att = Vec3A::from_array(v.attenuation_color().map( - // manually derived from attenuation coefficient formula. i hope this is correct. - |factor| -(factor.powf(1. / ref_dist)).ln(), - )); - debug!("attenuation is {att}"); - att - }); - let g_refractive_index = p.material().ior(); - if let Some(i) = g_refractive_index { - debug!("refractive index is {i}"); - } - let g_thickness = p.material().volume().map(|v| v.thickness_factor()); - - let g_unlit = bool_to_opt( - p.material() - .extension_value("KHR_materials_unlit") - .is_some(), - "unlit", - ); - - let g_double_sided = bool_to_opt(p.material().double_sided(), "double sided"); - - let hint_volume = bool_to_opt( - g_attenuation.is_some_and(|a| a.length() > 0.01), - "volume hint", - ); - - let hint_hide_first_person = bool_to_opt( - many_head_bones | vrm.hide_first_person.contains(&node.index()), - "hide first person hint", - ); - - let hint_mirror = bool_to_opt( - node.name().unwrap_or_default().ends_with("-mirror"), - "mirror hint", - ); - - let armature = node.skin().map(|_| 0); - - let mesh = MeshPart { - name, - index, - armature, - g_albedo, - g_alpha, - g_metallic, - g_roughness, - g_emission, - g_transmission, - g_attenuation, - g_thickness, - g_refractive_index, - g_dispersion, - g_unlit, - g_double_sided, - va_position, - va_normal, - va_tangent, - va_texcoord, - va_albedo, - va_alpha, - va_joint_index, - va_joint_weight, - tex_albedo, - tex_normal, - tex_roughness, - tex_metallic, - tex_alpha, - tex_emission, - tex_transmission, - tex_thickness, - tex_occlusion, - hint_hide_first_person, - hint_mirror, - hint_volume, - // not supported by gltf - hint_static: None, // TODO Set when instancing - va_transmission: None, - va_emission: None, - va_metallic: None, - va_roughness: None, - }; - meshes.push((node.index(), trans, store.set(&mesh)?)) - } - Ok(meshes) -} - -fn bool_to_opt(b: bool, log: &str) -> Option<()> { - if b { - debug!("{log}"); - Some(()) - } else { - None - } -} diff --git a/world/src/physics.rs b/world/src/physics.rs deleted file mode 100644 index 3beb49b..0000000 --- a/world/src/physics.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use anyhow::{Result, anyhow}; -use gltf::{Gltf, Node, buffer::Data, json::Value}; -use log::{debug, info}; -use weareshared::{ - Affine3A, packets::Resource, resources::CollisionPart, store::ResourceStore, vec3a, -}; - -pub fn import_physics( - gltf: &Gltf, - trans: Affine3A, - node: &Node, - store: &ResourceStore, - buffers: &[Data], -) -> Result)>> { - let mut collision = Vec::new(); - if let Some(physics) = node - .extensions() - .and_then(|e| e.get("KHR_physics_rigid_bodies")) - { - debug!("--- COLLISION ---"); - if let Some(collider) = physics.get("collider") { - if let Some(geometry) = collider.get("geometry") { - if let Some(&Value::Bool(chull)) = geometry.get("convexHull") { - let node = geometry - .get("node") - .and_then(|n| n.as_u64()) - .ok_or(anyhow!("coll geom node missing"))?; - let node = gltf - .nodes() - .nth(node as usize) - .ok_or(anyhow!("coll geom node reference invalid"))?; - let mesh = node.mesh().ok_or(anyhow!("coll geom node has no mesh"))?; - for p in mesh.primitives() { - let reader = p.reader(|buf| Some(&buffers[buf.index()])); - - let index = reader - .read_indices() - .ok_or(anyhow!("coll geom no index buffer"))? - .into_u32() - .array_chunks::<3>() - .collect::>(); - let position = reader - .read_positions() - .ok_or(anyhow!("coll geom no positions"))? - .map(|[x, y, z]| vec3a(x, y, z)) - .collect::>(); - - let mut collpart = CollisionPart { - name: node.name().map(|s| s.to_string()), - ..Default::default() - }; - - if chull { - debug!("convex hull has {} positions", position.len()); - collpart.sh_convex_hull = Some(store.set(&position)?); - } else { - debug!( - "mesh has {} indecies and {} positions", - index.len(), - position.len() - ); - collpart.sh_mesh = Some((store.set(&index)?, store.set(&position)?)); - } - - info!("added collision {:?}", node.name().unwrap_or_default()); - collision.push((trans, store.set(&collpart)?)); - } - } - } - } - } - Ok(collision) -} diff --git a/world/src/prefab.rs b/world/src/prefab.rs deleted file mode 100644 index bf45f01..0000000 --- a/world/src/prefab.rs +++ /dev/null @@ -1,426 +0,0 @@ -use crate::{ - Args, TextureCache, animation::import_animation, mesh::import_mesh, physics::import_physics, - transform_to_affine, vrm::extract_vrm_data, -}; -use anyhow::{Context, Result, anyhow}; -use glam::{Affine3A, Vec3, Vec3A, vec3a}; -use gltf::{Gltf, Node, import_buffers, scene::Transform}; -use log::{debug, info}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use serde::Deserialize; -use serde_json::Value; -use std::{ - borrow::Cow, - collections::BTreeMap, - fs::{File, read_to_string}, - io::Read, - path::Path, - sync::Mutex, -}; -use weareshared::{ - packets::Resource, - resources::{ - ArmaturePart, AvatarInfoPart, EnvironmentPart, Image, LightPart, ParticlesPart, Prefab, - }, - store::ResourceStore, -}; - -pub fn import_prefab( - store: &ResourceStore, - texture_cache: &TextureCache, - scenepath: &Path, - args: &Args, -) -> Result> { - let path_base = scenepath.parent().unwrap(); - let mut gltf = - Gltf::from_reader_without_validation(File::open(scenepath)?).context("gltf parsing")?; - let blob = gltf.blob.take(); - let buffers = import_buffers(&gltf, Some(path_base), blob).context("importing buffers")?; - - let root = gltf.default_scene().ok_or(anyhow!("no default scene"))?; - - // gltf.as_json().asset.copyright - // eprintln!("{:?}", gltf.extensions_used()); - // eprintln!("{:?}", gltf.extensions()); - // eprintln!("{:?}", root.extensions()); - // eprintln!("{:?}", root.extras()); - - let mut nodes = Vec::new(); - fn traverse<'a>(out: &mut Vec<(Affine3A, Node<'a>)>, node: Node<'a>, trans: Affine3A) { - let trans = trans * transform_to_affine(node.transform()); - for c in node.children() { - traverse(out, c, trans); - } - out.push((trans, node)); - } - - let mut root_affine = Affine3A::IDENTITY; - root_affine.matrix3 *= args.scale.unwrap_or(1.); - root_affine.translation *= args.scale.unwrap_or(1.); - - for node in root.nodes() { - traverse(&mut nodes, node, root_affine); - } - - let vrm = extract_vrm_data(&gltf)?; - - let mut skin_index_to_arm_index = BTreeMap::new(); - let mut joint_index_to_arm_index = BTreeMap::new(); - let joint_name_to_arm_index: BTreeMap; - let armature = { - let mut name = Vec::new(); - let mut parent_pre_map = Vec::new(); - let mut transform = Vec::new(); - let mut inverse_bind_transform = Vec::new(); - - for skin in gltf.skins() { - let mut inverse_bind_mat = skin - .reader(|buf| Some(&buffers[buf.index()])) - .read_inverse_bind_matrices(); - for (j_ind, j) in skin.joints().enumerate() { - let ibm = inverse_bind_mat.as_mut().map(|x| x.next().unwrap()); - let a_ind = match joint_index_to_arm_index.get(&j.index()) { - Some(i) => *i, - None => { - let a_ind = name.len(); - // name.push(j.name().unwrap_or("").to_string()); - name.push(format!("bone{}", a_ind)); - transform.push(transform_to_affine(j.transform())); - parent_pre_map.push( - gltf.nodes() - .find(|n| n.children().any(|c| c.index() == j.index())) - .map(|n| n.index()), - ); - inverse_bind_transform.push( - ibm.map(|a| transform_to_affine(Transform::Matrix { matrix: a })) - .unwrap_or(Affine3A::IDENTITY), - ); - - joint_index_to_arm_index.insert(j.index(), a_ind); - a_ind - } - }; - skin_index_to_arm_index.insert((skin.index(), j_ind as u16), a_ind as u32); - } - } - - let parent = parent_pre_map - .into_iter() - .enumerate() - .map(|(i, p)| { - p.and_then(|i| joint_index_to_arm_index.get(&i).copied()) - .unwrap_or(i) as u16 - }) - .collect::>(); - - for (node, bname) in &vrm.bone_node_names { - let ind = joint_index_to_arm_index[node]; - name[ind] = bname.to_owned(); - } - - joint_name_to_arm_index = name - .iter() - .cloned() - .enumerate() - .map(|(a, b)| (b, a)) - .collect(); - - ArmaturePart { - name: Some(name), - parent: Some(parent), - transform: Some(transform), - inverse_bind_transform: Some(inverse_bind_transform), - } - }; - - let head_bones = { - let pa = armature.parent.clone().unwrap_or_default(); - let na = armature.name.clone().unwrap_or_default(); - (0..pa.len()) - .filter(|&(mut i)| { - let mut f = false; - while pa[i] as usize != i { - f |= na[i] == "head"; - i = pa[i] as usize; - } - f - }) - .map(|e| e as u32) - .collect::>() - }; - - let prefab = Mutex::new(Prefab::default()); - let node_to_meshes = Mutex::new(BTreeMap::>::new()); - - nodes - .par_iter() - .map(|(trans, node)| { - // if node.name().unwrap_or_default() == "particles" { - // eprintln!("{:?}", node.transform()); - // eprintln!("{:?}", node.extensions()); - // eprintln!("{:?}", node.extras()); - // } - let name = node.name().unwrap_or_default(); - let extras: Value = node - .extras() - .to_owned() - .map(|v| serde_json::from_str(v.get()).unwrap()) - .unwrap_or(Value::Object(Default::default())); - - if !name.ends_with("-collider") { - if let Some(mesh) = node.mesh() { - let meshes = import_mesh( - mesh, - *trans, - &buffers, - &store, - path_base, - node, - &args, - &texture_cache, - &skin_index_to_arm_index, - &vrm, - &head_bones, - )?; - for (node, trans, mesh) in meshes { - let mut k = prefab.lock().unwrap(); - let i = k.mesh.len(); - k.mesh.push((trans, mesh)); - node_to_meshes - .lock() - .unwrap() - .entry(node) - .or_default() - .push(i) - } - } - } - if extras.get("particles") == Some(&Value::Bool(true)) && !args.no_particles { - #[derive(Deserialize)] - struct ParticlesAttr { - density: Option, - lifetime: Option, - lifetime_spread: Option, - velocity: Option, - velocity_spread: Option, - } - // let sprite = extras - // .get("sprite") - // .ok_or(anyhow!("particle volume is missing sprite"))?; - - let attr: ParticlesAttr = - serde_json::from_value(extras).context("particles attributes")?; - - info!("adding particles part"); - - let part = store.set(&ParticlesPart { - sprite: None, - density: attr.density, - lifetime: attr.lifetime, - lifetime_spread: attr.lifetime_spread, - velocity: attr.velocity, - velocity_spread: attr.velocity_spread, - })?; - - prefab - .lock() - .unwrap() - .particles - .push((transform_to_affine(node.transform()), part)); - } - - if let Some(light) = node.light() { - let name = node.name().map(|e| e.to_owned()); - if let Some(name) = &name { - info!("adding light {name:?}"); - } else { - info!("adding light"); - } - let emission = Some(Vec3A::from_array(light.color()) * light.intensity()); - if let Some(e) = emission { - debug!("emission is {e}"); - } - let (position, _, _) = node.transform().decomposed(); - let part = store.set(&LightPart { - emission, - name, - radius: None, - })?; - prefab - .lock() - .unwrap() - .light - .push((Vec3A::from_array(position), part)); - } - { - let collider = import_physics(&gltf, *trans, node, &store, &buffers)?; - prefab.lock().unwrap().collision.extend(collider); - } - - Ok::<_, anyhow::Error>(()) - }) - .reduce( - || Ok(()), - |a, b| match (a, b) { - (Ok(()), Ok(())) => Ok(()), - (Ok(()), a) => a, - (a, _) => a, - }, - )?; - - let mut prefab = prefab.into_inner().unwrap(); - let node_to_meshes = node_to_meshes.into_inner().unwrap(); - - if let Some(apath) = args.animation.clone() { - let path_base = apath.parent().unwrap(); - let mut gltf = - Gltf::from_reader_without_validation(File::open(&apath)?).context("gltf parsing")?; - let blob = gltf.blob.take(); - let buffers = import_buffers(&gltf, Some(path_base), blob).context("importing buffers")?; - - let anim_name_map = if let Some(ref map_path) = args.animation_bone_map { - let mut map = BTreeMap::new(); - for l in read_to_string(map_path)?.lines() { - if !l.trim().is_empty() && !l.starts_with(";") { - let (a, b) = l.split_once("=").unwrap(); - map.insert(a.to_string(), b.to_string()); - } - } - Some(map) - } else { - None - }; - - let mut anim_joint_index_to_arm_index = BTreeMap::new(); - let mut joint_index_to_ibm = BTreeMap::new(); - for n in gltf.nodes() { - if let Some(name) = n.name() { - let Some(vrm_name) = (if let Some(map) = &anim_name_map { - map.get(name).cloned() - } else { - Some(name.to_string()) - }) else { - continue; - }; - let anim_node_index = n.index(); - if let Some(scene_arm_index) = joint_name_to_arm_index.get(&vrm_name) { - debug!( - "mapping {name:?} (node={}) -> {vrm_name:?} (bone={})", - anim_node_index, scene_arm_index - ); - anim_joint_index_to_arm_index.insert(anim_node_index, *scene_arm_index); - } - } - } - if args.animation_apply_ibm { - for s in gltf.skins() { - let reader = s.reader(|buf| Some(&buffers[buf.index()])); - if let Some(ibms) = reader.read_inverse_bind_matrices() { - for (jn, ibm) in s.joints().zip(ibms) { - joint_index_to_ibm.insert( - jn.index(), - transform_to_affine(Transform::Matrix { matrix: ibm }), - ); - } - } - } - debug!("{} joint IBMs found", joint_index_to_ibm.len()); - } - let transform = args - .animation_rotation_y - .map(Affine3A::from_rotation_y) - .unwrap_or_default() - * args - .animation_scale - .map(Vec3::splat) - .map(Affine3A::from_scale) - .unwrap_or_default(); - for a in gltf.animations() { - prefab.animation.push(import_animation( - a, - &store, - transform, - &joint_index_to_ibm, - &anim_joint_index_to_arm_index, - &BTreeMap::new(), - &buffers, - )?); - } - } - - for a in gltf.animations() { - if !args.no_animations { - prefab.animation.push(import_animation( - a, - &store, - Affine3A::IDENTITY, - &BTreeMap::new(), - &joint_index_to_arm_index, - &node_to_meshes, - &buffers, - )?); - }; - } - - if vrm.camera_mount.is_some() - || vrm.camera_mount_offset.is_some() - || !vrm.bone_node_names.is_empty() - { - info!("avatar info enabled"); - prefab.avatar_info = Some( - store.set(&AvatarInfoPart { - armature: Some(0), // TODO - camera_mount: vrm - .camera_mount - .map(|e| joint_index_to_arm_index[&e] as u32), - camera_mount_offset: vrm.camera_mount_offset, - ..Default::default() - })?, - ); - } - - prefab.armature = if armature.parent.as_ref().is_some_and(|a| !a.is_empty()) { - vec![store.set(&armature)?] - } else { - vec![] - }; - - let skybox = if let Some(skybox) = &args.skybox { - let mut buf = Vec::new(); - File::open(skybox)?.read_to_end(&mut buf)?; - Some(store.set(&Image(Cow::Owned(buf)))?) - } else { - None - }; - let sun = if args.with_default_sun { - Some(( - vec3a(1., -5., 1.).normalize(), - vec3a(1., 1., 1.).normalize() * 100_000., - )) - } else { - None - }; - prefab.environment = if skybox.is_some() || sun.is_some() { - Some(store.set(&EnvironmentPart { skybox, sun })?) - } else { - None - }; - - prefab.name = args.name.clone().or(gltf - .default_scene() - .and_then(|n| n.name()) - .map(|n| n.to_owned())); - - if args.debug_light { - prefab.light.push(( - vec3a(5., 5., 5.), - store.set(&LightPart { - name: Some("debug light".to_owned()), - emission: Some(vec3a(10., 5., 15.)), - radius: Some(0.3), - })?, - )); - } - - Ok(store.set(&prefab)?) -} diff --git a/world/src/vrm.rs b/world/src/vrm.rs deleted file mode 100644 index f014e66..0000000 --- a/world/src/vrm.rs +++ /dev/null @@ -1,135 +0,0 @@ -/* - wearechat - generic multiplayer game with voip - Copyright (C) 2025 metamuffin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, version 3 of the License only. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -use anyhow::Result; -use gltf::Gltf; -use serde::Deserialize; -use std::collections::{BTreeMap, BTreeSet}; -use weareshared::Vec3A; - -#[derive(Debug, Default)] -pub struct VrmInfo { - pub bone_node_names: Vec<(usize, String)>, - pub hide_first_person: BTreeSet, - pub camera_mount: Option, - pub camera_mount_offset: Option, -} - -pub fn extract_vrm_data(gltf: &Gltf) -> Result { - let mut o = VrmInfo::default(); - let mut fp = None; - if let Some(vrm) = gltf.extension_value("VRM") { - // serde_json::to_writer(std::fs::File::create("/tmp/vrm").unwrap(), vrm).unwrap(); - let vrm: Vrm = serde_json::from_value(vrm.clone())?; - for bone in vrm.humanoid.human_bones { - o.bone_node_names.push((bone.node, bone.bone)) - } - fp = vrm.first_person; - } - if let Some(vrm) = gltf.extension_value("VRMC_vrm") { - // serde_json::to_writer(std::fs::File::create("/tmp/vrmc").unwrap(), vrm).unwrap(); - let vrm: Vrmc = serde_json::from_value(vrm.clone())?; - for (name, bone) in vrm.humanoid.human_bones { - o.bone_node_names.push((bone.node, name)) - } - fp = vrm.first_person; - } - if let Some(fp) = fp { - o.camera_mount = fp.first_person_bone; - o.camera_mount_offset = fp.first_person_bone_offset.map(convert_vrm_vec); - for ann in fp.mesh_annotations { - if let FirstPersonFlag::ThirdPersonOnly = ann.first_person_flag { - o.hide_first_person.insert(ann.node); - } - } - } - Ok(o) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Vrm { - humanoid: VrmHumanoid, - first_person: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmHumanoid { - human_bones: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmHumanBone { - bone: String, - node: usize, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Vrmc { - humanoid: VrmcHumanoid, - first_person: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmcFirstPerson { - first_person_bone: Option, - first_person_bone_offset: Option, - mesh_annotations: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmcFirstPersonMeshAnnotation { - node: usize, - #[serde(alias = "type")] // spec says something but their sample uses "type" instead - first_person_flag: FirstPersonFlag, -} - -#[derive(Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -enum FirstPersonFlag { - ThirdPersonOnly, - FirstPersonOnly, - Both, - #[default] - Auto, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmcHumanoid { - human_bones: BTreeMap, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct VrmcHumanBone { - node: usize, -} - -#[derive(Debug, Deserialize)] -struct VrmVec3 { - x: f32, - y: f32, - z: f32, -} -fn convert_vrm_vec(VrmVec3 { x, y, z }: VrmVec3) -> Vec3A { - Vec3A::new(x, y, z) -} -- cgit v1.2.3-70-g09d2