Avatar System

Skeletal animation, inverse kinematics, blend shapes, lip-sync, LOD management, GPU skinning, and performance rating for Aether avatars.

Avatars are the most complex visual objects in Aether. They combine real-time inverse kinematics, GPU-accelerated skinning, facial blend shapes, lip-sync, and distance-based LOD to deliver expressive, performant characters in VR.

The aether-avatar crate provides the full avatar pipeline, from VR tracking input to final GPU dispatch.

Key Concepts

Aether avatars are built on several interconnected subsystems:

  • Skeletal animation -- A bone hierarchy drives mesh deformation through a pose evaluation pipeline.
  • Inverse kinematics (IK) -- Sparse VR tracking data (headset, controllers, optional body trackers) is expanded into a full-body skeleton pose.
  • Blend shapes -- Morph targets on the mesh enable facial expressions and viseme-driven lip-sync.
  • GPU skinning -- Compute shaders transform vertices on the GPU using bone matrix palettes.
  • LOD management -- Distance-based level-of-detail reduces rendering cost for distant avatars.
  • Performance rating -- Polygon, bone, and material budgets enforce quality tiers so user-created avatars do not destroy frame rates.

Architecture

The avatar pipeline flows from tracking input through IK solving, skeleton evaluation, and finally GPU dispatch:

TrackingFrame -> IK Solver -> SkeletonPose -> BoneMatrixPalette -> GPU Skinning
VisemeWeights -> BlendShapeWeights -> GPU Blend Shape Dispatch
Camera Distance -> LOD Tier Selection -> Shader Permutation
AvatarMeshStats -> Performance Rating -> Allow / Block

Each stage is a pure function or lightweight struct with no external I/O, making the pipeline testable and composable.

Skeleton and Bone Types

The skeleton is a flat array of bones with parent indices forming a tree:

use aether_avatar::{Bone, Skeleton, IkTarget};

let bone = Bone {
    position: [0.0, 1.0, 0.0],
    rotation: [0.0, 0.0, 0.0, 1.0], // identity quaternion
    length: 0.3,
    parent: None, // root bone
};

let skeleton = Skeleton {
    bones: vec![bone],
    bone_names: vec!["hips".to_string()],
};

IkTarget represents a desired end-effector position with an optional rotation constraint:

let hand_target = IkTarget {
    position: [0.5, 1.2, 0.3],
    rotation: Some([0.0, 0.0, 0.0, 1.0]),
};

FABRIK Inverse Kinematics

Aether uses the FABRIK (Forward And Backward Reaching Inverse Kinematics) algorithm for bone chain solving. FABRIK iteratively adjusts bone positions through forward and backward passes until the end effector converges on the target.

Forward pass: Starting from the end effector, move it to the target position, then walk backward up the chain. Each parent joint is placed at child_position + direction_to_parent * bone_length.

Backward pass: Fix the root bone at its original position, then walk forward down the chain. Each child is placed at parent_position + direction_to_child * bone_length.

use aether_avatar::{FabrikSolver, FabrikConfig, IkTarget};

let config = FabrikConfig {
    max_iterations: 10,
    tolerance: 0.001,
};

let solver = FabrikSolver::new(config);
let target = IkTarget {
    position: [1.0, 0.5, 0.0],
    rotation: None,
};

// Solve a bone chain toward the target
let result = solver.solve(&mut chain, &target);

3-Point and 6-Point IK

Aether supports two tracking configurations:

3-point IK (head + 2 hands): The spine direction is estimated from head position and a default hip offset. Shoulders are derived from head rotation and arm length. Each arm is solved as a 2-bone FABRIK chain.

6-point IK (head + 2 hands + hip + 2 feet): The hip position comes directly from a tracker. The spine is solved between hip and head. Each leg is solved as a 2-bone FABRIK chain (thigh + shin).

use aether_avatar::{solve_three_point, solve_six_point, IkResult};

// 3-point: headset + two controllers
let result: IkResult = solve_three_point(&skeleton, &head, &left_hand, &right_hand);

// 6-point: full body tracking
let result: IkResult = solve_six_point(
    &skeleton, &head, &left_hand, &right_hand, &hip, &left_foot, &right_foot,
);

Joint Constraints

Per-joint angular limits prevent unnatural poses:

use aether_avatar::{JointConstraint, ConstraintSet};

let elbow_constraint = JointConstraint {
    min_angle: [0.0, -0.1, -0.1],
    max_angle: [2.6, 0.1, 0.1], // radians
    twist_limit: 0.2,
};

let constraints = ConstraintSet {
    constraints: vec![(3, elbow_constraint)], // bone index 3
};

Constraints are applied after each FABRIK pass by clamping computed angles to the allowed range per axis. Twist is separated and clamped independently.

T-Pose Calibration

Before IK begins, the system calibrates skeleton proportions from a reference T-pose:

use aether_avatar::{calibrate_from_tpose, CalibrationData};

let calibration: CalibrationData = calibrate_from_tpose(&tracking_frame, &reference_skeleton);
// calibration.arm_span, calibration.height are computed
// Bone lengths are scaled proportionally from the reference skeleton

Blend Shapes and Lip-Sync

Blend shapes (morph targets) drive facial expressions. Each target defines per-vertex position and normal deltas:

use aether_avatar::{BlendShapeTarget, BlendShapeSet, BlendShapeWeights};

let smile = BlendShapeTarget {
    name: "smile".to_string(),
    vertex_deltas: vec![], // position + normal deltas per vertex
};

let face = BlendShapeSet {
    targets: vec![smile],
    max_active: 8,
};

let mut weights = BlendShapeWeights::new();
weights.set("smile", 0.75); // 0.0 to 1.0

The VisemeEvaluator converts audio-driven LipSyncFrame data into blend shape weights, smoothing transitions between consecutive visemes:

use aether_avatar::{VisemeEvaluator, VisemeWeights};

let evaluator = VisemeEvaluator::new(0.05); // 50ms interpolation time
let weights: VisemeWeights = evaluator.evaluate(&lip_sync_frame);

GPU Skinning

Aether supports both Linear Blend Skinning and Dual Quaternion Skinning via compute shaders:

use aether_avatar::{GpuSkinningConfig, SkinningMethod, BoneMatrixPalette, SkinningDispatch};

let config = GpuSkinningConfig {
    workgroup_size: 64,
    max_bones: 256,
    max_vertices: 65536,
    method: SkinningMethod::DualQuaternion,
};

// After skeleton evaluation, upload the bone matrix palette to the GPU
let palette = BoneMatrixPalette::from_pose(&skeleton_pose);
let dispatch = SkinningDispatch::new(&config, vertex_count, &palette);

Each SkinVertex carries up to 4 bone indices and corresponding weights for smooth deformation across joints.

LOD Management

Distance-based LOD tiers reduce rendering cost when many avatars are visible:

TierDistanceDescription
FullMesh< 5 mFull geometry, all blend shapes active
Simplified5 -- 30 mReduced polygon mesh, limited blend shapes
Billboard30 -- 100 mCamera-facing billboard with baked animation
Dot> 100 mColored dot indicator

Hysteresis prevents rapid tier switching when an avatar hovers near a distance threshold:

use aether_avatar::{AvatarLodConfig, AvatarLodTier, select_lod_tier};

let config = AvatarLodConfig {
    thresholds: [5.0, 30.0, 100.0],
    hysteresis: 2.0,        // 2m buffer zone
    transition_duration: 0.3, // 300ms blend
};

let tier: AvatarLodTier = select_lod_tier(distance, &previous_tier, &config);

Performance Rating

Every avatar is validated against polygon, bone, and material budgets before it can be used in a world:

TierMax PolygonsDescription
S10,000Excellent -- runs on all hardware
A25,000Good -- suitable for most worlds
B50,000Medium -- may cause issues in crowded scenes
C75,000Poor -- world owners may block
use aether_avatar::{validate_avatar, PerformanceBudgetTable, AvatarRatingBucket};

let budget = PerformanceBudgetTable::default();
let rating: AvatarRatingBucket = validate_avatar(&mesh_stats, &budget);

// World owners set a minimum rating
if rating < world_minimum_rating {
    // Avatar is blocked from this world
}

World owners configure a minimum performance rating. Avatars that exceed the budget for that tier are blocked from entering.

Avatar Formats

Aether supports VRM format and a custom binary format optimized for the platform. The import pipeline validates format compliance, extracts skeleton data, and runs the performance rating check before the avatar is made available.

Animation State Machine

The AnimationStateMachine evaluates blend trees and state transitions, producing weighted (state, weight) pairs for the animation mixer. Transitions use configurable BlendCurve types (linear, ease-in-out, cubic) to smoothly interpolate between animation states. The state machine maintains current state plus active blend transitions and advances via update(dt, input).