1
0

2 Коміти adb703f04a ... 5e09cb162f

Автор SHA1 Опис Дата
  xzw 5e09cb162f Merge branch '3dgs_Add_sh' of http://192.168.0.115:3000/bill/potree-sdk into 3dgs_Add_sh 1 місяць тому
  xzw d4f8c1b238 0 1 місяць тому
1 змінених файлів з 735 додано та 0 видалено
  1. 735 0
      src/custom/modules/Animation/AnimationEditor.js

+ 735 - 0
src/custom/modules/Animation/AnimationEditor.js

@@ -0,0 +1,735 @@
+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<startTime2){//开始前维持第一个动作 
+                    return 1
+                }else if(i == keys.length-1 && 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
+ */