从底层对象开始,看看Threejs如何利用图形学知识,通过各种数据对象构建场景,最终通过渲染器绘制出来。

先来看看最基础的Object3D对象,内容包含:

  • 官方demo引入: 主要对象分析
  • Object3D的属性: 位置、欧拉角、四元数、变换矩阵等
  • Object3D的变换: 以世界空间或模型空间为参考系的基础变换

本文所参考的Three.js版本为0.116.1

系列文章

一个例子

首先从Three.js官方README的示例入手,看下它所使用了哪些对象。

import * as THREE from 'js/three.module.js';

var camera, scene, renderer;
var geometry, material, mesh;

init();
animate();

function init() {
	// 创建一个透视相机,定义视口比例、近裁面与远裁面
	camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 10 );
	camera.position.z = 1;
	// 创建一个场景对象,存储需要渲染的模型、灯光等物体
	scene = new THREE.Scene();
	// 创建模型所需的几何体与材质属性
	geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
	material = new THREE.MeshNormalMaterial();

	mesh = new THREE.Mesh( geometry, material );
	scene.add( mesh );
	// 初始化渲染器,导出dom元素,为渲染做准备
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setSize( window.innerWidth, window.innerHeight );
	document.body.appendChild( renderer.domElement );

}

function animate() {
	requestAnimationFrame( animate );
	// 修改欧拉角的值来实现旋转
	mesh.rotation.x += 0.01;2
	mesh.rotation.y += 0.02;
	// 传入场景(世界空间)与相机(视口空间)执行渲染
	renderer.render( scene, camera );
}

看看上面所用的这些对象的内部继承关系

  • PerspectiveCamera => Camera => Object3D
  • Scene => Object3D
  • BoxGeometry => Geometry
  • MeshNormalMaterial => Meterial
  • Mesh => Object3D

可以看出,不管是交给render函数用来渲染的对象(scene, camera),还是在scene中添加的对象(mesh),本质上都是Object3D对象,下面来简单看下Object3D中都设置了哪些属性与方法。

Object3D

Object3D为框架中最核心的类,相机、模型等多个上层对象都继承自该类。它的属性与方法均具有一定的通用性,如空间变换、特性处理、矩阵计算等等。

主要属性

  • 数据相关: type(类型字符串)、uuid(唯一ID)、parent、children
  • 位置与变换相关: position(位置向量)、scale(缩放向量)、rotation(欧拉角)、quaternion(四元数)、up(上方向向量,用于lookAt时确定旋转姿态朝向的唯一性)、
  • 特性开关与自定义数据相关: visible(可视性)、castShadow&receiveShadow(产生与接收阴影)、frustumCulled(视锥剔除)、renderOrder(渲染优先级)、userData(用户自定义数据)
  • 矩阵相关: matrix(模型空间)、matrixWorld(世界空间)、modelViewMatrix(模型视图矩阵,用于shader中的位置计算)、normalMatrix(法向矩阵,用于shader中的光照计算,可通过模型视图矩阵计算得出)

变换

旋转

Object3D对象提供旋转方法修改旋转属性值两种方式来执行旋转操作:

  • 旋转方法:使用如mesh.rotateX(Math.PI/2)这样的旋转方法进行旋转,其内部的操作是构建新的四元数与当前姿态的四元数做乘法运算,物体当前姿态对应的四元数属性对象为quaternion
  • 修改旋转属性值:上面例子中的mesh.rotation.x += 0.01即为这种方式,即通过改变欧拉角的值来实现旋转,对应操作的属性对象为rotation,其中默认旋转顺规为XYZ(泰特布莱恩角),参考坐标系为世界坐标系(外旋)。

关于四元数与欧拉角可以看下之前的简单总结: 欧拉角、万向节死锁与四元数

Object3D的rotate相关方法,本质上是利用轴角+四元数的方式处理旋转,它提供了预置常规旋转轴单位向量的rotateX、rotateY等方便调用的方法,如下所示:

// src/core/Object3D.js
var _xAxis = new Vector3( 1, 0, 0 );
function Object3D() {
	rotateX: function ( angle ) {
		// 将内置的X旋转轴(模型空间)与指定角度传入按轴旋转方法
		return this.rotateOnAxis( _xAxis, angle );
	},
	// 通用轴角(axis-angle)旋转方法
	rotateOnAxis: function ( axis, angle ) {
		// axis为模型空间的旋转轴(应为单位向量),angle为旋转角度
		_q1.setFromAxisAngle( axis, angle );
		// 将构建的四元数与之前的四元数相乘
		this.quaternion.multiply( _q1 );
		return this;
	},
	quaternion._onChange( onQuaternionChange );
	function onQuaternionChange() {
		// 模型四元数变化的同时更新欧拉角,同样当欧拉角变化时也会更新四元数
		rotation.setFromQuaternion( quaternion, undefined, false );
	}
}

还有一点众所周知的是,使用四元数处理旋转可以避免欧拉角的万向节死锁问题。

为了直观的体现这两种方式的不同,看看下面这个例子:如果有两个方块分别执行了下面两种旋转,想象一下渲染出来会是什么结果?

  1. 绿色方块使用内置方法执行旋转:
    cube.rotateY((45 * Math.PI) / 180);
    cube.rotateX((90 * Math.PI) / 180);
    
  2. 蓝色方块修改rotation属性执行旋转:
    cube.rotation.y += (45 * Math.PI) / 180;
    cube.rotation.x += (90 * Math.PI) / 180;
    

结果:

图中坐标轴及颜色:x(红),y(绿), z(蓝)。背景的辅助线为世界坐标系,方块上的为各自的物体坐标系。

看看跟你想的是否一致,产生这样效果的原因是参考系不同:使用旋转方法时,内部会根据物体空间中的轴进行旋转,而rotation欧拉角的值则会在世界空间中进行处理。

使用xyz表示世界空间,XYZ表示物体空间。实际上两种操作是:

  1. 绿色方块:先绕Y轴旋转45度(Y轴初始方向与y轴重合,旋转后XZ的方向发生了变化),再绕X轴旋转90度(绕自身的X轴旋转90度看起来没有变化)
  2. 蓝色方块:先绕y轴旋转45度,再绕x轴旋转90度

下面来看看剩下两种变换:平移和缩放

平移

平移与旋转类似,既可以使用内置了参考轴的方法来执行平移操作,也可以通过修改操position位置向量的值来实现。

其中在物体空间平移的方法中利用四元数还原旋转姿态再进行平移:

// src/core/Object3D.js
var _xAxis = new Vector3( 1, 0, 0 );
function Object3D() {
	translateX: function ( distance ) {
		// 将内置的X旋转轴(模型空间)与平移距离传入按轴平移方法
		return this.translateOnAxis( _xAxis, distance );
	},
	// 通用按轴平移方法
	translateOnAxis: function ( axis, distance ) {
		// 与轴角旋转方法同理,axis为模型空间的旋转轴(应为单位向量)
		// 根据旋转轴与旋转姿态计算平移朝向的方向向量
		_v1.copy( axis ).applyQuaternion( this.quaternion );
		// 将方向向量乘以位移值并与position向量相加完成平移
		this.position.add( _v1.multiplyScalar( distance ) );
		return this;
	}
}

在平移的实现中会①先按照参考轴与当前姿态四元数计算物体朝向的方向向量,②再乘以距离并与原始位置相加得到最终平移的位置。

这里也给出一个例子:有两个方块先执行了一次旋转,接着通过两种方式进行了平移:

  1. 绿色方块使用内置方法执行旋转:
    cube.rotateX((-45 * Math.PI) / 180);
    cube.translateZ(3);
    cube.translateX(3);
    
  2. 蓝色方块修改rotation属性执行旋转:
    cube.rotateX((-45 * Math.PI) / 180);
    cube.position.z += 3;
    cube.position.x += 3;
    

结果:

缩放

Object3D中没有提供缩放操作的方法,仅能通过修改scale向量的属性值来实现。

cube.scale.copy(new Vector3(2,1,1));
// 或 cube.scale.x = 2;

其他

除了主要属性与变换相关方法之外,还包含一些其他方法:

  • 数据操作方法: add()、remove()、attach()、getObjectById()、getWorldPosition()等
  • 通用方法: copy()、clone()、toJSON()等
  • 变换矩阵更新方法: updateMatrix()(更新模型空间变换矩阵)、updateMatrixWorld()(更新世界空间变换矩阵)、updateWorldMatrix()(更新父子元素的模型或世界空间变换矩阵)等

这次就先到这里,下一步分析下构成模型的Geometry与Material对象。

参考