/** * @author alteredq / http://alteredqualia.com/ */ THREE.PathControls = function ( object, domElement ) { this.object = object; this.domElement = ( domElement !== undefined ) ? domElement : document; this.id = "PathControls" + THREE.PathControlsIdCounter ++; // API this.duration = 10 * 1000; // milliseconds; // 运动时间 this.waypoints = [];// 轨道上的关键点,一般都是轨道上的转折点,因为非转折点一般可以通过插值算出来 this.useConstantSpeed = true;// 是否匀速 this.resamplingCoef = 50; this.debugPath = new THREE.Object3D();// 调试线,有了它可以判断相机是否按照预定轨道来行进 this.debugDummy = new THREE.Object3D(); this.animationParent = new THREE.Object3D(); this.lookSpeed = 0.005;// 转头速度 // 下面两个表示相机是否可以上下、左右摆动 this.lookVertical = true; this.lookHorizontal = true; this.verticalAngleMap = { srcRange: [ 0, 2 * Math.PI ], dstRange: [ 0, 2 * Math.PI ] }; this.horizontalAngleMap = { srcRange: [ 0, 2 * Math.PI ], dstRange: [ 0, 2 * Math.PI ] }; // internals this.target = new THREE.Object3D(); this.mouseX = 0; this.mouseY = 0; this.lat = 0; this.lon = 0; this.phi = 0; this.theta = 0; var PI2 = Math.PI * 2; // 在渲染时候,为了计算渲染的视图大小、鼠标所在位置等,我们需要提前知道视图大小; // 这里讲视图大小不能出在viewHalfX和viewHalfY变量中,这样当鼠标移动的时候, // 我们就可以知道鼠标在窗口中的相对位置了,这样可以根据鼠标来控制相机 this.viewHalfX = 0; this.viewHalfY = 0; if ( this.domElement !== document ) { this.domElement.setAttribute( 'tabindex', -1 ); } // methods this.handleResize = function () { if ( this.domElement === document ) { this.viewHalfX = window.innerWidth / 2; this.viewHalfY = window.innerHeight / 2; } else { this.viewHalfX = this.domElement.offsetWidth / 2; this.viewHalfY = this.domElement.offsetHeight / 2; } }; this.update = function ( delta ) { var srcRange, dstRange; if( this.lookHorizontal ) this.lon += this.mouseX * this.lookSpeed * delta; if( this.lookVertical ) this.lat -= this.mouseY * this.lookSpeed * delta; this.lon = Math.max( 0, Math.min( 360, this.lon ) ); this.lat = Math.max( - 85, Math.min( 85, this.lat ) ); this.phi = THREE.Math.degToRad( 90 - this.lat ); this.theta = THREE.Math.degToRad( this.lon ); this.phi = normalize_angle_rad( this.phi ); // constrain vertical look angle srcRange = this.verticalAngleMap.srcRange; dstRange = this.verticalAngleMap.dstRange; var tmpPhi = THREE.Math.mapLinear( this.phi, srcRange[ 0 ], srcRange[ 1 ], dstRange[ 0 ], dstRange[ 1 ] ); var tmpPhiFullRange = dstRange[ 1 ] - dstRange[ 0 ]; var tmpPhiNormalized = ( tmpPhi - dstRange[ 0 ] ) / tmpPhiFullRange; this.phi = QuadraticEaseInOut( tmpPhiNormalized ) * tmpPhiFullRange + dstRange[ 0 ]; // constrain horizontal look angle srcRange = this.horizontalAngleMap.srcRange; dstRange = this.horizontalAngleMap.dstRange; var tmpTheta = THREE.Math.mapLinear( this.theta, srcRange[ 0 ], srcRange[ 1 ], dstRange[ 0 ], dstRange[ 1 ] ); var tmpThetaFullRange = dstRange[ 1 ] - dstRange[ 0 ]; var tmpThetaNormalized = ( tmpTheta - dstRange[ 0 ] ) / tmpThetaFullRange; this.theta = QuadraticEaseInOut( tmpThetaNormalized ) * tmpThetaFullRange + dstRange[ 0 ]; var targetPosition = this.target.position, position = this.object.position; targetPosition.x = 100 * Math.sin( this.phi ) * Math.cos( this.theta ); targetPosition.y = 100 * Math.cos( this.phi ); targetPosition.z = 100 * Math.sin( this.phi ) * Math.sin( this.theta ); this.object.lookAt( this.target.position ); }; this.onMouseMove = function ( event ) { if ( this.domElement === document ) { this.mouseX = event.pageX - this.viewHalfX; this.mouseY = event.pageY - this.viewHalfY; } else { this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX; this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY; } }; // utils function normalize_angle_rad( a ) { var b = a % PI2; return b >= 0 ? b : b + PI2; }; function distance( a, b ) { var dx = a[ 0 ] - b[ 0 ], dy = a[ 1 ] - b[ 1 ], dz = a[ 2 ] - b[ 2 ]; return Math.sqrt( dx * dx + dy * dy + dz * dz ); }; function QuadraticEaseInOut ( k ) { if ( ( k *= 2 ) < 1 ) return 0.5 * k * k; return - 0.5 * ( --k * ( k - 2 ) - 1 ); }; function bind( scope, fn ) { return function () { fn.apply( scope, arguments ); }; }; function initAnimationPath( parent, spline, name, duration ) { var animationData = { name: name, fps: 0.6, length: duration, hierarchy: [] }; var i, parentAnimation, childAnimation, path = spline.getControlPointsArray(), sl = spline.getLength(), pl = path.length, t = 0, first = 0, last = pl - 1; // 将path路径中的所有点,包括开始点和结束点都转换成为一个个关键帧, // 将这些关键帧存到自定义的parentAnimation变量中,关键帧包括相机的位置、缩放、旋转参数等, // 将这些关键帧组成的动作,通过THREE.AnimationHandler的add函数,加到动画引擎中; // 动画引擎负责在关键帧之间插值,来决定关键帧中间的相机的位置、大小、缩放等。 parentAnimation = { parent: -1, keys: [] }; parentAnimation.keys[ first ] = { time: 0, pos: path[ first ], rot: [ 0, 0, 0, 1 ], scl: [ 1, 1, 1 ] }; parentAnimation.keys[ last ] = { time: duration, pos: path[ last ], rot: [ 0, 0, 0, 1 ], scl: [ 1, 1, 1 ] }; for ( i = 1; i < pl - 1; i++ ) { // real distance (approximation via linear segments) t = duration * sl.chunks[ i ] / sl.total; // equal distance //t = duration * ( i / pl ); // linear distance //t += duration * distance( path[ i ], path[ i - 1 ] ) / sl.total; parentAnimation.keys[ i ] = { time: t, pos: path[ i ] }; } animationData.hierarchy[ 0 ] = parentAnimation; // Animationhandler已经改名为AnimationMixer THREE.AnimationHandler.add( animationData ); return new THREE.Animation( parent, name, THREE.AnimationHandler.CATMULLROM_FORWARD, false ); }; function createSplineGeometry( spline, n_sub ) { var i, index, position, geometry = new THREE.Geometry(); for ( i = 0; i < spline.points.length * n_sub; i ++ ) { index = i / ( spline.points.length * n_sub ); position = spline.getPoint( index ); geometry.vertices[ i ] = new THREE.Vector3( position.x, position.y, position.z ); } return geometry; }; function createPath( parent, spline ) { var lineGeo = createSplineGeometry( spline, 10 ), particleGeo = createSplineGeometry( spline, 10 ), lineMat = new THREE.LineBasicMaterial( { color: 0xff0000, line 3 } ), lineObj = new THREE.Line( lineGeo, lineMat ), particleObj = new THREE.ParticleSystem( particleGeo, new THREE.ParticleBasicMaterial( { color: 0xffaa00, size: 3 } ) ); lineObj.scale.set( 1, 1, 1 ); parent.add( lineObj ); particleObj.scale.set( 1, 1, 1 ); parent.add( particleObj ); var waypoint, geo = new THREE.SphereGeometry( 1, 16, 8 ), mat = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); for ( var i = 0; i < spline.points.length; i ++ ) { waypoint = new THREE.Mesh( geo, mat ); waypoint.position.copy( spline.points[ i ] ); parent.add( waypoint ); } }; this.init = function ( ) { // constructor this.spline = new THREE.Spline();// 定义了一条显示路径的线 this.spline.initFromArray( this.waypoints );// 将关键点复制到线中去 if ( this.useConstantSpeed ) { // 如果相机速度时恒定的,对spline进行重采样 // 比如三个点生成一条直线,那么在转折角处,一定会出现速度不恒定情况, // 为解决这个问题,让相机在路径上基本都是匀速运行,我们可以根据三个点拟合出一条曲线, // 然后根据采样大小,将拟合后的点重新放入spline中,新采样点一定会比之前多 // 通过参数开控制采样点多少,参数越小采样点越多 this.spline.reparametrizeByArcLength( this.resamplingCoef ); } if ( this.createDebugDummy ) {// 是否绘制一直物体,调试作用 var dummyParentMaterial = new THREE.MeshLambertMaterial( { color: 0x0077ff } ), dummyChildMaterial = new THREE.MeshLambertMaterial( { color: 0x00ff00 } ), dummyParentGeo = new THREE.CubeGeometry( 10, 10, 20 ), dummyChildGeo = new THREE.CubeGeometry( 2, 2, 10 ); this.animationParent = new THREE.Mesh( dummyParentGeo, dummyParentMaterial ); var dummyChild = new THREE.Mesh( dummyChildGeo, dummyChildMaterial ); dummyChild.position.set( 0, 10, 0 ); this.animation = initAnimationPath( this.animationParent, this.spline, this.id, this.duration ); this.animationParent.add( this.object ); this.animationParent.add( this.target ); this.animationParent.add( dummyChild ); } else { this.animation = initAnimationPath( this.animationParent, this.spline, this.id, this.duration ); this.animationParent.add( this.target ); this.animationParent.add( this.object ); } if ( this.createDebugPath ) { createPath( this.debugPath, this.spline ); } this.domElement.addEventListener( 'mousemove', bind( this, this.onMouseMove ), false ); }; this.handleResize(); }; THREE.PathControlsIdCounter = 0;