本人前端开发并有一些3D建模和Unity经验。分享一些我个人在3D方面的经验与理解。本文并不会过多教你如何使用Threejs的API,仅向没有 3D 经验前端同学分享一些 3D 的基本概念,并初步了解 threejs。希望这篇文章能让对这方面感兴趣的同学在学习 threejs 时能有一些基础的知识,能更容易阅读 threejs 文档。
3D 基本概念
坐标系
先从三维空间最基本的坐标系说起,相比平面坐标系,空间坐标系多了一个 z 轴,任意 xyz 的值能确定空间中的一点。空间坐标系有两种,左手坐标系和右手坐标系。在threejs中使用的是右手坐标系,可以想象在屏幕上y 轴向上,x 轴向右。屏幕与xoy 平面重合,那么z轴就垂直屏幕指向屏幕外。
这里例举在空间中常用的三种坐标系:
- 世界坐标系
- 物体坐标系
- 摄像机坐标系
世界坐标系
世界坐标系用于描述物体在空间中的绝对位置,其他坐标系都会基于世界坐标系定位。确定一个世界坐标系才能描述两个物体之间的位置关系。
物体坐标系
物体坐标系是一个很重要的概念,场景中任何一个物体都有其物体坐标系。顾名思义,物体坐标系是物体本身的坐标系,其坐标系原点可以在物体任何位置,如物体中心、物体某个顶点,甚至在物体外。物体坐标系的原点在物体什么位置都能在建模软件中设置。如下图,一个边长为1的立方体。立方体物体坐标系原点与其本身Q点重合,那么P点在其自身坐标系中的坐标是 (1, 1, 1) 。 不难看出,物体坐标系的 x 、y 、z轴方向能描述物体在一个场景中的前后左右、上下朝向。
相机坐标系
相机是观察者,自然也是场景中的物体。因此相机坐标系可以看作一个特殊的物体坐标系, *****xoy *平面与屏幕平行,z轴方向就是摄像机的观察方向。
坐标
在空间中x 、y 、z 可定位一个点,x 、y 、z是点的坐标值。一个空间中的某一个物体位置是确定的,但其实际坐标值取决于你选择的坐标系。 常见坐标有如下:
- 世界坐标(对应世界坐标系)
- 局部坐标(对应物体坐标系)
世界坐标
世界坐标指场景中某一点相对于世界坐标系原点的位置。无论这个点在什么地方或什么其他坐标系内,这个点总是有一个唯一确定的世界坐标。
局部坐标
局部坐标指一个点在某个物体坐标系中的坐标。物体坐标系可以通过旋转平移与世界坐标系重合,局部坐标和世界坐标之间有转换关系。可以用平面坐标系简单理解世界坐标系和物体坐标系的关系。如下图,有一棵倾斜的树,其本身的物体坐标系原点在树根。树上这片树叶在树本身坐标系中坐标是 *(x2, y2) *,但在世界坐标系中坐标是 *(x1, y2) *。这片树叶没有发生位移,但它在不同坐标系中的坐标可能不同。
局部坐标对 3D 对象的嵌套有着十分重要的意义。3D 对象有着可多层嵌套的关系,在一个 3D 对象中其子物体的局部坐标是基于父物体的本身的坐标系(物体坐标系)而定位的。
场景(空间)
所有的3D物体都需要在一个场景中,有空间才能有物体。场景可以理解为一个空的,无限大的容器。这个容器里只有一个你看不见的世界坐标原点及 3 个坐标轴。可以在场景里放置任意的物体、灯光、摄像机。实际的一个应用或游戏中可以有多个场景,不同的场景会放置不同的物体、光照等。在游戏中所看到的"加载进度"其实就是在加载不同的场景实例。
几何体
几何体是数学模型,描述了一组点及其拓扑关系。几何体只是一些数据,不能直接将几何体视为可见的3D对象。几何体描述了一个物体的顶点、形状,是一个3D对象必要的构成部分。
点
一个点的坐标只需 xyz 三个值就能表示。点的位置在场景中固定,其坐标值因坐标系不同而可能不一样。
线
两个点能确定一条线段, 需要两组xyz值才能描述一条线段。同样地,一条线段在场景中的位置也是固定的,端点的坐标也会因坐标系不同而可能不一样。
面
确定一个面至少需要三个点,在计算机3D图形中最基本的面都是三角形。因为在空间中直接由三个以上点围成的图形不一定是个平面,这些点很有可能不在一个平面。如下图,四个点按顺序连接构成的图形可能是一个空间四边形。
立体多边形
三角形是最基本的平面,可使用多个三角形组成其他平面多边形或者立体多边形。多个三角形组成封闭的立体图形就近似我们真实世界所熟知的"物体"。比如正方体、棱锥体、棱柱体等。
曲面
计算机3D图形中曲面只是一个概念,实际图形中没有真正的曲面。渲染出的曲面都是许多小平面组成的一个近似曲面的立体多边形经过平滑处理成的。当多边形细分越多,就越接近其所模拟的曲面。创建越多的三角面就越耗费性能,所以一款游戏的模型精度也相当程度地反应了其质量和对硬件配置的要求。
空间向量
空间向量即三维向量,有a 、b 、c 三个分量。三维向量可用来表示任何三个维度的信息,如空间坐标、颜色RGB、物体分别在x 、y 、z三个轴向上的旋转或缩放等。三维向量在计算机3D中有着重要的运用,如平面法向量可以用来指明平面的正面和反面。
材质
材质是一组数据的集合,不是实际物体。材质决定物体的颜色(漫反射颜色)、反射颜色、光泽度、透明度、折射率等性质。在物体参与渲染时,材质将决定物体部分渲染效果。材质常见属性如下:
- color(颜色)
- map (贴图)
- alphaMap(透明度贴图)
- side (渲染面)
单面和双面材质
我们已知所有的立体图形都是由很多面构成的,其实这些立体图形就是一个壳。所以材质默认只会渲染平面的一侧的面以保证性能,面的另一侧就是透明的(在游戏中角色落入物体内部便可见到这种效果)。材质渲染面有如下三种类型:
- 双面渲染
- 仅渲染正面
- 仅渲染背面面
在某些平面两侧都会被观察到的情况可以启用双面材质,如游戏角色的片状衣服、树叶、草等这种非封闭的图形。角色如果只在某个密室内活动,那么可以在一个盒子上使用背面材质,从外部能看到盒子内部,但从盒子内部就只能看到材质的渲染效果。如下图,两个盒子内各放了一个红色小球,分别使用了正反面材质渲染。
材质贴图
贴图是材质的属性之一,当材质设置了位图做为其贴图后,位图上的颜色会以一定的方式来映射到物体的各个面上,看起来就像将图片贴到了物体上。比起简单给材质设置颜色,贴图的方式能给一个面的不同部分映射上不同的颜色,实现更丰富的效果。
UV映射贴图
图片是一个矩形,作为贴图有其自己坐标,水平方向是U ,垂直方向是V 。两个轴的坐标值只会在0到1之间。左下角顶点坐标是 *(0,0) *,右上角顶点坐标是 *(1,1) *。即使贴图不是正方形,右上角坐标依然是 (1,1) 。U和 V从0到1取值就能取到图片上每一处的颜色。如下,长方形贴图左下角和右上角分别与正方形网格对齐。长方形就会被压缩在正方形里将图片颜色映射到平面上。
盒形和圆柱形投影贴图
UV坐标这种贴图映射方式是最基本的贴图映射方式。常见的投影贴图还有盒形和圆柱形投影两种。拿圆柱体投影举例,可以将投影贴图过程拆解为如下步骤:
- 将圆柱侧面展开,把图贴在展开的侧面。
- 将圆柱还原(圆柱侧面每一个点都带颜色了)。
- 将要被投影贴图的几何体放在圆柱内部。
- 从圆柱侧面沿曲面法线向内部投影,将圆柱侧面的颜色投影在了内部的模型上。
复杂贴图
复杂的贴图需要在专门的建模软件中操作,建模软件中可以对模型上的每一个面做精确贴图操作。一些建模软件中的UV展开功能就可以将模型拆成很多平面。将这些平面手动与贴图中的某个部分对齐,就能实现更精确的贴图映射。
贴图的其他用途
映射颜色是贴图最常用的场景,在某些材质中还支持贴图的其他用途。
- alphaMap(透明度贴图)
- normalMap (法线贴图)
比如法线贴图(normalMap)并不会用来映射物体颜色,而常用于对某个平面制造凹凸的视觉效果。如墙壁、地面、屋顶这些在建模时只是一个平面,但是完全可以用法线贴图制造凹凸效果,让建模难度降低,提升应用性能。
3D对象
3D对象是能放在场景中的实际物体,如游戏中的房屋、人物等。3D对象是集众多属性一体的一个复杂对象,上述所说的坐标、几何体、材质只是3D对象的属性中的三个。3D对象的属性共同决定了它在最终渲染结果中的形状、颜色、位置、大小等。列举3D对象常见属性如下:
- geometry(几何体)
- material(材质)
- position(坐标)
- rotation(旋转)
- scale(缩放)
- parent(父级对象)
- children (子对象)
空对象
在计算机3D中最基本的3D对象就是一个没有形状,没有颜色的空对象。空对象依然是可以放置在场景中,可调整其坐标、旋转、缩放等属性。但不会产生任何实际的渲染效果。
对象嵌套
3D对象可以嵌套形成父子关系,在平移旋转父级对象时,其子对象都会和父级对象本身的坐标系原点保持相对静止。比如在游戏中一个角色手里需要拿着武器,就可以将武器设置为角色手的子物体,那么角色的手随着动画移动时,手里的武器也会跟着移动。
对象编组
前面说到空的对象也可以放在场景中,这并不是没有意义的。空对象在对 3D 物体编组的时候大有用处。因为 3D 对象可嵌套的关系,通常可以用空对象作为多个对象的父级对象,实现对多个对象编组的效果。比如一个房屋由多个部件组成,即可将这些部件调整好相对位置并编组,在需要改变房屋位置时改变最外层的空对象的坐标即可。
灯光
和真实的照相曝光一样,只有光线进入镜头打在感光元件上才能形成图像。在计算机3D中灯光用于给场景渲染提供光源,灯光和其他 3D 物体一样也可放置在场景中,并可设置其坐标、旋转等3D对象常规属性。除此之外还可设置灯光本身某些属性如光颜色、光强度等。较常见的灯光类型如下:
- 平行光,模拟太阳光(仅方向设置生效)
- 点光源,模拟灯泡这样的人造光源(仅坐标设置生效)
- 聚光灯,模拟舞台聚光灯(可设置光源和目标点的坐标,以此改变灯光照射方向)
某些材质在有灯光时才会有渲染效果,物理材质在没有灯光时将不可见,Phong 材质也会没有反射效果。不同的材质配合不同的灯光渲染效果也会大不相同。下图是Phong材质和物理材质在上下红蓝两个点光源照射下的渲染效果。
摄像机
摄像机算一种场景中特殊的物体,场景中也能设置摄像机的坐标、旋转等3D对象常规属性,也能将摄像机作为其他物体子物体跟随物体移动。摄像机不出现在渲染结果中,但摄像机在渲染中至关重要。摄像机在渲染时作为渲染的一个输入。如同人的眼睛,摄像机在场景中的位置、角度和其自带的一些参数设置都会影响到最终渲染结果。常使用的摄像机有两种:
- 透视摄像机,和人眼相似,观察物体远小近大。
- 正交摄像机 ,没有远大近小的效果,类似平行光投影。
观察或渲染场景大多数情况使用的是透视摄像机。但在3D建模中正交摄像机也大有用处,建模师会在正交视图下做对齐模型这样的操作。
渲染器
前述的所有内容都只是为了搭建一个场景,场景的所有内容包括3D对象、摄像机、光源都将作为渲染器的输入参数。渲染器的工作是根据这些参数将摄像机看到的内容输成一个像素阵列。
Threejs 概念
数学类型
Threejs中内置了很多数学类型,这些数学类型广泛地被Threejs使用。例如
- Vector2(二维向量)
- Vector3(三维向量)
- Matrix4(四维度矩阵)
- Ray(射线)
Threejs 中很多操作可以通过这些数学类型的运算完成。以Vector3为例,将3D对象沿着x轴正方向平移一个单位。
arduino
import { Vector3, Object3D } from 'three';
const xNormal = new Vector3(1, 0, 0); // 创建一个x轴向的单位向量
const obj = new Object3D(); // 创建3D对象
obj.position.add(xNormal); // 变更坐标
Threejs 场景
在 Threejs 中可以用Scene
实例化一个场景对象。场景实例的add()
方法可向场景中添加 3D 对象。
csharp
// threejs创建一个场景
import * as THREE from "three";
const scene = new THREE.Scene();
// 向场景中添加物体
scen.add(...);
Threejs 几何体
创建一个可见的3D对象前,我们需要为它创建一个几何体用于描述它的形状。在Threejs中可以使用BufferGeometry
类给一组连续的点创建几何体实例。如下,使用BufferGeometry
创建一个与xoy 平面重合且b点与世界坐标原点重合的等腰三角形。
javascript
import { Vector3, BufferGeometry } from 'three'
// 创建三个点
const a = new Vector3(0, 1, 0);
const b = new Vector3(0, 0, 0);
const c = new Vector3(1, 0, 0);
// 创建三角形几何体
const triangleGeometry = new BufferGeometry().setFromPoints([a, b, c]);
一般情况不会用BufferGeometry
去画复杂的模型。复杂的模型一般在如 3dsmax、blender 这样的建模软件中创建、编辑、导出。但Threejs 还是内置了一些基本常用的几何体,可用于做些测试,如下:
- 平面(PlaneGeometry)
- 球体(SphereGeometry)
- 立方体(BoxGeometry)
- 圆柱(CylinderGeometry)
- 圆锥(ConeGeometry)
用线框渲染Threejs内置几何体可以看到所有的面都是由三角形组成的。
Threejs 材质
Threejs 中内置有很多材质, 它们都继承了Material
类。有的材质不受光照影响,如MeshBasicMaterial
仅显示一个固有颜色。有的受光照影响, 如MeshPhysicalMaterial
基于物理原理计算光照。这些材质有各自不同的属性,同一种材质通过属性的修改能定制出不同的效果。如下创建一个灰色的物理材质。
php
// 实例化一个背面物理材质
const bSideMat = new THREE.MeshPhysicalMaterial({
color: 0xeeeeee,
side: THREE.BackSide,
});
也可以加载一张贴图,设置到材质的map属性上,创建一个有贴图的材质。
javascript
import * as THREE from 'three'
function loadTexture(url) {
const texture = new THREE.TextureLoader();
return new Promise(rs => {
texture.load(url, (texture) => rs(texture));
})
}
async function createMaterial(){
const texture = loadTexture("some/bitmap.png");
texture.wrapS = THREE.RepeatWrapping; // 设置水平方向重复
texture.wrapT = THREE.RepeatWrapping; // 设置垂直方向重复
texture.repeat.set(4, 4); // 设置水平垂直的重复数
const material = new THREE.MeshBasicMaterial({ map: texture });
return material;
}
Threejs 3D对象
有了几何体和材质,就能够描述一个物体的形状和颜色了。在Threejs中所有的 3D 对象都是继承Object3D
这个类的。它们的实例化都需要几何体和材质。常用来渲染场景的3D对象有以下几种:
- Points:以几何体的点信息创建空间中的点。
- Line:以几何体的边信息创建空间中的线。
- Mesh:以几何体的面信息创建空间中的平面。
这三个构造函数接受同一个BufferGeometry
实例并给予相应材质会得到不同的渲染结果。分别会渲染出几何体的点,线和面。
java
// 创建三角形几何体
const triangleGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(1, 0, 0),
]);
//点
const trianglePoints = new THREE.Points(
triangleGeometry, // 几何体
new THREE.PointsMaterial({ color: 0xffffff, size: 0.05 }) // 材质
);
// 线
const triangleLines = new THREE.Line(
triangleGeometry,
new THREE.LineBasicMaterial({ color: 0xffffff })
);
// 面
const trianglePanle = new THREE.Mesh(
triangleGeometry,
new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide })
);
Mesh 是最常用的 3D 对象,场景中的大多数 3D 模型都是 Mesh。Mesh可以理解为给几何体蒙皮,创建了一个和几何体一样的壳,这个壳是全部由三角形组成的。通过几何体信息和材质就可以创建出渲染结果中可见的 3D 对象。将Threejs内置的几何体以Mesh对象渲染出来效果如下。
Threejs 灯光
Threejs 中也内置了多种灯光,都继承了Light
类。这些光源可添加到场景中为渲染提供照明。常用光源类型有如下三种。
- DirectionalLight(平行光)
- PointLight(点光源)
- SpotLight(聚光灯)
以点光源为例,创建一个光源并设置其坐标。
csharp
//创建一个红色点光源
const redLight = new THREE.PointLight(0xff0000, 1);
// 将点光源放在场景的中央上方
scen.add(redLight);
redLight.position.set(0,3,0);
Threejs 摄像机
如前概念所述,摄像机也是一个特殊3D对象,可作为观察者放在场景中。Threejs 的摄像机都继承了Camera
类,常用摄像机有如下两种。
- PerspectiveCamera(透视摄像机)
- OrthographicCamera (正交摄像机)
创建一个透视摄像机,从 *(0, 0, 2) *坐标看向世界坐标原点,观察立方体线框。可看到立方体呈现远小近大的视觉效果。
arduino
const width = 800;//渲染的图像宽度
const height = 400;//渲染的图像高度
const pCamera = new THREE.PerspectiveCamera(75, width / height, 0.1);
pCamera.position.set(0, 0, 2);
pCamera.lookAt(0, 0, 0);
创建一个正交摄像机,从 *(0, 0, 2) *坐标看向世界坐标原点,观察立方体线框。可看到立方体近处和远处的线条重合。
arduino
const width = 800;
const height = 400;
const oCamera = new THREE.OrthographicCamera(
-4,
4,
(height / width) * 4,
(-height / width) * 4,
0,
100
);
oCamera.position.set(0, 0, 2);
oCamera.lookAt(0, 0, 0);
Threejs 渲染器
Threejs 中可使用WebGLRenderer
创建一个 WEBGL 渲染器。渲染器接受一个Scene
实例和Camera
实例作为渲染的输入。最终可将渲染的结果输出到canvas上。
ini
const width = 800;
const height = 400;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
//renderer.domElement是canvas,渲染器会将结果输出到这个canvas
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
//指定渲染器从哪个场景和摄像机渲染
renderer.render(scene, camera);