mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Merge branch 'main' into add-level-blocking
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# Adapted from
|
||||
# https://github.com/actions/starter-workflows/ci/docker-publish.yml
|
||||
# If using base images, don't forget to add READ access, eg. from a package settings page like:
|
||||
# https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings/actions_access
|
||||
name: "Build GHCR Rust Docker artifact"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# NB: Default-branch doesn't work, despite Github's documentation.
|
||||
#- $default-branch
|
||||
- main
|
||||
env:
|
||||
IMAGE_NAME: studio-headless
|
||||
|
||||
jobs:
|
||||
# Push image to GitHub Packages.
|
||||
# See also https://docs.docker.com/docker-hub/builds/
|
||||
# See also https://depot.dev/blog/docker-layer-caching-in-github-actions
|
||||
# See also https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching
|
||||
build-docker-image:
|
||||
name: "Build Docker Image"
|
||||
#runs-on: ubuntu-22.04 # runs-on: ubuntu-latest
|
||||
#runs-on: github-ubuntu-16core # NB: Premium GitHub runner with more cores
|
||||
runs-on: github-ubuntu-64core # NB: Premium GitHub runner with more cores
|
||||
if: github.event_name == 'push'
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# - uses: docker/setup-buildx-action@v1
|
||||
# name: Set up Docker Buildx
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Calculate short SHA
|
||||
id: vars
|
||||
# echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
echo "sha_short=$(echo ${{ github.sha }} | cut -c1-12)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Emit short SHA
|
||||
run: |
|
||||
echo "Full SHA: ${{ github.sha }}"
|
||||
echo "Short SHA: ${{ steps.vars.outputs.sha_short }}"
|
||||
|
||||
#- uses: actions/cache@v2
|
||||
# name: Cache Docker layers
|
||||
# with:
|
||||
# path: /tmp/.buildx-cache
|
||||
# key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-buildx-
|
||||
|
||||
# Docs for this action are at https://github.com/marketplace/actions/build-docker-images-using-cache
|
||||
- uses: whoan/docker-build-with-cache-action@v6
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
image_name: storytold/storyteller-bevy
|
||||
dockerfile: Dockerfile
|
||||
# NB: The `git_push_tag: true` option does not work as expected, so we put the sha in `image_tag`:
|
||||
# https://github.com/whoan/docker-build-with-cache-action/issues/72
|
||||
image_tag: latest,${{ github.sha }},${{ steps.vars.outputs.sha_short }}
|
||||
# NB: Inject the Git SHA into the container so the servers can emit their own version:
|
||||
build_extra_args: "--build-arg=GIT_SHA=${{ github.sha }}"
|
||||
|
||||
# - name: Build image
|
||||
# run: |
|
||||
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
|
||||
# docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" --build-arg GIT_SHA=${SHORT_SHA}
|
||||
|
||||
# - name: Push image to GitHub Container Registry
|
||||
# run: |
|
||||
# IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
|
||||
# # Change all uppercase to lowercase
|
||||
# IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
|
||||
# # Strip git ref prefix from version
|
||||
# VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# # Strip "v" prefix from tag name
|
||||
# [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# # Use Docker `latest` tag convention
|
||||
# [ "$VERSION" == "$default-branch" ] && VERSION=latest
|
||||
|
||||
# echo IMAGE_ID=$IMAGE_ID
|
||||
# echo VERSION=$VERSION
|
||||
|
||||
# docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
# docker push $IMAGE_ID:$VERSION
|
||||
|
||||
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
|
||||
|
||||
# docker tag $IMAGE_NAME $IMAGE_ID:$SHORT_SHA
|
||||
# docker push $IMAGE_ID:$SHORT_SHA
|
||||
|
||||
- name: Cleanup / Debug
|
||||
# This job will run even on failures to help with debugging.
|
||||
if: always()
|
||||
run: |
|
||||
echo "Report at final github action step:"
|
||||
echo "Disk usage:"
|
||||
pwd
|
||||
du -hsc * | sort -hr
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Run formatter, linter and tests
|
||||
on:
|
||||
merge_group:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Check Rust formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
lint-and-test:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-lint-and-test-${{ hashFiles('**/Cargo.toml') }}
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libasound2-dev \
|
||||
libudev-dev \
|
||||
git
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
- name: Ensure unambiguous `main`
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: |
|
||||
git remote set-branches origin main
|
||||
git fetch --depth 1 origin main
|
||||
git branch main origin/main
|
||||
- name: Run Nx-configured `lint` and `test` targets
|
||||
run: npx nx affected -t lint test --base=main --head=HEAD
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
FROM ubuntu:jammy
|
||||
# as rust-base
|
||||
|
||||
# NB: This can be "stable" or another version.
|
||||
ARG RUST_TOOLCHAIN="1.75.0"
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# NB: cmake is required for freetype-sys-0.13.1, which in turn has only been added for egui.
|
||||
# NB: fontconfig is required by servo-fontconfig-sys, which is in the dependency chain for egui.
|
||||
# NB: libfontconfig-dev is required by servo-fontconfig-sys, which is in the dependency chain for egui.
|
||||
# NB: pkg-config and libssl are for container TLS; we may switch to rustls in the future.
|
||||
# BEVY SPECIFIC -
|
||||
# NB: libasound2-dev for alsa-sys
|
||||
# NB: libudev-dev for libudev-sys
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
libasound2-dev \
|
||||
libudev-dev \
|
||||
curl \
|
||||
fontconfig \
|
||||
libfontconfig1-dev \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
|
||||
# Install Rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- --default-toolchain $RUST_TOOLCHAIN -y
|
||||
|
||||
# Report Rust version for build debugging
|
||||
RUN $HOME/.cargo/bin/rustup show
|
||||
RUN $HOME/.cargo/bin/rustc --version
|
||||
RUN $HOME/.cargo/bin/cargo --version
|
||||
|
||||
# Install NPM
|
||||
# https://nodejs.org/en
|
||||
# https://github.com/nodesource/distributions
|
||||
#RUN curl -fsSL https://deb.nodesource.com/setup_21.x | bash - && \
|
||||
# apt-get install -y nodejs
|
||||
|
||||
# NB: Now we build and test our code.
|
||||
COPY Cargo.lock .
|
||||
COPY Cargo.toml .
|
||||
COPY ./ .
|
||||
|
||||
|
||||
RUN LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
|
||||
$HOME/.cargo/bin/cargo build \
|
||||
--release \
|
||||
--bin studio-headless
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Install dependencies
|
||||
rustup target add wasm32-unknown-unknown
|
||||
npm ci
|
||||
|
||||
# Build projects
|
||||
npx nx run studio:build:web
|
||||
npx nx run studio-frontend:build:production
|
||||
|
||||
@@ -59,11 +59,9 @@
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"passWithNoTests": true,
|
||||
"reportsDirectory": "../coverage/react-frontend"
|
||||
"command": "\n"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -500,7 +500,7 @@ mod tests {
|
||||
// Needed for macros
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
// #[test] // FIXME
|
||||
fn macro_create() {
|
||||
let bvh = bvh! {
|
||||
HIERARCHY
|
||||
|
||||
@@ -548,7 +548,7 @@ impl Bvh {
|
||||
let mut tokens = line.fields();
|
||||
|
||||
let frame_time_kw = tokens.next();
|
||||
if frame_time_kw == FRAME_TIME_KEYWORDS.get(0).copied() {
|
||||
if frame_time_kw == FRAME_TIME_KEYWORDS.first().copied() {
|
||||
// do nothing
|
||||
} else {
|
||||
return Err(LoadMotionError::MissingFrameTime {
|
||||
|
||||
@@ -22,6 +22,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";
|
||||
@@ -49,6 +50,7 @@ export class AppElement extends LitElement {
|
||||
@state() selection?: string | null;
|
||||
|
||||
@state() bvhPath?: string;
|
||||
@state() mixamoPath?: string;
|
||||
@state() mode?: StudioMode;
|
||||
|
||||
#sceneObjects: studio.SceneObject[] = [];
|
||||
@@ -70,6 +72,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,
|
||||
@@ -201,6 +206,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="base-human-female.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
@@ -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()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
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 Ok(src_root) = q_src_root.get_single() else {
|
||||
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: [(&'static str, &'static 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: [(&'static str, &'static 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
@@ -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
@@ -12,11 +12,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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -80,6 +80,34 @@ fn sync_editor_selected(
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_editor_selected(
|
||||
mut cmd: Commands,
|
||||
q_new_selected: Query<Entity, (Added<Selected>, Without<space_editor::prelude::Selected>)>,
|
||||
q_editor_selected: Query<Entity, With<space_editor::prelude::Selected>>,
|
||||
q_selected: Query<Entity, With<Selected>>,
|
||||
q_added_editor_selected: Query<
|
||||
Entity,
|
||||
(Without<Selected>, Added<space_editor::prelude::Selected>),
|
||||
>,
|
||||
) {
|
||||
for ent in q_new_selected.iter() {
|
||||
for ed_ent in q_editor_selected.iter() {
|
||||
cmd.entity(ed_ent)
|
||||
.remove::<space_editor::prelude::Selected>();
|
||||
}
|
||||
cmd.entity(ent).insert(space_editor::prelude::Selected);
|
||||
}
|
||||
|
||||
for ent in q_added_editor_selected.iter() {
|
||||
cmd.entity(ent).insert(Selected);
|
||||
cmd.insert_resource(Selection(ent));
|
||||
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
|
||||
|
||||
+39
-64
@@ -7,14 +7,17 @@ use bevy_web_asset::WebAssetPlugin;
|
||||
#[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;
|
||||
pub mod math;
|
||||
mod mixamo;
|
||||
mod persistance;
|
||||
mod scene;
|
||||
mod skybox;
|
||||
@@ -25,12 +28,16 @@ pub mod blocking;
|
||||
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,
|
||||
@@ -70,7 +77,8 @@ impl PluginGroup for CommonPlugins {
|
||||
.add(StudioInputPlugin)
|
||||
.add(UiLayerPlugin)
|
||||
.add(WorldGridPlugin)
|
||||
.add(persistance::PersistancePlugin)
|
||||
.add(PersistancePlugin)
|
||||
.add(AnimPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,67 +91,6 @@ impl PluginGroup for EditorPlugins {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
pub fn init(viewport_size: ViewportSize) {
|
||||
#[cfg(feature = "wasm")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_plugins(viewport::wasm::ViewportPlugin);
|
||||
|
||||
app.add_plugins((
|
||||
DefaultPlugins
|
||||
// TODO: To generate asset .meta files (to prevent 404 errors on the
|
||||
// web), it's currently necessary to uncomment the following
|
||||
// block and run the app with this command:
|
||||
// > cargo run -p studio --features bevy/asset_processor,bevy/tonemapping_luts
|
||||
// .set(AssetPlugin {
|
||||
// mode: AssetMode::Processed,
|
||||
// ..default()
|
||||
// })
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
resolution: viewport_size.into(),
|
||||
title: "Storyteller Studio".into(),
|
||||
canvas: Some("#studio_canvas".into()),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
DefaultPickingPlugins,
|
||||
DebugPlugin,
|
||||
InteractionPlugin,
|
||||
InspectorPlugin,
|
||||
StudioInputPlugin,
|
||||
InterpolationPlugin,
|
||||
CameraControllerPlugin,
|
||||
ScenePlugin,
|
||||
SkyboxPlugin,
|
||||
UiLayerPlugin,
|
||||
WorldGridPlugin,
|
||||
persistance::PersistancePlugin,
|
||||
))
|
||||
.add_systems(
|
||||
Startup,
|
||||
move |mut cmd: Commands, mut ew: EventWriter<LoadSceneEvent>| {
|
||||
cmd.insert_resource(SkyboxId("gum_trees_4k".to_string()));
|
||||
ew.send(LoadSceneEvent("bookcase".to_string()));
|
||||
},
|
||||
)
|
||||
.insert_resource(StudioMode::Editor);
|
||||
|
||||
// Only for registering components for undo
|
||||
use space_editor::prelude::space_undo::AppAutoUndo;
|
||||
app.auto_reflected_undo::<Transform>();
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
wasm::initialize();
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = startViewer))]
|
||||
/// Start the application as a read-only viewer for a single library object.
|
||||
///
|
||||
@@ -208,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,
|
||||
|
||||
@@ -19,7 +19,7 @@ pub trait InvLerp<T: Sized> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
/// use studio::math::{NearlyEq, InvLerp};
|
||||
///
|
||||
/// let t = 48_f32.inv_lerp(32., 64.);
|
||||
/// // The expected result is 0.5, because 48 is halfway between 32 and 64.
|
||||
@@ -38,7 +38,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
/// use studio::math::{NearlyEq, Remap};
|
||||
///
|
||||
/// let x = 48_f32.remap((32., 64.), (32., 128.));
|
||||
/// // The expected result is 80, because 48 is halfway between 32 and 64,
|
||||
@@ -56,7 +56,7 @@ pub trait CurveSin {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// // This is fine, but it will feel "jerky" because the velocity is
|
||||
/// // constant from start to end.
|
||||
/// fn update_position(dt: f32, start: Vec3, end: Vec3, t: f32) -> Vec3 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+9
-56
@@ -9,7 +9,7 @@ use bevy::{
|
||||
math::{vec2, Affine3A},
|
||||
pbr::ShadowFilteringMethod,
|
||||
prelude::*,
|
||||
render::{mesh::skinning::SkinnedMesh, primitives::Aabb, view::NoFrustumCulling},
|
||||
render::primitives::Aabb,
|
||||
scene::InstanceId,
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ use space_editor::{prelude::PrefabMarker, space_prefab::component::SceneAutoChil
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::{
|
||||
bvh::{Bvh, CommandsSpawnBvhExt},
|
||||
camera_controller::{CameraController, CameraControllerBundle},
|
||||
debug::DebugPoint,
|
||||
math::{Combinable, Extrema},
|
||||
@@ -37,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),
|
||||
@@ -184,21 +176,14 @@ pub struct SceneData {
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SceneData {
|
||||
pub scene_path: String,
|
||||
}
|
||||
|
||||
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, Deref, DerefMut)]
|
||||
pub struct BindPoseLocal(pub Transform);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Deref, DerefMut)]
|
||||
pub struct BindPoseGlobal(pub Affine3A);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Deref, DerefMut)]
|
||||
pub struct InvBindPose(pub Affine3A);
|
||||
|
||||
fn initialize(mut cmd: Commands) {
|
||||
// Camera, graphics settings
|
||||
@@ -232,23 +217,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);
|
||||
@@ -267,6 +235,9 @@ fn init_scene_loading(mut cmd: Commands, mut er: EventReader<LoadSceneEvent>) {
|
||||
#[derive(Component)]
|
||||
pub struct NoMeshElement;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct InitedKointSkinnedMesh;
|
||||
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
// A "glTF mesh" is actually a container for one or more "glTF primitives",
|
||||
@@ -286,8 +257,6 @@ fn tag_scene_elements(
|
||||
Without<NoMeshElement>,
|
||||
),
|
||||
>,
|
||||
q_skinned_meshes: Query<(Entity, &SkinnedMesh), Without<SkeletalJoint>>,
|
||||
mut q_xforms: ParamSet<(TransformHelper, Query<&Transform>)>,
|
||||
mesh_entities: Query<Entity, With<Handle<Mesh>>>,
|
||||
) {
|
||||
for (e, children) in q_named_non_primitives.iter() {
|
||||
@@ -304,22 +273,6 @@ fn tag_scene_elements(
|
||||
cmd.entity(e).insert(NoMeshElement);
|
||||
}
|
||||
}
|
||||
|
||||
for (ent, skinned_mesh) in q_skinned_meshes.iter() {
|
||||
cmd.entity(ent).insert(NoFrustumCulling);
|
||||
|
||||
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(
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user