import * as THREE from "../../../libs/three.js/build/three.module.js"; import { Utils } from "../../utils.js"; import math from "../../utils/math.js"; import {LineDraw} from "../../utils/DrawUtil.js"; import CurveCtrl from "../../objects/tool/CurveCtrl.js"; import HandleSvg from "../../objects/tool/HandleSvg.js"; import {/* transitions,*/ easing, lerp} from '../../utils/transitions.js' import { EventDispatcher } from "../../EventDispatcher.js"; const colors = { position: 'red', target : 'blue' } let lineMats const getLineMat = function(name){ if(!lineMats){ lineMats = { position: LineDraw.createFatLineMat({ color: colors.position, lineWidth: 3 }), target : LineDraw.createFatLineMat({ color: colors.target, lineWidth: 3 }), frustum: LineDraw.createFatLineMat({ color: colors.position, lineWidth: 2 }), aimAtTarget: new THREE.LineBasicMaterial({color:colors.target}) } } return lineMats[name] } export class CameraAnimation extends EventDispatcher{ constructor(viewer){ super(); this.viewer = viewer; this.selectedElement = null; //this.controlPoints = []; this.uuid = THREE.Math.generateUUID(); this.node = new THREE.Object3D(); this.node.name = "camera animation"; this.viewer.scene.scene.add(this.node); this.frustum = this.createFrustum(); this.node.add(this.frustum); this.name = "Camera Animation"; // "centripetal", "chordal", "catmullrom" this.curveType = "centripetal" this.visible = true; this.targets = []; this.createPath(); this.duration = 5; this.percent = 0; this.currentIndex = 0 this.durations = [] this.quaternions = []; if(!Potree.settings.isTest){ this.setVisible(false) } this.addEventListener('dispose', ()=>{ this.dispose() }) this.targetLines = new THREE.Object3D this.node.add(this.targetLines) } static defaultFromView(viewer){ const animation = new CameraAnimation(viewer); const camera = viewer.scene.getActiveCamera(); const target = viewer.scene.view.getPivot(); const cpCenter = new THREE.Vector3( 0.3 * camera.position.x + 0.7 * target.x, 0.3 * camera.position.y + 0.7 * target.y, 0.3 * camera.position.z + 0.7 * target.z, ); const targetCenter = new THREE.Vector3( 0.05 * camera.position.x + 0.95 * target.x, 0.05 * camera.position.y + 0.95 * target.y, 0.05 * camera.position.z + 0.95 * target.z, ); const r = 2//camera.position.distanceTo(target) * 0.3; //const dir = target.clone().sub(camera.position).normalize(); const angle = Utils.computeAzimuth(camera.position, target); const n = 5; for(let i = 0; i < n; i++){ let u = 1.5 * Math.PI * (i / n) + angle; const dx = r * Math.cos(u); const dy = r * Math.sin(u); const cpPos = new THREE.Vector3( cpCenter.x + dx, cpCenter.y + dy, cpCenter.z, ) const targetPos = new THREE.Vector3( targetCenter.x + dx * 0.1, targetCenter.y + dy * 0.1, targetCenter.z, ) animation.createControlPoint(null,{position:cpPos, target:targetPos}); } animation.changeCallback() return animation; } createControlPoint(index, posInfo ){ const length = this.posCurve.points.length const position = new THREE.Vector3 const target = new THREE.Vector3 if(index == void 0 ){ index = length; } if(!posInfo){ if(length >= 2 && index === 0){ const dir = new THREE.Vector3().subVectors(this.posCurve.points[0], this.posCurve.points[1] ) position.copy(this.posCurve.points[0]).add(dir); const tDir = new THREE.Vector3().subVectors(this.targets[0].position, this.targets[1].position ) target.copy(this.targets[0].position).add(dir); }else if(length >= 2 && index === length){ const dir = new THREE.Vector3().subVectors(this.posCurve.points[length-1], this.posCurve.points[length-2] ) position.copy(this.posCurve.points[length-2]).add(dir); const tDir = new THREE.Vector3().subVectors(this.targets[length-1].position, this.targets[length-2].position ) target.copy(this.targets[length-2].position).add(dir); }else if(length >= 2){ position.copy(this.posCurve.points[index-1].clone().add(this.posCurve.points[index]).multiplyScalar(0.5)); target.copy(this.targets[length-1].position.clone().add(this.targets[length]).multiplyScalar(0.5)); } }else{ position.copy(posInfo.position) target.copy(posInfo.target) } this.posCurve.addPoint(position, index/* , true */) //this.targetCurve.addPoint(target, index/* , true */) let targetSvg = new HandleSvg(target, colors.target) targetSvg.visible = this.visible this.targets = [...this.targets.slice(0,index), targetSvg, ...this.targets.slice(index,length)] if(this.useDurSlice){//不使用全局的duration,而是分段的 this.durations = [...this.durations.slice(0,index), posInfo.duration, ...this.durations.slice(index,length)] } this.dispatchEvent({ type: "controlpoint_added", index }); { let targetLine = LineDraw.createLine([position,target] ,{mat: getLineMat('aimAtTarget')}) this.targetLines.children = [...this.targetLines.children.slice(0,index), targetLine, ...this.targetLines.children.slice(index,length)] this.targets[index].addEventListener('dragged', (e)=>{ this.updatePathCallback() this.dragPointCallback(e) }) } } dragPointCallback(e){ let index = e.index if(e.index == void 0){ index = this.targets.indexOf(e.target) } LineDraw.moveLine(this.targetLines.children[index], [this.posCurve.points[index], this.targets[index].position] ) this.updateFrustum() } updatePathCallback(){ { this.quaternions = []; let length = this.posCurve.points.length; for(let i=0; ie.z *= -1) //因为得到的rotation是camera的,作用在物体上要反向,所以这里反向一下 //geometry.computeBoundingSphere();//? const line = LineDraw.createFatLine( positions, {material:getLineMat('frustum')}) //line.scale.set(20, 20, 20); line.visible = false return line; } reMapCurvePercent(){ //因在不同点在相同位置旋转,由于间隔仅和位置距离相关,导致时间间隔为0,采取重新调整间隔的策略。 var length = this.posCurve.points.length if(length<2){ return this.newPointsPercents = [] } var newPercents = [0]; if(this.useDurSlice){ //已经设定好了每一段的duration的话 let sums = [0] let sum = 0, last for(let i=0;i1 if( !math.closeTo(maxPercent, 1)){ let scale = 1 / maxPercent //需要压缩的比例 <1 这一步会让实际得到的间隔更小 newPercents = newPercents.map(e=> e*=scale ) } } this.newPointsPercents = newPercents; //console.log(newPercents) } at(originPercent, delta, transitionRatio){ originPercent = THREE.Math.clamp(originPercent, 0, 1) //修改第一层:起始时间 let percent = originPercent; /* const easePercent = 0.3; //缓动占比 //如果能在所有从静止到运动的中间加缓动就好了呀:lastPos * 0.9 + currentPos * 0.1 ? if(originPercent < easePercent){ console.log('easeIn') percent = easing.easeInSine(originPercent, 0, easePercent, easePercent) //currentTime, startY, wholeY, duration 选了一个衔接时接近斜率1的缓动函数 }else if(originPercent > 1-easePercent){ console.log('easeOut') percent = easing.easeOutSine(originPercent-(1-easePercent), 1-easePercent, easePercent, easePercent) } */ let quaternion if(percent < 1){ //修改第二层:使用每个点的重定位的 newPointsPercents this.currentIndex = this.newPointsPercents.findIndex(e=> e>percent ) - 1 //假设每个节点的百分比是精确的,那么: let curIndexPercent = this.newPointsPercents[this.currentIndex]; let nextIndexPercent = this.newPointsPercents[this.currentIndex+1]; let progress = (percent - curIndexPercent) / (nextIndexPercent - curIndexPercent)//在这两个节点间的百分比 //投影到原本的 posCurve.pointsPercent上: let curIndexOriPercent = this.posCurve.pointsPercent[this.currentIndex] let nextIndexOriPercent = this.posCurve.pointsPercent[this.currentIndex+1] percent = curIndexOriPercent + (nextIndexOriPercent - curIndexOriPercent) * progress let endQuaternion = this.quaternions[this.currentIndex+1] let startQuaternion = this.quaternions[this.currentIndex] quaternion = (new THREE.Quaternion()).copy(startQuaternion) lerp.quaternion(quaternion, endQuaternion)(progress) }else{ this.currentIndex = this.posCurve.points.length - 1; quaternion = math.getQuaFromPosAim(this.posCurve.points[this.currentIndex], this.targets[this.currentIndex].position) } const position = this.posCurve.getPointAt(percent); // 需要this.posCurve.points.length>1 否则报错 //console.log(this.currentIndex, originPercent) //缓动: var aimQua, aimPos; if(delta != void 0 ){ if(Potree.settings.tourTestCameraMove){ aimQua = this.frustum.quaternion.clone(); aimPos = this.frustum.position.clone(); }else{ var camera = viewer.scene.getActiveCamera(); aimQua = camera.quaternion.clone(); aimPos = camera.position.clone(); } transitionRatio = transitionRatio || 1 / Potree.settings.cameraAniSmoothRatio//渐变系数,越小缓动程度越高,越平滑 transitionRatio *= delta * 60 //假设标准帧率为60fps,当帧率低时(delta大时)要降低缓动 //console.log(transitionRatio, delta) //画面ui变化会使delta变大 transitionRatio = THREE.Math.clamp(transitionRatio, 0, 1); lerp.quaternion(aimQua, quaternion)(transitionRatio) //每次只改变一点点 lerp.vector(aimPos, position)(transitionRatio) }else{ aimQua = quaternion; aimPos = position } let rotation = new THREE.Euler().setFromQuaternion(aimQua ) const frame = { position: aimPos, rotation }; return frame; } set(percent){ this.percent = percent; } setVisible(visible){ this.node.visible = visible; this.posCurve.visible = visible this.targets.forEach(e=>e.visible = visible ) this.visible = visible; } setDuration(duration){ if(duration != this.duration){ this.duration = duration; if(this.quaternions.length == this.posCurve.points.length)this.reMapCurvePercent() } } getDuration(duration){ return this.duration; } play(startOptions={}){ if(this.onUpdate){ return console.error('已经开始播放') } let startPercent = 0, currentIndex = 0 if(startOptions.percent != void 0 ){ startPercent = startOptions.percent }else if(startOptions.index){ currentIndex = index //startPercent = index/(this.posCurve.points.length-1) startPercent = this.posCurve.pointsPercent[index] } //const tStart = performance.now(); const duration = this.duration; this.originalyVisible = this.visible; Potree.settings.tourTestCameraMove || this.setVisible(false); let tStart, startTransitionRatio = 0.2 let startDelay = 1/startTransitionRatio / 20 ;//因为缓动所以延迟开始,前面前都是at(0),使过渡到开始点位(但是依旧不能准确停在起始点,因为缓动是乘百分比有残留。所以直接平滑衔接到开始后的位置) let hasPlayedTime = 0 let finishDelay = Potree.settings.cameraAniSmoothRatio / 60 * 3//结束后还需要多久时间才能大致达到缓动的最终目标 let hasStoppedTime = 0 this.onUpdate = (e) => { if(this.posCurve.points.length<2){ if(this.posCurve.points.length == 1){ viewer.scene.view.position.copy(this.posCurve.points[0]); viewer.scene.view.rotation = new THREE.Euler().setFromQuaternion(this.quaternions[0]) } this.pause() return } let percent, transitionRatio if(tStart){ let tNow = performance.now(); let elapsed = (tNow - tStart) / 1000; percent = elapsed / duration + startPercent; }else{//从当前位置过渡到开始位置 percent = 0 hasPlayedTime += e.delta transitionRatio = startTransitionRatio; //console.log('延迟开始') if(hasPlayedTime > startDelay){ tStart = performance.now(); } } this.set(percent); const frame = this.at(percent, e.delta, transitionRatio); if(currentIndex != this.currentIndex){ currentIndex = this.currentIndex console.log('updateCurrentIndex', currentIndex) this.dispatchEvent({type:'updateCurrentIndex', currentIndex }) } if(!Potree.settings.tourTestCameraMove){ viewer.scene.view.position.copy(frame.position); //viewer.scene.view.lookAt(frame.target); viewer.scene.view.rotation = frame.rotation; } this.updateFrustum(frame) if(percent >= 1){ if(hasStoppedTime > finishDelay){ this.pause() }else{ hasStoppedTime += e.delta //console.log('延迟结束') } } }; this.viewer.addEventListener("update", this.onUpdate); } pause(){ this.setVisible(this.originalyVisible); this.viewer.removeEventListener("update", this.onUpdate); this.dispatchEvent('playDone') this.onUpdate = null } updateFrustum(frame){ const frustum = this.frustum; if(this.posCurve.points.length>1){ frustum.visible = true }else{ frustum.visible = false; return } frame = frame || this.at(this.percent); frustum.position.copy(frame.position); //frustum.lookAt(...frame.target.toArray()); frustum.rotation.copy(frame.rotation) } changeCallback(){ this.posCurve.update() //this.targetCurve.update() this.targets.forEach(e=>e.update()) this.updateFrustum() } dispose(){//add this.posCurve.dispose() //this.targetCurve.dispatchEvent({type:'dispose'}) this.targets.forEach(e=>e.dispose()) this.durations = [] this.node.parent.remove(this.node); } } //scene.removeCameraAnimation //修改:不使用targetCurve作为target曲线,因为播放时posCuve的节点和targetCurve并没有对应,且使用target的曲线会使角度变化大的情况过渡生硬。 // 改完旋转了。但是位置也有问题。速度完全和路程相关,当在同一位置设置多点时,这段的总时长为0. (是否要设置最小时长?不过也做不到 - -)