Complete first release candidate audit (#107)
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
284
crates/libremetaverse/src/avatar_physics.rs
Normal file
284
crates/libremetaverse/src/avatar_physics.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
//! Native spring-mass-damper simulation for avatar physics wearables.
|
||||
|
||||
use crate::Error;
|
||||
use libremetaverse_types::compat::Vector3;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
type BoneProvider = Arc<dyn Fn(String) -> Vector3 + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Channel {
|
||||
driver: i32,
|
||||
driven: &'static [i32],
|
||||
direction: [f32; 3],
|
||||
params: [i32; 7],
|
||||
position: f32,
|
||||
velocity: f32,
|
||||
joint_velocity: f32,
|
||||
joint_acceleration: f32,
|
||||
last_time: f32,
|
||||
world_position: [f32; 3],
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn new(driver: i32, driven: &'static [i32], direction: [f32; 3], params: [i32; 7]) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
driven,
|
||||
direction,
|
||||
params,
|
||||
position: 0.5,
|
||||
velocity: 0.0,
|
||||
joint_velocity: 0.0,
|
||||
joint_acceleration: 0.0,
|
||||
last_time: -1.0,
|
||||
world_position: [0.0; 3],
|
||||
}
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.position = 0.5;
|
||||
self.velocity = 0.0;
|
||||
self.joint_velocity = 0.0;
|
||||
self.joint_acceleration = 0.0;
|
||||
self.last_time = -1.0;
|
||||
self.world_position = [0.0; 3];
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
channels: Vec<Channel>,
|
||||
weights: HashMap<i32, f32>,
|
||||
provider: Option<BoneProvider>,
|
||||
}
|
||||
|
||||
/// Stateful simulator matching the viewer's six LLPhysicsMotion channels.
|
||||
pub struct AvatarPhysicsSimulator {
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
impl AvatarPhysicsSimulator {
|
||||
pub(crate) fn native_new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
state: Mutex::new(State {
|
||||
channels: vec![
|
||||
Channel::new(
|
||||
1100,
|
||||
&[1200],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10000, 10001, 10002, 10006, 10003, 10004, 10005],
|
||||
),
|
||||
Channel::new(
|
||||
1101,
|
||||
&[1201],
|
||||
[-1.0, 0.0, 0.0],
|
||||
[10000, 10001, 10002, 10010, 10007, 10008, 10009],
|
||||
),
|
||||
Channel::new(
|
||||
1105,
|
||||
&[1207],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10000, 10001, 10002, 10032, 10029, 10030, 10031],
|
||||
),
|
||||
Channel::new(
|
||||
1103,
|
||||
&[1205],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10018, 10019, 10020, 10024, 10021, 10022, 10023],
|
||||
),
|
||||
Channel::new(
|
||||
1104,
|
||||
&[1206],
|
||||
[0.0, -1.0, 0.0],
|
||||
[10018, 10019, 10020, 10028, 10025, 10026, 10027],
|
||||
),
|
||||
Channel::new(
|
||||
1102,
|
||||
&[1202, 1203, 1204],
|
||||
[0.0, 0.0, -1.0],
|
||||
[10011, 10012, 10013, 10017, 10014, 10015, 10016],
|
||||
),
|
||||
],
|
||||
weights: HashMap::new(),
|
||||
provider: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_wearable_params(
|
||||
&self,
|
||||
values: HashMap<i32, f32>,
|
||||
) -> Result<(), Error> {
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
state.weights = values;
|
||||
state.channels.iter_mut().for_each(Channel::reset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_set_bone_position_provider(
|
||||
&self,
|
||||
provider: Box<dyn Fn(String) -> Vector3 + Send + Sync>,
|
||||
) -> Result<(), Error> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.provider = Some(provider.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_tick(&self, time: f32) -> Result<HashMap<i32, f32>, Error> {
|
||||
if !time.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut state = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let weights = state.weights.clone();
|
||||
let provider = state.provider.clone();
|
||||
let mut output = HashMap::new();
|
||||
for channel in &mut state.channels {
|
||||
let position = step(channel, time, &weights, provider.as_ref());
|
||||
for id in channel.driven {
|
||||
output.insert(*id, position);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
fn param(weights: &HashMap<i32, f32>, id: i32, fallback: f32) -> f32 {
|
||||
weights
|
||||
.get(&id)
|
||||
.copied()
|
||||
.or_else(|| crate::visual_catalog::param_by_id(id).map(|p| p.default_value))
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
fn driver_position(channel: &Channel, weights: &HashMap<i32, f32>) -> f32 {
|
||||
let Some(definition) = crate::visual_catalog::param_by_id(channel.driver) else {
|
||||
return 0.5;
|
||||
};
|
||||
let range = definition.max_value - definition.min_value;
|
||||
if range.abs() < 1.0e-6 {
|
||||
return 0.0;
|
||||
}
|
||||
((weights
|
||||
.get(&channel.driver)
|
||||
.copied()
|
||||
.unwrap_or(definition.default_value)
|
||||
- definition.min_value)
|
||||
/ range)
|
||||
.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||
}
|
||||
fn subtract(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||
[left[0] - right[0], left[1] - right[1], left[2] - right[2]]
|
||||
}
|
||||
fn vector(value: Vector3) -> [f32; 3] {
|
||||
value.0
|
||||
}
|
||||
|
||||
fn step(
|
||||
channel: &mut Channel,
|
||||
time: f32,
|
||||
weights: &HashMap<i32, f32>,
|
||||
provider: Option<&BoneProvider>,
|
||||
) -> f32 {
|
||||
let joint_name = if matches!(channel.driver, 1100 | 1101 | 1105) {
|
||||
"mChest"
|
||||
} else {
|
||||
"mPelvis"
|
||||
};
|
||||
let joint = provider.map_or(channel.world_position, |get| vector(get(joint_name.into())));
|
||||
let target = driver_position(channel, weights);
|
||||
if channel.last_time < 0.0 {
|
||||
channel.last_time = time;
|
||||
channel.position = target;
|
||||
channel.world_position = joint;
|
||||
return target;
|
||||
}
|
||||
let dt = time - channel.last_time;
|
||||
if dt <= 0.0 || dt > 1.0 {
|
||||
channel.last_time = time;
|
||||
return channel.position.clamp(0.0, 1.0);
|
||||
}
|
||||
let [
|
||||
mass_id,
|
||||
gravity_id,
|
||||
drag_id,
|
||||
damping_id,
|
||||
max_id,
|
||||
spring_id,
|
||||
gain_id,
|
||||
] = channel.params;
|
||||
let mass = param(weights, mass_id, 0.1).max(1.0e-6);
|
||||
let gravity = param(weights, gravity_id, 0.0);
|
||||
let drag = param(weights, drag_id, 1.0);
|
||||
let damping = param(weights, damping_id, 0.2);
|
||||
let max_effect = param(weights, max_id, 0.0);
|
||||
let spring = param(weights, spring_id, 10.0);
|
||||
let gain = param(weights, gain_id, 10.0);
|
||||
let scaled_dt = dt * 30.0;
|
||||
let joint_velocity =
|
||||
dot(subtract(joint, channel.world_position), channel.direction) * 100.0 / scaled_dt;
|
||||
let acceleration = ((joint_velocity - channel.joint_velocity) / scaled_dt) / 3.0
|
||||
+ channel.joint_acceleration * (2.0 / 3.0);
|
||||
if max_effect == 0.0 && channel.position.clamp(0.0, 1.0) == target {
|
||||
channel.last_time = time;
|
||||
channel.world_position = joint;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
return target;
|
||||
}
|
||||
let steps = (dt / 0.05) as u32 + 1;
|
||||
let sub_dt = dt / steps as f32;
|
||||
let mut position = channel.position.clamp(0.0, 1.0);
|
||||
let mut velocity = channel.velocity;
|
||||
for _ in 0..steps {
|
||||
let force = gain * acceleration * mass
|
||||
+ dot([0.0, 0.0, 1.0], channel.direction) * gravity * mass
|
||||
- (position - target) * spring
|
||||
- damping * velocity
|
||||
+ 0.5 * drag * joint_velocity * joint_velocity * joint_velocity.signum();
|
||||
velocity = (velocity + force / mass * sub_dt).clamp(-100.0, 100.0);
|
||||
position += velocity * sub_dt;
|
||||
if (position < 0.0 && velocity < 0.0) || (position > 1.0 && velocity > 0.0) {
|
||||
velocity = 0.0;
|
||||
}
|
||||
}
|
||||
if !position.is_finite() || !velocity.is_finite() {
|
||||
position = 0.0;
|
||||
velocity = 0.0;
|
||||
channel.joint_velocity = 0.0;
|
||||
channel.joint_acceleration = 0.0;
|
||||
}
|
||||
channel.last_time = time;
|
||||
channel.position = position;
|
||||
channel.velocity = velocity;
|
||||
channel.joint_velocity = joint_velocity;
|
||||
channel.joint_acceleration = acceleration;
|
||||
channel.world_position = joint;
|
||||
position.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn exposes_all_driven_channels_and_resets_to_driver_weights() {
|
||||
let simulator = AvatarPhysicsSimulator::native_new().unwrap();
|
||||
simulator
|
||||
.native_set_wearable_params(HashMap::from([(1100, 0.25)]))
|
||||
.unwrap();
|
||||
let output = simulator.native_tick(1.0).unwrap();
|
||||
assert_eq!(output.len(), 8);
|
||||
assert!((0.0..=1.0).contains(&output[&1200]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user