diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 5acf8ace..d24aaa56 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -115,7 +115,7 @@ export default function Editor({confirmDialog,animationManager, blinkManager, lo - + ) } diff --git a/src/components/TraitInformation.jsx b/src/components/TraitInformation.jsx index f6fdc0d3..c7f570ec 100644 --- a/src/components/TraitInformation.jsx +++ b/src/components/TraitInformation.jsx @@ -5,7 +5,7 @@ import { SceneContext } from "../context/SceneContext"; import Slider from "./Slider"; import { cullHiddenMeshes } from "../library/utils"; -export default function TraitInformation({currentVRM, animationManager}){ +export default function TraitInformation({currentVRM, animationManager, lookatManager}){ const { displayTraitOption, avatar @@ -15,6 +15,7 @@ export default function TraitInformation({currentVRM, animationManager}){ const [cullInDistance, setCullInDistance] = useState(0); const [cullLayer, setCullLayer] = useState(0); const [animationName, setAnimationName] = useState(animationManager.getCurrentAnimationName()); + const [hasMouseLook, setHasMouseLook] = useState(lookatManager.enabled); useEffect(() => { if (currentVRM != null){ @@ -47,6 +48,7 @@ export default function TraitInformation({currentVRM, animationManager}){ }; const handleCullLayerChange = (event) => { + console.log(lookatManager.enabled); if (currentVRM?.data){ setCullLayer(event.target.value); currentVRM.data.cullingLayer = event.target.value; @@ -62,6 +64,12 @@ export default function TraitInformation({currentVRM, animationManager}){ await animationManager.loadPreviousAnimation(); setAnimationName(animationManager.getCurrentAnimationName()); } + const handleMouseLookEnable = (event) => { + setHasMouseLook(event.target.checked); + lookatManager.setActive(event.target.checked); + animationManager.enableMouseLook(event.target.checked); + // Perform any additional actions or logic based on the checkbox state change + }; return ( displayTraitOption != null ? ( @@ -131,6 +139,23 @@ export default function TraitInformation({currentVRM, animationManager}){ onClick={nextAnimation} > +
+
+
+ + Mouse Follow +
+ +
+
+ diff --git a/src/components/TraitInformation.module.css b/src/components/TraitInformation.module.css index 9e1eed72..5708bc45 100644 --- a/src/components/TraitInformation.module.css +++ b/src/components/TraitInformation.module.css @@ -98,4 +98,45 @@ .anim-button:hover { opacity: 1; +} + +/* Hide the default checkbox */ +.custom-checkbox input[type="checkbox"] { + display: none; +} + +/* Style the custom checkbox */ +.custom-checkbox .checkbox-container { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid #284b39; /* Change border color as needed */ + border-radius: 5px; + cursor: pointer; +} + +.custom-checkbox .checkbox-container.checked { + background-color: #5eb086; /* Change background color when checked */ +} + +.custom-checkbox .checkbox-container .checkmark { + display: none; +} + +/* Style the checkmark when the checkbox is checked */ +.custom-checkbox input[type="checkbox"]:checked + .checkbox-container { + background-color: #5eb086; /* Change background color when checked */ +} + +.custom-checkbox input[type="checkbox"]:checked + .checkbox-container .checkmark { + display: block; +} + +.checkboxHolder { + display: flex; + gap: 30px; + align-items: center; + justify-content: center; + align-content: center; + height: 40px; } \ No newline at end of file diff --git a/src/library/animationManager.js b/src/library/animationManager.js index b2b29bd0..3084b992 100644 --- a/src/library/animationManager.js +++ b/src/library/animationManager.js @@ -23,6 +23,14 @@ class AnimationControl { this.vrm = vrm; this.animationManager = null; this.animationManager = animationManager; + this.mixamoModel = null; + + this.fadeOutActions = null; + this.newAnimationWeight = 1; + + this.neckBone = vrm?.humanoid?.humanBones?.neck; + this.spineBone = vrm?.humanoid?.humanBones?.spine; + this.setAnimations(animations); @@ -42,27 +50,72 @@ class AnimationControl { this.actions[curIdx].time = animationManager.getToActionTime(); this.actions[curIdx].play(); } - setAnimations(animations, mixamoModel){ - this.mixer.stopAllAction(); + + setMouseLookEnabled(mouseLookEnabled){ + this.setAnimations(this.animations, this.mixamoModel, mouseLookEnabled); + } + + setAnimations(animations, mixamoModel, mouseLookEnabled = null){ + mouseLookEnabled = mouseLookEnabled == null ? this.animationManager.mouseLookEnabled : mouseLookEnabled; + this.animations = animations; + //this.mixer.stopAllAction(); if (mixamoModel != null){ - if (this.vrm != null) - animations = [getMixamoAnimation(animations, mixamoModel , this.vrm)] - // modify animations - } - animations[0].tracks.map((track, index) => { - if(track.name === "neck.quaternion" || track.name === "spine.quaternion"){ - animations[0].tracks.splice(index, 1) + if (this.vrm != null){ + const mixamoAnimation = getMixamoAnimation(animations, mixamoModel , this.vrm); + if (mixamoAnimation){ + animations = [mixamoAnimation] + this.mixamoModel = mixamoModel; + } } - }) + } else{ + const cloneAnims = []; + animations.forEach(animation => { + cloneAnims.push(animation.clone()); + }); + animations = cloneAnims; + } + // modify animations + if (mouseLookEnabled){ + animations[0].tracks.map((track, index) => { + if(track.name === "neck.quaternion" || track.name === "spine.quaternion"){ + animations[0].tracks.splice(index, 1) + } + }) + } + + this.fadeOutActions = this.actions; this.actions = []; + this.newAnimationWeight = 0; for (let i =0; i < animations.length;i++){ this.actions.push(this.mixer.clipAction(animations[i])); } + this.actions[0].weight = 0; this.actions[0].play(); } update(weightIn,weightOut){ + if (this.fadeOutActions != null){ + this.newAnimationWeight += 1/5; + this.fadeOutActions.forEach(action => { + action.weight = 1 - this.newAnimationWeight; + }); + + if (this.newAnimationWeight >= 1){ + this.newAnimationWeight = 1; + this.fadeOutActions.forEach(action => { + action.weight = 0; + action.stop(); + }); + this.fadeOutActions = null; + } + + this.actions.forEach(action => { + action.weight = this.newAnimationWeight; + }); + + } + if (this.from != null) { this.from.weight = weightOut; } @@ -106,6 +159,7 @@ export class AnimationManager{ this.curAnimID = 0; this.animationControls = []; this.started = false; + this.mouseLookEnabled = true; this.mixamoModel = null; this.mixamoAnimations = null; @@ -122,14 +176,17 @@ export class AnimationManager{ }, 1000/30); } - + enableMouseLook(enable){ + this.mouseLookEnabled = enable; + this.animationControls.forEach(animControls => { + animControls.setMouseLookEnabled(enable); + }); + } async loadAnimation(paths, isfbx = true, pathBase = "", name = ""){ - console.log(paths) const path = pathBase + (pathBase != "" ? "/":"") + getAsArray(paths)[0]; name = name == "" ? getFileNameWithoutExtension(path) : name; this.currentAnimationName = name; - console.log(this.currentAnimationName); const loader = isfbx ? fbxLoader : gltfLoader; const animationModel = await loader.loadAsync(path); // if we have mixamo animations store the model @@ -153,7 +210,7 @@ export class AnimationManager{ else{ //cons this.animationControls.forEach(animationControl => { - animationControl.setAnimations(animationModel.animations, this.mixamoModel) + animationControl.setAnimations(animationModel.animations, this.mixamoModel, this.mouseLookEnabled) }); } diff --git a/src/library/loadMixamoAnimation.js b/src/library/loadMixamoAnimation.js index 1b51e703..a4c97d0c 100644 --- a/src/library/loadMixamoAnimation.js +++ b/src/library/loadMixamoAnimation.js @@ -10,7 +10,8 @@ import { VRMRigMapMixamo } from './VRMRigMapMixamo.js'; */ export function getMixamoAnimation( animations, model, vrm ) { const clip = THREE.AnimationClip.findByName( animations, 'mixamo.com' ); // extract the AnimationClip - + if (clip == null) + return null; const tracks = []; // KeyframeTracks compatible with VRM will be added here const restRotationInverse = new THREE.Quaternion(); @@ -24,7 +25,6 @@ export function getMixamoAnimation( animations, model, vrm ) { const vrmRootY = vrm.scene.getWorldPosition( _vec3 ).y; const vrmHipsHeight = Math.abs( vrmHipsY - vrmRootY ); const hipsPositionScale = vrmHipsHeight / motionHipsHeight; - clip.tracks.forEach( ( origTrack ) => { const track = origTrack.clone(); // Convert each tracks for VRM use, and push to `tracks` diff --git a/src/library/lookatManager.js b/src/library/lookatManager.js index 4e745f44..4dfdde30 100644 --- a/src/library/lookatManager.js +++ b/src/library/lookatManager.js @@ -9,6 +9,7 @@ export class LookAtManager { this.leftEyeBones = [] this.rightEyesBones = [] this.curMousePos = new THREE.Vector2() + this.enabled = true; this.hotzoneSection = getHotzoneSection() this.enabled = true @@ -56,6 +57,9 @@ export class LookAtManager { // this.update(); // }, 1000/60); } + setActive(active){ + this.enabled = active; + } setCamera(camera){ this.camera = camera } @@ -127,7 +131,7 @@ export class LookAtManager { const cameraRotationThreshold = localVector.z > 0.; // if camera rotation is not larger than 90 if (this.curMousePos.x > this.hotzoneSection.xStart && this.curMousePos.x < this.hotzoneSection.xEnd && this.curMousePos.y > this.hotzoneSection.yStart && this.curMousePos.y < this.hotzoneSection.yEnd && - cameraRotationThreshold) { + cameraRotationThreshold && this.enabled) { this.neckBones.forEach(neck => { this._moveJoint(neck, this.maxLookPercent.neck) })