diff --git a/app/page.tsx b/app/page.tsx
index 69661b7..df8dd61 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -14,6 +14,7 @@ export default function Home() {
const [eyePosition, setEyePosition] = useState({ x: 10, y: 10 });
const [modelReady, setModelReady] = useState(false);
const [clickerCount, setClickerCount] = useState(1);
+ const [testAnimation, setTestAnimation] = useState(false);
const { handleClick } = useClick({
setHearts,
@@ -41,10 +42,11 @@ export default function Home() {
return (
-
+
+ {!testAnimation && }
diff --git a/components/ThreeJs.tsx b/components/ThreeJs.tsx
index 8151626..c0158a3 100644
--- a/components/ThreeJs.tsx
+++ b/components/ThreeJs.tsx
@@ -13,10 +13,14 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
clickerCount,
eyePosition,
setReady,
+ testAnimation,
+ setTestAnimation,
}: {
clickerCount: number;
eyePosition: { x: number; y: number };
setReady: () => void;
+ testAnimation: boolean;
+ setTestAnimation: (value: boolean) => void;
}) {
const containerRef = useRef(null);
const rendererRef = useRef(null);
@@ -44,6 +48,7 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
const pose1ActionData = useRef(null);
const happyActionData = useRef(null);
const excitedActionData = useRef(null);
+ const testActionData = useRef(null);
const tapAnimations = [happyActionData, excitedActionData];
const idleFBXUrl = "/assets/models/Idle.fbx";
const loserFBXUrl = "/assets/models/Loser.fbx";
@@ -52,6 +57,7 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
const pose1FBXUrl = "/assets/models/Pose_1.fbx";
const happyFBXURL = "/assets/models/Happy.fbx";
const excitedFBXURL = "/assets/models/Excited.fbx";
+ const testFBXURL = "/assets/models/Test.fbx";
const smileAndBowTimeoutRef = useRef(null); // Ref to store the smile timeout
const expressionTimeoutRef = useRef(null);
@@ -249,6 +255,23 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
}, startWavingTimeout);
}, [animationsLoaded]);
+ useEffect(() => {
+ if (!testAnimation) return;
+ // don't let other animations or expressions play
+ setAnimationPlaying(true);
+ testActionData.current.reset();
+ testActionData.current.play();
+ idleActionData.current.crossFadeTo(testActionData.current, 0.4);
+ setTimeout(() => {
+ idleActionData.current.reset();
+ idleActionData.current.play();
+ testActionData.current.crossFadeTo(idleActionData.current, 0.4);
+ setAnimationPlaying(false);
+ }, 3000);
+ setTestAnimation(false);
+
+ }, [testAnimation, setTestAnimation]);
+
const handleAnimation = (counter) => {
if (counter % 40 === 0 && counter !== 0) {
// only play new animation if a current one is not playing
@@ -387,9 +410,6 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
loadMixamoAnimation(wavingFBXUrl, currentVrm.current).then((clip) => {
wavingActionData.current = currentMixer.current.clipAction(clip);
}),
- loadMixamoAnimation(idleFBXUrl, currentVrm.current).then((clip) => {
- idleActionData.current = currentMixer.current.clipAction(clip);
- }),
loadMixamoAnimation(loserFBXUrl, currentVrm.current).then((clip) => {
loserActionData.current = currentMixer.current.clipAction(clip);
}),
@@ -405,6 +425,9 @@ const ThreeJSComponent = memo(function ThreeJSComponent({
loadMixamoAnimation(excitedFBXURL, currentVrm.current).then((clip) => {
excitedActionData.current = currentMixer.current.clipAction(clip);
}),
+ loadMixamoAnimation(testFBXURL, currentVrm.current).then((clip) => {
+ testActionData.current = currentMixer.current.clipAction(clip);
+ }),
];
return Promise.all(promises);
diff --git a/public/assets/models/Spin.fbx b/public/assets/models/Spin.fbx
deleted file mode 100644
index c8b91b8..0000000
Binary files a/public/assets/models/Spin.fbx and /dev/null differ
diff --git a/public/assets/models/Test.fbx b/public/assets/models/Test.fbx
new file mode 100644
index 0000000..eec9005
Binary files /dev/null and b/public/assets/models/Test.fbx differ
diff --git a/utils/armatureVRMRigMap.ts b/utils/armatureVRMRigMap.ts
new file mode 100644
index 0000000..56949ac
--- /dev/null
+++ b/utils/armatureVRMRigMap.ts
@@ -0,0 +1,60 @@
+/**
+ * A map from common armature rig names to VRM Humanoid bone names
+ */
+export const armatureVRMRigMap = {
+ Hips: "hips",
+ Spine: "spine",
+ Spine1: "spine",
+ Spine2: "chest",
+ Spine3: "upperChest",
+ Neck: "neck",
+ Head: "head",
+ LeftShoulder: "leftShoulder",
+ LeftArm: "leftUpperArm",
+ LeftForeArm: "leftLowerArm",
+ LeftHand: "leftHand",
+ LeftHandThumb1: "leftThumbProximal",
+ LeftHandThumb2: "leftThumbIntermediate",
+ LeftHandThumb3: "leftThumbDistal",
+ LeftHandIndex1: "leftIndexProximal",
+ LeftHandIndex2: "leftIndexIntermediate",
+ LeftHandIndex3: "leftIndexDistal",
+ LeftHandMiddle1: "leftMiddleProximal",
+ LeftHandMiddle2: "leftMiddleIntermediate",
+ LeftHandMiddle3: "leftMiddleDistal",
+ LeftHandRing1: "leftRingProximal",
+ LeftHandRing2: "leftRingIntermediate",
+ LeftHandRing3: "leftRingDistal",
+ LeftHandPinky1: "leftLittleProximal",
+ LeftHandPinky2: "leftLittleIntermediate",
+ LeftHandPinky3: "leftLittleDistal",
+ RightShoulder: "rightShoulder",
+ RightArm: "rightUpperArm",
+ RightForeArm: "rightLowerArm",
+ RightHand: "rightHand",
+ RightHandThumb1: "rightThumbProximal",
+ RightHandThumb2: "rightThumbIntermediate",
+ RightHandThumb3: "rightThumbDistal",
+ RightHandIndex1: "rightIndexProximal",
+ RightHandIndex2: "rightIndexIntermediate",
+ RightHandIndex3: "rightIndexDistal",
+ RightHandMiddle1: "rightMiddleProximal",
+ RightHandMiddle2: "rightMiddleIntermediate",
+ RightHandMiddle3: "rightMiddleDistal",
+ RightHandRing1: "rightRingProximal",
+ RightHandRing2: "rightRingIntermediate",
+ RightHandRing3: "rightRingDistal",
+ RightHandPinky1: "rightLittleProximal",
+ RightHandPinky2: "rightLittleIntermediate",
+ RightHandPinky3: "rightLittleDistal",
+ LeftUpLeg: "leftUpperLeg",
+ LeftLeg: "leftLowerLeg",
+ LeftFoot: "leftFoot",
+ LeftToeBase: "leftToes",
+ RightUpLeg: "rightUpperLeg",
+ RightLeg: "rightLowerLeg",
+ RightFoot: "rightFoot",
+ RightToeBase: "rightToes",
+ HeadTop_End: null, // Optional, not mapped in VRM by default
+ };
+
\ No newline at end of file
diff --git a/utils/loadMixamoAnimation.ts b/utils/loadMixamoAnimation.ts
index ce90b86..e90587a 100644
--- a/utils/loadMixamoAnimation.ts
+++ b/utils/loadMixamoAnimation.ts
@@ -1,18 +1,20 @@
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { mixamoVRMRigMap } from "./mixamoVRMRigMap";
+import { armatureVRMRigMap } from "./armatureVRMRigMap"; // Import your custom map
/**
- * Load Mixamo animation, convert for three-vrm use, and return it.
+ * Load Mixamo or Armature animation, convert for three-vrm use, and return it.
*
- * @param {string} url A url of mixamo animation data
+ * @param {string} url A url of mixamo or armature animation data
* @param {VRM} vrm A target VRM
* @returns {Promise} The converted AnimationClip
*/
export function loadMixamoAnimation(url, vrm) {
const loader = new FBXLoader(); // A loader which loads FBX
return loader.loadAsync(url).then((asset) => {
- const clip = THREE.AnimationClip.findByName(asset.animations, "mixamo.com"); // extract the AnimationClip
+ const name = asset.animations[0].name;
+ const clip = THREE.AnimationClip.findByName(asset.animations, name); // extract the AnimationClip
const tracks = []; // KeyframeTracks compatible with VRM will be added here
@@ -21,36 +23,65 @@ export function loadMixamoAnimation(url, vrm) {
const _quatA = new THREE.Quaternion();
const _vec3 = new THREE.Vector3();
- // Adjust with reference to hips height.
- const motionHipsHeight = asset.getObjectByName("mixamorigHips").position.y;
- const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode("hips").getWorldPosition(_vec3).y;
+ // Determine which rig map to use
+ const rigMap = name.includes("mixamo.com") ? mixamoVRMRigMap : armatureVRMRigMap;
+
+ // Handle different naming conventions for hips bone
+ const hipsBoneName = name.includes("mixamo.com") ? "mixamorigHips" : "Hips"; // Adjust for non-mixamo rigs
+ const motionHips = asset.getObjectByName(hipsBoneName);
+
+ if (!motionHips) {
+ console.error("No hips bone found in the rig!");
+ return;
+ }
+
+ const motionHipsHeight = motionHips.position.y;
+ const vrmHips = vrm.humanoid?.getNormalizedBoneNode("hips");
+ const vrmHipsY = vrmHips?.getWorldPosition(_vec3).y;
const vrmRootY = vrm.scene.getWorldPosition(_vec3).y;
const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY);
const hipsPositionScale = vrmHipsHeight / motionHipsHeight;
-
+ let firstQuaternionTrackLength: number | null = null;
+
clip.tracks.forEach((track) => {
- // Convert each tracks for VRM use, and push to `tracks`
+ // Convert each track for VRM use and push to `tracks`
const trackSplitted = track.name.split(".");
- const mixamoRigName = trackSplitted[0];
- const vrmBoneName = mixamoVRMRigMap[mixamoRigName];
+ const rigName = trackSplitted[0];
+ const vrmBoneName = rigMap[rigName];
+
+ if (!vrmBoneName) {
+ // If there is no corresponding VRM bone, skip this track
+ return;
+ }
+
const vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name;
- const mixamoRigNode = asset.getObjectByName(mixamoRigName);
+ const rigNode = asset.getObjectByName(rigName);
- if (vrmNodeName != null) {
+ if (vrmNodeName != null && rigNode) {
const propertyName = trackSplitted[1];
// Store rotations of rest-pose.
- mixamoRigNode.getWorldQuaternion(restRotationInverse).invert();
- mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation);
+ rigNode.getWorldQuaternion(restRotationInverse).invert();
+ rigNode.parent.getWorldQuaternion(parentRestWorldRotation);
if (track instanceof THREE.QuaternionKeyframeTrack) {
- // Retarget rotation of mixamoRig to NormalizedBone.
+ // Store the length of the first quaternion track's values
+ if (firstQuaternionTrackLength === null) {
+ firstQuaternionTrackLength = track.values.length;
+ }
+
+ // Check if the current track's values length is less than or equal to the first track's values length
+ if (track.values.length > firstQuaternionTrackLength) {
+ // Skip this track if its length is greater than the first track's length
+ return;
+ }
+ // Retarget rotation of rig to NormalizedBone.
for (let i = 0; i < track.values.length; i += 4) {
const flatQuaternion = track.values.slice(i, i + 4);
_quatA.fromArray(flatQuaternion);
- // 親のレスト時ワールド回転 * トラックの回転 * レスト時ワールド回転の逆
+ // Parent's rest world rotation * track rotation * rest world rotation inverse
_quatA.premultiply(parentRestWorldRotation).multiply(restRotationInverse);
_quatA.toArray(flatQuaternion);
@@ -68,6 +99,9 @@ export function loadMixamoAnimation(url, vrm) {
),
);
} else if (track instanceof THREE.VectorKeyframeTrack) {
+ if (vrmBoneName != "hips" || propertyName.toString() !== "position") {
+ return;
+ }
const value = track.values.map((v, i) => (vrm.meta?.metaVersion === "0" && i % 3 !== 1 ? -v : v) * hipsPositionScale);
tracks.push(new THREE.VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, track.times, value));
}