summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/resources.md85
-rw-r--r--shared/src/resources.rs3
-rw-r--r--world/src/main.rs21
-rw-r--r--world/src/mesh.rs10
-rw-r--r--world/src/vrm.rs107
5 files changed, 160 insertions, 66 deletions
diff --git a/doc/resources.md b/doc/resources.md
index 344101c..7ef22be 100644
--- a/doc/resources.md
+++ b/doc/resources.md
@@ -40,46 +40,47 @@ array. ombinations of g_\*, va_\* and tex_\* are multiplied except normal which
is added. Defaults should be the identity for that operation, so default is 1 /
white except normals are zero.
-| Key | Value Type | |
-| -------------------- | ------------------- | ------------------ |
-| `name` | `String` | |
-| `index` | `Res<[u32; 3]>` | |
-| `armature` | `u32` | |
-| `g_metallic` | `Float` | |
-| `g_roughness` | `Float` | |
-| `g_albedo` | `Vec3` | |
-| `g_alpha` | `Float` | |
-| `g_transmission` | `Float` | |
-| `g_emission` | `Vec3` | |
-| `g_refractive_index` | `Float` | |
-| `g_attenuation` | `Vec3` | |
-| `g_dispersion` | `Float` | |
-| `g_thickness` | `Float` | |
-| `g_unlit` | | |
-| `g_double_sided` | | |
-| `va_position` | `Res<[Vec3]>` | |
-| `va_normal` | `Res<[Vec3]>` | |
-| `va_tangent` | `Res<[Vec4]>` | Tangent+Handedness |
-| `va_texcoord` | `Res<[Vec2]>` | |
-| `va_roughness` | `Res<[Float]>` | |
-| `va_metallic` | `Res<[Float]>` | |
-| `va_albedo` | `Res<[Vec3]>` | |
-| `va_alpha` | `Res<[Float]>` | |
-| `va_transmission` | `Res<[Float]>` | |
-| `va_emission` | `Res<[Vec3]>` | |
-| `va_joint_weight` | `Res<[[Float; 4]]>` | |
-| `va_joint_index` | `Res<[[u32; 4]]>` | |
-| `tex_normal` | `Res<Texture>` | Use color channels |
-| `tex_roughness` | `Res<Texture>` | Use green channel |
-| `tex_metallic` | `Res<Texture>` | Use blue channel |
-| `tex_albedo` | `Res<Texture>` | Use color channels |
-| `tex_alpha` | `Res<Texture>` | Use alpha channel |
-| `tex_transmission` | `Res<Texture>` | Use red channel |
-| `tex_emission` | `Res<Texture>` | Use color channels |
-| `tex_thickness` | `Res<Texture>` | Use green channel |
-| `tex_occlusion` | `Res<Texture>` | Use red channel |
-| `hint_mirror` | | |
-| `hint_static` | | |
+| Key | Value Type | |
+| ------------------------ | ------------------- | ------------------ |
+| `name` | `String` | |
+| `index` | `Res<[u32; 3]>` | |
+| `armature` | `u32` | |
+| `g_metallic` | `Float` | |
+| `g_roughness` | `Float` | |
+| `g_albedo` | `Vec3` | |
+| `g_alpha` | `Float` | |
+| `g_transmission` | `Float` | |
+| `g_emission` | `Vec3` | |
+| `g_refractive_index` | `Float` | |
+| `g_attenuation` | `Vec3` | |
+| `g_dispersion` | `Float` | |
+| `g_thickness` | `Float` | |
+| `g_unlit` | | |
+| `g_double_sided` | | |
+| `va_position` | `Res<[Vec3]>` | |
+| `va_normal` | `Res<[Vec3]>` | |
+| `va_tangent` | `Res<[Vec4]>` | Tangent+Handedness |
+| `va_texcoord` | `Res<[Vec2]>` | |
+| `va_roughness` | `Res<[Float]>` | |
+| `va_metallic` | `Res<[Float]>` | |
+| `va_albedo` | `Res<[Vec3]>` | |
+| `va_alpha` | `Res<[Float]>` | |
+| `va_transmission` | `Res<[Float]>` | |
+| `va_emission` | `Res<[Vec3]>` | |
+| `va_joint_weight` | `Res<[[Float; 4]]>` | |
+| `va_joint_index` | `Res<[[u32; 4]]>` | |
+| `tex_normal` | `Res<Texture>` | Use color channels |
+| `tex_roughness` | `Res<Texture>` | Use green channel |
+| `tex_metallic` | `Res<Texture>` | Use blue channel |
+| `tex_albedo` | `Res<Texture>` | Use color channels |
+| `tex_alpha` | `Res<Texture>` | Use alpha channel |
+| `tex_transmission` | `Res<Texture>` | Use red channel |
+| `tex_emission` | `Res<Texture>` | Use color channels |
+| `tex_thickness` | `Res<Texture>` | Use green channel |
+| `tex_occlusion` | `Res<Texture>` | Use red channel |
+| `hint_mirror` | | |
+| `hint_hide_first_person` | | |
+| `hint_static` | | |
- **Attenuation**: Attenuation coefficient for each color channel due to
scattering within the material volume expressed as e-folding distance (m^-1).
@@ -100,6 +101,10 @@ white except normals are zero.
the mirrors perspective. It can be assumed that the mesh is on a single plane.
- **Static Hint**: Object will not move often. This allows implementations to
choose instanced rending when MeshPart are added more than once.
+- **Hide First-Person Hint**: Object should not be rendered if the prefab is the
+ own avatar and first person view is used. This is set for the head and hair
+ that should not be visible to yourself. See `thirdPersonOnly` variant of
+ [VRMC_vrm] `firstPerson.meshAnnotations.firstPersonFlag`.
## ArmaturePart
diff --git a/shared/src/resources.rs b/shared/src/resources.rs
index 4cfc6fa..66ecf7a 100644
--- a/shared/src/resources.rs
+++ b/shared/src/resources.rs
@@ -87,6 +87,7 @@ pub struct MeshPart {
pub tex_occlusion: Option<Resource<Image<'static>>>,
pub hint_mirror: Option<()>,
pub hint_static: Option<()>,
+ pub hint_hide_first_person: Option<()>,
}
#[derive(Debug, Default, Clone)]
@@ -303,6 +304,7 @@ impl ReadWrite for MeshPart {
write_kv_opt(w, b"tex_occlusion", &self.tex_occlusion)?;
write_kv_opt(w, b"hint_mirror", &self.hint_mirror)?;
write_kv_opt(w, b"hint_static", &self.hint_static)?;
+ write_kv_opt(w, b"hint_hide_first_person", &self.hint_hide_first_person)?;
Ok(())
}
fn read(r: &mut dyn Read) -> Result<Self> {
@@ -341,6 +343,7 @@ impl ReadWrite for MeshPart {
b"tex_occlusion" => Ok(s.tex_occlusion = Some(read_slice(v)?)),
b"hint_mirror" => Ok(s.hint_mirror = Some(read_slice(v)?)),
b"hint_static" => Ok(s.hint_static = Some(read_slice(v)?)),
+ b"hint_hide_first_person" => Ok(s.hint_hide_first_person = Some(read_slice(v)?)),
x => Ok(warn!(
"unknown mesh part key: {:?}",
String::from_utf8_lossy(x)
diff --git a/world/src/main.rs b/world/src/main.rs
index 055be47..e3c365a 100644
--- a/world/src/main.rs
+++ b/world/src/main.rs
@@ -42,7 +42,7 @@ use std::{
thread::{self, sleep},
time::Duration,
};
-use vrm::{Vrm, Vrmc};
+use vrm::extract_vrm_data;
use weareshared::{
Affine3A, Vec3A,
helper::ReadWrite,
@@ -137,6 +137,8 @@ fn main() -> Result<()> {
traverse(&mut nodes, node, root_affine);
}
+ let vrm = extract_vrm_data(&gltf)?;
+
let mut skin_index_to_arm_index = BTreeMap::new();
let armature = {
let mut joint_index_to_arm_index = BTreeMap::new();
@@ -176,19 +178,9 @@ fn main() -> Result<()> {
})
.collect::<Vec<_>>();
- if let Some(vrm) = gltf.extension_value("VRM") {
- let vrm: Vrm = serde_json::from_value(vrm.clone())?;
- for bone in vrm.humanoid.human_bones {
- let ind = joint_index_to_arm_index[&bone.node];
- name[ind] = bone.bone;
- }
- }
- if let Some(vrm) = gltf.extension_value("VRMC_vrm") {
- let vrm: Vrmc = serde_json::from_value(vrm.clone())?;
- for (bname, bone) in vrm.humanoid.human_bones {
- let ind = joint_index_to_arm_index[&bone.node];
- name[ind] = bname;
- }
+ for (node, bname) in &vrm.bone_node_names {
+ let ind = joint_index_to_arm_index[node];
+ name[ind] = bname.to_owned();
}
for (i, (name, tr)) in name.iter().zip(transform.iter()).enumerate() {
@@ -226,6 +218,7 @@ fn main() -> Result<()> {
&args,
&texture_cache,
&skin_index_to_arm_index,
+ &vrm,
)?;
}
}
diff --git a/world/src/mesh.rs b/world/src/mesh.rs
index 6fff9c4..ebecfa5 100644
--- a/world/src/mesh.rs
+++ b/world/src/mesh.rs
@@ -14,7 +14,7 @@
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, load_texture};
+use crate::{Args, TextureCache, load_texture, vrm::VrmInfo};
use anyhow::Result;
use gltf::{Mesh, Node, buffer::Data};
use log::{debug, info, warn};
@@ -37,6 +37,7 @@ pub fn import_mesh(
args: &Args,
texture_cache: &TextureCache,
joint_index_map: &BTreeMap<(usize, u16), u32>,
+ vrm: &VrmInfo,
) -> Result<()> {
for p in mesh.primitives() {
let name = mesh.name().or(node.name()).map(|e| e.to_owned());
@@ -348,6 +349,12 @@ pub fn import_mesh(
None
};
+ let hint_hide_first_person = if vrm.hide_first_person.contains(&node.index()) {
+ Some(())
+ } else {
+ None
+ };
+
let armature = node.skin().map(|_| 0);
let mesh = store.set(&MeshPart {
@@ -383,6 +390,7 @@ pub fn import_mesh(
tex_transmission,
tex_thickness,
tex_occlusion,
+ hint_hide_first_person,
// not supported by gltf
hint_mirror: None, // TODO
hint_static: None, // TODO Set when instancing
diff --git a/world/src/vrm.rs b/world/src/vrm.rs
index a9ea6f4..1b89642 100644
--- a/world/src/vrm.rs
+++ b/world/src/vrm.rs
@@ -1,6 +1,65 @@
-use std::collections::BTreeMap;
+/*
+ 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 anyhow::Result;
+use gltf::Gltf;
use serde::Deserialize;
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ fs::File,
+};
+
+pub struct VrmInfo {
+ pub bone_node_names: Vec<(usize, String)>,
+ pub hide_first_person: BTreeSet<usize>,
+}
+
+pub fn extract_vrm_data(gltf: &Gltf) -> Result<VrmInfo> {
+ let mut bone_node_names = Vec::new();
+ let mut hide_first_person = BTreeSet::new();
+ if let Some(vrm) = gltf.extension_value("VRM") {
+ serde_json::to_writer(File::create("/tmp/vrm").unwrap(), vrm).unwrap();
+ let vrm: Vrm = serde_json::from_value(vrm.clone())?;
+ for bone in vrm.humanoid.human_bones {
+ bone_node_names.push((bone.node, bone.bone))
+ }
+ }
+ if let Some(vrm) = gltf.extension_value("VRMC_vrm") {
+ serde_json::to_writer(File::create("/tmp/vrmc").unwrap(), vrm).unwrap();
+ let vrm: Vrmc = serde_json::from_value(vrm.clone())?;
+ for (name, bone) in vrm.humanoid.human_bones {
+ bone_node_names.push((bone.node, name))
+ }
+ if let Some(fp) = vrm.first_person {
+ for ann in fp.mesh_annotations {
+ match ann.first_person_flag {
+ FirstPersonFlag::ThirdPersonOnly => {
+ hide_first_person.insert(ann.node);
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ eprintln!("hide {hide_first_person:?}");
+ Ok(VrmInfo {
+ bone_node_names,
+ hide_first_person,
+ })
+}
#[derive(Debug, Deserialize)]
pub struct Vrm {
@@ -10,29 +69,55 @@ pub struct Vrm {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VrmHumanoid {
- pub human_bones: Vec<VrmHumanBone>,
+ human_bones: Vec<VrmHumanBone>,
+}
+
+#[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<VrmcFirstPerson>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct VrmHumanBone {
- pub bone: String,
- pub node: usize,
+struct VrmcFirstPerson {
+ mesh_annotations: Vec<VrmcFirstPersonMeshAnnotation>,
}
#[derive(Debug, Deserialize)]
-pub struct Vrmc {
- pub humanoid: VrmcHumanoid,
+#[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")]
-pub struct VrmcHumanoid {
- pub human_bones: BTreeMap<String, VrmcHumanBone>,
+struct VrmcHumanoid {
+ human_bones: BTreeMap<String, VrmcHumanBone>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct VrmcHumanBone {
- pub node: usize,
+struct VrmcHumanBone {
+ node: usize,
}