Object3D:lookAt方法
1. lookAt示例
官方文档中给出了lookAt
方法的作用:
Rotates the object to face a point in world space.
即,旋转对象面向世界空间中的一个点。
但是,则个面向是指什么?我们先通过一个例子有一个直观的感性认识。
这个例子改编自官方文档three.js examples,为了方便观察,我减少了对象的数量,同时给每个对象添加了AxesHelper
以观察他们自身坐标空间的三个坐标轴。红色为x轴,绿色为y轴,蓝色为z轴。可以看到,对lookAt
的直观感受就是,旋转自己,让自己的Z
轴对准一个世界空间中的点。
我们自己也尝试着写一下。
const scene = new THREE.Scene();
const cylinderGeometry = new THREE.CylinderGeometry( 0, 2, 6, 12 );
const material = new THREE.MeshNormalMaterial();
const cylinder = new THREE.Mesh(cylinderGeometry, material);
const sphereGeometry = new THREE.SphereGeometry(3)
const sphere = new THREE.Mesh(sphereGeometry, material);
sphere.translateOnAxis(new Vector3(1, 1, 1), 10);
scene.add(cylinder);
scene.add(sphere);
cylinder.lookAt(sphere.getWorldPosition(new THREE.Vector3()));
这和我们上面提到的,让Z
轴对准一个点不冲突,但是问题来了,这个锥体的”尖尖“不在Z
轴上?
变换是对空间的变换
平移、旋转和缩放三种变换,可以理解为是对局部空间的变换。举个直观的例子,在没有进行变换时,子对象的局部空间和父对象的局部空间是重合的,相对于父对象的局部空间来说,子对象的局部空间的原点为,X轴上有一点为,Y轴上有一点为,Z轴上有一点为
经过子对象的局部变换矩阵进行局部变换后,上面提到的四个点分别变换到了(变换后的子对象局部空间的原点),(变换后的子对象局部空间的X轴上的一点),,。通过上面的四个点,我们可以”画“出来变换后的”坐标系“。也就是说,我们对“空间”进行了一次变换。
理解了这一点,我们再回头看上面的例子,因为这个锥体最初在放到局部空间时,”尖尖“对准的就是Y
轴,因此,在我们对局部空间进行变换后,”尖尖“对准的仍然是Y
轴。问题找到了,解决办法就是:在一开始往局部空间”放“对象时,就把”尖尖“对准Z
轴。
Geometry中的position
在Geometry
中,atrributes.position
存储了一个对象的顶点信息,CylinderGeometry
在具体实现时,就把”尖尖“对准了Y
,我们要做的,就是把这些顶点进行一次变换,以改变它们在局部空间的位置。我们想要的变换很简单,就是把它绕着X
轴转Math.PI/2
。
cylinderGeometry.rotateX(Math.PI/2);
2. 细节
有一个问题我们还没有讨论,上面对lookAt
的描述,没有提X
轴和Y
轴,这两个轴是怎么确定的?这就要深入到代码中了。
我们只了解流程,关于一些影响阅读的部分,会进行删减
lookAt( x, y, z ) {
// This method does not support objects having non-uniformly-scaled parent(s)
if ( x.isVector3 ) {
_target.copy( x );
} else {
_target.set( x, y, z );
}
// 将要对准的点存入target
const parent = this.parent;
// 需要获取”自己“的世界坐标,所以要更新一次世界坐标
this.updateWorldMatrix( true, false );
// 获取世界坐标
_position.setFromMatrixPosition( this.matrixWorld );
// 通过Matrix4.lookAt构建旋转矩阵
if ( this.isCamera || this.isLight ) {
_m1.lookAt( _position, _target, this.up );
} else {
_m1.lookAt( _target, _position, this.up );
}
// 设置旋转变换
this.quaternion.setFromRotationMatrix( _m1 );
//...
}
lookAt
通过自身的世界坐标和目标的世界坐标,构建了一个旋转矩阵,构建旋转矩阵的过程中,调用了Matrix4.lookAt
,我们来看这个方法:
lookAt( eye, target, up ) {
const te = this.elements;
_z.subVectors( eye, target ); //z轴向量通过两个点的差获得
_z.normalize(); // 把z向量单位化
_x.crossVectors( up, _z ); // up向量和z向量叉乘得到x向量
_x.normalize();
_y.crossVectors( _z, _x ); // z向量和x向量叉乘得到y向量
// 构建旋转矩阵
te[ 0 ] = _x.x; te[ 4 ] = _y.x; te[ 8 ] = _z.x;
te[ 1 ] = _x.y; te[ 5 ] = _y.y; te[ 9 ] = _z.y;
te[ 2 ] = _x.z; te[ 6 ] = _y.z; te[ 10 ] = _z.z;
return this;
}
这里我们就看到了通过对象自身的位置以及目标的位置,我们可以找到Z
轴,对于另外两个轴,我们还需要通过指定一个up
向量,通过向量叉乘得到其他的轴。
Camera和Light的lookAt
再回头看这部分代码,对于camera
和light
来说,它们会用Z
的负方向对准目标。
if ( this.isCamera || this.isLight ) {
_m1.lookAt( _position, _target, this.up );
} else {
_m1.lookAt( _target, _position, this.up );
}
我们再在上面的例子中添加一个camera
:
const camera = new THREE.PerspectiveCamera();
camera.translateOnAxis(new Vector3(-1, 2, 3), 10);
scene.add(camera);
camera.lookAt(sphere.getWorldPosition(new THREE.Vector3()));
const camreaHelper = new THREE.CameraHelper(camera);
scene.add(camreaHelper);
const cameraAxesHelper = new THREE.AxesHelper( 100 );
camera.add( cameraAxesHelper );
Three.js中,相机”看向“Z
的负方向,通过lookAt
,把Z
轴的负方向对准目标。对于不是Camera
和Light
的对象,则是把Z
轴的正方向对准目标。
- lookAt出错的情况
官方文档对lookAt
的介绍中有一句话
This method does not support objects having non-uniformly-scaled parent(s).
如果对象的父对象的三个坐标轴缩放变换”不统一“,则会出错。
对一开始的例子做一下修改,先把调用lookAt
的对象,放到一个Group
中:
const group = new THREE.Group();
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xffffff );
const geometry = new THREE.CylinderGeometry( 0, 10, 100, 12 );
geometry.rotateX( Math.PI / 2 );
const material = new THREE.MeshNormalMaterial();
scene.add(group);
for ( let i = 0; i < 5; i ++ ) {
const mesh = new THREE.Mesh( geometry, material );
mesh.position.x = Math.random() * 4000 - 2000;
mesh.position.y = Math.random() * 4000 - 2000;
mesh.position.z = Math.random() * 4000 - 2000;
mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 4 + 2;
group.add( mesh );
const axesHelper = new THREE.AxesHelper( 1000 );
mesh.add( axesHelper );
}
现在对group
进行缩放变换:
group.scale.x = 1;
group.scale.y = 2;
group.scale.z = 3;
可以看到,调用lookAt
后Z
轴没有对准target
。
4. 总结
Geometry
中的变换,变换的是模型在空间中的位置;Object3D
中的变换,变换的是局部空间。
lookAt
对于不同类型的对象有一定区别,Camera
和Light
把局部空间Z
轴的负方向对准目标;其他对象把局部空间Z
轴的正方向对准目标。
最后,介绍了lookAt
会在父对象缩放不统一的时候,发生错误。