自转?公转?任意转!

511 阅读4分钟

本文正在参加「金石计划」 

众所周知,地球在自转,月球绕着地球公转。那么自转是否可以看做是一种特殊的公转? 特殊就在于,自转的旋转轴过自身的中心。

简单实现一下自转

照例,初始化一个场景,一个渲染器,相机,相机轨道控制器。

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) ;

通常的任意轴旋转

上面其实已经实现了绕任意轴旋转,方法就是套壳。 给要旋转的物体套一层壳,然后把壳移到旋转中心,再绕目标轴旋转即可。

通常的任意旋转,就是分三步。

  1. 将物体移到旋转中心 ;
  2. 绕目标轴旋转物体;
  3. 将物体移回原位;

用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上有一系列的旋转方法,这些方法最终都是调用rotateOnAxisrotateOnWorldAxis . 这两个方法非常简单,就三行代码,就是修改了四元数。

//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,但是没去更新旋转 、缩放、 位移,先位移到目标位置,然后通过调用旋转方法或者直接修改欧拉对象,再移回去,等等都是无用的。