import * as THREE from "../../../../libs/three.js/build/three.module.js"; import math from '../../utils/math.js' import Tween from '../../utils/Tween.js' import {easing, lerp} from '../../utils/transitions.js' //有的动画,如小狗,进来如果不play停在第一帧,mesh会错,爪子在前面;但如果都停在第一帧,动作有可能很奇怪 const tweens = {} const maxClipFadeTime = Potree.settings.maxClipFadeTime//渐变时间 s /* const pathStates = new Map */ //actions中可能包含没有动作的 如TPose //包括无动画的模型在内的各项属性的过渡 const rot90Qua = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0),-Math.PI/2) export default class AnimationEditor extends THREE.EventDispatcher{ constructor(){ super() this.poseKeys = new Map //transform this.descKeys = new Map //字幕 this.pathKeys = new Map //之前设置好的Path , 优先级高于pose this.clipKeys = new Map //glb animation actions this.duration = 0 //动画时长 this.time = 0 //当前播放时间 this.cursorTime = 0 //时间轴指针时间 this.keepDistance = true //focus的物体和相机保持不变的距离 this.poseTransition = false //pose缓动 if(Potree.settings.isOfficial){ viewer.modules.MergeEditor.bus.addEventListener('changeSelect',()=>{ let targetObject = viewer.modules.MergeEditor.selected targetObject = this.ifContainsModel(targetObject) ? targetObject : null this.setCameraFollow(targetObject) }) } } addKey(model, keyType, key ){ let keys = this[keyType+'Keys'].get(model) if(!keys){ keys = [] } let index = keys.findIndex(e=>e.time>key.time) if(index == -1){ index = keys.length } keys = [...keys.slice(0,index), key, ...keys.slice(index,keys.length)] this[keyType+'Keys'].set(model,keys) this.updateTimeRange() } removeKey(model, keyType, key ){ let keys = this[keyType+'Keys'].get(model) if(!keys)return console.warn('removeKey没找到key') let index = keys.indexOf(key) if(index > -1){ if(keyType == 'clip'){ key.action.stop() } keys.splice(index,1) this.updateTimeRange() } } reOrderKey(model, keyType, key ){ this.removeKey(model, keyType, key ) this.addKey(model, keyType, key ) } at(time, delta, force){ this.dispatchEvent({type:'atTime', time}) //该时间可以大于本动画持续时间 this.cursorTime = time /* if(time > this.duration + maxClipFadeTime/2){ for(let [model, keys] of this.clipKeys){ model.actions.forEach(a=>a.stop()) } } */ let maxTime = this.duration+maxClipFadeTime/2 if(time >= maxTime) time = maxTime if(this.time == time && !force)return this.time = time //真实值 let oldDisToCam = this.camFollowObject?.length == 1 && this.keepDistance && this.camFollowObject[0].boundCenter.distanceTo(viewer.mainViewport.view.position) let transitionRatio = 0.05 * delta * 60 //渐变系数,越小缓动程度越高,越平滑 //假设标准帧率为60fps,当帧率低时(delta大时) 降低缓动。速度快时缓动太高会偏移路径 let transitionRatio2 = 0.8 * delta * 60 let posePathModels = []; [this.poseKeys, this.pathKeys/* , this.clipKeys */].forEach((map)=>{ Array.from(map.keys()).forEach(model=>{ posePathModels.includes(model) || posePathModels.push(model) }) }) /* 路径>关键帧。但是如果每条路径开头和结尾以及过渡时没有关键帧,保持路径开头和结尾的姿态 */ posePathModels.forEach(model=>{ let pathKeys = this.pathKeys.get(model) || [] let poseKeys = this.poseKeys.get(model) || [] let atPath //是否在path中 至多只有一个 let lastPath let nextPath if(pathKeys.length){ pathKeys.find(key=>{ if(key.path.points.length < 2) return let startToFade = key.time - maxClipFadeTime/2 let endFade = key.time + key.dur + maxClipFadeTime/2 atPath = time >= key.time && time <= key.time + key.dur if(atPath){ atPath = key //找到一个就退出 return true } if(key.time + key.dur < time) lastPath = key else if(key.time > time && !nextPath) nextPath = key }) } if(poseKeys.length){ tweens.scale = new Tween(poseKeys.map(e=>e.time), poseKeys.map(e=>e.scale)) model.scale.copy(tweens.scale.lerp(time)) }else{ /* if(pathKeys.length){ model.quaternion.copy(model.defaultAniPose?.quaternion || new THREE.Quaternion()) //设置路径朝向前要先还原 } */ } if(atPath){//沿着curve行走,目视curve前方 (参照CameraAnimationCurve,搜quaFromCurveTan) let percent = THREE.Math.clamp((time - atPath.time) / atPath.dur, 0, 1) let {position , quaternion} = this.getPoseAtPathKey(atPath, percent, model) //模型文件先保证其center在脚底,如果要我手动将bound底部对齐路径高度再说 model.position.copy(position); model.quaternion.copy(quaternion) model.atPath = atPath }else{ model.atPath = null poseKeys = poseKeys.slice() let addPathToPoseKey = (pathKey, percent)=>{ //把当前前后的path姿态加入帧 let {position , quaternion} = this.getPoseAtPathKey(pathKey, percent, model) let fakeKey = { isPath : true, time: pathKey.time + pathKey.dur * percent, pos:position , qua:quaternion } let index = poseKeys.findIndex(e=>e.time > fakeKey.time) if(index == -1){ index = poseKeys.length } poseKeys = [...poseKeys.slice(0,index), fakeKey, ...poseKeys.slice(index,poseKeys.length)] } lastPath && addPathToPoseKey(lastPath, 1) nextPath && addPathToPoseKey(nextPath, 0) if(poseKeys.length){ tweens.pos = new Tween(poseKeys.map(e=>e.time), poseKeys.map(e=>e.pos)) model.position.copy(tweens.pos.lerp(time)) tweens.qua = new Tween(poseKeys.map(e=>e.time), poseKeys.map(e=>e.qua)) model.quaternion.copy(tweens.qua.lerp(time)) /* let poseKeys2 = poseKeys.filter(e=>e.isPath)//妈呀为什么这么写我忘了 if(poseKeys2.length ){ tweens.qua = new Tween(poseKeys2.map(e=>e.time), poseKeys2.map(e=>e.qua)) model.quaternion.copy(tweens.qua.lerp(time)) } */ } } if(poseKeys.length || pathKeys.length){ model.dispatchEvent('position_changed') model.dispatchEvent('rotation_changed') } }) for(let [model, keys] of this.clipKeys){ if(keys.length == 0) continue let weights = keys.map((key,i)=>{ //计算每个动作权重(幅度)。 /* if(delta == void 0){//无缓动 但会造成和缓动时动作time不同 return time >= key.time && time <= key.time + key.dur ? 1 : 0 } */ key.index_ = i let fadeTimeStart = Math.min(maxClipFadeTime, key.dur, (keys[i-1]?.dur || maxClipFadeTime )) / 2 //过渡时间不超过当前和前一个的 half of dur let fadeTimeEnd = Math.min(maxClipFadeTime, key.dur, (keys[i+1]?.dur || maxClipFadeTime )) / 2 //过渡时间不超过当前和后一个的 half of dur let startTime1 = key.time - fadeTimeStart let endTime1 = key.time + key.dur + fadeTimeEnd let startTime2 = key.time + fadeTimeStart let endTime2 = key.time + key.dur - fadeTimeEnd key.action.tempSW_ = {scale:0,weight:0,time: null, sameLinks:[]}, key.tempTime_ = THREE.Math.clamp(time - key.time, 0, key.dur) //time - startTime1 //当前动作时间 key.startTime1 = startTime1 key.endTime1 = endTime1 if(i==0 && time endTime2){//所有动作播完后维持最后一个动作 return 1 }else{ if(time < startTime1 || time > endTime1)return 0 //out bound if(time >= startTime2 && time <= endTime2 ) return 1 if(time < startTime2 ){ return Potree.math.linearClamp(time, [startTime1,startTime2],[0,1]) }else{ return Potree.math.linearClamp(time, [endTime2,endTime1],[1,0]) } } })//最多有两个>0的,在过渡 let animateActions = [] //在播的动作 keys.forEach((key,i)=>{ weights[i]>0 && !animateActions.includes(key.action) && (key.action.tempSW_ = {scale:0,weight:0,time: null, sameLinks:[]}, animateActions.push(key.action) )}) //万一前后是一个动作…… 所以用tempSW_计算总值 keys.forEach((key,i)=>{ //要找到当前action之前所有不间断的所有key,他们之间要连续播放 if(animateActions.includes(key.action)){ if(key.startTime1 < time){//已播部分的key let last = key.action.tempSW_.sameLinks[key.action.tempSW_.sameLinks.length - 1] if(last){ if( key.index_ == last.index_ + 1 && key.startTime1 <= last.endTime1 ){ //相连 key.action.tempSW_.sameLinks.push(key) }else{ key.action.tempSW_.sameLinks = [key] //clear } }else{ key.action.tempSW_.sameLinks = [key] } } } }) keys.forEach((key,i)=>{ if(animateActions.includes(key.action)){ let weight = weights[i] * key.weight //权重乘以自身幅度 if(weight>0){//最多两个 key.action.play() key.action.paused = true //停在某帧 //如果没有点击该动作块的话 不停 key.action.tempSW_.weight += weight key.tempTime_ >= 0 && (key.action.tempSW_.scale = key.speed) //相同动作不允许叠加速度 if(key.action.tempSW_.time == null){//如果两个动作相同 只需在第一个计算出总和 let timeSum = 0 key.action.tempSW_.sameLinks.forEach(key_ =>{ timeSum += (key_.tempTime_ % (key_.action._clip.duration / key_.speed)) * key_.speed //相同动作可能速度不同,算出每个clip的时间 }) key.action.tempSW_.time = timeSum % key.action._clip.duration } //(老版本,过渡时播放时间会延长一点,有交集): //key.action.tempSW_.time == null && (key.action.tempSW_.time = key.tempTime_) //相同动作优先用前一个的时间 //key.action.tempSW_.scale += key.speed // * weights[i] //乘以weight在开始和结束作为缓动效果好,但是不好计算实时time //speed time都没有交集,只有weight有,为了过渡 } }else{ key.action.stop() //不启动动画 } }) animateActions.forEach(action=>{ action.setEffectiveTimeScale(action.tempSW_.scale) //speed 只有没paused时有效 这里都paused的所以没用 action.setEffectiveWeight(action.tempSW_.weight ); action.time = action.tempSW_.time //(action.tempSW_.time % (action._clip.duration / action.tempSW_.scale)) * action.tempSW_.scale //只有paused时有效 //console.log('action', action._clip.name, action.time, action.weight, action.tempSW_.scale ) }) //model.mixer.timeScale = 1 ; } viewer.objs.children.forEach(obj=>{ if(!obj.actions?.length)return let clipState = obj.actions.map(action=>{ let played = action._mixer._isActiveAction( action ); let paused = action.paused let time = action.time let weight = action.weight return {played,paused,time,weight} }) clipState = JSON.stringify(clipState) if(obj.clipState != clipState){//动作是否改变 obj.traverse(e=>e.isSkinnedMesh && (e.boundingSphere = null)) //动画会导致bound改变,清空,raycast时重新计算bound,否则hover不到模型 obj.clipChanged = true } obj.clipState = clipState }) { if(this.camFollowObject && !viewer.scene.monitors.some(e=>e.isWatching)){//in front of model if(this.camFollowObject.length == 1){ let model = this.camFollowObject[0] if(viewer.images360.latestRequestMode == 'showPointCloud'){ if(this.camFaceToObject){ let dis = 4; let dir = new THREE.Vector3(0,0.1,1).normalize()//稍微朝上 dir.multiplyScalar(dis).applyQuaternion(model.quaternion) let pos = new THREE.Vector3().addVectors(model.boundCenter, dir) viewer.mainViewport.view.position.copy(pos) viewer.mainViewport.view.lookAt(model.boundCenter) }else if(this.keepDistance){ //不改镜头方向 保持一定角度。如果要改镜头方向,把lookAt提前 viewer.mainViewport.view.position.subVectors(model.boundCenter, viewer.mainViewport.view.direction.clone().multiplyScalar(oldDisToCam)) viewer.mainViewport.view.radius = oldDisToCam }else{ viewer.mainViewport.view.lookAt(model.boundCenter) } }else{ viewer.mainViewport.view.lookAt(model.boundCenter) } }else{ viewer.modules.MergeEditor.focusOn(this.camFollowObject, 0, true/* ,false,dirAve */) } } } viewer.dispatchEvent('content_changed') } getPoseAtPathKey(key, percent, model){ let delta = 0.001 let percent2 = percent + delta let curve = key.path.curve.clone() if(key.reverse) curve.points.reverse() let position = curve.getPointAt(percent); let pathQua, quaternion if(percent2 <= 1){ let position2 = curve.getPointAt(percent2); pathQua = math.getQuaFromPosAim(position2, position) }else{ percent2 = percent - delta let position2 = curve.getPointAt(percent2); pathQua = math.getQuaFromPosAim(position, position2) } pathQua.multiplyQuaternions( pathQua, rot90Qua ); //这是当模型导进来就旋转正确时的quaternion key.curQua_ = pathQua.clone() //记录下 if(model.quaAtPath){ quaternion = new THREE.Quaternion().multiplyQuaternions(pathQua, model.quaAtPath) }else{ quaternion = pathQua.clone() } //model && quaternion.multiplyQuaternions( quaternion, model.quaternion ); //应用当前已有的quaternion //如果要将模型底部中心对准路径,需要先修改好模型scale ,然后boundingBox中心应用scale和qua, 加到position里 //目前两个人物模型刚好模型pivot在脚底,如果是其他物体甚至直接用curve的朝向不太对,没有明确朝向。除非所有模型都保持上路径前的朝向 //或者pos的z还用之前的 //产品说位置偏移不管它,因为路径可以隐藏和修改。只要记录相对旋转即可。 return {position, quaternion} } getModelQuaAtPath(model){ //当前时间在路径上时,旋转模型后立即执行该函数,获取相对旋转值 if(!model.atPath)return let qua = new THREE.Quaternion().multiplyQuaternions(model.atPath.curQua_.clone().invert(), model.quaternion) //console.log('getModelQuaAtPath',qua) model.quaAtPath = qua //相对旋转 return qua } play({ time = -maxClipFadeTime/2}={}){//动画时长比duration多一个maxClipFadeTime,为了给开始和结束动画过渡 this.updateTimeRange() this.playing && this.pause() let maxTime = this.duration+maxClipFadeTime/2 this.playing = true this.cursorTime = time this.onUpdate = (e)=>{ this.cursorTime += e.delta if(!Potree.settings.isOfficial && time > maxTime) this.cursorTime = maxTime this.at(this.cursorTime, e.delta) if(!Potree.settings.isOfficial && time > maxTime) { this.dispatchEvent('stop') for(let [model, keys] of this.clipKeys){ model.actions.forEach(a=>a.stop()) } this.pause() } } viewer.addEventListener("update_start", this.onUpdate); } pause(){ this.playing = false viewer.removeEventListener("update_start", this.onUpdate); /* for(let [model, keys] of this.clipKeys){ model.actions.forEach(a=>a.stop()) } */ viewer.dispatchEvent('content_changed') } setCameraFollow(camFollowObject){//for test this.camFollowObject = camFollowObject if(!camFollowObject)return //Potree.settings.displayMode = 'showPointCloud' if(!(this.camFollowObject instanceof Array)){//支持相机跟随多个物体,对着bound的中心 this.camFollowObject = [this.camFollowObject] } this.camFollowObject = this.camFollowObject.map(object=>{ if(typeof object == 'string'){ return viewer.objs.children.find(e=>e.name == object) }else{ return object } }) } updateTimeRange(){ let maxTime = 0 for(let [model, keys] of this.poseKeys){ keys.length>0 && (maxTime = Math.max(maxTime, keys[keys.length - 1].time)) } for(let [model, keys] of this.clipKeys){ keys.length>0 && (maxTime = Math.max(maxTime, keys[keys.length - 1].time + keys[keys.length - 1].dur)) } for(let [model, keys] of this.pathKeys){ keys.length>0 && (maxTime = Math.max(maxTime, keys[keys.length - 1].time + keys[keys.length - 1].dur)) } this.duration = maxTime //不算开始和结束动画的过渡时间的话 /* for(let [model, keys] of this.clipKeys){ max = Math.max(maxTime, keys[keys.length - 1].time + keys[keys.length - 1].dur) } */ } /* removeModelCallback(model){ this.poseKeys.get } */ ///////////////////////////////// addPoseKey({model,time,index }={}){ /* if(replace){ this.removeKey(model,'pose', index) } */ let keys = this.poseKeys.get(model) if(!keys){ keys = [] } let key = { time, qua: model.quaternion.clone(), scale: model.scale.clone(), pos: model.position.clone() } if(index == void 0)index = keys.length keys = [...keys.slice(0,index), key, ...keys.slice(index,keys.length)] this.poseKeys.set(model,keys) return key } addClipKey({model, time, index, dur, actionIndex, weight=1, speed=1/* , replace */}={}){ /* if(replace){ this.removeKey(model,'clip',index) } */ let keys = this.clipKeys.get(model) if(!keys){ keys = [] } let key = { time, //startTime dur, action: model.actions[actionIndex], speed, weight, } if(index == void 0)index = keys.length keys = [...keys.slice(0,index), key, ...keys.slice(index,keys.length)] this.clipKeys.set(model,keys) } addPathKey({model, time, index, dur, path/* , replace */ }={}){//what if path is deleted ? /* if(replace){ this.removeKey(model,'path',index) } */ let keys = this.pathKeys.get(model) if(!keys){ keys = [] } let key = { time, //startTime dur, path, } if(index == void 0)index = keys.length keys = [...keys.slice(0,index), key, ...keys.slice(index,keys.length)] this.pathKeys.set(model,keys) } addDescKey({model, time, index, dur, desc/* , replace */ }={}){ /* if(replace){ this.removeKey(model,'desc',index) } */ let keys = this.descKeys.get(model) if(!keys){ keys = [] } let key = { time, //startTime dur, desc, } if(index == void 0)index = keys.length keys = [...keys.slice(0,index), key, ...keys.slice(index,keys.length)] this.descKeys.set(model,keys) } save(){//for test, 注意保证每个模型名字不同 let data = {poseKeys:{}, clipKeys:{}} for(let [model, keys] of this.clipKeys){ data.clipKeys[model.name] = keys.map(key=>{ return { actionIndex: model.actions.indexOf(key.action), time: key.time, dur:key.dur, weight:key.weight, speed:key.speed } }) } for(let [model, keys] of this.poseKeys){ data.poseKeys[model.name] = keys.map(key=>{ return { qua: key.qua.toArray(), pos: key.pos.toArray(), scale:key.scale.toArray(), time: key.time, } }) } console.log(JSON.stringify(data)) return data } buildFromData(data){ if(typeof data == 'string'){ data = JSON.parse(data) } for(let name in data.poseKeys){ let model = viewer.objs.children.find(e=>e.name == name) if(!model){ console.warn('没找到pose模型',name) continue } let keys = data.poseKeys[name].map(e=>{ return { qua: new THREE.Quaternion().fromArray(e.qua), pos: new THREE.Vector3().fromArray(e.pos), scale: new THREE.Vector3().fromArray(e.scale), time: e.time } }) this.poseKeys.set(model,keys) } for(let name in data.clipKeys){ let model = viewer.objs.children.find(e=>e.name == name) if(!model){ console.warn('没找到clip模型',name) continue } let keys = data.clipKeys[name].map(e=>{ return { action: model.actions[e.actionIndex], time: e.time, dur: e.dur, weight:e.weight, speed:e.speed } }) this.clipKeys.set(model,keys) } } ifContainsModel(model){//动画帧里是否包含它 return [this.poseKeys, this.pathKeys, this.clipKeys].some((e)=>{ return e.has(model) }) } } /* function executeCrossFade( startAction, endAction, duration ) { // Not only the start action, but also the end action must get a weight of 1 before fading // (concerning the start action this is already guaranteed in this place) if ( endAction ) { setWeight( endAction, 1 ); endAction.time = 0; if ( startAction ) { // Crossfade with warping startAction.crossFadeTo( endAction, duration, true ); } else { // Fade in endAction.fadeIn( duration ); } } else { // Fade out startAction.fadeOut( duration ); } } function setWeight( action, weight ) { action.enabled = true; window.ani1 || action.setEffectiveTimeScale( 1 ); action.setEffectiveWeight( weight ); } */ /* autoActionSpeed(action){ //要获取这段时间走过的路程很难,还是延期吧. 而且一段动作要对应多个速度不同的位移,是不可能的。 return dis / dur } */ /* 动作自动计算步伐 幅度(weight)或 速度 timeScale * modelStepSizeRatio * weight = dis / dur 速度 每个模型的步长系数 幅度 如果人的bone attach 物品,物品就被add到人身上,需要click出物品 //测试:为人加物体, 需要先选中物品 let obj = viewer.modules.MergeEditor.selected viewer.objs.children.find(e=>e.name == 'Man.glb').skeletonHelper.bones[34].attach(obj); //左手骨和物品绑定 //viewer.objs.children.find(e=>e.name == 'Soldier.glb').skeletonHelper.bones[9].attach(obj); //右手骨和物品绑定 obj.updateMatrixWorld() obj.dispatchEvent({type:'position_changed',byControl:true }) obj.dispatchEvent({type:'rotation_changed',byControl:true }) 物体带动骨骼自动做动作 setAniIK */