22

深入学习Three.js核心对象之(一)Object3D | ¥ЯႭ1I0

 3 years ago
source link: https://yrq110.me/post/front-end/deep-in-threejs-core-objects-object3d/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入学习Three.js核心对象之(一)Object3D

2020年5月6日

从底层对象开始,看看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;
    
rotate.png

图中坐标轴及颜色: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;
    
translate.png

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

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

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

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

这次就先到这里,下次分析下继承自Object3D的其他上层对象。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK