Mixamo animation support (#37)

This PR adds support for Mixamo FBX imports (via backend conversion to
glTF/glB) and some miscellaneous improvements to the animation systems.

### User-facing improvements
* Retargeting math has been fixed to eliminate jittering issues
* Retargeter can now handle animated bone translations when present
  (generally this is only used for root motion)

### Housekeeping / Code improvements

* Animation-related components and system logic have been moved to a
  dedicated `anim` module and `AnimPlugin`
* Retargeting stuff has been moved from the `bvh` module to
  `anim::retargeting`
* Component-based solution for marking an entity as an animation
  retargeting source makes it easy to extend the retargeting functionality
  to new types of source animations

### Limitations

Currently the code assumes that there will be at most one animation
source at a time. That single animation (if present) will be retargeted
to all other skeletons in the scene. A component-based solution for
directing a specific target skeleton to use a specific source animation
is possible, but will require some additional consideration around the
implementation design for the read-only "viewer" modes.

### Test animations:

* Hip-Hop Dancing (Mixamo): ?mode=viewer&mixamo=mocap/hip-hop-dancing.gltf
* Shuffle (MocapNET): ?mode=viewer&bvh=mocap/shuffle.bvh
* Scott (MocapNET): ?mode=viewer&bvh=mocap/scott.bvh
* Scott 1 (MocapNET): ?mode=viewer&bvh=mocap/scott1.bvh
This commit is contained in:
Danny McGee
2024-02-09 13:21:16 -05:00
committed by GitHub
parent d1b4aab8ca
commit a5aeff7a3a
13 changed files with 4109 additions and 299 deletions
+11
View File
@@ -21,6 +21,7 @@ import { Ref, createRef, ref } from "lit-html/directives/ref.js";
import "@storyteller/studio-web/canvas-provider";
import "@storyteller/studio-web/viewer";
import "@storyteller/studio-web/bvh-viewer";
import "@storyteller/studio-web/mixamo-viewer";
import "./button.element";
import "./help.element";
import "./icon.element";
@@ -46,6 +47,7 @@ export class AppElement extends LitElement {
@state() selection?: string | null;
@state() bvhPath?: string;
@state() mixamoPath?: string;
@state() mode?: StudioMode;
#sceneObjects: studio.SceneObject[] = [];
@@ -67,6 +69,9 @@ export class AppElement extends LitElement {
if (params.has("bvh"))
this.bvhPath = params.get("bvh")!;
if (params.has("mixamo"))
this.mixamoPath = params.get("mixamo")!;
this.mode = match (params.get("mode"), {
"studio": () => StudioMode.Editor,
"viewer": () => StudioMode.Viewer,
@@ -195,6 +200,12 @@ export class AppElement extends LitElement {
skyboxId="test_scene"
@scene-state=${this.onSceneStateChange}
></sts-bvh-viewer>
` : this.mixamoPath ? html`
<sts-mixamo-viewer
mixamoPath=${this.mixamoPath}
skyboxId="test_scene"
@scene-state=${this.onSceneStateChange}
></sts-mixamo-viewer>
` : html`
<sts-viewer
objectId="sample-room.gltf"
@@ -0,0 +1,34 @@
import * as studio from "@storyteller/studio";
import { ViewportSize } from "@storyteller/studio";
import { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
import { StudioBaseElement } from "./studio-base.element";
@customElement("sts-mixamo-viewer")
export class MixamoViewerElement extends StudioBaseElement {
@property() mixamoPath?: string;
@property() skyboxId?: string;
onStart(viewportSize: ViewportSize): void {
if (!this.mixamoPath) return;
if (!this.skyboxId) return;
studio.startMixamoViewer(
this.mixamoPath,
this.skyboxId,
`#${this._canvasProvider!.canvasId}`,
"Storyteller Studio",
viewportSize,
);
}
onChanges(changes: PropertyValues<this>): void {
if (changes.has("mixamoPath") && !!this.mixamoPath) {
// TODO
}
if (changes.has("skyboxId") && !!this.skyboxId)
studio.setSkybox(this.skyboxId);
}
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
use bevy::{
math::Affine3A,
prelude::*,
render::{mesh::skinning::SkinnedMesh, view::NoFrustumCulling},
utils::{EntityHashMap, EntityHashSet},
};
use space_editor::space_prefab::editor_registry::EditorRegistryExt;
use crate::hierarchy;
pub use retargeting::*;
mod retargeting;
pub struct AnimPlugin;
impl Plugin for AnimPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Skeleton>()
.editor_registry::<Skeleton>()
.register_type::<SkeletalJoint>()
.editor_registry::<SkeletalJoint>()
.register_type::<ProcessedSkinnedMesh>()
.editor_registry::<ProcessedSkinnedMesh>()
.register_type::<BindPoseLocal>()
.editor_registry::<BindPoseLocal>()
.register_type::<BindPoseGlobal>()
.editor_registry::<BindPoseGlobal>()
.register_type::<InvBindPose>()
.editor_registry::<InvBindPose>();
app.add_systems(Update, process_skinned_meshes);
app.add_plugins(retargeting::RetargetingPlugin);
}
}
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Skeleton;
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct SkeletalJoint(pub Color);
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct ProcessedSkinnedMesh;
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct BindPoseLocal(pub Transform);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct BindPoseGlobal(pub Affine3A);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct InvBindPose(pub Affine3A);
fn process_skinned_meshes(
mut cmd: Commands,
q_skinned_meshes: Query<(Entity, &SkinnedMesh), Without<ProcessedSkinnedMesh>>,
mut q_xforms: ParamSet<(TransformHelper, Query<&Transform>)>,
q_parents: Query<Option<&Parent>>,
) {
if q_skinned_meshes.is_empty() {
return;
}
// Map of skinned-mesh parent entities to the set of joints bound to those
// skinned meshes. We use the skinned-mesh parent because individual skinned
// meshes in glTF format are often only parts of the whole "model," in which
// case the parts will be siblings in the hierarchy.
//
// I.e., considering a hierarchy like this:
// ```
// └ RootNode
// ├ Body [SkinnedMesh]
// ├ Head [SkinnedMesh]
// ├ Eyes [SkinnedMesh]
// └ Hips [Joint]
// ├ UpperLeg_L [Joint]
// │ └ ...
// ├ UpperLeg_R [Joint]
// │ └ ...
// └ Spine [Joint]
// └ ...
// ```
// The hashmap should contain an entry like this:
// ```
// {
// RootNode: { Hips, UpperLeg_L, UpperLeg_R, Spine, ... }
// }
// ```
// Building this hashmap before processing each joint avoids redundancy in
// the common case where the same joints are bound to multiple skinned meshes
let mut joint_collections = EntityHashMap::<Entity, EntityHashSet<Entity>>::default();
for (ent, skinned_mesh) in q_skinned_meshes.iter() {
cmd.entity(ent)
.insert(NoFrustumCulling)
.insert(ProcessedSkinnedMesh);
let Some(parent) = q_parents.get(ent).ok().flatten() else {
continue;
};
let parent_ent = **parent;
let joints = match joint_collections.get_mut(&parent_ent) {
Some(set) => set,
None => {
let (_, set) =
joint_collections.insert_unique_unchecked(parent_ent, Default::default());
set
}
};
joints.extend(skinned_mesh.joints.iter());
}
for joints in joint_collections.values() {
// Find the common ancestor of this joint collection and tag it as the
// skeleton
let joints = joints.iter().copied().collect::<Vec<_>>();
if let Some(root) = hierarchy::nearest_common_ancestor(&joints, &q_parents) {
cmd.entity(root).insert(Skeleton);
}
// Tag each joint and find its bind poses
for &joint in joints.iter() {
let bind_pose_global = q_xforms.p0().compute_global_transform(joint).unwrap();
let bind_pose_local = *q_xforms.p1().get(joint).unwrap();
cmd.entity(joint).insert((
SkeletalJoint(Color::BLACK),
BindPoseLocal(bind_pose_local),
BindPoseGlobal(bind_pose_global.affine()),
InvBindPose(bind_pose_global.affine().inverse()),
));
}
}
}
+431
View File
@@ -0,0 +1,431 @@
use bevy::{prelude::*, utils::HashMap};
use space_editor::space_prefab::editor_registry::EditorRegistryExt;
use crate::math::NearlyEq;
use super::{BindPoseGlobal, BindPoseLocal, InvBindPose, SkeletalJoint, Skeleton};
pub(super) struct RetargetingPlugin;
impl Plugin for RetargetingPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SkeletonHeight>()
.editor_registry::<SkeletonHeight>()
.register_type::<RetargetMap>();
app.insert_resource(RetargetMap::default());
app.add_systems(Update, copy_pose);
}
}
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct RetargetSource;
#[derive(Component, Clone, Copy, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Component)]
pub struct SkeletonHeight(f32);
fn copy_pose(
mut cmd: Commands,
r_retarget_map: Res<RetargetMap>,
q_src_root: Query<Entity, With<RetargetSource>>,
q_tgt_roots: Query<Entity, (With<Skeleton>, Without<RetargetSource>)>,
q_parents: Query<&Parent>,
q_children: Query<&Children>,
q_joint_names: Query<&Name, With<SkeletalJoint>>,
q_inv_bind_poses: Query<&InvBindPose>,
q_world_bind_poses: Query<&BindPoseGlobal>,
q_local_bind_poses: Query<&BindPoseLocal>,
q_skel_height: Query<&SkeletonHeight>,
mut q_xforms: Query<&mut Transform>,
q_world_xforms: Query<&GlobalTransform>,
) {
let src_root = match q_src_root.get_single() {
Ok(src_root) => src_root,
Err(err) => {
error!("{err}");
return;
}
};
let src_height = match q_skel_height.get(src_root) {
Ok(skel_height) => **skel_height,
Err(_) => {
let Some(value) = compute_skeleton_height(src_root, &q_children, &q_world_bind_poses)
else {
return;
};
cmd.entity(src_root).insert(SkeletonHeight(value));
value
}
};
let src_joints = q_children
.iter_descendants(src_root)
.filter_map(|ent| match q_joint_names.get(ent) {
Ok(name) => Some((ent, name)),
Err(_) => None,
})
.collect::<Vec<_>>();
// FIXME:
// This is very much not fast, and probably shouldn't be done every frame.
// Tentative plan to optimize this without adding too much additional
// complexity:
//
// 1) Add some sort of "AnimationRecorder" component to the target root
// 2) Beginning with `src_anim.seek_time == 0.0`, record each local
// rotation and translation performed on the target bones as a new
// keyframe on an `AnimationClip` held by `AnimationRecorder`
// 3) When `src_anim.seek_time == src_anim.duration`, finalize the new
// `AnimationClip` and add it to the asset server
// 4) Stop running this system and instead start playing the new animation
// with an `AnimationPlayer` component on the target root
for tgt_root in q_tgt_roots.iter() {
let tgt_height = match q_skel_height.get(tgt_root) {
Ok(skel_height) => **skel_height,
Err(_) => {
let Some(value) =
compute_skeleton_height(tgt_root, &q_children, &q_world_bind_poses)
else {
continue;
};
cmd.entity(tgt_root).insert(SkeletonHeight(value));
value
}
};
let xlation_scale = tgt_height / src_height;
for tgt_joint in q_children.iter_descendants(tgt_root) {
let Ok(tgt_name) = q_joint_names.get(tgt_joint) else {
continue;
};
// Look up the source bone in the retarget map
// TODO: Snatching O(n) performance from the jaws of an O(1) data
// structure here by using a many-source to single-target
// hashmap, but then looking up the source from the target
// instead of the other way around
let Some(src_joint) = src_joints.iter().copied().find_map(|(joint, src_name)| {
match r_retarget_map.src_to_tgt.get(src_name.as_str()) {
Some(name) if name == tgt_name.as_str() => Some(joint),
_ => None,
}
}) else {
continue;
};
// Get the query data needed to compute the target bone's local rotation
let Ok(src_inv_bind_pose) = q_inv_bind_poses.get(src_joint) else {
continue;
};
let Ok(src_world_xform) = q_world_xforms.get(src_joint) else {
continue;
};
let Ok(tgt_world_bind_pose) = q_world_bind_poses.get(tgt_joint) else {
continue;
};
// The math here was adapted from this writeup:
// https://wickedengine.net/2022/09/23/animation-retargeting/
//
// ```pseudocode
// // Apply target-local rotation
// tgt_working_xform = src_world_xform * src_inv_bind_pose
// tgt_working_xform = tgt_inv_parent_xform * tgt_working_xform * tgt_world_bind_pose
// tgt_xform.rotation = tgt_working_xform.rotation
//
// // Check if the translation component is animated
// if src_local_xform.translation != src_local_bind_pose.translation {
// // Apply target-local translation
// src_world_xlation = src_inv_bind_pose * src_local_xform.translation
// tgt_local_xlation = tgt_inv_parent_xform * src_world_xlation
// tgt_xform.translation = tgt_local_xlation
// }
// ```
let mut tgt_working_xform = src_world_xform.affine() * (**src_inv_bind_pose);
// The parent bone's world transform must be computed each iteration,
// since it will be affected by the rotations applied on the previous
// iteration.
let tgt_parent = q_parents.get(tgt_joint);
let parent_xform = tgt_parent
.and_then(|parent| q_xforms.get(**parent))
.copied()
.unwrap_or_default();
let mut parent_world_xform = GlobalTransform::from(parent_xform);
if let Ok(parent) = tgt_parent {
for ancestor in q_parents.iter_ancestors(**parent) {
let xform = q_xforms.get(ancestor).copied().unwrap_or_default();
parent_world_xform = xform * parent_world_xform;
}
}
let tgt_inv_parent_xform = parent_world_xform.affine().inverse();
tgt_working_xform = tgt_inv_parent_xform * tgt_working_xform * (**tgt_world_bind_pose);
// Apply the local rotation
if let Ok(mut tgt_xform) = q_xforms.get_mut(tgt_joint) {
tgt_xform.rotation = Quat::from_affine3(&tgt_working_xform);
}
// Get the query data needed to compute the target bone's local translation
let Ok(src_local_bind_pose) = q_local_bind_poses.get(src_joint) else {
continue;
};
let Ok(src_world_bind_pose) = q_world_bind_poses.get(src_joint) else {
continue;
};
let Ok(src_local_xlation) = q_xforms.get(src_joint).map(|xform| xform.translation)
else {
continue;
};
// If the source bone translation != its bind pose translation,
// this translation component is animated (likely root motion)
if !src_local_xlation.nearly_eq_within(src_local_bind_pose.translation, 1e-3) {
let src_world_xlation = src_world_bind_pose.transform_vector3(src_local_xlation);
let tgt_local_xlation = tgt_inv_parent_xform.transform_vector3(src_world_xlation);
// Apply the local translation
if let Ok(mut tgt_xform) = q_xforms.get_mut(tgt_joint) {
tgt_xform.translation = tgt_local_xlation * xlation_scale;
}
}
}
}
}
fn compute_skeleton_height(
root: Entity,
q_children: &Query<&Children>,
q_world_bind_poses: &Query<&BindPoseGlobal>,
) -> Option<f32> {
let joint_positions = q_children
.iter_descendants(root)
.filter_map(|joint| {
q_world_bind_poses
.get(joint)
.map(|bind_pose| bind_pose.translation)
.ok()
})
.collect::<Vec<_>>();
if joint_positions.is_empty() {
return None;
}
let y_max = joint_positions
.iter()
.fold(f32::MIN, |accum, current| f32::max(accum, current.y));
let y_min = joint_positions
.iter()
.fold(f32::MAX, |accum, current| f32::min(accum, current.y));
Some(y_max - y_min)
}
#[derive(Resource, Clone, Debug, Reflect)]
#[reflect(Resource)]
pub struct RetargetMap {
pub src_to_tgt: HashMap<String, String>,
}
impl RetargetMap {
const MOCAP_NET: [(&str, &str); 80] = [
// Spine
("LowerBack", "DEF-spine"),
("hip", "DEF-spine"),
("abdomen", "DEF-spine.002"),
("chest", "DEF-spine.003"),
("Neck", "DEF-spine.004"),
("neck", "DEF-spine.004"),
("Neck1", "DEF-spine.005"),
("neck1", "DEF-spine.005"),
("Head", "DEF-spine.006"),
("head", "DEF-spine.006"),
// Right arm
("rcollar", "DEF-shoulder.R"),
("rCollar", "DEF-shoulder.R"),
("rshoulder", "DEF-upper_arm.R"),
("rShldr", "DEF-upper_arm.R"),
("relbow", "DEF-forearm.R"),
("rForeArm", "DEF-forearm.R"),
("rhand", "DEF-hand.R"),
("rHand", "DEF-hand.R"),
// Right fingers
// -- Index
("metacarpal1.r", "DEF-palm.01.R"),
("finger2-1.r", "DEF-f_index.01.R"),
("finger2-2.r", "DEF-f_index.02.R"),
("finger2-3.r", "DEF-f_index.03.R"),
// -- Middle
("metacarpal2.r", "DEF-palm.02.R"),
("finger3-1.r", "DEF-f_middle.01.R"),
("finger3-2.r", "DEF-f_middle.02.R"),
("finger3-3.r", "DEF-f_middle.03.R"),
// -- Ring
("metacarpal3.r", "DEF-palm.03.R"),
("finger4-1.r", "DEF-f_ring.01.R"),
("finger4-2.r", "DEF-f_ring.02.R"),
("finger4-3.r", "DEF-f_ring.03.R"),
// -- Pinky
("metacarpal4.r", "DEF-palm.04.R"),
("finger5-1.r", "DEF-f_pinky.01.R"),
("finger5-2.r", "DEF-f_pinky.02.R"),
("finger5-3.r", "DEF-f_pinky.03.R"),
// -- Thumb
("rthumb", "DEF-thumb.01.R"),
("finger1-2.r", "DEF-thumb.02.R"),
("finger1-3.r", "DEF-thumb.03.R"),
// Right leg
("rhip", "DEF-thigh.R"),
("rThigh", "DEF-thigh.R"),
("rknee", "DEF-shin.R"),
("rShin", "DEF-shin.R"),
("rfoot", "DEF-foot.R"),
("rFoot", "DEF-foot.R"),
("RightToeBase", "DEF-toe.R"),
// Left arm
("lcollar", "DEF-shoulder.L"),
("lCollar", "DEF-shoulder.L"),
("lshoulder", "DEF-upper_arm.L"),
("lShldr", "DEF-upper_arm.L"),
("lelbow", "DEF-forearm.L"),
("lForeArm", "DEF-forearm.L"),
("lhand", "DEF-hand.L"),
("lHand", "DEF-hand.L"),
// Left fingers
// -- Index
("metacarpal1.l", "DEF-palm.01.L"),
("finger2-1.l", "DEF-f_index.01.L"),
("finger2-2.l", "DEF-f_index.02.L"),
("finger2-3.l", "DEF-f_index.03.L"),
// -- Middle
("metacarpal2.l", "DEF-palm.02.L"),
("finger3-1.l", "DEF-f_middle.01.L"),
("finger3-2.l", "DEF-f_middle.02.L"),
("finger3-3.l", "DEF-f_middle.03.L"),
// -- Ring
("metacarpal3.l", "DEF-palm.03.L"),
("finger4-1.l", "DEF-f_ring.01.L"),
("finger4-2.l", "DEF-f_ring.02.L"),
("finger4-3.l", "DEF-f_ring.03.L"),
// -- Pinky
("metacarpal4.l", "DEF-palm.04.L"),
("finger5-1.l", "DEF-f_pinky.01.L"),
("finger5-2.l", "DEF-f_pinky.02.L"),
("finger5-3.l", "DEF-f_pinky.03.L"),
// -- Thumb
("lthumb", "DEF-thumb.01.L"),
("finger1-2.l", "DEF-thumb.02.L"),
("finger1-3.l", "DEF-thumb.03.L"),
// Left leg
("lhip", "DEF-thigh.L"),
("lThigh", "DEF-thigh.L"),
("lknee", "DEF-shin.L"),
("lShin", "DEF-shin.L"),
("lfoot", "DEF-foot.L"),
("lFoot", "DEF-foot.L"),
("LeftToeBase", "DEF-toe.L"),
// Pelvis
("RHipJoint", "DEF-pelvis.R"),
("LHipJoint", "DEF-pelvis.L"),
];
const MIXAMO: [(&str, &str); 60] = [
// Spine
("mixamorig:Hips", "DEF-spine"),
("mixamorig:Spine", "DEF-spine.001"),
("mixamorig:Spine1", "DEF-spine.002"),
("mixamorig:Spine2", "DEF-spine.003"),
("mixamorig:Neck", "DEF-spine.004"),
("mixamorig:Head", "DEF-spine.006"),
// Right arm
("mixamorig:RightShoulder", "DEF-shoulder.R"),
("mixamorig:RightArm", "DEF-upper_arm.R"),
("mixamorig:RightForeArm", "DEF-forearm.R"),
("mixamorig:RightHand", "DEF-hand.R"),
// Right fingers
// -- index
("mixamorig:RightHandIndex1", "DEF-palm.01.R"),
("mixamorig:RightHandIndex2", "DEF-f_index.01.R"),
("mixamorig:RightHandIndex3", "DEF-f_index.02.R"),
("mixamorig:RightHandIndex4", "DEF-f_index.03.R"),
// -- middle
("mixamorig:RightHandMiddle1", "DEF-palm.02.R"),
("mixamorig:RightHandMiddle2", "DEF-f_middle.01.R"),
("mixamorig:RightHandMiddle3", "DEF-f_middle.02.R"),
("mixamorig:RightHandMiddle4", "DEF-f_middle.03.R"),
// -- ring
("mixamorig:RightHandRing1", "DEF-palm.03.R"),
("mixamorig:RightHandRing2", "DEF-f_ring.01.R"),
("mixamorig:RightHandRing3", "DEF-f_ring.02.R"),
("mixamorig:RightHandRing4", "DEF-f_ring.03.R"),
// -- pinky
("mixamorig:RightHandPinky1", "DEF-palm.04.R"),
("mixamorig:RightHandPinky2", "DEF-f_pinky.01.R"),
("mixamorig:RightHandPinky3", "DEF-f_pinky.02.R"),
("mixamorig:RightHandPinky4", "DEF-f_pinky.03.R"),
// -- thumb
("mixamorig:RightHandThumb1", "DEF-thumb.01.R"),
("mixamorig:RightHandThumb2", "DEF-thumb.02.R"),
("mixamorig:RightHandThumb3", "DEF-thumb.03.R"),
// Right Leg
("mixamorig:RightUpLeg", "DEF-thigh.R"),
("mixamorig:RightLeg", "DEF-shin.R"),
("mixamorig:RightFoot", "DEF-foot.R"),
("mixamorig:RightToeBase", "DEF-toe.R"),
// Left arm
("mixamorig:LeftShoulder", "DEF-shoulder.L"),
("mixamorig:LeftArm", "DEF-upper_arm.L"),
("mixamorig:LeftForeArm", "DEF-forearm.L"),
("mixamorig:LeftHand", "DEF-hand.L"),
// Left fingers
// -- index
("mixamorig:LeftHandIndex1", "DEF-palm.01.L"),
("mixamorig:LeftHandIndex2", "DEF-f_index.01.L"),
("mixamorig:LeftHandIndex3", "DEF-f_index.02.L"),
("mixamorig:LeftHandIndex4", "DEF-f_index.03.L"),
// -- middle
("mixamorig:LeftHandMiddle1", "DEF-palm.02.L"),
("mixamorig:LeftHandMiddle2", "DEF-f_middle.01.L"),
("mixamorig:LeftHandMiddle3", "DEF-f_middle.02.L"),
("mixamorig:LeftHandMiddle4", "DEF-f_middle.03.L"),
// -- ring
("mixamorig:LeftHandRing1", "DEF-palm.03.L"),
("mixamorig:LeftHandRing2", "DEF-f_ring.01.L"),
("mixamorig:LeftHandRing3", "DEF-f_ring.02.L"),
("mixamorig:LeftHandRing4", "DEF-f_ring.03.L"),
// -- pinky
("mixamorig:LeftHandPinky1", "DEF-palm.04.L"),
("mixamorig:LeftHandPinky2", "DEF-f_pinky.01.L"),
("mixamorig:LeftHandPinky3", "DEF-f_pinky.02.L"),
("mixamorig:LeftHandPinky4", "DEF-f_pinky.03.L"),
// -- thumb
("mixamorig:LeftHandThumb1", "DEF-thumb.01.L"),
("mixamorig:LeftHandThumb2", "DEF-thumb.02.L"),
("mixamorig:LeftHandThumb3", "DEF-thumb.03.L"),
// Left Leg
("mixamorig:LeftUpLeg", "DEF-thigh.L"),
("mixamorig:LeftLeg", "DEF-shin.L"),
("mixamorig:LeftFoot", "DEF-foot.L"),
("mixamorig:LeftToeBase", "DEF-toe.L"),
];
}
impl Default for RetargetMap {
fn default() -> Self {
Self {
src_to_tgt: Self::MOCAP_NET
.iter()
.chain(Self::MIXAMO.iter())
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
}
+40 -218
View File
@@ -5,7 +5,6 @@ use bevy::{
},
prelude::*,
transform::TransformSystem,
utils::HashMap,
};
use studio_bvh::{frames::Frame, ChannelType};
@@ -13,8 +12,9 @@ mod error;
mod loader;
use crate::{
math::{Lerp, MatrixExtras},
scene::{BindPoseGlobal, InvBindPose, SceneState, SkeletalJoint},
anim::{BindPoseGlobal, BindPoseLocal, InvBindPose, RetargetSource, SkeletalJoint},
math::Lerp,
scene::SceneState,
};
use self::loader::BvhLoader;
@@ -29,21 +29,39 @@ impl Plugin for BvhPlugin {
// module's `SceneState`) to manage the order/frequency at
// which these systems run instead of just running them all
// every frame
.add_systems(Update, spawn_test_bvh.run_if(resource_exists::<TestBvh>()))
.add_systems(
PostUpdate,
(
compute_inverse_bindposes,
apply_deferred,
animate_bvh,
retarget_bvh,
)
(compute_inverse_bindposes, apply_deferred, animate_bvh)
.chain()
.run_if(in_state(SceneState::Active))
.after(TransformSystem::TransformPropagate),
)
.register_type::<BvhJoint>()
.register_type::<BvhAnim>()
.insert_resource(RetargetMap::default());
.register_type::<BvhAnim>();
}
}
// TODO: Rename & reorganize
#[derive(Resource, Deref, DerefMut)]
pub struct TestBvh(pub Handle<Bvh>);
// TODO: Rename & reorganize
fn spawn_test_bvh(
mut cmd: Commands,
mut er_bvh_events: EventReader<AssetEvent<Bvh>>,
r_bvh: Res<TestBvh>,
) {
for event in er_bvh_events.read() {
if matches!(event, AssetEvent::LoadedWithDependencies { .. }) {
let ent = cmd.spawn_bvh_with_transform(
&r_bvh.0,
Transform::from_scale(Vec3::splat(0.06))
.with_translation(Vec3::new(0., 1.0, 0.524)),
);
cmd.entity(ent).insert(RetargetSource);
}
}
}
@@ -68,13 +86,20 @@ pub struct BvhAnim {
timer: Timer,
}
// TODO: Rename
fn compute_inverse_bindposes(
mut cmd: Commands,
q_bvh_joints: Query<(Entity, &GlobalTransform), (With<BvhJoint>, Without<InvBindPose>)>,
q_bvh_joints: Query<
(Entity, &Transform, &GlobalTransform),
(With<BvhJoint>, Without<InvBindPose>),
>,
) {
for (ent, world_xform) in q_bvh_joints.iter() {
let inv_bind_pose = world_xform.affine().inverse();
cmd.entity(ent).insert(InvBindPose(inv_bind_pose));
for (joint, xform, world_xform) in q_bvh_joints.iter() {
cmd.entity(joint).insert((
BindPoseLocal(*xform),
BindPoseGlobal(world_xform.affine()),
InvBindPose(world_xform.affine().inverse()),
));
}
}
@@ -179,76 +204,6 @@ fn lerp_frames<T: ReadOnlyWorldQuery>(
}
}
fn retarget_bvh(
r_retarget_map: Res<RetargetMap>,
mut l_target_ents: Local<HashMap<String, Entity>>,
q_source_bones: Query<(&Name, &InvBindPose, &GlobalTransform), With<BvhJoint>>,
q_target_ents: Query<(Entity, &Name), (With<SkeletalJoint>, Without<BvhJoint>)>,
mut q_target_xforms: Query<
(&Parent, &BindPoseGlobal, &mut Transform),
(With<SkeletalJoint>, Without<BvhJoint>),
>,
q_world_xforms: Query<&GlobalTransform>,
) {
for (source_name, source_inv_bind_pose, source_world_xform) in q_source_bones.iter() {
// Lookup the target name in the retarget map
let Some(target_name) = r_retarget_map.source_to_target.get(source_name.as_str()) else {
continue;
};
// Lookup the target entity in the local target bones hashmap,
// or insert it if this is the system's first run
let target_ent = match l_target_ents.get(target_name) {
Some(ent) => Some(*ent),
None => {
if let Some(ent) = q_target_ents.iter().find_map(|(ent, name)| {
if name.as_str() == target_name {
Some(ent)
} else {
None
}
}) {
l_target_ents.insert(target_name.clone(), ent);
Some(ent)
} else {
None
}
}
};
// Lookup the transform data for the matching entity
let Some((target_parent_ent, target_bind_pose_global, mut target_xform)) =
target_ent.and_then(|ent| q_target_xforms.get_mut(ent).ok())
else {
continue;
};
// Copy rotations from source -> target
//
// This code is adapted from the writeup here:
// https://wickedengine.net/2022/09/23/animation-retargeting/
// with the main difference being that the order of operations is
// exactly reversed (DirectX uses row-major matrices, while Bevy uses
// column-major ordering)
// [psuedo] local_mat = source_inv_bind_mat * source_world_mat;
let mut local_mat = source_world_xform.affine() * (**source_inv_bind_pose);
local_mat.matrix3 = local_mat.matrix3.orthonormalize();
// [psuedo] local_mat = target_world_mat * local_mat * target_inv_parent_mat;
let parent_world_xform = match q_world_xforms.get(**target_parent_ent) {
Ok(world_xform) => world_xform,
Err(_) => &GlobalTransform::IDENTITY,
};
let target_inv_parent_mat = parent_world_xform.affine().inverse();
local_mat = target_inv_parent_mat * local_mat * (**target_bind_pose_global);
local_mat.matrix3 = local_mat.matrix3.orthonormalize();
// [psuedo] target_xform.rotation = local_mat.rotation;
target_xform.rotation = Quat::from_affine3(&local_mat);
}
}
//==============================================================================
// Bvh Anim Component
//==============================================================================
@@ -352,136 +307,3 @@ fn spawn_bvh_recursive(world: &mut World, entity: &Entity, joint: studio_bvh::Jo
world.entity_mut(*entity).push_children(&[child_entity]);
}
}
//==============================================================================
// RetargetMap
//==============================================================================
#[derive(Resource, Debug)]
pub struct RetargetMap {
pub source_to_target: HashMap<String, String>,
}
#[rustfmt::skip]
static SOURCE_TO_TARGET: [(&str, &str); 80] = [
// Spine
("LowerBack", "DEF-spine"),
("hip", "DEF-spine"),
("abdomen", "DEF-spine.002"),
("chest", "DEF-spine.003"),
("Neck", "DEF-spine.004"),
("neck", "DEF-spine.004"),
("Neck1", "DEF-spine.005"),
("neck1", "DEF-spine.005"),
("Head", "DEF-spine.006"),
("head", "DEF-spine.006"),
// Right arm
("rcollar", "DEF-shoulder.R"),
("rCollar", "DEF-shoulder.R"),
("rshoulder", "DEF-upper_arm.R"),
("rShldr", "DEF-upper_arm.R"),
("relbow", "DEF-forearm.R"),
("rForeArm", "DEF-forearm.R"),
("rhand", "DEF-hand.R"),
("rHand", "DEF-hand.R"),
// Right fingers
// -- Index
("metacarpal1.r", "DEF-palm.01.R"),
("finger2-1.r", "DEF-f_index.01.R"),
("finger2-2.r", "DEF-f_index.02.R"),
("finger2-3.r", "DEF-f_index.03.R"),
// -- Middle
("metacarpal2.r", "DEF-palm.02.R"),
("finger3-1.r", "DEF-f_middle.01.R"),
("finger3-2.r", "DEF-f_middle.02.R"),
("finger3-3.r", "DEF-f_middle.03.R"),
// -- Ring
("metacarpal3.r", "DEF-palm.03.R"),
("finger4-1.r", "DEF-f_ring.01.R"),
("finger4-2.r", "DEF-f_ring.02.R"),
("finger4-3.r", "DEF-f_ring.03.R"),
// -- Pinky
("metacarpal4.r", "DEF-palm.04.R"),
("finger5-1.r", "DEF-f_pinky.01.R"),
("finger5-2.r", "DEF-f_pinky.02.R"),
("finger5-3.r", "DEF-f_pinky.03.R"),
// -- Thumb
("rthumb", "DEF-thumb.01.R"),
("finger1-2.r", "DEF-thumb.02.R"),
("finger1-3.r", "DEF-thumb.03.R"),
// Right leg
("rhip", "DEF-thigh.R"),
("rThigh", "DEF-thigh.R"),
("rknee", "DEF-shin.R"),
("rShin", "DEF-shin.R"),
("rfoot", "DEF-foot.R"),
("rFoot", "DEF-foot.R"),
("RightToeBase", "DEF-toe.R"),
// Left arm
("lcollar", "DEF-shoulder.L"),
("lCollar", "DEF-shoulder.L"),
("lshoulder", "DEF-upper_arm.L"),
("lShldr", "DEF-upper_arm.L"),
("lelbow", "DEF-forearm.L"),
("lForeArm", "DEF-forearm.L"),
("lhand", "DEF-hand.L"),
("lHand", "DEF-hand.L"),
// Left fingers
// -- Index
("metacarpal1.l", "DEF-palm.01.L"),
("finger2-1.l", "DEF-f_index.01.L"),
("finger2-2.l", "DEF-f_index.02.L"),
("finger2-3.l", "DEF-f_index.03.L"),
// -- Middle
("metacarpal2.l", "DEF-palm.02.L"),
("finger3-1.l", "DEF-f_middle.01.L"),
("finger3-2.l", "DEF-f_middle.02.L"),
("finger3-3.l", "DEF-f_middle.03.L"),
// -- Ring
("metacarpal3.l", "DEF-palm.03.L"),
("finger4-1.l", "DEF-f_ring.01.L"),
("finger4-2.l", "DEF-f_ring.02.L"),
("finger4-3.l", "DEF-f_ring.03.L"),
// -- Pinky
("metacarpal4.l", "DEF-palm.04.L"),
("finger5-1.l", "DEF-f_pinky.01.L"),
("finger5-2.l", "DEF-f_pinky.02.L"),
("finger5-3.l", "DEF-f_pinky.03.L"),
// -- Thumb
("lthumb", "DEF-thumb.01.L"),
("finger1-2.l", "DEF-thumb.02.L"),
("finger1-3.l", "DEF-thumb.03.L"),
// Left leg
("lhip", "DEF-thigh.L"),
("lThigh", "DEF-thigh.L"),
("lknee", "DEF-shin.L"),
("lShin", "DEF-shin.L"),
("lfoot", "DEF-foot.L"),
("lFoot", "DEF-foot.L"),
("LeftToeBase", "DEF-toe.L"),
// Pelvis
("RHipJoint", "DEF-pelvis.R"),
("LHipJoint", "DEF-pelvis.L"),
];
impl Default for RetargetMap {
fn default() -> Self {
Self {
source_to_target: SOURCE_TO_TARGET
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
}
+2 -3
View File
@@ -13,11 +13,10 @@ use space_editor::{
};
use crate::{
anim::SkeletalJoint,
camera_controller::{CameraController, CameraMotionSystem},
input::{Action, InputAction, Lifecycle},
is_editor,
scene::SkeletalJoint,
MainCamera,
is_editor, MainCamera,
};
#[derive(Resource, Deref, DerefMut)]
+98
View File
@@ -0,0 +1,98 @@
use bevy::{
prelude::*,
utils::{EntityHashMap, EntityHashSet},
};
/// Find the parent entity of `target`. Returns `None` if `target` has no
/// parents.
///
/// - Time: Constant
/// - Space: Constant
pub(crate) fn find_parent(target: Entity, q_parents: &Query<Option<&Parent>>) -> Option<Entity> {
q_parents
.get(target)
.ok()
.and_then(|parent| parent.map(|p| **p))
}
/// Find the nearest common ancestor entity of a set of `entities`. Returns
/// `None` if the provided `entities` do not have a common ancestor.
///
/// - Time: O(DN)
/// - Space: O(N)
///
/// where
/// - D = depth of the most deeply-nested member of `entites`
/// - N = the number of `entities` provided
pub(crate) fn nearest_common_ancestor(
entities: &[Entity],
q_parents: &Query<Option<&Parent>>,
) -> Option<Entity> {
// Find the depth of each entity in the tree
let mut depths: EntityHashMap<Entity, usize> = EntityHashMap::default();
for &ent in entities {
depths.insert(ent, find_depth(ent, q_parents));
}
// Find the lowest depth of all the entities
let Some(lowest_depth) = depths.values().copied().reduce(usize::min) else {
return None;
};
// Traverse up the tree from each entity until we find each one's ancestor
// at the level of lowest_depth
let mut equalized: EntityHashSet<Entity> = EntityHashSet::default();
for ent in entities {
let mut depth = depths[ent];
let mut result = *ent;
while depth > lowest_depth {
result = find_parent(result, q_parents).unwrap();
depth -= 1;
}
equalized.insert(result);
}
// NOTE: If a member of `entities` should be considered a valid result (i.e.
// if it should count as its own "ancestor"), then we should check the
// length of `equalized` here and return if it only has one member.
// This isn't a use case we need currently, but it could be supported
// by adding a some kind of mode switch to this function's parameters.
// Walk up the tree from each of those entities until we find the common ancestor
let mut parents: EntityHashSet<Entity> = EntityHashSet::default();
loop {
for &ent in equalized.iter() {
if let Some(parent) = find_parent(ent, q_parents) {
parents.insert(parent);
} else {
return None;
}
}
if parents.len() == 1 {
break parents.iter().copied().next();
}
// Reset the loop:
// equalized = parents
// parents = {}
equalized.clear();
equalized.extend(parents.iter());
parents.clear();
}
}
fn find_depth(target: Entity, q_parents: &Query<Option<&Parent>>) -> usize {
let mut result = 0;
let mut parent = find_parent(target, q_parents);
while let Some(p) = parent {
result += 1;
parent = find_parent(p, q_parents);
}
result
}
+39 -5
View File
@@ -2,20 +2,22 @@
use bevy::{app::PluginGroupBuilder, prelude::*};
use bevy_mod_picking::DefaultPickingPlugins;
use bevy_web_asset::WebAssetPlugin;
use bvh::BvhPlugin;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
mod anim;
mod bvh;
mod camera_controller;
mod debug;
mod design_system;
mod hierarchy;
mod input;
mod inspector;
mod interaction;
mod math;
mod mixamo;
mod persistance;
mod scene;
mod skybox;
@@ -24,12 +26,16 @@ pub mod viewport;
mod world_grid;
pub use crate::{
anim::AnimPlugin,
bvh::BvhPlugin,
camera_controller::CameraControllerPlugin,
debug::DebugPlugin,
input::StudioInputPlugin,
inspector::InspectorPlugin,
interaction::InteractionPlugin,
math::InterpolationPlugin,
mixamo::{MixamoImportEvent, MixamoPlugin},
persistance::PersistancePlugin,
scene::{LoadSceneEvent, ScenePlugin},
skybox::{SkyboxId, SkyboxPlugin},
ui::UiLayerPlugin,
@@ -69,8 +75,8 @@ impl PluginGroup for CommonPlugins {
.add(StudioInputPlugin)
.add(UiLayerPlugin)
.add(WorldGridPlugin)
.add(persistance::PersistancePlugin)
.add(BvhPlugin)
.add(PersistancePlugin)
.add(AnimPlugin)
}
}
@@ -149,16 +155,44 @@ pub fn start_bvh_viewer(
let bvh_path = bvh_path.to_string();
app.add_plugins(BvhPlugin);
app.add_systems(
Startup,
move |mut cmd: Commands, r_assets: Res<AssetServer>| {
cmd.insert_resource(scene::TestBvh(r_assets.load(bvh_path.clone())));
cmd.insert_resource(bvh::TestBvh(r_assets.load(bvh_path.clone())));
},
);
app.run();
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = startMixamoViewer))]
pub fn start_mixamo_viewer(
mixamo_path: &str,
skybox_id: &str,
canvas_selector: &str,
canvas_alt: &str,
viewport_size: ViewportSize,
) {
let mut app = start_internal(
"Mannequin.gltf",
skybox_id,
canvas_selector,
canvas_alt,
viewport_size,
StudioMode::Viewer,
);
let mixamo_path = mixamo_path.to_string();
app.add_plugins(MixamoPlugin);
app.add_systems(Startup, move |mut ew: EventWriter<MixamoImportEvent>| {
ew.send(MixamoImportEvent(mixamo_path.clone()));
});
app.run();
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn start(
object_id: &str,
+137
View File
@@ -0,0 +1,137 @@
use bevy::{gltf::Gltf, prelude::*, scene::InstanceId};
use crate::anim::RetargetSource;
pub struct MixamoPlugin;
impl Plugin for MixamoPlugin {
fn build(&self, app: &mut App) {
app.add_event::<MixamoImportEvent>();
app.add_state::<MixamoState>();
app.add_systems(
Update,
(
import_mixamo_scene,
tick_mixamo_import.run_if(resource_exists::<MixamoImport>()),
tick_scene_spawning.run_if(in_state(MixamoState::Spawning)),
start_mixamo_anim.run_if(in_state(MixamoState::Processing)),
),
);
app.add_systems(OnEnter(MixamoState::Spawning), spawn_mixamo_scene);
}
}
// TODO: Simplify this flow based on rewin's scene module refactor
#[derive(States, Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
pub enum MixamoState {
#[default]
None,
Spawning,
Processing,
}
#[derive(Resource, Deref)]
pub struct MixamoImport(pub Handle<Gltf>);
#[derive(Event, Deref)]
pub struct MixamoImportEvent(pub String);
#[derive(Resource, Deref)]
pub struct MixamoScene(pub InstanceId);
fn import_mixamo_scene(
mut cmd: Commands,
r_assets: Res<AssetServer>,
mut er: EventReader<MixamoImportEvent>,
) {
if let Some(event) = er.read().last() {
let path = &**event;
cmd.insert_resource(MixamoImport(r_assets.load(path)));
}
}
fn tick_mixamo_import(
r_mixamo_import: Res<MixamoImport>,
ra_gltfs: Res<Assets<Gltf>>,
mut er_scene_events: EventReader<AssetEvent<Scene>>,
mut st_next: ResMut<NextState<MixamoState>>,
) {
for event in er_scene_events.read() {
if let AssetEvent::LoadedWithDependencies { id } = event {
let Some(gltf) = ra_gltfs.get(&**r_mixamo_import) else {
return;
};
if gltf.default_scene.is_some() && gltf.default_scene.as_ref().unwrap().id() == *id {
st_next.set(MixamoState::Spawning);
break;
}
}
}
}
fn spawn_mixamo_scene(
mut cmd: Commands,
ra_gltfs: Res<Assets<Gltf>>,
r_mixamo_import: Res<MixamoImport>,
mut r_scene_spawner: ResMut<SceneSpawner>,
) {
let gltf = ra_gltfs.get(&**r_mixamo_import).unwrap();
let scene = gltf.default_scene.as_ref().unwrap();
let parent = cmd
.spawn((
Name::new("Mixamo"),
SpatialBundle {
visibility: Visibility::Hidden,
transform: Transform::from_xyz(0., 0., -1.),
..default()
},
))
.id();
let inst = r_scene_spawner.spawn_as_child(scene.clone(), parent);
cmd.insert_resource(MixamoScene(inst));
}
fn tick_scene_spawning(
r_scene_spawner: Res<SceneSpawner>,
r_mixamo_scene: Res<MixamoScene>,
mut st_next: ResMut<NextState<MixamoState>>,
) {
if r_scene_spawner.instance_is_ready(**r_mixamo_scene) {
st_next.set(MixamoState::Processing);
}
}
fn start_mixamo_anim(
mut cmd: Commands,
ra_gltfs: Res<Assets<Gltf>>,
r_mixamo_import: Res<MixamoImport>,
r_mixamo_scene: Res<MixamoScene>,
ra_animations: Res<Assets<AnimationClip>>,
r_scene_spawner: Res<SceneSpawner>,
q_names: Query<&Name>,
mut st_next: ResMut<NextState<MixamoState>>,
) {
let gltf = ra_gltfs.get(&**r_mixamo_import).unwrap();
let src_anim_handle = gltf.animations[0].clone();
let src_anim = ra_animations.get(src_anim_handle.clone()).unwrap();
let Some(src_root) = r_scene_spawner
.iter_instance_entities(**r_mixamo_scene)
.find(|ent| match q_names.get(*ent) {
Ok(name) => src_anim.compatible_with(name),
Err(_) => false,
})
else {
error!("Failed to find compatible entity for animation");
return;
};
let mut src_player = AnimationPlayer::default();
src_player.play(src_anim_handle.clone()).repeat();
cmd.entity(src_root).insert((RetargetSource, src_player));
st_next.set(MixamoState::None);
}
+4 -73
View File
@@ -6,22 +6,18 @@ use bevy::{
tonemapping::Tonemapping,
},
gltf::Gltf,
math::{vec2, Affine3A},
math::vec2,
pbr::ShadowFilteringMethod,
prelude::*,
render::{mesh::skinning::SkinnedMesh, primitives::Aabb, view::NoFrustumCulling},
render::primitives::Aabb,
scene::InstanceId,
};
use space_editor::{
prelude::PrefabMarker,
space_prefab::{component::SceneAutoChild, editor_registry::EditorRegistryExt},
};
use space_editor::{prelude::PrefabMarker, space_prefab::component::SceneAutoChild};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
bvh::{Bvh, CommandsSpawnBvhExt},
camera_controller::{CameraController, CameraControllerBundle},
debug::DebugPoint,
math::{Combinable, Extrema},
@@ -40,14 +36,7 @@ impl Plugin for ScenePlugin {
.add_state::<SceneState>()
.add_event::<LoadSceneEvent>()
.add_systems(Startup, initialize)
.add_systems(
Update,
(
spawn_test_bvh.run_if(resource_exists::<TestBvh>()), // TODO
init_scene_loading,
)
.in_set(SceneSpawnerSystem),
)
.add_systems(Update, init_scene_loading.in_set(SceneSpawnerSystem))
.add_systems(
OnEnter(SceneState::Active),
(tag_scene_elements, frame_scene_in_view).in_set(SceneSpawnerSystem),
@@ -71,10 +60,6 @@ impl Plugin for ScenePlugin {
//setup default light
app.add_systems(Startup, minimal_scene_setup);
app.editor_registry::<BindPoseLocal>();
app.editor_registry::<BindPoseGlobal>();
app.editor_registry::<InvBindPose>();
#[cfg(feature = "wasm")]
app.add_systems(
Update,
@@ -190,25 +175,8 @@ pub struct SceneData {
pub scene_path: String,
}
#[derive(Component)]
pub struct SkeletalJoint(pub Color);
#[derive(Component)]
pub struct SceneRoot;
#[derive(Resource, Deref, DerefMut)]
pub struct TestBvh(pub Handle<Bvh>); // TODO
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct BindPoseLocal(pub Transform);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct BindPoseGlobal(pub Affine3A);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct InvBindPose(pub Affine3A);
fn initialize(mut cmd: Commands) {
// Camera, graphics settings
@@ -242,23 +210,6 @@ fn initialize(mut cmd: Commands) {
));
}
// TODO
fn spawn_test_bvh(
mut commands: Commands,
mut er_bvh_events: EventReader<AssetEvent<Bvh>>,
r_bvh: Res<TestBvh>,
) {
for event in er_bvh_events.read() {
if matches!(event, AssetEvent::LoadedWithDependencies { .. }) {
commands.spawn_bvh_with_transform(
&r_bvh.0,
Transform::from_scale(Vec3::splat(0.06))
.with_translation(Vec3::new(0., 1.0, 0.524)),
);
}
}
}
fn init_scene_loading(mut cmd: Commands, mut er: EventReader<LoadSceneEvent>) {
if let Some(id) = er.read().last() {
info!("Loading scene: {}", id.0);
@@ -299,8 +250,6 @@ fn tag_scene_elements(
Without<NoMeshElement>,
),
>,
q_skinned_meshes: Query<(Entity, &SkinnedMesh), Without<InitedKointSkinnedMesh>>,
mut q_xforms: ParamSet<(TransformHelper, Query<&Transform>)>,
mesh_entities: Query<Entity, With<Handle<Mesh>>>,
) {
for (e, children) in q_named_non_primitives.iter() {
@@ -317,24 +266,6 @@ fn tag_scene_elements(
cmd.entity(e).insert(NoMeshElement);
}
}
for (ent, skinned_mesh) in q_skinned_meshes.iter() {
cmd.entity(ent)
.insert(NoFrustumCulling)
.insert(InitedKointSkinnedMesh);
for &joint in skinned_mesh.joints.iter() {
let global_bind_pose = q_xforms.p0().compute_global_transform(joint).unwrap();
let local_bind_pose = *q_xforms.p1().get(joint).unwrap();
cmd.entity(joint).insert((
SkeletalJoint(Color::BLACK),
BindPoseLocal(local_bind_pose),
BindPoseGlobal(global_bind_pose.affine()),
InvBindPose(global_bind_pose.affine().inverse()),
));
}
}
}
fn frame_scene_in_view(
+1
View File
@@ -21,6 +21,7 @@
"@storyteller/studio-web/canvas-provider": ["studio-web/src/lib/canvas-provider.element.ts"],
"@storyteller/studio-web/viewer": ["studio-web/src/lib/viewer.element.ts"],
"@storyteller/studio-web/bvh-viewer": ["studio-web/src/lib/bvh-viewer.element.ts"],
"@storyteller/studio-web/mixamo-viewer": ["studio-web/src/lib/mixamo-viewer.element.ts"],
"@storyteller/utility": ["utility/src/index.ts"]
}
},