Implement avatar animation and skinning (#67)
This commit is contained in:
@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
||||
|
||||
| Assembly | Types | Members | Status |
|
||||
|---|---:|---:|---|
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 207 types / 15,315 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 225 types / 15,463 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
|
||||
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
|
||||
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
|
||||
|
||||
335
crates/libremetaverse/src/animation.rs
Normal file
335
crates/libremetaverse/src/animation.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
//! Bounded decoder for the simulator's binary BVH animation asset format.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value, clippy::unused_self)] // Mapped C# signatures are fixed.
|
||||
|
||||
use libremetaverse_types::compat::Object;
|
||||
use libremetaverse_types::{Error, Vector3};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
const MAX_ANIMATION_BYTES: usize = 32 * 1024 * 1024;
|
||||
const MAX_JOINTS: usize = 512;
|
||||
const MAX_KEYS_PER_JOINT: usize = 10_000;
|
||||
const MAX_JOINT_NAME_BYTES: usize = 255;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHJointKey {
|
||||
pub key_element: Vector3,
|
||||
pub time: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHJoint {
|
||||
pub name: String,
|
||||
pub priority: i32,
|
||||
pub tag: Object,
|
||||
pub positionkeys: Vec<BinBVHJointKey>,
|
||||
pub rotationkeys: Vec<BinBVHJointKey>,
|
||||
}
|
||||
|
||||
impl BinBVHJoint {
|
||||
pub(crate) fn native_equals(&self, other: &Self) -> bool {
|
||||
self.priority == other.priority
|
||||
&& self.name == other.name
|
||||
&& keys_equal(&self.rotationkeys, &other.rotationkeys)
|
||||
&& keys_equal(&self.positionkeys, &other.positionkeys)
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.priority.hash(&mut hasher);
|
||||
self.name.hash(&mut hasher);
|
||||
for key in self.rotationkeys.iter().chain(&self.positionkeys) {
|
||||
key.time.to_bits().hash(&mut hasher);
|
||||
key.key_element.x.to_bits().hash(&mut hasher);
|
||||
key.key_element.y.to_bits().hash(&mut hasher);
|
||||
key.key_element.z.to_bits().hash(&mut hasher);
|
||||
}
|
||||
fold_hash(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key_equal(left: &BinBVHJointKey, right: &BinBVHJointKey) -> bool {
|
||||
left.time.to_bits() == right.time.to_bits() && Vector3::eq(left.key_element, right.key_element)
|
||||
}
|
||||
|
||||
pub(crate) fn keys_equal(left: &[BinBVHJointKey], right: &[BinBVHJointKey]) -> bool {
|
||||
left.len() == right.len()
|
||||
&& left
|
||||
.iter()
|
||||
.zip(right)
|
||||
.all(|(left, right)| key_equal(left, right))
|
||||
}
|
||||
|
||||
pub(crate) fn joints_equal(left: &[BinBVHJoint], right: &[BinBVHJoint]) -> bool {
|
||||
left.len() == right.len()
|
||||
&& left
|
||||
.iter()
|
||||
.zip(right)
|
||||
.all(|(left, right)| left.native_equals(right))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BinBVHAnimationReader {
|
||||
pub ease_in_time: f32,
|
||||
pub ease_out_time: f32,
|
||||
pub expression_name: String,
|
||||
pub hand_pose: u32,
|
||||
pub in_point: f32,
|
||||
pub joint_count: u32,
|
||||
pub length: f32,
|
||||
pub loop_: bool,
|
||||
pub out_point: f32,
|
||||
pub priority: i32,
|
||||
pub joints: Vec<BinBVHJoint>,
|
||||
pub unknown0: u16,
|
||||
pub unknown1: u16,
|
||||
}
|
||||
|
||||
impl BinBVHAnimationReader {
|
||||
pub(crate) fn native_new(data: Vec<u8>) -> Result<Self, Error> {
|
||||
if data.len() > MAX_ANIMATION_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut cursor = Cursor::new(&data);
|
||||
let unknown0 = cursor.u16()?;
|
||||
let unknown1 = cursor.u16()?;
|
||||
let priority = cursor.i32()?;
|
||||
let length = cursor.f32()?;
|
||||
let expression_name = cursor.string(MAX_JOINT_NAME_BYTES)?;
|
||||
let in_point = cursor.f32()?;
|
||||
let out_point = cursor.f32()?;
|
||||
let loop_ = cursor.i32()? != 0;
|
||||
let ease_in_time = cursor.f32()?;
|
||||
let ease_out_time = cursor.f32()?;
|
||||
let hand_pose = cursor.u32()?;
|
||||
let joint_count = cursor.u32()?;
|
||||
if length < 0.0
|
||||
|| in_point < 0.0
|
||||
|| out_point < in_point
|
||||
|| ease_in_time < 0.0
|
||||
|| ease_out_time < 0.0
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let count = usize::try_from(joint_count).map_err(|_| Error::Argument)?;
|
||||
if count > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut result = Self {
|
||||
ease_in_time,
|
||||
ease_out_time,
|
||||
expression_name,
|
||||
hand_pose,
|
||||
in_point,
|
||||
joint_count,
|
||||
length,
|
||||
loop_,
|
||||
out_point,
|
||||
priority,
|
||||
joints: Vec::with_capacity(count),
|
||||
unknown0,
|
||||
unknown1,
|
||||
};
|
||||
for _ in 0..count {
|
||||
result.joints.push(result.read_joint_cursor(&mut cursor)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn read_joint_cursor(&self, cursor: &mut Cursor<'_>) -> Result<BinBVHJoint, Error> {
|
||||
let name = cursor.string(MAX_JOINT_NAME_BYTES)?;
|
||||
let priority = cursor.i32()?;
|
||||
let rotation_count = cursor.count(MAX_KEYS_PER_JOINT)?;
|
||||
let rotationkeys = self.read_keys_cursor(cursor, rotation_count, -1.0, 1.0)?;
|
||||
let position_count = cursor.count(MAX_KEYS_PER_JOINT)?;
|
||||
let positionkeys = self.read_keys_cursor(cursor, position_count, -0.5, 1.5)?;
|
||||
Ok(BinBVHJoint {
|
||||
name,
|
||||
priority,
|
||||
tag: Object::Undefined,
|
||||
positionkeys,
|
||||
rotationkeys,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_keys_cursor(
|
||||
&self,
|
||||
cursor: &mut Cursor<'_>,
|
||||
count: usize,
|
||||
min: f32,
|
||||
max: f32,
|
||||
) -> Result<Vec<BinBVHJointKey>, Error> {
|
||||
let mut keys = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
keys.push(BinBVHJointKey {
|
||||
time: decode_u16(cursor.u16()?, self.in_point, self.out_point),
|
||||
key_element: Vector3 {
|
||||
x: decode_u16(cursor.u16()?, min, max),
|
||||
y: decode_u16(cursor.u16()?, min, max),
|
||||
z: decode_u16(cursor.u16()?, min, max),
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub(crate) fn native_read_bytes_until_null(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
offset: &mut i32,
|
||||
) -> Result<String, Error> {
|
||||
let start = usize::try_from(*offset).map_err(|_| Error::Argument)?;
|
||||
let mut cursor = Cursor::at(&data, start)?;
|
||||
let value = cursor.string(MAX_JOINT_NAME_BYTES)?;
|
||||
*offset = i32::try_from(cursor.position).map_err(|_| Error::Argument)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn native_read_joint(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
offset: &mut i32,
|
||||
) -> Result<BinBVHJoint, Error> {
|
||||
let start = usize::try_from(*offset).map_err(|_| Error::Argument)?;
|
||||
let mut cursor = Cursor::at(&data, start)?;
|
||||
let joint = self.read_joint_cursor(&mut cursor)?;
|
||||
*offset = i32::try_from(cursor.position).map_err(|_| Error::Argument)?;
|
||||
Ok(joint)
|
||||
}
|
||||
|
||||
pub(crate) fn native_read_keys(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
offset: &mut i32,
|
||||
key_count: i32,
|
||||
min: f32,
|
||||
max: f32,
|
||||
) -> Result<Vec<BinBVHJointKey>, Error> {
|
||||
let count = usize::try_from(key_count).map_err(|_| Error::Argument)?;
|
||||
if count > MAX_KEYS_PER_JOINT || !min.is_finite() || !max.is_finite() || min > max {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let start = usize::try_from(*offset).map_err(|_| Error::Argument)?;
|
||||
let mut cursor = Cursor::at(&data, start)?;
|
||||
let keys = self.read_keys_cursor(&mut cursor, count, min, max)?;
|
||||
*offset = i32::try_from(cursor.position).map_err(|_| Error::Argument)?;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub(crate) fn native_equals(&self, other: Option<&Self>) -> bool {
|
||||
let Some(other) = other else { return false };
|
||||
self.loop_ == other.loop_
|
||||
&& self.out_point.to_bits() == other.out_point.to_bits()
|
||||
&& self.in_point.to_bits() == other.in_point.to_bits()
|
||||
&& self.length.to_bits() == other.length.to_bits()
|
||||
&& self.hand_pose == other.hand_pose
|
||||
&& self.joint_count == other.joint_count
|
||||
&& self.ease_in_time.to_bits() == other.ease_in_time.to_bits()
|
||||
&& self.ease_out_time.to_bits() == other.ease_out_time.to_bits()
|
||||
&& self.priority == other.priority
|
||||
&& self.unknown0 == other.unknown0
|
||||
&& self.unknown1 == other.unknown1
|
||||
&& self.joints.len() == other.joints.len()
|
||||
&& self
|
||||
.joints
|
||||
.iter()
|
||||
.zip(&other.joints)
|
||||
.all(|(left, right)| left.native_equals(right))
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_hash_code(&self) -> i32 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.loop_.hash(&mut hasher);
|
||||
self.out_point.to_bits().hash(&mut hasher);
|
||||
self.in_point.to_bits().hash(&mut hasher);
|
||||
self.length.to_bits().hash(&mut hasher);
|
||||
self.hand_pose.hash(&mut hasher);
|
||||
self.priority.hash(&mut hasher);
|
||||
for joint in &self.joints {
|
||||
joint.native_get_hash_code().hash(&mut hasher);
|
||||
}
|
||||
fold_hash(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
fn fold_hash(hash: u64) -> i32 {
|
||||
let folded = hash ^ (hash >> 32);
|
||||
u32::try_from(folded & u64::from(u32::MAX))
|
||||
.unwrap_or_default()
|
||||
.cast_signed()
|
||||
}
|
||||
|
||||
fn decode_u16(value: u16, min: f32, max: f32) -> f32 {
|
||||
min + (f32::from(value) / f32::from(u16::MAX)) * (max - min)
|
||||
}
|
||||
|
||||
struct Cursor<'a> {
|
||||
data: &'a [u8],
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
const fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, position: 0 }
|
||||
}
|
||||
|
||||
fn at(data: &'a [u8], position: usize) -> Result<Self, Error> {
|
||||
if position > data.len() {
|
||||
return Err(Error::IndexOutOfRange);
|
||||
}
|
||||
Ok(Self { data, position })
|
||||
}
|
||||
|
||||
fn take<const N: usize>(&mut self) -> Result<[u8; N], Error> {
|
||||
let end = self.position.checked_add(N).ok_or(Error::IndexOutOfRange)?;
|
||||
let bytes = self
|
||||
.data
|
||||
.get(self.position..end)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
self.position = end;
|
||||
bytes.try_into().map_err(|_| Error::IndexOutOfRange)
|
||||
}
|
||||
|
||||
fn u16(&mut self) -> Result<u16, Error> {
|
||||
Ok(u16::from_le_bytes(self.take()?))
|
||||
}
|
||||
|
||||
fn u32(&mut self) -> Result<u32, Error> {
|
||||
Ok(u32::from_le_bytes(self.take()?))
|
||||
}
|
||||
|
||||
fn i32(&mut self) -> Result<i32, Error> {
|
||||
Ok(i32::from_le_bytes(self.take()?))
|
||||
}
|
||||
|
||||
fn f32(&mut self) -> Result<f32, Error> {
|
||||
let value = f32::from_le_bytes(self.take()?);
|
||||
value.is_finite().then_some(value).ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
fn count(&mut self, maximum: usize) -> Result<usize, Error> {
|
||||
let value = usize::try_from(self.i32()?).map_err(|_| Error::Argument)?;
|
||||
(value <= maximum).then_some(value).ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
fn string(&mut self, maximum: usize) -> Result<String, Error> {
|
||||
let remaining = self
|
||||
.data
|
||||
.get(self.position..)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
let length = remaining
|
||||
.iter()
|
||||
.take(maximum.saturating_add(1))
|
||||
.position(|byte| *byte == 0)
|
||||
.ok_or(Error::Argument)?;
|
||||
if length > maximum {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let bytes = &remaining[..length];
|
||||
self.position = self
|
||||
.position
|
||||
.checked_add(length + 1)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
String::from_utf8(bytes.to_vec()).map_err(|_| Error::Argument)
|
||||
}
|
||||
}
|
||||
499
crates/libremetaverse/src/animesh.rs
Normal file
499
crates/libremetaverse/src/animesh.rs
Normal file
@@ -0,0 +1,499 @@
|
||||
//! Deterministic animation playback and object-animation asset coordination.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] // Mapped constructors are fixed.
|
||||
|
||||
use crate::animation::{BinBVHAnimationReader, BinBVHJointKey};
|
||||
use crate::client_core::ClientWeakHandle;
|
||||
use crate::packet_catalog::PacketType;
|
||||
use crate::packets::ObjectAnimationPacket;
|
||||
use crate::{Error, GridClient};
|
||||
use libremetaverse_types::compat::{CancellationTokenSource, Subscription};
|
||||
use libremetaverse_types::{AssetType, Quaternion, UUID, Vector3};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
const MAX_TRACKS_PER_PLAYER: usize = 64;
|
||||
const MAX_PLAYERS: usize = 4096;
|
||||
const MAX_ANIMATIONS_PER_UPDATE: usize = 64;
|
||||
const MAX_CONCURRENT_FETCHES: usize = 16;
|
||||
|
||||
fn mutex<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
lock.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn read<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
|
||||
lock.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn write<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
|
||||
lock.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct JointPose {
|
||||
pub ease_weight: f32,
|
||||
pub has_position: bool,
|
||||
pub has_rotation: bool,
|
||||
pub position: Vector3,
|
||||
pub priority: i32,
|
||||
pub rotation: Quaternion,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TrackState {
|
||||
data: Option<BinBVHAnimationReader>,
|
||||
current_time: f32,
|
||||
is_finished: bool,
|
||||
}
|
||||
|
||||
pub struct AnimationTrack {
|
||||
animation_id: UUID,
|
||||
state: Mutex<TrackState>,
|
||||
}
|
||||
|
||||
impl AnimationTrack {
|
||||
pub(crate) fn native_new(animation_id: UUID) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
animation_id,
|
||||
state: Mutex::new(TrackState::default()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_animation_id(&self) -> UUID {
|
||||
self.animation_id
|
||||
}
|
||||
|
||||
pub(crate) fn native_data(&self) -> Option<BinBVHAnimationReader> {
|
||||
mutex(&self.state).data.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_data(&self, data: Option<BinBVHAnimationReader>) {
|
||||
let mut state = mutex(&self.state);
|
||||
state.current_time = 0.0;
|
||||
state.is_finished = false;
|
||||
state.data = data;
|
||||
}
|
||||
|
||||
pub(crate) fn native_current_time(&self) -> f32 {
|
||||
mutex(&self.state).current_time
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_current_time(&self, value: f32) {
|
||||
if value.is_finite() {
|
||||
mutex(&self.state).current_time = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_is_finished(&self) -> bool {
|
||||
mutex(&self.state).is_finished
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_is_finished(&self, value: bool) {
|
||||
mutex(&self.state).is_finished = value;
|
||||
}
|
||||
|
||||
pub(crate) fn native_advance(&self, dt: f32) -> Result<(), Error> {
|
||||
if !dt.is_finite() || dt < 0.0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = mutex(&self.state);
|
||||
if state.is_finished || state.data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let data = state.data.as_ref().expect("checked animation data");
|
||||
let (looping, in_point, out_point) = (data.loop_, data.in_point, data.out_point);
|
||||
state.current_time += dt;
|
||||
if looping {
|
||||
let span = out_point - in_point;
|
||||
if span > 0.0 && state.current_time > out_point {
|
||||
state.current_time = in_point + (state.current_time - in_point) % span;
|
||||
}
|
||||
} else if state.current_time >= out_point {
|
||||
state.current_time = out_point;
|
||||
state.is_finished = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_ease_weight(&self) -> f32 {
|
||||
let state = mutex(&self.state);
|
||||
ease_weight(&state)
|
||||
}
|
||||
|
||||
pub(crate) fn native_evaluate_pose(
|
||||
&self,
|
||||
pose: &mut HashMap<String, JointPose>,
|
||||
) -> Result<(), Error> {
|
||||
let state = mutex(&self.state);
|
||||
let Some(data) = &state.data else {
|
||||
return Ok(());
|
||||
};
|
||||
let ease = ease_weight(&state);
|
||||
for joint in data.joints.iter().take(MAX_TRACKS_PER_PLAYER * 8) {
|
||||
let rotation = (!joint.rotationkeys.is_empty())
|
||||
.then(|| interpolate_rotation(&joint.rotationkeys, state.current_time))
|
||||
.transpose()?;
|
||||
let position = (!joint.positionkeys.is_empty())
|
||||
.then(|| interpolate_position(&joint.positionkeys, state.current_time))
|
||||
.transpose()?;
|
||||
if rotation.is_none() && position.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (rotation, position) = if let Some(existing) = pose.get(&joint.name).copied() {
|
||||
if joint.priority < existing.priority {
|
||||
continue;
|
||||
}
|
||||
if joint.priority == existing.priority {
|
||||
let total = existing.ease_weight + ease;
|
||||
let weight = if total > 0.0 { ease / total } else { 0.5 };
|
||||
(
|
||||
match (rotation, existing.has_rotation) {
|
||||
(Some(value), true) => {
|
||||
Some(Quaternion::slerp(existing.rotation, value, weight)?)
|
||||
}
|
||||
(Some(value), false) => Some(value),
|
||||
(None, true) => Some(existing.rotation),
|
||||
(None, false) => None,
|
||||
},
|
||||
match (position, existing.has_position) {
|
||||
(Some(value), true) => {
|
||||
Some(Vector3::lerp(existing.position, value, weight)?)
|
||||
}
|
||||
(Some(value), false) => Some(value),
|
||||
(None, true) => Some(existing.position),
|
||||
(None, false) => None,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(rotation, position)
|
||||
}
|
||||
} else {
|
||||
(rotation, position)
|
||||
};
|
||||
pose.insert(
|
||||
joint.name.clone(),
|
||||
JointPose {
|
||||
rotation: rotation.unwrap_or_else(Quaternion::identity),
|
||||
has_rotation: rotation.is_some(),
|
||||
position: position.unwrap_or_else(Vector3::zero),
|
||||
has_position: position.is_some(),
|
||||
priority: joint.priority,
|
||||
ease_weight: ease,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_decode_rotation(value: Vector3) -> Result<Quaternion, Error> {
|
||||
let w_squared = 1.0 - value.x * value.x - value.y * value.y - value.z * value.z;
|
||||
Quaternion::normalize(Quaternion {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
z: value.z,
|
||||
w: w_squared.max(0.0).sqrt(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ease_weight(state: &TrackState) -> f32 {
|
||||
let Some(data) = &state.data else { return 0.0 };
|
||||
if data.ease_in_time > 0.0 && state.current_time < data.ease_in_time {
|
||||
return (state.current_time / data.ease_in_time).clamp(0.0, 1.0);
|
||||
}
|
||||
let start = data.out_point - data.ease_out_time;
|
||||
if data.ease_out_time > 0.0 && state.current_time > start {
|
||||
return ((data.out_point - state.current_time) / data.ease_out_time).clamp(0.0, 1.0);
|
||||
}
|
||||
1.0
|
||||
}
|
||||
|
||||
fn bracket(keys: &[BinBVHJointKey], time: f32) -> usize {
|
||||
keys.partition_point(|key| key.time < time)
|
||||
}
|
||||
|
||||
fn interpolate_rotation(keys: &[BinBVHJointKey], time: f32) -> Result<Quaternion, Error> {
|
||||
let upper = bracket(keys, time);
|
||||
if upper == 0 {
|
||||
return AnimationTrack::native_decode_rotation(keys[0].key_element);
|
||||
}
|
||||
if upper >= keys.len() {
|
||||
return AnimationTrack::native_decode_rotation(keys[keys.len() - 1].key_element);
|
||||
}
|
||||
let (left, right) = (&keys[upper - 1], &keys[upper]);
|
||||
let fraction = if right.time > left.time {
|
||||
(time - left.time) / (right.time - left.time)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Quaternion::slerp(
|
||||
AnimationTrack::native_decode_rotation(left.key_element)?,
|
||||
AnimationTrack::native_decode_rotation(right.key_element)?,
|
||||
fraction,
|
||||
)
|
||||
}
|
||||
|
||||
fn interpolate_position(keys: &[BinBVHJointKey], time: f32) -> Result<Vector3, Error> {
|
||||
let upper = bracket(keys, time);
|
||||
if upper == 0 {
|
||||
return Ok(keys[0].key_element);
|
||||
}
|
||||
if upper >= keys.len() {
|
||||
return Ok(keys[keys.len() - 1].key_element);
|
||||
}
|
||||
let (left, right) = (&keys[upper - 1], &keys[upper]);
|
||||
let fraction = if right.time > left.time {
|
||||
(time - left.time) / (right.time - left.time)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Vector3::lerp(left.key_element, right.key_element, fraction)
|
||||
}
|
||||
|
||||
struct PlayerInner {
|
||||
object_id: UUID,
|
||||
tracks: RwLock<HashMap<UUID, Arc<AnimationTrack>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnimeshPlayer(Arc<PlayerInner>);
|
||||
|
||||
impl AnimeshPlayer {
|
||||
pub(crate) fn native_new(object_id: UUID) -> Result<Self, Error> {
|
||||
Ok(Self(Arc::new(PlayerInner {
|
||||
object_id,
|
||||
tracks: RwLock::new(HashMap::new()),
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn native_object_id(&self) -> UUID {
|
||||
self.0.object_id
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_or_add_track(
|
||||
&self,
|
||||
animation_id: UUID,
|
||||
) -> Result<Arc<AnimationTrack>, Error> {
|
||||
if let Some(track) = read(&self.0.tracks).get(&animation_id).cloned() {
|
||||
return Ok(track);
|
||||
}
|
||||
let mut tracks = write(&self.0.tracks);
|
||||
if let Some(track) = tracks.get(&animation_id).cloned() {
|
||||
return Ok(track);
|
||||
}
|
||||
if tracks.len() >= MAX_TRACKS_PER_PLAYER {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
let track = Arc::new(AnimationTrack::native_new(animation_id)?);
|
||||
tracks.insert(animation_id, Arc::clone(&track));
|
||||
Ok(track)
|
||||
}
|
||||
|
||||
fn native_track(&self, animation_id: UUID) -> Option<Arc<AnimationTrack>> {
|
||||
read(&self.0.tracks).get(&animation_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn native_retain_only(&self, active: &HashSet<UUID>) {
|
||||
write(&self.0.tracks).retain(|id, _| active.contains(id));
|
||||
}
|
||||
|
||||
pub(crate) fn native_track_count(&self) -> i32 {
|
||||
i32::try_from(read(&self.0.tracks).len()).unwrap_or(i32::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn native_update(&self, dt: f32) -> Result<(), Error> {
|
||||
let mut tracks: Vec<_> = read(&self.0.tracks).values().cloned().collect();
|
||||
tracks.sort_by_key(|track| track.native_animation_id().to_string());
|
||||
for track in tracks {
|
||||
track.native_advance(dt)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_evaluate_pose(&self) -> Result<HashMap<String, JointPose>, Error> {
|
||||
let mut tracks: Vec<_> = read(&self.0.tracks).values().cloned().collect();
|
||||
tracks.sort_by_key(|track| track.native_animation_id().to_string());
|
||||
let mut pose = HashMap::new();
|
||||
for track in tracks {
|
||||
track.native_evaluate_pose(&mut pose)?;
|
||||
}
|
||||
Ok(pose)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AnimeshManagerInner {
|
||||
client: ClientWeakHandle,
|
||||
players: RwLock<HashMap<UUID, AnimeshPlayer>>,
|
||||
cancellation: CancellationTokenSource,
|
||||
fetches_in_flight: Arc<AtomicUsize>,
|
||||
subscriptions: Mutex<Vec<Subscription>>,
|
||||
}
|
||||
|
||||
impl Drop for AnimeshManagerInner {
|
||||
fn drop(&mut self) {
|
||||
self.cancellation.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnimeshManager(Arc<AnimeshManagerInner>);
|
||||
|
||||
impl AnimeshManager {
|
||||
pub(crate) fn native_new(client: GridClient) -> Result<Self, Error> {
|
||||
let inner = Arc::new(AnimeshManagerInner {
|
||||
client: client.native_weak_handle(),
|
||||
players: RwLock::new(HashMap::new()),
|
||||
cancellation: CancellationTokenSource::new(),
|
||||
fetches_in_flight: Arc::new(AtomicUsize::new(0)),
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
});
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let subscription = client
|
||||
.native_network()?
|
||||
.subscribe_raw_packet(Arc::new(move |event| {
|
||||
if event.packet_type != PacketType::ObjectAnimation {
|
||||
return;
|
||||
}
|
||||
let Some(inner) = weak.upgrade() else { return };
|
||||
handle_object_animation(&inner, event.data);
|
||||
}));
|
||||
mutex(&inner.subscriptions).push(subscription);
|
||||
Ok(Self(inner))
|
||||
}
|
||||
|
||||
pub(crate) fn native_inner(&self) -> Arc<AnimeshManagerInner> {
|
||||
Arc::clone(&self.0)
|
||||
}
|
||||
|
||||
pub(crate) fn native_from_inner(inner: Arc<AnimeshManagerInner>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_player(&self, object_id: UUID) -> Option<AnimeshPlayer> {
|
||||
read(&self.0.players).get(&object_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn native_all_players(&self) -> Vec<AnimeshPlayer> {
|
||||
read(&self.0.players).values().cloned().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn native_remove_player(&self, object_id: UUID) {
|
||||
write(&self.0.players).remove(&object_id);
|
||||
}
|
||||
|
||||
pub(crate) fn native_update(&self, dt: f32) -> Result<(), Error> {
|
||||
let players = self.native_all_players();
|
||||
for player in players {
|
||||
player.native_update(dt)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_object_animation(inner: &Arc<AnimeshManagerInner>, data: Vec<u8>) {
|
||||
let mut offset = 0;
|
||||
let Ok(packet) = ObjectAnimationPacket::new_with_bytes_int32(data, &mut offset) else {
|
||||
return;
|
||||
};
|
||||
if packet.animation_list.len() > MAX_ANIMATIONS_PER_UPDATE {
|
||||
return;
|
||||
}
|
||||
let object_id = packet.sender.id;
|
||||
let player = {
|
||||
let mut players = write(&inner.players);
|
||||
if let Some(player) = players.get(&object_id).cloned() {
|
||||
player
|
||||
} else {
|
||||
if players.len() >= MAX_PLAYERS {
|
||||
return;
|
||||
}
|
||||
let Ok(player) = AnimeshPlayer::native_new(object_id) else {
|
||||
return;
|
||||
};
|
||||
players.insert(object_id, player.clone());
|
||||
player
|
||||
}
|
||||
};
|
||||
let active: HashSet<_> = packet
|
||||
.animation_list
|
||||
.iter()
|
||||
.map(|value| value.anim_id)
|
||||
.collect();
|
||||
player.native_retain_only(&active);
|
||||
for animation in packet.animation_list {
|
||||
let Ok(track) = player.native_get_or_add_track(animation.anim_id) else {
|
||||
continue;
|
||||
};
|
||||
if track.native_data().is_none() {
|
||||
fetch_animation(inner, player.clone(), animation.anim_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_animation(inner: &Arc<AnimeshManagerInner>, player: AnimeshPlayer, animation_id: UUID) {
|
||||
if inner
|
||||
.fetches_in_flight
|
||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
|
||||
(count < MAX_CONCURRENT_FETCHES).then_some(count + 1)
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let weak = Arc::downgrade(inner);
|
||||
let fetches_in_flight = Arc::clone(&inner.fetches_in_flight);
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("animesh-asset".to_owned())
|
||||
.spawn(move || {
|
||||
struct FetchGuard(Arc<AtomicUsize>);
|
||||
impl Drop for FetchGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
let _guard = FetchGuard(fetches_in_flight);
|
||||
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
runtime.block_on(async move {
|
||||
let (token, assets) = {
|
||||
let Some(inner) = weak.upgrade() else { return };
|
||||
let token = inner.cancellation.token();
|
||||
let Some(client) = inner.client.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Ok(assets) = client.native_assets() else {
|
||||
return;
|
||||
};
|
||||
(token, assets)
|
||||
};
|
||||
let Ok(Some(asset)) = assets
|
||||
.request_asset_with_uuid_asset_type_boolean_cancellation_token(
|
||||
animation_id,
|
||||
AssetType::Animation,
|
||||
false,
|
||||
Some(token),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(decoded) = BinBVHAnimationReader::native_new(asset.asset_data) else {
|
||||
return;
|
||||
};
|
||||
if let Some(track) = player.native_track(animation_id) {
|
||||
track.native_set_data(Some(decoded));
|
||||
}
|
||||
});
|
||||
});
|
||||
if spawned.is_err() {
|
||||
inner.fetches_in_flight.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
238
crates/libremetaverse/src/animesh_skinning.rs
Normal file
238
crates/libremetaverse/src/animesh_skinning.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
//! CPU-side forward kinematics and bounded linear-blend skinning.
|
||||
|
||||
#![allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] // Mapped signatures are fixed.
|
||||
|
||||
use crate::animesh_runtime::JointPose;
|
||||
use crate::rendering::{Face, Joint, LindenSkeleton};
|
||||
use libremetaverse_types::{Error, Matrix4, Quaternion, Vector3};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const MAX_SKIN_JOINTS: usize = 512;
|
||||
const MAX_VERTICES: usize = 1_000_000;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MeshSkinData {
|
||||
pub alt_inverse_bind_matrices: Vec<f32>,
|
||||
pub bind_shape_matrix: Vec<f32>,
|
||||
pub inverse_bind_matrices: Vec<f32>,
|
||||
pub joint_names: Vec<String>,
|
||||
pub lock_scale_if_joint_position: bool,
|
||||
pub pelvis_offset: f32,
|
||||
}
|
||||
|
||||
impl MeshSkinData {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnimeshSkinning;
|
||||
|
||||
impl AnimeshSkinning {
|
||||
pub(crate) fn native_compute_skinning_matrices(
|
||||
pose: Option<HashMap<String, JointPose>>,
|
||||
skeleton: Option<LindenSkeleton>,
|
||||
skin_data: Option<MeshSkinData>,
|
||||
) -> Result<Vec<Matrix4>, Error> {
|
||||
let pose = pose.ok_or(Error::ArgumentNull)?;
|
||||
let skeleton = skeleton.ok_or(Error::ArgumentNull)?;
|
||||
let skin_data = skin_data.ok_or(Error::ArgumentNull)?;
|
||||
if skin_data.joint_names.len() > MAX_SKIN_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut world = HashMap::new();
|
||||
let mut visited = 0_usize;
|
||||
compute_world(
|
||||
&skeleton.bone(),
|
||||
Matrix4::identity(),
|
||||
&pose,
|
||||
&mut world,
|
||||
&mut visited,
|
||||
)?;
|
||||
skin_data
|
||||
.joint_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, name)| {
|
||||
let Some(world) = world.get(name).copied() else {
|
||||
return Ok(Matrix4::identity());
|
||||
};
|
||||
let inverse = extract_matrix(&skin_data.inverse_bind_matrices, index);
|
||||
Matrix4::multiply_with_matrix4_matrix4(inverse, world)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn native_deform_vertices(
|
||||
face: Face,
|
||||
matrices: Vec<Matrix4>,
|
||||
bind_shape: Matrix4,
|
||||
positions: &mut [Vector3],
|
||||
mut normals: Option<&mut [Vector3]>,
|
||||
) -> Result<(), Error> {
|
||||
let count = face.vertices.len();
|
||||
if count > MAX_VERTICES || positions.len() < count {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if normals.as_ref().is_some_and(|values| values.len() < count) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
for (index, vertex) in face.vertices.iter().enumerate() {
|
||||
let Some(weights) = face.weights.as_ref().and_then(|values| values.get(index)) else {
|
||||
positions[index] = vertex.position;
|
||||
if let Some(output) = normals.as_deref_mut() {
|
||||
output[index] = vertex.normal;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if matrices.is_empty() {
|
||||
positions[index] = vertex.position;
|
||||
if let Some(output) = normals.as_deref_mut() {
|
||||
output[index] = vertex.normal;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let position = Vector3::transform(vertex.position, bind_shape)?;
|
||||
let influences = [
|
||||
(weights.joint0, weights.weight0),
|
||||
(weights.joint1, weights.weight1),
|
||||
(weights.joint2, weights.weight2),
|
||||
(weights.joint3, weights.weight3),
|
||||
];
|
||||
let mut result = Vector3::zero();
|
||||
for (joint, weight) in influences {
|
||||
if weight > 0.0 && weight.is_finite() {
|
||||
let joint = usize::try_from(joint).map_err(|_| Error::Argument)?;
|
||||
let matrix = matrices.get(joint).ok_or(Error::Argument)?;
|
||||
result = Vector3::add_with_vector3_vector3(
|
||||
result,
|
||||
Vector3::multiply_with_vector3_single(
|
||||
Vector3::transform(position, *matrix)?,
|
||||
weight,
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
positions[index] = result;
|
||||
if let Some(output) = normals.as_deref_mut() {
|
||||
let mut result = Vector3::zero();
|
||||
for (joint, weight) in influences {
|
||||
if weight > 0.0 && weight.is_finite() {
|
||||
let joint = usize::try_from(joint).map_err(|_| Error::Argument)?;
|
||||
let matrix = matrices.get(joint).ok_or(Error::Argument)?;
|
||||
result = Vector3::add_with_vector3_vector3(
|
||||
result,
|
||||
Vector3::multiply_with_vector3_single(
|
||||
Vector3::transform_normal(vertex.normal, *matrix)?,
|
||||
weight,
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
output[index] = if result == Vector3::zero() {
|
||||
result
|
||||
} else {
|
||||
Vector3::normalize(result)?
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_world(
|
||||
joint: &Joint,
|
||||
parent: Matrix4,
|
||||
pose: &HashMap<String, JointPose>,
|
||||
output: &mut HashMap<String, Matrix4>,
|
||||
visited: &mut usize,
|
||||
) -> Result<(), Error> {
|
||||
*visited = visited.checked_add(1).ok_or(Error::Argument)?;
|
||||
if *visited > MAX_SKIN_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let local = local_transform(joint, pose)?;
|
||||
let world = Matrix4::multiply_with_matrix4_matrix4(local, parent)?;
|
||||
output.insert(joint.base.name(), world);
|
||||
for alias in joint.get_aliases_list()? {
|
||||
output.insert(alias, world);
|
||||
}
|
||||
for child in joint.bone().unwrap_or_default() {
|
||||
compute_world(&child, world, pose, output, visited)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_transform(joint: &Joint, pose: &HashMap<String, JointPose>) -> Result<Matrix4, Error> {
|
||||
let mut translation = vector3(&joint.base.pos());
|
||||
let mut rotation = rotation(&joint.base.rot())?;
|
||||
let override_pose = pose.get(&joint.base.name()).copied().or_else(|| {
|
||||
joint
|
||||
.get_aliases_list()
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.find_map(|alias| pose.get(&alias).copied())
|
||||
});
|
||||
if let Some(value) = override_pose {
|
||||
if value.has_position {
|
||||
translation = value.position;
|
||||
}
|
||||
if value.has_rotation {
|
||||
rotation = value.rotation;
|
||||
}
|
||||
}
|
||||
Matrix4::multiply_with_matrix4_matrix4(
|
||||
Matrix4::create_from_quaternion(rotation)?,
|
||||
Matrix4::create_translation(translation)?,
|
||||
)
|
||||
}
|
||||
|
||||
fn vector3(values: &[f32]) -> Vector3 {
|
||||
if values.len() < 3 {
|
||||
Vector3::zero()
|
||||
} else {
|
||||
Vector3 {
|
||||
x: values[0],
|
||||
y: values[1],
|
||||
z: values[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rotation(values: &[f32]) -> Result<Quaternion, Error> {
|
||||
if values.len() < 3 {
|
||||
return Ok(Quaternion::identity());
|
||||
}
|
||||
Quaternion::create_from_eulers_with_single_single_single(
|
||||
values[0].to_radians(),
|
||||
values[1].to_radians(),
|
||||
values[2].to_radians(),
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_matrix(values: &[f32], index: usize) -> Matrix4 {
|
||||
let Some(offset) = index.checked_mul(16) else {
|
||||
return Matrix4::identity();
|
||||
};
|
||||
let Some(values) = values.get(offset..offset.saturating_add(16)) else {
|
||||
return Matrix4::identity();
|
||||
};
|
||||
Matrix4 {
|
||||
m11: values[0],
|
||||
m12: values[1],
|
||||
m13: values[2],
|
||||
m14: values[3],
|
||||
m21: values[4],
|
||||
m22: values[5],
|
||||
m23: values[6],
|
||||
m24: values[7],
|
||||
m31: values[8],
|
||||
m32: values[9],
|
||||
m33: values[10],
|
||||
m34: values[11],
|
||||
m41: values[12],
|
||||
m42: values[13],
|
||||
m43: values[14],
|
||||
m44: values[15],
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,7 @@ fn make_single_joint_skeleton(joint_name: &str) -> LindenSkeleton {
|
||||
|
||||
fn identity_floats() -> Vec<f32> {
|
||||
vec![
|
||||
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
||||
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
1021
crates/libremetaverse/src/avatar_manager.rs
Normal file
1021
crates/libremetaverse/src/avatar_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
776
crates/libremetaverse/src/avatar_rig.rs
Normal file
776
crates/libremetaverse/src/avatar_rig.rs
Normal file
@@ -0,0 +1,776 @@
|
||||
//! Backend-independent avatar hierarchy and rigged-attachment math.
|
||||
|
||||
#![allow(
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::too_many_arguments,
|
||||
clippy::unnecessary_wraps
|
||||
)] // Mapped signatures and recursive hierarchy state are fixed.
|
||||
|
||||
use crate::Error;
|
||||
use crate::animesh_skinning::MeshSkinData;
|
||||
use crate::rendering::{BoneTransform, Joint, LindenSkeleton};
|
||||
use crate::visual_catalog::VisualParams;
|
||||
use libremetaverse_types::compat::{
|
||||
Matrix4x4, Quaternion as NumericsQuaternion, Vector3 as NumericsVector3,
|
||||
};
|
||||
use libremetaverse_types::{Matrix4, Quaternion, UUID, Vector3};
|
||||
use roxmltree::Document;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
const MAX_JOINTS: usize = 512;
|
||||
const MAX_LAD_BYTES: u64 = 8 * 1024 * 1024;
|
||||
const MAX_ATTACHMENT_POINTS: usize = 256;
|
||||
const MAX_MESH_DEFINITIONS: usize = 512;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AvatarAttachmentPoint {
|
||||
group: i32,
|
||||
id: i32,
|
||||
joint: String,
|
||||
location: String,
|
||||
name: String,
|
||||
position: Vector3,
|
||||
rotation: Vector3,
|
||||
visible_in_first_person: bool,
|
||||
}
|
||||
|
||||
impl AvatarAttachmentPoint {
|
||||
pub(crate) fn native_group(&self) -> i32 {
|
||||
self.group
|
||||
}
|
||||
|
||||
pub(crate) fn native_id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub(crate) fn native_joint(&self) -> String {
|
||||
self.joint.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_location(&self) -> String {
|
||||
self.location.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_position(&self) -> Vector3 {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub(crate) fn native_rotation(&self) -> Vector3 {
|
||||
self.rotation
|
||||
}
|
||||
|
||||
pub(crate) fn native_visible_in_first_person(&self) -> bool {
|
||||
self.visible_in_first_person
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AvatarMeshDefinition {
|
||||
file_name: String,
|
||||
lod_level: i32,
|
||||
min_pixel_width: i32,
|
||||
type_: String,
|
||||
}
|
||||
|
||||
impl AvatarMeshDefinition {
|
||||
pub(crate) fn native_file_name(&self) -> String {
|
||||
self.file_name.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_lod_level(&self) -> i32 {
|
||||
self.lod_level
|
||||
}
|
||||
|
||||
pub(crate) fn native_min_pixel_width(&self) -> i32 {
|
||||
self.min_pixel_width
|
||||
}
|
||||
|
||||
pub(crate) fn native_type(&self) -> String {
|
||||
self.type_.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LindenAvatarDefinition {
|
||||
attachment_points: Vec<AvatarAttachmentPoint>,
|
||||
mesh_definitions: Vec<AvatarMeshDefinition>,
|
||||
skeleton: LindenSkeleton,
|
||||
}
|
||||
|
||||
impl LindenAvatarDefinition {
|
||||
pub(crate) fn native_load(
|
||||
lad_file_name: Option<String>,
|
||||
skeleton_file_name: Option<String>,
|
||||
) -> Result<Self, Error> {
|
||||
let skeleton = LindenSkeleton::load_with_string(skeleton_file_name)?;
|
||||
let owned_xml;
|
||||
let xml = if let Some(file_name) = lad_file_name {
|
||||
owned_xml = read_bounded(&file_name)?;
|
||||
owned_xml.as_str()
|
||||
} else {
|
||||
include_str!("../../../codegen/inputs/avatar_lad.xml")
|
||||
};
|
||||
let document = Document::parse(xml).map_err(|_| Error::Argument)?;
|
||||
let mut attachment_points = Vec::new();
|
||||
let mut mesh_definitions = Vec::new();
|
||||
for node in document.descendants().filter(roxmltree::Node::is_element) {
|
||||
match node.tag_name().name() {
|
||||
"attachment_point" => {
|
||||
let Some(id) = parse_i32(node.attribute("id")) else {
|
||||
continue;
|
||||
};
|
||||
if attachment_points.len() >= MAX_ATTACHMENT_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
attachment_points.push(AvatarAttachmentPoint {
|
||||
group: parse_i32(node.attribute("group")).unwrap_or_default(),
|
||||
id,
|
||||
joint: node.attribute("joint").unwrap_or_default().to_owned(),
|
||||
location: node.attribute("location").unwrap_or_default().to_owned(),
|
||||
name: node.attribute("name").unwrap_or_default().to_owned(),
|
||||
position: parse_vector(node.attribute("position")),
|
||||
rotation: parse_vector(node.attribute("rotation")),
|
||||
visible_in_first_person: node
|
||||
.attribute("visible_in_first_person")
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("true")),
|
||||
});
|
||||
}
|
||||
"mesh" => {
|
||||
let (Some(type_), Some(lod_level)) =
|
||||
(node.attribute("type"), parse_i32(node.attribute("lod")))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if mesh_definitions.len() >= MAX_MESH_DEFINITIONS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
mesh_definitions.push(AvatarMeshDefinition {
|
||||
file_name: node.attribute("file_name").unwrap_or_default().to_owned(),
|
||||
lod_level,
|
||||
min_pixel_width: parse_i32(node.attribute("min_pixel_width"))
|
||||
.unwrap_or_default(),
|
||||
type_: type_.to_owned(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
attachment_points,
|
||||
mesh_definitions,
|
||||
skeleton,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_compute_bone_transforms(
|
||||
&self,
|
||||
param_values: &HashMap<i32, f32>,
|
||||
) -> Result<HashMap<String, BoneTransform>, Error> {
|
||||
let mut result = HashMap::new();
|
||||
let joints = self.skeleton.get_all_joints()?.collect::<Vec<_>>();
|
||||
if joints.len() > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
for joint in joints {
|
||||
seed_transform(&mut result, &joint.base);
|
||||
for volume in joint.collision_volume() {
|
||||
seed_transform(&mut result, &volume.base);
|
||||
}
|
||||
}
|
||||
for param in VisualParams::params().0.into_values() {
|
||||
let raw_value = param_values
|
||||
.get(¶m.param_id)
|
||||
.copied()
|
||||
.unwrap_or(param.default_value);
|
||||
let weight = raw_value.clamp(param.min_value, param.max_value);
|
||||
if !weight.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
for distortion in param.skeletal_distortions.unwrap_or_default() {
|
||||
let Some(transform) = result.get_mut(&distortion.bone_name) else {
|
||||
continue;
|
||||
};
|
||||
transform.scale = add_scaled(transform.scale, distortion.scale_deformation, weight);
|
||||
if distortion.has_position_deformation {
|
||||
transform.position =
|
||||
add_scaled(transform.position, distortion.position_deformation, weight);
|
||||
}
|
||||
}
|
||||
for morph in param.volume_morphs.unwrap_or_default() {
|
||||
let Some(transform) = result.get_mut(&morph.bone_name) else {
|
||||
continue;
|
||||
};
|
||||
if morph.has_scale {
|
||||
transform.scale = add_scaled(transform.scale, morph.scale_delta, weight);
|
||||
}
|
||||
if morph.has_position {
|
||||
transform.position =
|
||||
add_scaled(transform.position, morph.position_delta, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
if result
|
||||
.values()
|
||||
.any(|value| !finite(value.position) || !finite(value.scale))
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_attachment_point_by_id(
|
||||
&self,
|
||||
id: i32,
|
||||
) -> Option<AvatarAttachmentPoint> {
|
||||
self.attachment_points
|
||||
.iter()
|
||||
.find(|point| point.id == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_attachment_point_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Option<AvatarAttachmentPoint> {
|
||||
self.attachment_points
|
||||
.iter()
|
||||
.find(|point| point.name.eq_ignore_ascii_case(name))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn native_attachment_points(&self) -> Vec<AvatarAttachmentPoint> {
|
||||
self.attachment_points.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_mesh_definitions(&self) -> Vec<AvatarMeshDefinition> {
|
||||
self.mesh_definitions.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn native_skeleton(&self) -> LindenSkeleton {
|
||||
self.skeleton.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AttachmentRiggedSkin {
|
||||
pub inv_bind_matrices: Vec<Matrix4x4>,
|
||||
pub joint_names: Vec<String>,
|
||||
pub joint_position_overrides: Vec<(String, NumericsVector3)>,
|
||||
pub joints: Vec<i32>,
|
||||
pub lock_scale_if_joint_position: bool,
|
||||
pub mesh_id: UUID,
|
||||
pub weights: Vec<f32>,
|
||||
}
|
||||
|
||||
impl AttachmentRiggedSkin {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AvatarBoneMath;
|
||||
|
||||
impl AvatarBoneMath {
|
||||
pub(crate) fn native_build_bone_world_matrices(
|
||||
skeleton: LindenSkeleton,
|
||||
bone_transforms: HashMap<String, BoneTransform>,
|
||||
) -> Result<HashMap<String, Matrix4x4>, Error> {
|
||||
let mut output = HashMap::new();
|
||||
let mut visited = 0;
|
||||
build_joint(
|
||||
&skeleton.bone(),
|
||||
Quaternion::identity(),
|
||||
Vector3::zero(),
|
||||
Vector3::one(),
|
||||
&bone_transforms,
|
||||
None,
|
||||
&mut output,
|
||||
&mut visited,
|
||||
)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn native_compute_animated_bone_world_matrices(
|
||||
avatar_definition: &LindenAvatarDefinition,
|
||||
bone_transforms: &HashMap<String, BoneTransform>,
|
||||
rotation_deltas: &HashMap<String, NumericsQuaternion>,
|
||||
) -> Result<HashMap<String, Matrix4x4>, Error> {
|
||||
let mut output = HashMap::new();
|
||||
let mut visited = 0;
|
||||
build_joint(
|
||||
&avatar_definition.skeleton.bone(),
|
||||
Quaternion::identity(),
|
||||
Vector3::zero(),
|
||||
Vector3::one(),
|
||||
bone_transforms,
|
||||
Some(rotation_deltas),
|
||||
&mut output,
|
||||
&mut visited,
|
||||
)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn native_compute_attachment_bone_world_matrices(
|
||||
avatar_definition: &LindenAvatarDefinition,
|
||||
bone_transforms: &HashMap<String, BoneTransform>,
|
||||
rotation_deltas: &HashMap<String, NumericsQuaternion>,
|
||||
) -> Result<HashMap<String, Matrix4x4>, Error> {
|
||||
Self::native_compute_animated_bone_world_matrices(
|
||||
avatar_definition,
|
||||
bone_transforms,
|
||||
rotation_deltas,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn native_strip_scale(matrix: Matrix4x4) -> Matrix4x4 {
|
||||
let mut value = matrix.0;
|
||||
for row in 0..3 {
|
||||
let offset = row * 4;
|
||||
let length = (value[offset].mul_add(
|
||||
value[offset],
|
||||
value[offset + 1].mul_add(value[offset + 1], value[offset + 2] * value[offset + 2]),
|
||||
))
|
||||
.sqrt();
|
||||
if length > 1.0e-5 {
|
||||
value[offset] /= length;
|
||||
value[offset + 1] /= length;
|
||||
value[offset + 2] /= length;
|
||||
}
|
||||
value[offset + 3] = 0.0;
|
||||
}
|
||||
Matrix4x4(value)
|
||||
}
|
||||
|
||||
pub(crate) fn native_compute_ground_adjustment(
|
||||
transforms: Option<&HashMap<String, BoneTransform>>,
|
||||
) -> f32 {
|
||||
let Some(transforms) = transforms else {
|
||||
return 1.0;
|
||||
};
|
||||
let names = [
|
||||
"mPelvis",
|
||||
"mSkull",
|
||||
"mNeck",
|
||||
"mChest",
|
||||
"mHead",
|
||||
"mTorso",
|
||||
"mHipLeft",
|
||||
"mKneeLeft",
|
||||
"mAnkleLeft",
|
||||
"mFootLeft",
|
||||
];
|
||||
let Some(values) = names
|
||||
.iter()
|
||||
.map(|name| transforms.get(*name))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
return 1.0;
|
||||
};
|
||||
let [
|
||||
pelvis,
|
||||
skull,
|
||||
neck,
|
||||
chest,
|
||||
head,
|
||||
torso,
|
||||
hip,
|
||||
knee,
|
||||
ankle,
|
||||
foot,
|
||||
] = values.as_slice()
|
||||
else {
|
||||
return 1.0;
|
||||
};
|
||||
let pelvis_to_foot = hip.position.z * pelvis.scale.z
|
||||
- knee.position.z * hip.scale.z
|
||||
- ankle.position.z * knee.scale.z
|
||||
- foot.position.z * ankle.scale.z;
|
||||
let height = pelvis_to_foot
|
||||
+ std::f32::consts::SQRT_2 * skull.position.z * head.scale.z
|
||||
+ head.position.z * neck.scale.z
|
||||
+ neck.position.z * chest.scale.z
|
||||
+ chest.position.z * torso.scale.z
|
||||
+ torso.position.z * pelvis.scale.z;
|
||||
let adjustment = height - pelvis_to_foot;
|
||||
if adjustment.is_finite() && adjustment > 0.0 {
|
||||
adjustment
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_joint(
|
||||
joint: &Joint,
|
||||
parent_rotation: Quaternion,
|
||||
parent_position: Vector3,
|
||||
parent_scale: Vector3,
|
||||
transforms: &HashMap<String, BoneTransform>,
|
||||
rotation_deltas: Option<&HashMap<String, NumericsQuaternion>>,
|
||||
output: &mut HashMap<String, Matrix4x4>,
|
||||
visited: &mut usize,
|
||||
) -> Result<(), Error> {
|
||||
*visited = visited.checked_add(1).ok_or(Error::Argument)?;
|
||||
if *visited > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let name = joint.base.name();
|
||||
let transform = transforms.get(&name);
|
||||
let position = transform.map_or_else(
|
||||
|| vector(&joint.base.pos(), Vector3::zero()),
|
||||
|v| v.position,
|
||||
);
|
||||
let scale = transform.map_or_else(|| vector(&joint.base.scale(), Vector3::one()), |v| v.scale);
|
||||
if !finite(position) || !finite(scale) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut rotation = euler(&joint.base.rot())?;
|
||||
if let Some(delta) = rotation_deltas.and_then(|values| values.get(&name)) {
|
||||
rotation = Quaternion::multiply_with_quaternion_quaternion(rotation, quaternion(*delta)?)?;
|
||||
}
|
||||
let world_rotation =
|
||||
Quaternion::multiply_with_quaternion_quaternion(parent_rotation, rotation)?;
|
||||
let scaled_position = Vector3::multiply_with_vector3_vector3(position, parent_scale)?;
|
||||
let rotated_position = Vector3::transform_normal(
|
||||
scaled_position,
|
||||
Matrix4::create_from_quaternion(parent_rotation)?,
|
||||
)?;
|
||||
let world_position = Vector3::add_with_vector3_vector3(rotated_position, parent_position)?;
|
||||
let world = Matrix4::multiply_with_matrix4_matrix4(
|
||||
Matrix4::multiply_with_matrix4_matrix4(
|
||||
Matrix4::create_scale(scale)?,
|
||||
Matrix4::create_from_quaternion(world_rotation)?,
|
||||
)?,
|
||||
Matrix4::create_translation(world_position)?,
|
||||
)?;
|
||||
let world = numerics(world);
|
||||
if !name.is_empty() {
|
||||
output.insert(name, world);
|
||||
}
|
||||
for alias in joint.get_aliases_list()? {
|
||||
output.entry(alias).or_insert(world);
|
||||
}
|
||||
for volume in joint.collision_volume() {
|
||||
*visited = visited.checked_add(1).ok_or(Error::Argument)?;
|
||||
if *visited > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let volume_name = volume.base.name();
|
||||
let volume_transform = transforms.get(&volume_name);
|
||||
let volume_position = volume_transform.map_or_else(
|
||||
|| vector(&volume.base.pos(), Vector3::zero()),
|
||||
|value| value.position,
|
||||
);
|
||||
let volume_scale = volume_transform.map_or_else(
|
||||
|| vector(&volume.base.scale(), Vector3::one()),
|
||||
|value| value.scale,
|
||||
);
|
||||
if !finite(volume_position) || !finite(volume_scale) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let volume_rotation = euler(&volume.base.rot())?;
|
||||
let volume_world_rotation =
|
||||
Quaternion::multiply_with_quaternion_quaternion(world_rotation, volume_rotation)?;
|
||||
let volume_scaled_position =
|
||||
Vector3::multiply_with_vector3_vector3(volume_position, scale)?;
|
||||
let volume_rotated_position = Vector3::transform_normal(
|
||||
volume_scaled_position,
|
||||
Matrix4::create_from_quaternion(world_rotation)?,
|
||||
)?;
|
||||
let volume_world_position =
|
||||
Vector3::add_with_vector3_vector3(volume_rotated_position, world_position)?;
|
||||
let volume_world = Matrix4::multiply_with_matrix4_matrix4(
|
||||
Matrix4::multiply_with_matrix4_matrix4(
|
||||
Matrix4::create_scale(volume_scale)?,
|
||||
Matrix4::create_from_quaternion(volume_world_rotation)?,
|
||||
)?,
|
||||
Matrix4::create_translation(volume_world_position)?,
|
||||
)?;
|
||||
if !volume_name.is_empty() {
|
||||
output.entry(volume_name).or_insert(numerics(volume_world));
|
||||
}
|
||||
}
|
||||
for child in joint.bone().unwrap_or_default() {
|
||||
build_joint(
|
||||
&child,
|
||||
world_rotation,
|
||||
world_position,
|
||||
scale,
|
||||
transforms,
|
||||
rotation_deltas,
|
||||
output,
|
||||
visited,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn vector(values: &[f32], fallback: Vector3) -> Vector3 {
|
||||
if values.len() < 3 {
|
||||
fallback
|
||||
} else {
|
||||
Vector3 {
|
||||
x: values[0],
|
||||
y: values[1],
|
||||
z: values[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finite(value: Vector3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
fn read_bounded(path: impl AsRef<Path>) -> Result<String, Error> {
|
||||
let path = path.as_ref();
|
||||
if std::fs::metadata(path).map_err(|_| Error::Argument)?.len() > MAX_LAD_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let contents = std::fs::read_to_string(path).map_err(|_| Error::Argument)?;
|
||||
if contents.len() as u64 > MAX_LAD_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
fn parse_i32(value: Option<&str>) -> Option<i32> {
|
||||
value?.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_vector(value: Option<&str>) -> Vector3 {
|
||||
let mut values = value.unwrap_or_default().split_ascii_whitespace();
|
||||
let mut next = || {
|
||||
values
|
||||
.next()
|
||||
.and_then(|part| part.parse::<f32>().ok())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
Vector3 {
|
||||
x: next(),
|
||||
y: next(),
|
||||
z: next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn seed_transform(
|
||||
output: &mut HashMap<String, BoneTransform>,
|
||||
joint: &crate::rendering::JointBase,
|
||||
) {
|
||||
let name = joint.name();
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
output.insert(
|
||||
name,
|
||||
BoneTransform {
|
||||
position: vector(&joint.pos(), Vector3::zero()),
|
||||
scale: vector(&joint.scale(), Vector3::one()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn add_scaled(value: Vector3, delta: Vector3, weight: f32) -> Vector3 {
|
||||
Vector3 {
|
||||
x: delta.x.mul_add(weight, value.x),
|
||||
y: delta.y.mul_add(weight, value.y),
|
||||
z: delta.z.mul_add(weight, value.z),
|
||||
}
|
||||
}
|
||||
|
||||
fn quaternion(value: NumericsQuaternion) -> Result<Quaternion, Error> {
|
||||
if value.0.iter().all(|component| component.is_finite()) {
|
||||
Quaternion::new_with_single_single_single_single(
|
||||
value.0[0], value.0[1], value.0[2], value.0[3],
|
||||
)
|
||||
} else {
|
||||
Err(Error::Argument)
|
||||
}
|
||||
}
|
||||
|
||||
fn euler(values: &[f32]) -> Result<Quaternion, Error> {
|
||||
let value = vector(values, Vector3::zero());
|
||||
Quaternion::create_from_eulers_with_single_single_single(
|
||||
value.x.to_radians(),
|
||||
value.y.to_radians(),
|
||||
value.z.to_radians(),
|
||||
)
|
||||
}
|
||||
|
||||
fn numerics(value: Matrix4) -> Matrix4x4 {
|
||||
Matrix4x4([
|
||||
value.m11, value.m12, value.m13, value.m14, value.m21, value.m22, value.m23, value.m24,
|
||||
value.m31, value.m32, value.m33, value.m34, value.m41, value.m42, value.m43, value.m44,
|
||||
])
|
||||
}
|
||||
|
||||
pub struct RiggedSkinMath;
|
||||
|
||||
impl RiggedSkinMath {
|
||||
pub(crate) fn native_floats_to_matrix(values: &[f32]) -> Matrix4x4 {
|
||||
let Ok(values) = <&[f32; 16]>::try_from(values) else {
|
||||
return Matrix4x4::default();
|
||||
};
|
||||
Matrix4x4(*values)
|
||||
}
|
||||
|
||||
pub(crate) fn native_build_inv_bind_matrices(
|
||||
skin: &MeshSkinData,
|
||||
) -> Result<Vec<Matrix4x4>, Error> {
|
||||
if skin.joint_names.len() > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok((0..skin.joint_names.len())
|
||||
.map(|index| {
|
||||
let start = index * 16;
|
||||
skin.inverse_bind_matrices
|
||||
.get(start..start + 16)
|
||||
.map_or_else(identity, Self::native_floats_to_matrix)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn native_extract_joint_position_overrides(
|
||||
skin: &MeshSkinData,
|
||||
) -> Result<Vec<(String, NumericsVector3)>, Error> {
|
||||
let count = skin.joint_names.len();
|
||||
if count > MAX_JOINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if skin.alt_inverse_bind_matrices.len() / 16 != count
|
||||
|| !skin.alt_inverse_bind_matrices.len().is_multiple_of(16)
|
||||
{
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Ok(skin
|
||||
.joint_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, name)| !name.is_empty())
|
||||
.map(|(index, name)| {
|
||||
let offset = index * 16;
|
||||
(
|
||||
name.clone(),
|
||||
NumericsVector3([
|
||||
skin.alt_inverse_bind_matrices[offset + 12],
|
||||
skin.alt_inverse_bind_matrices[offset + 13],
|
||||
skin.alt_inverse_bind_matrices[offset + 14],
|
||||
]),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn native_normalize_skin_weights(
|
||||
joint_count: i32,
|
||||
joints: [&mut i32; 4],
|
||||
weights: [&mut f32; 4],
|
||||
) {
|
||||
let mut sum = 0.0;
|
||||
for index in 0..4 {
|
||||
if *joints[index] < 0 || *joints[index] >= joint_count {
|
||||
*weights[index] = 0.0;
|
||||
}
|
||||
*weights[index] = if weights[index].is_finite() {
|
||||
weights[index].clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
sum += *weights[index];
|
||||
}
|
||||
if sum > 1.0e-6 {
|
||||
for weight in weights {
|
||||
*weight /= sum;
|
||||
}
|
||||
} else {
|
||||
for index in 0..4 {
|
||||
*joints[index] = 0;
|
||||
*weights[index] = if index == 0 && joint_count > 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn identity() -> Matrix4x4 {
|
||||
Matrix4x4([
|
||||
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn embedded_avatar_definition_exposes_generated_skeleton_and_lad_records() {
|
||||
let definition =
|
||||
LindenAvatarDefinition::native_load(None, None).expect("avatar definition");
|
||||
assert!(!definition.native_attachment_points().is_empty());
|
||||
assert!(!definition.native_mesh_definitions().is_empty());
|
||||
assert!(
|
||||
definition
|
||||
.native_skeleton()
|
||||
.get_all_joints()
|
||||
.expect("joints")
|
||||
.count()
|
||||
> 1
|
||||
);
|
||||
let point = definition.native_attachment_points()[0].clone();
|
||||
assert_eq!(
|
||||
definition
|
||||
.native_get_attachment_point_by_id(point.native_id())
|
||||
.expect("point by id"),
|
||||
point
|
||||
);
|
||||
assert_eq!(
|
||||
definition
|
||||
.native_get_attachment_point_by_name(&point.native_name().to_uppercase())
|
||||
.expect("point by name"),
|
||||
point
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_and_attachment_world_matrices_apply_rotation_deltas() {
|
||||
let definition =
|
||||
LindenAvatarDefinition::native_load(None, None).expect("avatar definition");
|
||||
let transforms = definition
|
||||
.native_compute_bone_transforms(&HashMap::new())
|
||||
.expect("bone transforms");
|
||||
let bind = AvatarBoneMath::native_compute_animated_bone_world_matrices(
|
||||
&definition,
|
||||
&transforms,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.expect("bind matrices");
|
||||
let delta = NumericsQuaternion([
|
||||
0.0,
|
||||
0.0,
|
||||
std::f32::consts::FRAC_1_SQRT_2,
|
||||
std::f32::consts::FRAC_1_SQRT_2,
|
||||
]);
|
||||
let deltas = HashMap::from([("mPelvis".to_owned(), delta)]);
|
||||
let animated = AvatarBoneMath::native_compute_animated_bone_world_matrices(
|
||||
&definition,
|
||||
&transforms,
|
||||
&deltas,
|
||||
)
|
||||
.expect("animated matrices");
|
||||
let attachment = AvatarBoneMath::native_compute_attachment_bone_world_matrices(
|
||||
&definition,
|
||||
&transforms,
|
||||
&deltas,
|
||||
)
|
||||
.expect("attachment matrices");
|
||||
assert_ne!(animated["mPelvis"], bind["mPelvis"]);
|
||||
assert_eq!(attachment, animated);
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,8 @@ struct ClientRuntime {
|
||||
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
|
||||
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
|
||||
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
|
||||
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
|
||||
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
|
||||
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
|
||||
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
|
||||
asset_manager: Mutex<Option<Arc<crate::asset_manager::AssetManagerInner>>>,
|
||||
@@ -151,6 +153,8 @@ impl ClientRuntime {
|
||||
network_manager: Mutex::new(std::sync::Weak::new()),
|
||||
agent_manager: Mutex::new(std::sync::Weak::new()),
|
||||
appearance_manager: Mutex::new(None),
|
||||
avatar_manager: Mutex::new(None),
|
||||
animesh_manager: Mutex::new(None),
|
||||
inventory_manager: Mutex::new(None),
|
||||
inventory_ais_client: Mutex::new(None),
|
||||
asset_manager: Mutex::new(None),
|
||||
@@ -491,6 +495,57 @@ impl GridClient {
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
pub(crate) fn native_avatars(&self) -> Result<crate::AvatarManager, crate::Error> {
|
||||
let mut cached = self
|
||||
.runtime
|
||||
.avatar_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(inner) = cached.as_ref() {
|
||||
return Ok(crate::avatar_manager::AvatarManager::native_from_inner(
|
||||
Arc::clone(inner),
|
||||
));
|
||||
}
|
||||
let manager =
|
||||
crate::avatar_manager::AvatarManager::native_new(Some(Arc::new(self.clone())))?;
|
||||
*cached = Some(manager.native_inner());
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn native_set_avatars(&mut self, value: crate::AvatarManager) {
|
||||
*self
|
||||
.runtime
|
||||
.avatar_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
|
||||
}
|
||||
|
||||
pub(crate) fn native_animesh(&self) -> Result<crate::animesh::AnimeshManager, crate::Error> {
|
||||
let mut cached = self
|
||||
.runtime
|
||||
.animesh_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(inner) = cached.as_ref() {
|
||||
return Ok(crate::animesh_runtime::AnimeshManager::native_from_inner(
|
||||
Arc::clone(inner),
|
||||
));
|
||||
}
|
||||
let manager = crate::animesh_runtime::AnimeshManager::native_new(self.clone())?;
|
||||
*cached = Some(manager.native_inner());
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn native_set_animesh(&mut self, value: crate::animesh::AnimeshManager) {
|
||||
*self
|
||||
.runtime
|
||||
.animesh_manager
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn native_set_assets(&mut self, value: crate::AssetManager) {
|
||||
*self
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,18 @@ mod attention_catalog;
|
||||
mod agent_manager;
|
||||
mod agent_messages;
|
||||
mod agent_movement;
|
||||
mod animation;
|
||||
#[path = "animesh.rs"]
|
||||
mod animesh_runtime;
|
||||
mod animesh_skinning;
|
||||
mod appearance_baker;
|
||||
mod appearance_manager;
|
||||
mod asset_cache;
|
||||
mod asset_manager;
|
||||
mod asset_material;
|
||||
mod asset_models;
|
||||
mod avatar_manager;
|
||||
mod avatar_rig;
|
||||
mod baking_texture_provider;
|
||||
mod bit_pack;
|
||||
mod caps;
|
||||
@@ -85,45 +91,41 @@ impl assets::OarFile {
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl animesh::AnimationTrack {
|
||||
fn new(_animation_id: libremetaverse_types::UUID) -> Result<Self, Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Animesh.AnimationTrack.#ctor(LibreMetaverse.UUID)",
|
||||
)
|
||||
fn new(animation_id: libremetaverse_types::UUID) -> Result<Self, Error> {
|
||||
Self::native_new(animation_id)
|
||||
}
|
||||
|
||||
fn decode_rotation(
|
||||
_value: libremetaverse_types::Vector3,
|
||||
value: libremetaverse_types::Vector3,
|
||||
) -> Result<libremetaverse_types::Quaternion, Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Animesh.AnimationTrack.DecodeRotation(LibreMetaverse.Vector3)",
|
||||
)
|
||||
Self::native_decode_rotation(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self, dead_code)]
|
||||
#[allow(
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::unused_self,
|
||||
dead_code
|
||||
)]
|
||||
impl animesh::AnimeshPlayer {
|
||||
fn new(_object_id: libremetaverse_types::UUID) -> Result<Self, Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Animesh.AnimeshPlayer.#ctor(LibreMetaverse.UUID)",
|
||||
)
|
||||
fn new(object_id: libremetaverse_types::UUID) -> Result<Self, Error> {
|
||||
Self::native_new(object_id)
|
||||
}
|
||||
|
||||
fn get_or_add_track(
|
||||
&mut self,
|
||||
_animation_id: libremetaverse_types::UUID,
|
||||
animation_id: libremetaverse_types::UUID,
|
||||
) -> Result<std::sync::Arc<animesh::AnimationTrack>, Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Animesh.AnimeshPlayer.GetOrAddTrack(LibreMetaverse.UUID)",
|
||||
)
|
||||
self.native_get_or_add_track(animation_id)
|
||||
}
|
||||
|
||||
fn retain_only(
|
||||
&mut self,
|
||||
_animation_ids: std::collections::HashSet<libremetaverse_types::UUID>,
|
||||
animation_ids: std::collections::HashSet<libremetaverse_types::UUID>,
|
||||
) -> Result<(), Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Animesh.AnimeshPlayer.RetainOnly(System.Collections.Generic.ISet{LibreMetaverse.UUID})",
|
||||
)
|
||||
self.native_retain_only(&animation_ids);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ use roxmltree::{Document, Node};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
const MAX_SKELETON_BYTES: u64 = 8 * 1024 * 1024;
|
||||
const MAX_SKELETON_BONES: usize = 512;
|
||||
const MAX_COLLISION_VOLUMES: usize = 512;
|
||||
const MAX_SKELETON_DEPTH: usize = 128;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JointBase {
|
||||
name: String,
|
||||
@@ -310,7 +315,17 @@ impl LindenSkeleton {
|
||||
let Some(file_name) = file_name else {
|
||||
return Self::get_default();
|
||||
};
|
||||
if std::fs::metadata(&file_name)
|
||||
.map_err(|_| crate::Error::Argument)?
|
||||
.len()
|
||||
> MAX_SKELETON_BYTES
|
||||
{
|
||||
return Err(crate::Error::Argument);
|
||||
}
|
||||
let text = std::fs::read_to_string(file_name).map_err(|_| crate::Error::Argument)?;
|
||||
if text.len() as u64 > MAX_SKELETON_BYTES {
|
||||
return Err(crate::Error::Argument);
|
||||
}
|
||||
parse_skeleton_xml(&text)
|
||||
}
|
||||
|
||||
@@ -469,10 +484,20 @@ fn parse_base(node: Node<'_, '_>) -> Result<JointBase, crate::Error> {
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_joint(node: Node<'_, '_>, names: &mut HashSet<String>) -> Result<Joint, crate::Error> {
|
||||
fn parse_joint(
|
||||
node: Node<'_, '_>,
|
||||
names: &mut HashSet<String>,
|
||||
depth: usize,
|
||||
bone_count: &mut usize,
|
||||
collision_volume_count: &mut usize,
|
||||
) -> Result<Joint, crate::Error> {
|
||||
if !node.has_tag_name("bone") {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
if depth > MAX_SKELETON_DEPTH || *bone_count >= MAX_SKELETON_BONES {
|
||||
return Err(parse_error(node_position(node)));
|
||||
}
|
||||
*bone_count += 1;
|
||||
let base = parse_base(node)?;
|
||||
if !names.insert(base.name.clone()) {
|
||||
return Err(parse_error(node_position(node)));
|
||||
@@ -487,12 +512,22 @@ fn parse_joint(node: Node<'_, '_>, names: &mut HashSet<String>) -> Result<Joint,
|
||||
let mut bones = Vec::new();
|
||||
for child in node.children().filter(Node::is_element) {
|
||||
if child.has_tag_name("collision_volume") {
|
||||
if *collision_volume_count >= MAX_COLLISION_VOLUMES {
|
||||
return Err(parse_error(node_position(child)));
|
||||
}
|
||||
if child.children().any(|node| node.is_element()) {
|
||||
return Err(parse_error(node_position(child)));
|
||||
}
|
||||
collision_volumes.push(CollisionVolume::from_base(parse_base(child)?));
|
||||
*collision_volume_count += 1;
|
||||
} else if child.has_tag_name("bone") {
|
||||
bones.push(parse_joint(child, names)?);
|
||||
bones.push(parse_joint(
|
||||
child,
|
||||
names,
|
||||
depth + 1,
|
||||
bone_count,
|
||||
collision_volume_count,
|
||||
)?);
|
||||
} else {
|
||||
return Err(parse_error(node_position(child)));
|
||||
}
|
||||
@@ -508,6 +543,9 @@ fn parse_joint(node: Node<'_, '_>, names: &mut HashSet<String>) -> Result<Joint,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_skeleton_xml(text: &str) -> Result<LindenSkeleton, crate::Error> {
|
||||
if text.len() as u64 > MAX_SKELETON_BYTES {
|
||||
return Err(parse_error(0));
|
||||
}
|
||||
let document = Document::parse(text).map_err(|_| parse_error(0))?;
|
||||
let root = document.root_element();
|
||||
if !root.has_tag_name("linden_skeleton") {
|
||||
@@ -528,12 +566,23 @@ pub(crate) fn parse_skeleton_xml(text: &str) -> Result<LindenSkeleton, crate::Er
|
||||
let expected_collision_volumes = num_collision_volumes
|
||||
.parse::<usize>()
|
||||
.map_err(|_| parse_error(node_position(root)))?;
|
||||
if expected_bones > MAX_SKELETON_BONES || expected_collision_volumes > MAX_COLLISION_VOLUMES {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
let root_bones = root.children().filter(Node::is_element).collect::<Vec<_>>();
|
||||
if root_bones.len() != 1 || !root_bones[0].has_tag_name("bone") {
|
||||
return Err(parse_error(node_position(root)));
|
||||
}
|
||||
let mut names = HashSet::new();
|
||||
let bone = parse_joint(root_bones[0], &mut names)?;
|
||||
let mut bone_count = 0;
|
||||
let mut collision_volume_count = 0;
|
||||
let bone = parse_joint(
|
||||
root_bones[0],
|
||||
&mut names,
|
||||
0,
|
||||
&mut bone_count,
|
||||
&mut collision_volume_count,
|
||||
)?;
|
||||
let (actual_bones, actual_collision_volumes) = skeleton_counts(&bone);
|
||||
if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes {
|
||||
return Err(parse_error(node_position(root)));
|
||||
|
||||
@@ -291,7 +291,15 @@ NATIVE_TYPES = {
|
||||
# hand-written, while its fixed public methods remain generator-audited.
|
||||
NATIVE_DECLARATIONS = {
|
||||
"T:LibreMetaverse.AgentManager": "crate::agent_manager::AgentManager",
|
||||
"T:LibreMetaverse.Animation": "crate::avatar_manager::Animation",
|
||||
"T:LibreMetaverse.Animesh.AnimationTrack": "crate::animesh_runtime::AnimationTrack",
|
||||
"T:LibreMetaverse.Animesh.AnimeshManager": "crate::animesh_runtime::AnimeshManager",
|
||||
"T:LibreMetaverse.Animesh.AnimeshPlayer": "crate::animesh_runtime::AnimeshPlayer",
|
||||
"T:LibreMetaverse.Animesh.JointPose": "crate::animesh_runtime::JointPose",
|
||||
"T:LibreMetaverse.AnimeshSkinning": "crate::animesh_skinning::AnimeshSkinning",
|
||||
"T:LibreMetaverse.AppearanceManager": "crate::appearance_manager::AppearanceManager",
|
||||
"T:LibreMetaverse.AvatarManager": "crate::avatar_manager::AvatarManager",
|
||||
"T:LibreMetaverse.AvatarAnimationEventArgs": "crate::avatar_manager::AvatarAnimationEventArgs",
|
||||
"T:LibreMetaverse.AppearanceSetEventArgs": "crate::appearance_manager::AppearanceSetEventArgs",
|
||||
"T:LibreMetaverse.Appearance.CompositeCurrentOutfitPolicy": "crate::current_outfit::CompositeCurrentOutfitPolicy",
|
||||
"T:LibreMetaverse.Appearance.CurrentOutfitFolder": "crate::current_outfit::CurrentOutfitFolder",
|
||||
@@ -301,10 +309,20 @@ NATIVE_DECLARATIONS = {
|
||||
"T:LibreMetaverse.GridClientBakingTextureProvider": "crate::baking_texture_provider::GridClientBakingTextureProvider",
|
||||
"T:LibreMetaverse.Imaging.Baker": "crate::appearance_baker::Baker",
|
||||
"T:LibreMetaverse.GridClient": "crate::client_core::GridClient",
|
||||
"T:LibreMetaverse.BinBVHAnimationReader": "crate::animation::BinBVHAnimationReader",
|
||||
"T:LibreMetaverse.binBVHJoint": "crate::animation::BinBVHJoint",
|
||||
"T:LibreMetaverse.binBVHJointKey": "crate::animation::BinBVHJointKey",
|
||||
"T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager",
|
||||
"T:LibreMetaverse.InventoryAISClient": "crate::inventory_ais::InventoryAISClient",
|
||||
"T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager",
|
||||
"T:LibreMetaverse.ObjectManager": "crate::object_material::ObjectManager",
|
||||
"T:LibreMetaverse.Rendering.MeshSkinData": "crate::animesh_skinning::MeshSkinData",
|
||||
"T:LibreMetaverse.Rendering.AttachmentRiggedSkin": "crate::avatar_rig::AttachmentRiggedSkin",
|
||||
"T:LibreMetaverse.Rendering.AvatarAttachmentPoint": "crate::avatar_rig::AvatarAttachmentPoint",
|
||||
"T:LibreMetaverse.Rendering.AvatarBoneMath": "crate::avatar_rig::AvatarBoneMath",
|
||||
"T:LibreMetaverse.Rendering.AvatarMeshDefinition": "crate::avatar_rig::AvatarMeshDefinition",
|
||||
"T:LibreMetaverse.Rendering.LindenAvatarDefinition": "crate::avatar_rig::LindenAvatarDefinition",
|
||||
"T:LibreMetaverse.Rendering.RiggedSkinMath": "crate::avatar_rig::RiggedSkinMath",
|
||||
"T:LibreMetaverse.RebakeAvatarTexturesEventArgs": "crate::appearance_manager::RebakeAvatarTexturesEventArgs",
|
||||
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
|
||||
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
|
||||
@@ -312,6 +330,162 @@ NATIVE_DECLARATIONS = {
|
||||
}
|
||||
|
||||
NATIVE_MEMBER_BODIES = {
|
||||
"M:LibreMetaverse.AvatarAnimationEventArgs.#ctor(LibreMetaverse.UUID,System.Collections.Generic.List{LibreMetaverse.Animation})":
|
||||
"crate::avatar_manager::AvatarAnimationEventArgs::native_new(avatar_id, anims)",
|
||||
"P:LibreMetaverse.AvatarAnimationEventArgs.Animations":
|
||||
"self.native_animations()",
|
||||
"P:LibreMetaverse.AvatarAnimationEventArgs.AvatarID":
|
||||
"self.native_avatar_id()",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarAnimation": "self.native_subscribe_avatar_animation(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarAppearance": "self.native_subscribe_avatar_appearance(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarClassifiedReply": "self.native_subscribe_avatar_classified_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarGroupsReply": "self.native_subscribe_avatar_groups_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarInterestsReply": "self.native_subscribe_avatar_interests_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarNotesReply": "self.native_subscribe_avatar_notes_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarPickerReply": "self.native_subscribe_avatar_picker_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarPicksReply": "self.native_subscribe_avatar_picks_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.AvatarPropertiesReply": "self.native_subscribe_avatar_properties_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.ClassifiedInfoReply": "self.native_subscribe_classified_info_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.DisplayNameUpdate": "self.native_subscribe_display_name_update(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.PickInfoReply": "self.native_subscribe_pick_info_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.UUIDNameReply": "self.native_subscribe_uuid_name_reply(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.ViewerEffect": "self.native_subscribe_viewer_effect(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.ViewerEffectLookAt": "self.native_subscribe_viewer_effect_look_at(handler)",
|
||||
"E:LibreMetaverse.AvatarManager.ViewerEffectPointAt": "self.native_subscribe_viewer_effect_point_at(handler)",
|
||||
"M:LibreMetaverse.Animesh.AnimationTrack.Advance(System.Single)":
|
||||
"self.native_advance(dt)",
|
||||
"M:LibreMetaverse.Animesh.AnimationTrack.EvaluatePose(System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Animesh.JointPose})":
|
||||
"self.native_evaluate_pose(pose)",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.AnimationID":
|
||||
"self.native_animation_id()",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.CurrentTime":
|
||||
"self.native_current_time()",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.CurrentTime#set":
|
||||
"self.native_set_current_time(value)",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.Data":
|
||||
"self.native_data()",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.Data#set":
|
||||
"self.native_set_data(value)",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.EaseWeight":
|
||||
"self.native_ease_weight()",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.IsFinished":
|
||||
"self.native_is_finished()",
|
||||
"P:LibreMetaverse.Animesh.AnimationTrack.IsFinished#set":
|
||||
"self.native_set_is_finished(value)",
|
||||
"M:LibreMetaverse.Animesh.AnimeshManager.GetPlayer(LibreMetaverse.UUID)":
|
||||
"Ok(self.native_get_player(object_id))",
|
||||
"M:LibreMetaverse.Animesh.AnimeshManager.RemovePlayer(LibreMetaverse.UUID)":
|
||||
"self.native_remove_player(object_id); Ok(())",
|
||||
"M:LibreMetaverse.Animesh.AnimeshManager.Update(System.Single)":
|
||||
"self.native_update(dt)",
|
||||
"P:LibreMetaverse.Animesh.AnimeshManager.AllPlayers":
|
||||
"Box::new(self.native_all_players().into_iter())",
|
||||
"M:LibreMetaverse.Animesh.AnimeshPlayer.EvaluatePose":
|
||||
"self.native_evaluate_pose()",
|
||||
"M:LibreMetaverse.Animesh.AnimeshPlayer.Update(System.Single)":
|
||||
"self.native_update(dt)",
|
||||
"P:LibreMetaverse.Animesh.AnimeshPlayer.ObjectID":
|
||||
"self.native_object_id()",
|
||||
"P:LibreMetaverse.Animesh.AnimeshPlayer.TrackCount":
|
||||
"self.native_track_count()",
|
||||
"M:LibreMetaverse.AnimeshSkinning.ComputeSkinningMatrices(System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Animesh.JointPose},LibreMetaverse.Rendering.LindenSkeleton,LibreMetaverse.Rendering.MeshSkinData)":
|
||||
"crate::animesh_skinning::AnimeshSkinning::native_compute_skinning_matrices(pose, skeleton, skin_data)",
|
||||
"M:LibreMetaverse.AnimeshSkinning.DeformVertices(LibreMetaverse.Rendering.Face,LibreMetaverse.Matrix4[],LibreMetaverse.Matrix4,System.Span{LibreMetaverse.Vector3},System.Span{LibreMetaverse.Vector3})":
|
||||
"crate::animesh_skinning::AnimeshSkinning::native_deform_vertices(face, skinning_matrices, bind_shape_matrix, out_positions, out_normals)",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.#ctor(System.Byte[])":
|
||||
"crate::animation::BinBVHAnimationReader::native_new(animationdata)",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.Equals(LibreMetaverse.BinBVHAnimationReader)":
|
||||
"self.native_equals(other.as_ref())",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.Equals(LibreMetaverse.binBVHJoint[],LibreMetaverse.binBVHJoint[])":
|
||||
"crate::animation::joints_equal(&arr1, &arr2)",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.Equals(System.Object)":
|
||||
"obj.as_ref().and_then(|value| value.downcast_ref::<crate::animation::BinBVHAnimationReader>()).is_some_and(|other| self.native_equals(Some(other)))",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.GetHashCode":
|
||||
"self.native_get_hash_code()",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.ReadBytesUntilNull(System.Byte[],System.Int32@)":
|
||||
"self.native_read_bytes_until_null(data, i)",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.readJoint(System.Byte[],System.Int32@)":
|
||||
"self.native_read_joint(data, i)",
|
||||
"M:LibreMetaverse.BinBVHAnimationReader.readKeys(System.Byte[],System.Int32@,System.Int32,System.Single,System.Single)":
|
||||
"self.native_read_keys(data, i, keycount, min, max)",
|
||||
"M:LibreMetaverse.binBVHJoint.Equals(LibreMetaverse.binBVHJoint)":
|
||||
"self.native_equals(&other)",
|
||||
"M:LibreMetaverse.binBVHJoint.Equals(LibreMetaverse.binBVHJointKey,LibreMetaverse.binBVHJointKey)":
|
||||
"crate::animation::key_equal(&arr1, &arr2)",
|
||||
"M:LibreMetaverse.binBVHJoint.Equals(LibreMetaverse.binBVHJointKey[],LibreMetaverse.binBVHJointKey[])":
|
||||
"crate::animation::keys_equal(&arr1, &arr2)",
|
||||
"M:LibreMetaverse.binBVHJoint.Equals(System.Object)":
|
||||
"obj.as_ref().and_then(|value| value.downcast_ref::<crate::animation::BinBVHJoint>()).is_some_and(|other| self.native_equals(other))",
|
||||
"M:LibreMetaverse.binBVHJoint.GetHashCode":
|
||||
"self.native_get_hash_code()",
|
||||
"M:LibreMetaverse.binBVHJoint.op_Equality(LibreMetaverse.binBVHJoint,LibreMetaverse.binBVHJoint)":
|
||||
"left.native_equals(&right)",
|
||||
"M:LibreMetaverse.binBVHJoint.op_Inequality(LibreMetaverse.binBVHJoint,LibreMetaverse.binBVHJoint)":
|
||||
"!left.native_equals(&right)",
|
||||
"M:LibreMetaverse.Rendering.MeshSkinData.#ctor":
|
||||
"crate::animesh_skinning::MeshSkinData::native_new()",
|
||||
"M:LibreMetaverse.Rendering.AttachmentRiggedSkin.#ctor":
|
||||
"crate::avatar_rig::AttachmentRiggedSkin::native_new()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Group":
|
||||
"self.native_group()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Id":
|
||||
"self.native_id()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Joint":
|
||||
"self.native_joint()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Location":
|
||||
"self.native_location()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Name":
|
||||
"self.native_name()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Position":
|
||||
"self.native_position()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.Rotation":
|
||||
"self.native_rotation()",
|
||||
"P:LibreMetaverse.Rendering.AvatarAttachmentPoint.VisibleInFirstPerson":
|
||||
"self.native_visible_in_first_person()",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.BuildBoneWorldMatrices(LibreMetaverse.Rendering.LindenSkeleton,System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform})":
|
||||
"crate::avatar_rig::AvatarBoneMath::native_build_bone_world_matrices(skeleton, bone_transforms)",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.ComputeAnimatedBoneWorldMatrices(LibreMetaverse.Rendering.LindenAvatarDefinition,System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform},System.Collections.Generic.IReadOnlyDictionary{System.String,System.Numerics.Quaternion})":
|
||||
"crate::avatar_rig::AvatarBoneMath::native_compute_animated_bone_world_matrices(&avatar_def, &bone_transforms, &rot_deltas)",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.ComputeAnimatedBoneWorldMatrices(LibreMetaverse.Rendering.LindenAvatarDefinition,System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform},System.Collections.Generic.IReadOnlyDictionary{System.String,System.Numerics.Quaternion},System.Collections.Generic.Dictionary{System.String,System.Numerics.Matrix4x4})":
|
||||
"let computed = crate::avatar_rig::AvatarBoneMath::native_compute_animated_bone_world_matrices(&avatar_def, &bone_transforms, &rot_deltas)?; let mut result = result; result.clear(); result.extend(computed); Ok(())",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.ComputeAttachmentBoneWorldMatrices(LibreMetaverse.Rendering.LindenAvatarDefinition,System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform},System.Collections.Generic.IReadOnlyDictionary{System.String,System.Numerics.Quaternion})":
|
||||
"crate::avatar_rig::AvatarBoneMath::native_compute_attachment_bone_world_matrices(&avatar_def, &bone_transforms, &rot_deltas)",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.ComputeAttachmentBoneWorldMatrices(LibreMetaverse.Rendering.LindenAvatarDefinition,System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform},System.Collections.Generic.IReadOnlyDictionary{System.String,System.Numerics.Quaternion},System.Collections.Generic.Dictionary{System.String,System.Numerics.Matrix4x4})":
|
||||
"let computed = crate::avatar_rig::AvatarBoneMath::native_compute_attachment_bone_world_matrices(&avatar_def, &bone_transforms, &rot_deltas)?; let mut result = result; result.clear(); result.extend(computed); Ok(())",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.ComputeGroundAdjustment(System.Collections.Generic.Dictionary{System.String,LibreMetaverse.Rendering.BoneTransform})":
|
||||
"Ok(crate::avatar_rig::AvatarBoneMath::native_compute_ground_adjustment(bone_transforms.as_ref()))",
|
||||
"M:LibreMetaverse.Rendering.AvatarBoneMath.StripScale(System.Numerics.Matrix4x4)":
|
||||
"Ok(crate::avatar_rig::AvatarBoneMath::native_strip_scale(m))",
|
||||
"P:LibreMetaverse.Rendering.AvatarMeshDefinition.FileName":
|
||||
"self.native_file_name()",
|
||||
"P:LibreMetaverse.Rendering.AvatarMeshDefinition.LodLevel":
|
||||
"self.native_lod_level()",
|
||||
"P:LibreMetaverse.Rendering.AvatarMeshDefinition.MinPixelWidth":
|
||||
"self.native_min_pixel_width()",
|
||||
"P:LibreMetaverse.Rendering.AvatarMeshDefinition.Type":
|
||||
"self.native_type()",
|
||||
"M:LibreMetaverse.Rendering.LindenAvatarDefinition.ComputeBoneTransforms(System.Collections.Generic.IReadOnlyDictionary{System.Int32,System.Single})":
|
||||
"self.native_compute_bone_transforms(¶m_values)",
|
||||
"M:LibreMetaverse.Rendering.LindenAvatarDefinition.GetAttachmentPoint(System.Int32)":
|
||||
"Ok(self.native_get_attachment_point_by_id(id))",
|
||||
"M:LibreMetaverse.Rendering.LindenAvatarDefinition.GetAttachmentPoint(System.String)":
|
||||
"Ok(self.native_get_attachment_point_by_name(&name))",
|
||||
"M:LibreMetaverse.Rendering.LindenAvatarDefinition.Load(System.String,System.String)":
|
||||
"crate::avatar_rig::LindenAvatarDefinition::native_load(lad_file_name, skeleton_file_name)",
|
||||
"P:LibreMetaverse.Rendering.LindenAvatarDefinition.AttachmentPoints":
|
||||
"self.native_attachment_points()",
|
||||
"P:LibreMetaverse.Rendering.LindenAvatarDefinition.MeshDefinitions":
|
||||
"self.native_mesh_definitions()",
|
||||
"P:LibreMetaverse.Rendering.LindenAvatarDefinition.Skeleton":
|
||||
"self.native_skeleton()",
|
||||
"M:LibreMetaverse.Rendering.RiggedSkinMath.BuildInvBindMatrices(LibreMetaverse.Rendering.MeshSkinData)":
|
||||
"crate::avatar_rig::RiggedSkinMath::native_build_inv_bind_matrices(&skin)",
|
||||
"M:LibreMetaverse.Rendering.RiggedSkinMath.ExtractJointPositionOverrides(LibreMetaverse.Rendering.MeshSkinData)":
|
||||
"crate::avatar_rig::RiggedSkinMath::native_extract_joint_position_overrides(&skin)",
|
||||
"M:LibreMetaverse.Rendering.RiggedSkinMath.FloatsToMatrix(System.Single[])":
|
||||
"Ok(crate::avatar_rig::RiggedSkinMath::native_floats_to_matrix(&f))",
|
||||
"M:LibreMetaverse.Rendering.RiggedSkinMath.NormalizeSkinWeights(System.Int32,System.Int32@,System.Single@,System.Int32@,System.Single@,System.Int32@,System.Single@,System.Int32@,System.Single@)":
|
||||
"crate::avatar_rig::RiggedSkinMath::native_normalize_skin_weights(joint_count, [j0, j1, j2, j3], [w0, w1, w2, w3]); Ok(())",
|
||||
"M:LibreMetaverse.NameValue.#ctor(System.String,LibreMetaverse.NameValue.ValueType,LibreMetaverse.NameValue.ClassType,LibreMetaverse.NameValue.SendtoType,System.Object)":
|
||||
"Ok(crate::NameValue { class_: class_type, name, sendto: sendto_type, type_: value_type, value: Some(value) })",
|
||||
"M:LibreMetaverse.Primitive.#ctor":
|
||||
@@ -592,6 +766,56 @@ NATIVE_MEMBER_BODIES = {
|
||||
"self.native_appearance().unwrap_or_else(|_| panic!(\"failed to construct AppearanceManager\"))",
|
||||
"P:LibreMetaverse.GridClient.Appearance#set":
|
||||
"self.native_set_appearance(value)",
|
||||
"P:LibreMetaverse.GridClient.Animesh":
|
||||
"self.native_animesh().unwrap_or_else(|_| panic!(\"failed to construct AnimeshManager\"))",
|
||||
"P:LibreMetaverse.GridClient.Animesh#set":
|
||||
"self.native_set_animesh(value)",
|
||||
"P:LibreMetaverse.GridClient.Avatars":
|
||||
"self.native_avatars().unwrap_or_else(|_| panic!(\"failed to construct AvatarManager\"))",
|
||||
"P:LibreMetaverse.GridClient.Avatars#set":
|
||||
"self.native_set_avatars(value)",
|
||||
"M:LibreMetaverse.AvatarManager.#ctor(LibreMetaverse.GridClient)":
|
||||
"crate::avatar_manager::AvatarManager::native_new(client)",
|
||||
"M:LibreMetaverse.AvatarManager.AgentProfileAvailable":
|
||||
"self.native_capability_available(\"AgentProfile\")",
|
||||
"M:LibreMetaverse.AvatarManager.DisplayNamesAvailable":
|
||||
"self.native_capability_available(\"GetDisplayNames\")",
|
||||
"M:LibreMetaverse.AvatarManager.Dispose":
|
||||
"self.native_dispose()",
|
||||
"M:LibreMetaverse.AvatarManager.RequestOwnAvatarTextures":
|
||||
"self.native_request_own_avatar_textures()",
|
||||
"M:LibreMetaverse.AvatarManager.RequestTrackAgent(LibreMetaverse.UUID)":
|
||||
"self.native_request_track_agent(prey_id)",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarName(LibreMetaverse.UUID)":
|
||||
"self.native_request_avatar_names(&[id])",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarNames(System.Collections.Generic.List{LibreMetaverse.UUID})":
|
||||
"self.native_request_avatar_names(&ids)",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarProperties(LibreMetaverse.UUID)":
|
||||
"self.native_request_avatar_properties(avatarid)",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarNameSearch(System.String,LibreMetaverse.UUID)":
|
||||
"self.native_request_avatar_name_search(name, query_id)",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarNotes(LibreMetaverse.UUID)":
|
||||
"self.native_request_generic(\"avatarnotesrequest\", &[avatarid])",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarPicks(LibreMetaverse.UUID)":
|
||||
"self.native_request_generic(\"avatarpicksrequest\", &[avatarid])",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAvatarClassified(LibreMetaverse.UUID)":
|
||||
"self.native_request_generic(\"avatarclassifiedsrequest\", &[avatarid])",
|
||||
"M:LibreMetaverse.AvatarManager.RequestPickInfo(LibreMetaverse.UUID,LibreMetaverse.UUID)":
|
||||
"self.native_request_generic(\"pickinforequest\", &[avatarid, pickid])",
|
||||
"M:LibreMetaverse.AvatarManager.RequestClassifiedInfo(LibreMetaverse.UUID)":
|
||||
"self.native_request_classified_info(classifiedid)",
|
||||
"M:LibreMetaverse.AvatarManager.GetDisplayNamesAsync(System.Collections.Generic.List{LibreMetaverse.UUID},System.Threading.CancellationToken)":
|
||||
"self.native_get_display_names(ids, cancellation_token).await",
|
||||
"M:LibreMetaverse.AvatarManager.RequestAgentProfileAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)":
|
||||
"self.native_request_agent_profile(avatarid, cancellation_token).await",
|
||||
"M:LibreMetaverse.Messages.Linden.AgentProfileMessage.#ctor":
|
||||
"Ok(crate::avatar_manager::agent_profile_defaults())",
|
||||
"M:LibreMetaverse.Messages.Linden.AgentProfileMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)":
|
||||
"crate::avatar_manager::deserialize_agent_profile(self, &map)",
|
||||
"M:LibreMetaverse.Messages.Linden.AgentProfileMessage.Serialize":
|
||||
"crate::avatar_manager::serialize_agent_profile(self)",
|
||||
"M:LibreMetaverse.Messages.Linden.AgentProfileMessage.GroupData.#ctor":
|
||||
"Ok(crate::avatar_manager::agent_profile_group_defaults())",
|
||||
"M:LibreMetaverse.Avatar.AvatarProperties.FromOSD(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::agent_messages::avatar_properties_from_osd(o)",
|
||||
"M:LibreMetaverse.Avatar.AvatarProperties.GetOSD":
|
||||
|
||||
@@ -985,12 +985,17 @@ def validate_generated_shims() -> None:
|
||||
"crate::agent_manager::",
|
||||
"crate::agent_messages::",
|
||||
"crate::agent_movement::",
|
||||
"crate::animation::",
|
||||
"crate::animesh_runtime::",
|
||||
"crate::animesh_skinning::",
|
||||
"crate::asset_cache::",
|
||||
"crate::asset_manager::",
|
||||
"crate::asset_material::",
|
||||
"crate::asset_models::",
|
||||
"crate::appearance_baker::",
|
||||
"crate::appearance_manager::",
|
||||
"crate::avatar_manager::",
|
||||
"crate::avatar_rig::",
|
||||
"crate::baking_texture_provider::",
|
||||
"crate::byte_order::",
|
||||
"crate::attention_catalog::",
|
||||
@@ -1021,6 +1026,7 @@ def validate_generated_shims() -> None:
|
||||
"crate::xml_codec::",
|
||||
"stored_visual_params",
|
||||
"self.native_",
|
||||
"left.native_equals(",
|
||||
)
|
||||
)
|
||||
if uses_standardized_or_native_path:
|
||||
|
||||
Reference in New Issue
Block a user