summaryrefslogtreecommitdiff
path: root/import/src/prefab.rs
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-03-12 17:54:43 +0100
committermetamuffin <metamuffin@disroot.org>2025-03-12 17:54:43 +0100
commitdc8304afefa71037bea99722bee29f7645753836 (patch)
tree4fd2b70d6faec18573bd590442b443b7d7a53c1a /import/src/prefab.rs
parent3ed621256f1e02032250477fa574eab38bd34976 (diff)
downloadweareserver-dc8304afefa71037bea99722bee29f7645753836.tar
weareserver-dc8304afefa71037bea99722bee29f7645753836.tar.bz2
weareserver-dc8304afefa71037bea99722bee29f7645753836.tar.zst
rename crates and binaries
Diffstat (limited to 'import/src/prefab.rs')
-rw-r--r--import/src/prefab.rs442
1 files changed, 442 insertions, 0 deletions
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 <https://www.gnu.org/licenses/>.
+*/
+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<Resource<Prefab>> {
+ 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<String, usize>;
+ 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::<Vec<_>>();
+
+ 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::<Vec<_>>()
+ };
+
+ let prefab = Mutex::new(Prefab::default());
+ let node_to_meshes = Mutex::new(BTreeMap::<usize, Vec<usize>>::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<f32>,
+ lifetime: Option<f32>,
+ lifetime_spread: Option<f32>,
+ velocity: Option<Vec3A>,
+ velocity_spread: Option<Vec3A>,
+ }
+ // 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)?)
+}