本文正在参加「金石计划」
众所周知,地球在自转,月球绕着地球公转。那么自转是否可以看做是一种特殊的公转? 特殊就在于,自转的旋转轴过自身的中心。
简单实现一下自转
照例,初始化一个场景,一个渲染器,相机,相机轨道控制器。
const textureLoader = new TextureLoader();
const scene = new Scene();
const [vw, vh] = [window.innerWidth - 20, window.innerHeight - 20]
const camera = new PerspectiveCamera(75, vw / vh, 0.1, 1000); // 透视相机除了远近,只需要视角 和宽高比
const renderer = new WebGLRenderer({ antialias: true });
camera.position.set( 0,0,-20);
const orbitControl =new OrbitControls(camera, renderer.domElement);
renderer.setSize(vw, vh);
document.body.appendChild(renderer.domElement)
然后,来一个球形几何体,地球大些,月球小些。地球蓝色,月球白色。 等有了贴图再换上。
这里使用了phong材质,开启了flatshading, 因为没有贴图,这样便于观察旋转。
用了光照模型自然就要打光,一个点光源,加上环境光。
const spheregeo = new SphereGeometry(), planeGeo = new PlaneGeometry(10, 10);
const material3 = new MeshPhongMaterial({ flatShading:true });// 朗博 无光泽,就是无镜面高光
// 上色
const earth = new Mesh( spheregeo.clone(), material3.clone()) ;
earth.material.color.set('#3388ff') ;
earth.geometry.scale(3,3,3) ;
const moon = new Mesh( spheregeo, material3) ;
moon.position.set(10,0,0) ;
earth.add(moon) ;
// 光
const sun = new PointLight() , ambli = new AmbientLight(0xffffff, .2);
sun.position.set( 3,0, 10);
scene.add(earth, sun,ambli) ;
最后,在渲染函数里让地球转起来。
function render(){
renderer.render(scene, camera) ;
requestAnimationFrame(render)
earth.rotateY(.01);
}
render();
可以看到,不仅地球自转了,似乎还带着月球一起公转了。
但是和实际情况不符,现在地球和月亮的角度速度是一样的,这就导致在地球上的人和月亮是相对静止的。
简单实现一下公转
简单一点的办法就是,再套一层。在地球带这个层转动的同时,给这个层再加一个转动的速度。这样,月亮最终的角速度肯定比地球快了。
const track = new Group() ;
track.add(moon);
moon.position.set(10,0,0) ;
earth.add(track) ;
通常的任意轴旋转
上面其实已经实现了绕任意轴旋转,方法就是套壳。 给要旋转的物体套一层壳,然后把壳移到旋转中心,再绕目标轴旋转即可。
通常的任意旋转,就是分三步。
- 将物体移到旋转中心 ;
- 绕目标轴旋转物体;
- 将物体移回原位;
用threejs 实现的话大概就是下面这个样子;为了实现效果,需要把moon.从地球里面拿出来,这样就不受地球的影响了。
//删掉前面部分代码 直接把moon加入场景。
scene.add(earth, sun,moon,ambli) ;
......
const matRotate = new Matrix4(),matTranslate1 = new Matrix4(),matTranslate2 = new Matrix4() ;
matRotate.makeRotationY(.1) ;
matTranslate1.makeTranslation(0,0,0) ;
matTranslate2.makeTranslation(0,0,0) ;
//render函数里
moon.applyMatrix4(matTranslate1);
moon.applyMatrix4(matRotate);
moon.applyMatrix4(matTranslate2);
可以看到,结果和上面没有什么两样。 这里的两次位移都是0 ,这是因为,月亮原本的基点就是 (0,0,0),并不会因为修改了position 就变化了。 如果不使用矩阵,在设置了position之后,再旋转,似乎基点就变了,真的是这样吗? 且看下文
Object3D的方法为什么都是自转
Object3D
上有一系列的旋转方法,这些方法最终都是调用rotateOnAxis
和 rotateOnWorldAxis
. 这两个方法非常简单,就三行代码,就是修改了四元数。
//Object3D.js
rotateOnAxis( axis, angle ) {
// rotate object on axis in object space
// axis is assumed to be normalized
_q1.setFromAxisAngle( axis, angle );
this.quaternion.multiply( _q1 );
return this;
}
rotateOnWorldAxis( axis, angle ) {
// rotate object on axis in world space
// 旋转轴必须归一化,也就是单位向量
// method assumes no rotated parent 假定此物体的父级是没有旋转的
_q1.setFromAxisAngle( axis, angle );
this.quaternion.premultiply( _q1 );
return this;
}
也就是说,所有的方法最终都只是修改了四元数而已, 要知道,最终输入到着色器里的是矩阵,而不是四元数。
至于直接修改 rotation
,实际上这里等于是做了一个双向绑定, 欧拉对象变化时会去更新四元数,四元数变化时也会去更新欧拉对象。
//Object3D.js
function onRotationChange() {
quaternion.setFromEuler( rotation, false );
}
function onQuaternionChange() {
rotation.setFromQuaternion( quaternion, undefined, false );
}
rotation._onChange( onRotationChange );
quaternion._onChange( onQuaternionChange );
继续下去,我们再来看看three的更新矩阵的方法。render的时候会更新世界矩阵,调用updateMatrixWorld
,这里又会去先更新自身的矩阵updateMatrix
。
//Object3D.js
updateMatrix() {
this.matrix.compose( this.position, this.quaternion, this.scale );
this.matrixWorldNeedsUpdate = true;
}
很明显,就是更新的时候,把位置,旋转,缩放,组合起来成为一个矩阵,并且其顺序是 位移最后。 这就导致,对于每一个物体,他们都是先以中心基点,为旋转中心和缩放中心,进行缩放和旋转,最后才位移。
所以,通过调用Object3D的旋转方法,只能是自转 。
结束
虽然,这里有通用的任意旋转的方法,但是也许套壳实现更方便呢, 使用什么视具体业务而定 。
明白了three的更新机制,就不会去做无用功了。比如直接修改object3D.matrix,但是没去更新旋转 、缩放、 位移,先位移到目标位置,然后通过调用旋转方法或者直接修改欧拉对象,再移回去,等等都是无用的。