普通视图
第8章 Three.js入门
8.1 初始化项目
three.js 是一个基于JavaScript 的WebGL 引擎,可直接在浏览器中运行GPU 驱动的游戏与图形驱动的应用。 three.js 的库提供了大量用于在浏览器中绘制3D场景的特性与API。我们的入门就基于three.js库去调用对应的API。
需要完成的前置条件如下4点:
(1)创建一个空项目THREEJS。
(2)创建index.html和main.ts文件,用于后续编写示例代码。
(3)安装three.js库和对应声明文件。
(4)安装Vite用于启动项目。
// 安装three.js库和对应声明文件
npm i three
npm i --save-dev @types/three
// 安装Vite用于启动项目
npm i vite -D
其中index.html是作为展示3D场景界面的文件,然后需要导入main.ts文件,main.ts文件是作为编写three.js的代码文件。
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<script src="./main.ts" type="module"></script>
</body>
</html>
接着到package.json文件中配置vite的启动命令,然后启动项目。
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"three": "^0.182.0"
},
"devDependencies": {
"@types/three": "^0.182.0",
"vite": "^7.3.0"
}
}
8.2 案例搭建
完成项目的初始化并启动项目后,项目界面是一片空白,因为我们还没有编写对应代码,接下来回到main.ts文件中,完成以下3点操作:
(1)引入three库。
(2)创建场景
(3)创建网格。
创建场景永远是第一件要做的事情,它是所有3D物体的容器,就像在2D Canvas中必须先获取画布上下文一样基础。接着是创建网格,three.js中的所有可见的3D物体都是基于网格去组成,网格可以有多个,网格包含几何体和材质。
几何体(Geometry):定义了物体的形状,即顶点、面等结构信息。
材质(Material):定义了物体表面的外观,例如颜色、纹理、光滑度等。
创建网格也意味着需要创建几何体和材质,然后放入网格中。几何体有多种形状,对应不同方法,填入不同参数;材质也有多种材质选择,通过不同方法去操作。
import * as THREE from 'three';
//创建场景
const scene = new THREE.Scene();
//创建几何体
const geometry = new THREE.BoxGeometry(100, 100, 100); // x,y,z三轴
//创建材质
//MeshBasicMaterial 这个材质是不受光照影响
//MeshLambertMaterial 这个材质是受光照影响 漫反射材质
//MeshPhongMaterial 这个材质是受光照影响 镜面高光
const material = new THREE.MeshBasicMaterial({color: 0x00ff00});
//网格 几何体 + 材质 可以有多个
const mesh = new THREE.Mesh(geometry, material);
//将网格添加到场景中
scene.add(mesh);
网格需要包含几何体和材质是很好理解的,几何体是物品的形状,而材质是物品的表面,网格就是将两者结合起来的“完整物体”,类似于3D建模。
创建网格属于场景的部分,而一个最简的Three.js代码结构需要包含三个核心组件:
(1)Scene(场景):是舞台。所有演员、道具、灯光都必须放在这个舞台上。
(2)Camera(相机):是摄像机。它决定了你从哪个角度、以何种视野去观看舞台。
(3)Renderer(渲染器):是负责把摄像机拍到的画面,实际绘制到屏幕画布上的“渲染引擎”。没有它,一切准备都只是数据,看不到图像。
因此创建网格并填充对应的几何体和材质意味着我们拥有了一个最简单的物品填入场景中作为被观察对象(网格需要添加到场景中),在这之后还需要创建相机和渲染器。接下来我们开始创建相机。
相机通过THREE.PerspectiveCamera()创建,需要四个参数分别是:视野角度(fov)、宽高比(aspect)、近裁剪面(near)、远裁剪面(far)。它们一起构成了一个视锥体,决定了相机能看到什么。视野角度控制可见范围的垂直开合程度,类似摄像机的镜头焦距;宽高比确保渲染不变形,通常直接使用窗口比例;远近裁剪面则定义了相机能看清的最小和最大距离,就像人眼的最近视点和最远视点。相机视角如图8-1所示。
![]()
图8-1 相机视角
在我们以下代码示例中,第一个参数 75 是垂直视野角度,类似人眼睁开的角度,值越大看到的场景越广;第二个参数 window.innerWidth / window.innerHeight 是宽高比,通常设置为渲染区域的宽除以高,以确保物体不被拉伸变形;第三个参数 0.1 是近裁剪面,表示相机能看清的最短距离,比这更近的物体将被裁剪不可见;第四个参数 1000 是远裁剪面,表示相机能看清的最远距离,比这更远的物体同样不可见,这四个参数共同划定了相机在三维空间中实际能观察到的范围。
接着我们需要设置相机放置的位置,就和现实一样,拍摄所在的位置决定了画面的叙事视角、视觉重点和情感基调。将相机靠近物体并采用低角度,能像电影特写一样赋予主体压迫感和权威性,常用于突出核心元素或营造紧张氛围;反之,将相机拉远并提升高度,则形成俯瞰式的宏观视角,适合展现场景全貌、空间关系或个体的渺小感。通过精确控制相机与主体的距离、高度和角度,能够决定整个场景是通过一个“第一人称”的沉浸式窗口呈现,还是作为一个“上帝视角”的客观全景被观察。
最后,我们需要将相机加入场景中,正如前面所说的所有演员、道具、灯光都必须放在这个舞台(场景)上。
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 设置相机放置的位置
camera.position.set(0, 0, 400);
// 将相机加入场景
scene.add(camera);
最后,我们需要创建渲染器,将摄像机拍到的画面,实际绘制到屏幕画布。
// 创建WebGL渲染器实例,这是Three.js用来绘制3D场景的核心工具
const renderer = new THREE.WebGLRenderer();
// 设置渲染器输出画布的尺寸为整个浏览器窗口的宽度和高度
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器自动生成的<canvas>画布DOM元素添加到网页的<body>中,这样画面才能显示出来
document.body.appendChild(renderer.domElement);
// 执行一次性的渲染操作:命令渲染器从指定相机(camera)的视角,将场景(scene)中的所有物体绘制到画布上
renderer.render(scene, camera);
实际完整Demo代码如下:
如果创建材质选择MeshPhongMaterial这种受光照影响的要素,那么需要添加光照,否则看不见。如果你的画面看不到物体的话,你需要考虑去看下代码部分中的材质是否受光照影响。
import * as THREE from 'three';
//创建场景
const scene = new THREE.Scene();
//创建几何体
const geometry = new THREE.BoxGeometry(100, 100, 100); // x,y,z三轴
//创建材质
//MeshBasicMaterial 这个材质是不受光照影响
//MeshLambertMaterial 这个材质是受光照影响 漫反射材质
//MeshPhongMaterial 这个材质是受光照影响 镜面高光
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
//网格 几何体 + 材质 可以有多个
const mesh = new THREE.Mesh(geometry, material);
//将网格添加到场景中
scene.add(mesh);
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 设置相机放置的位置
camera.position.set(0, 0, 400);
// 将相机加入场景
scene.add(camera);
// 创建WebGL渲染器实例,这是Three.js用来绘制3D场景的核心工具
const renderer = new THREE.WebGLRenderer();
// 设置渲染器输出画布的尺寸为整个浏览器窗口的宽度和高度
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器自动生成的<canvas>画布DOM元素添加到网页的<body>中,这样画面才能显示出来
document.body.appendChild(renderer.domElement);
// 执行一次性的渲染操作:命令渲染器从指定相机(camera)的视角,将场景(scene)中的所有物体绘制到画布上
renderer.render(scene, camera);
Three.js创建的Demo画面如图8-2所示。
![]()
图8-2 Three.js创建Demo画面
目前我们场景内只有一个正方块物体,正被相机拍摄着,但看着就像2D的画面。因此我们可以通过引入OrbitControls(轨道控制器)来实现拖动效果,从而实现3D效果。
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
//创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
轨道控制器拖动效果如图8-3所示。由于目前正方体是没有边缘线的,因此静止的时候看起来更像是不规则的平面物体,我稍微添加了几条红色线条(不太规范)来辅助理解,大致能看出这是一个正方体。
![]()
图8-3 轨道控制器拖动效果
以上是Three.js入门的一个简单案例。在创建轨道控制器的时候,我们添加了一个定时器,并且使用了递归,但不会出现死循环导致爆栈的情况。因为使用的是浏览器原生API requestAnimationFrame 实现的动画循环,它并不是传统意义上的递归死循环。
代码步骤思路为以下2步:
(1)requestAnimationFrame(animate):向浏览器“预约”下一帧,告诉浏览器:“在下次屏幕刷新绘制时,请调用animate函数”。它不会立即、连续地调用自身。
(2)浏览器控制节奏:浏览器会以屏幕刷新率(通常是60FPS,即每秒约60次) 的节奏来回调animate函数。当页面隐藏或最小化时,浏览器会自动暂停这些回调以节省资源。
所以通过requestAnimationFrame执行循环,每帧执行完后会释放主线程,等待浏览器下一次绘制时机(执行时机在DOM回流和重绘之前),浏览器牢牢掌控住绘制的运行时间间隔,甚至决定了什么时候会暂停,所以自然不会出现死循环的情况。
这种非阻塞的协作式循环在性能优化(与屏幕刷新同步,避免不必要的重复渲染),节能(页面不可见时自动暂停)和流畅动画(动画更新与屏幕刷新率一致)方面都很不错。这种技术被称为RAF技术。
const animate = () => {
// 递归调用animate
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
通过以上思路,需要先有整体的场景,然后往场景里添加被观察对象(演员、道具、灯光等),接着是观察对象(相机),最后用渲染器将相机拍到的画面渲染出来。在这里场景是我们(导演)的视角,而摄像机才是观众的视角。相机所拍摄的部分才是我们想展现的部分。我们应该从实际摄影所带来的经验中去思考如何拍摄。
8.3 添加灯光
接下来,我们修改材质,将其设置为MeshPhongMaterial这种受光照影响的材质,然后加入灯光,看效果如何。
const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 });
// 添加平行光源(模拟太阳光)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
模拟太阳光对材质进行照射如图8-4所示。可见物体呈现了不一样的质感。像太阳光属于平行光的一种,只能照射到正方体的正面,因此正方体背面还是不可见的。
![]()
图8-4 模拟太阳光
在Three.js中,灯光是塑造三维空间体积感、材质属性和场景氛围的核心工具,本质上是通过模拟光线与物体材质的交互来定义视觉层次。主要分为四种基本类型:
(1)环境光提供均匀无方向的基底照明,如同阴天的漫射光,用于消除纯黑阴影;
(2)平行光模拟无限远处的光源(如太阳),发出平行光线,产生方向明确的阴影,适合户外场景;
(3)点光源从一个点向所有方向均匀辐射光线,像灯泡或蜡烛,能营造真实的衰减和柔和的明暗过渡;
(4)聚光灯则形成锥形的光束,像手电筒或舞台追光,带有清晰的照射范围和边缘衰减,常用于突出特定物体或制造戏剧性焦点。
实际应用中,通常需要组合多种灯光——例如用环境光奠定基调,再用平行光或点光源刻画主次和投影——才能构建出既有层次又自然可信的三维视觉空间。
Three.js:Web 最重要的 3D 渲染引擎的技术综述
理解赋能 Web 实时 3D 图形的抽象层、渲染管线和性能特征。
现代 Web 越来越依赖丰富的视觉体验——数据可视化、模拟仿真、产品预览、生成艺术以及沉浸式 UI。虽然浏览器早已通过 Canvas 和 SVG 支持 2D 图形,但实时 3D 渲染需要一套复杂得多的 GPU 驱动操作。Three.js 已成为填补这一空白的事实标准(de facto standard) 。
虽然大多数介绍将 Three.js 描述为“一个 JavaScript 3D 库”,但它在架构上的角色更为基础。Three.js 是 WebGL 之上的一个结构化抽象层,旨在减少直接与 GPU 交互时的脚手架代码、复杂性和脆弱性。开发者无需手动管理着色器(shaders)、缓冲区(buffers)和渲染状态,而是使用连贯的高级构造——场景、相机、网格、材质——而库则负责高效地编排底层的 GPU 管线。
本文将从技术角度概述 Three.js 的工作原理、支持其性能的内部系统,以及为什么一旦应用程序超越简单的演示(demo)阶段,理解这些系统就变得至关重要。
I. Three.js 作为 WebGL 的抽象层
WebGL 是一个低级 API,它将可编程图形管线暴露给浏览器。在其核心,WebGL 要求开发者手动处理:
- 着色器的编译和链接
- 顶点和索引缓冲区的创建
- 属性(Attribute)和统一变量(Uniform)的绑定
- 纹理上传
- 状态变更
- 绘制调用(Draw call)的执行
Three.js 通过统一的渲染架构抽象了这些职责。
架构目的:结构化的 GPU 交互
Three.js 不是 WebGL 的替代品;它是一个控制层,旨在消除冗余的复杂性,同时保留对底层 GPU 特性的访问能力。
它提供了:
- 场景图(Scene graph)
- 几何体(Geometry)和材质(Material)抽象
- 中心化的渲染器(Renderer)
- 相机系统
- 对光照、阴影、动画和加载器的内置支持
这在不降低能力的情况下减少了认知负荷。
![]()
II. 场景图:核心数据结构
Three.js 将 3D 世界组织成一个分层的场景图(Scene Graph) ,其中每个对象都表示为一个具有变换(transform)和可选子节点的节点。
Scene (场景)
├── Mesh (网格)
│ ├── Geometry (几何体)
│ └── Material (材质)
├── Group (组)
├── Camera (相机)
└── Lights (光源)
场景图在技术上的重要性
每个节点都携带:
- 局部变换矩阵(Local transformation matrix)
- 世界变换矩阵(World transformation matrix)
- 位置、旋转、缩放
- 父子关系
在渲染期间,Three.js 遍历场景图以计算:
- 更新后的世界矩阵
- 基于视锥体剔除(Frustum culling)的可见性
- 材质 + 几何体的组合
- 渲染顺序和绘制调用
这种层级结构使得复杂的动画、实例化(instancing)和空间组织变得可预测且高效。如果没有场景图,开发者将需要手动同步数以百计或千计的独立 GPU 绑定对象。
III. 几何体、缓冲区和类型化数组
在 GPU 层面,所有 3D 网格最终只是结构化的数字数组。Three.js 通过 BufferGeometry 暴露了这一点,它直接反映了 GPU 如何使用顶点数据。
一个 BufferGeometry 包含:
-
position属性:每个顶点的 3D 坐标 -
normal属性:用于光照计算 -
uv属性:用于纹理映射 - 可选的
index缓冲区 — 定义哪些顶点构成三角形
每个属性都由类型化数组(Typed Array)支持,如 Float32Array 或 Uint16Array。
为什么类型化数组是必要的
类型化数组提供:
- 连续的内存布局
- 可预测的性能
- 到 GPU 缓冲区的直接二进制传输
- 每帧更新时的最小开销
JavaScript 对象无法匹配这种级别的可预测性或效率。通过将几何体结构化为连续的缓冲区,Three.js 最小化了 CPU-GPU 的同步开销,即使在包含数万个顶点的场景中也能确保稳定的性能。
![]()
IV. 材质和着色器程序的生成
Three.js 提供了多种材质类型——MeshBasicMaterial、MeshStandardMaterial、MeshPhysicalMaterial、ShaderMaterial 等。无论抽象级别如何,所有材质最终都会编译成在 GPU 上执行的 GLSL 着色器程序。
内部着色器系统
Three.js 根据以下内容动态生成着色器:
- 光照配置
- 阴影设置
- 雾化参数
- 纹理使用情况
- 材质类型和参数
- 精度和性能指令
这种动态编译允许材质保持灵活性,同时确保着色器程序针对特定的场景配置进行优化。
为什么理解着色器仍然重要
虽然 Three.js 抽象了着色器的创建,但开发者通常需要理解:
- 法线映射(Normal mapping)
- 粗糙度/金属度工作流(Roughness/metalness workflows)
- BRDF 计算
- 片元操作(Fragment operations)
- 渲染目标(Render target)行为
自定义材质或高级效果几乎总是需要手动编写着色器,这使得 GLSL 读写能力成为严肃的 Three.js 开发的一项宝贵技能。
V. 渲染循环和帧生命周期
Three.js 运行在一个可预测的渲染循环上,通常由 requestAnimationFrame 驱动。
每一帧涉及:
- 处理动画更新
- 更新相机矩阵
- 遍历场景图
- 运行视锥体剔除
- 准备材质 + 着色器程序
- 准备几何体缓冲区
- 执行 WebGL 绘制调用
- 呈现帧
整个过程必须在约 16 毫秒内完成,以维持 60 FPS。
关于渲染成本的技术观察
- 每个网格至少触发一次绘制调用
- 材质切换会产生 GPU 状态变更
- 阴影需要额外的渲染通道(Render passes)
- 动态对象比静态对象更昂贵
渲染循环的效率直接决定了应用程序的性能。
VI. 性能架构:Three.js 中真正关键的因素
Three.js 的性能瓶颈通常不在于 JavaScript 的执行,而在于 GPU 限制、显存带宽和绘制调用的开销。
以下是影响实际性能的领域:
1. 最小化绘制调用(Draw Call)
GPU 执行少量的大型绘制调用比执行许多小型绘制调用更高效。每次绘制调用都需要状态绑定、程序切换和缓冲区设置。
优化措施包括:
- 几何体合并(Geometry merging)
- 实例化网格(
InstancedMesh) - 减少材质变体
- 策略性地使用图层(Layers)和分组
2. 纹理和内存策略
高分辨率纹理会增加:
- VRAM(显存)使用
- 上传成本
- Mipmap 生成时间
WebGPU 将改善某些方面,但在 WebGL 上,使用压缩纹理格式(如 Basis/KTX2)可提供显著的性能提升。
3. CPU-GPU 同步约束
在渲染循环内分配对象或每帧修改几何体属性会导致垃圾回收(GC)压力和缓冲区重新上传。
性能准则包括:
- 避免在循环内重新创建向量或矩阵
- 除非必要,避免修改缓冲区几何体
- 优先使用基于着色器的变换
4. 材质复杂性
基于物理的渲染(PBR)材质(如 MeshStandardMaterial)计算量大,原因在于:
- 环境采样
- 多光源计算
- 基于 BRDF 的着色
选择最简单的适用材质通常能立即带来 FPS 的提升。
VII. Three.js 与 TypeScript:强大的架构组合
Three.js 提供了健壮的 TypeScript 定义,强制执行:
- 属性类型安全
- 几何体一致性
- 材质参数正确性
- 相机和渲染器配置的有效性
在大型应用程序——可视化仪表盘、模拟仿真或产品配置器——中,Three.js 与类型化场景定义的结合显著减少了运行时缺陷。
VIII. 技术学习路径:从高级 API 到 GPU 理解
Three.js 让人可以在几分钟内构建出功能性的 3D 场景。然而,一旦项目对性能、保真度或自定义视觉效果提出要求,深入的理解就变得不可或缺。
关键领域包括:
- GLSL 着色器开发
- WebGL 管线状态机行为
- GPU 内存限制
- 纹理流式传输(Streaming)和 Mipmapping
- 实例化(Instancing)和批处理(Batching)
- 渲染目标管理
- 后处理链(Post-processing chains)
Three.js 提供了脚手架,但高性能的 3D 开发要求对库本身以及底层图形学原理都能熟练掌握。
![]()
结论
Three.js 远不止是一个 WebGL 的便捷包装器。它是一个精心设计的渲染层,旨在使 GPU 编程变得易于上手,同时在需要时保留低级控制权。它在几何体、着色器、渲染循环和性能优化方面的结构化方法,为 JavaScript 和实时 3D 图形之间搭建了一座高效的桥梁。
随着 3D 界面、模拟仿真、数字孪生和 AR/VR 驱动的应用程序变得越来越普遍,从技术层面理解 Three.js 将成为一种有意义的工程优势。那些既理解抽象层又理解其背后 GPU 层面含义的开发者,将能够设计出不仅视觉震撼,而且稳健、高性能且可扩展的系统。
翻译整理自:Three.js: A Technical Overview of the Web’s Most Important 3D Rendering Engine