Object3D:lookAt方法

1,258 阅读5分钟

Object3D:lookAt方法

1. lookAt示例

官方文档中给出了lookAt方法的作用:

Rotates the object to face a point in world space.

即,旋转对象面向世界空间中的一个点。

但是,则个面向是指什么?我们先通过一个例子有一个直观的感性认识。

example-1.JPG

这个例子改编自官方文档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()));
example-2.JPG

这和我们上面提到的,让Z轴对准一个点不冲突,但是问题来了,这个锥体的”尖尖“不在Z轴上?

变换是对空间的变换

平移、旋转和缩放三种变换,可以理解为是对局部空间的变换。举个直观的例子,在没有进行变换时,子对象的局部空间和父对象的局部空间是重合的,相对于父对象的局部空间来说,子对象的局部空间的原点为Olocal(0,0,0){O}_{local}(0, 0, 0),X轴上有一点为Xlocal(1,0,0)X_{local} (1, 0, 0),Y轴上有一点为Ylocal(0,1,0) Y_{local}(0, 1, 0),Z轴上有一点为Zlocal(0,0,1) Z_{local}(0, 0, 1)

经过子对象的局部变换矩阵进行局部变换后,上面提到的四个点分别变换到了OparentO_{parent}(变换后的子对象局部空间的原点),Xparent{X}_{parent}(变换后的子对象局部空间的X轴上的一点),Yparent{Y}_{parent}Zparent{Z}_{parent}。通过上面的四个点,我们可以”画“出来变换后的”坐标系“。也就是说,我们对“空间”进行了一次变换。

理解了这一点,我们再回头看上面的例子,因为这个锥体最初在放到局部空间时,”尖尖“对准的就是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

再回头看这部分代码,对于cameralight来说,它们会用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 );

example-3.JPG

Three.js中,相机”看向“Z的负方向,通过lookAt,把Z轴的负方向对准目标。对于不是CameraLight的对象,则是把Z轴的正方向对准目标。

  1. 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;
example-4.JPG

可以看到,调用lookAtZ轴没有对准target

4. 总结

Geometry中的变换,变换的是模型在空间中的位置;Object3D中的变换,变换的是局部空间。

lookAt对于不同类型的对象有一定区别,CameraLight把局部空间Z轴的负方向对准目标;其他对象把局部空间Z轴的正方向对准目标。

最后,介绍了lookAt会在父对象缩放不统一的时候,发生错误。