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)); }