Skip to content
WebGL 与 three.js 概述
定位与协作关系
WebGL 是浏览器内置的底层 3D 绘图 API,运行在 <canvas> 元素上,不经插件直接调用 GPU。它脱胎于 OpenGL ES 2.0,负责将顶点、纹理等原始数据送入 GPU 完成光栅化,最终输出到屏幕像素。WebGL 2 在此基础上引入了 OpenGL ES 3.0 功能集,支持更多纹理格式、统一缓冲对象等,但思维模型没有根本改变——开发者仍然直接面对着色器程序、缓冲区绑定和绘制调用。
three.js 则是在 WebGL 之上构建的场景图驱动库。它把 WebGL 中零散的状态(着色器编译、缓冲区创建、uniform 绑定、纹理上传)提炼成 Scene、Camera、Mesh、Material 等可组合对象。开发者的工作从“管理显存里的字节块”转变为“声明一个场景、添加物体、设置相机,然后逐帧渲染”。three.js 替代了那些重复度高且容易出错的样板代码。
可以这样理解:WebGL 提供的是直接操作 GPU 的手段,相当于一支笔和一套绘制原语;three.js 则管理着整块画布以及画布上物体的层级关系,只需告诉它“在指定位置渲染一个红色立方体从某个视角观察”。
3D 场景的基本构建块
任何 three.js 场景都包含六类基本构件:
- 场景 (Scene):容纳所有 3D 对象的容器。所有 Mesh、光源、相机都需要通过
scene.add(obj)挂接到场景树中。 - 几何体 (BufferGeometry):顶点数据的集合,包含位置、法线、UV 坐标等属性,由类型化数组(如
Float32Array)支撑。three.js 内置了几十种基本形状:BoxGeometry、SphereGeometry等。 - 材质 (Material):决定几何体表面的外观。
MeshBasicMaterial无视光照直接着色,MeshStandardMaterial基于物理渲染,ShaderMaterial则允许注入自定义 GLSL。所有材质最终都会编译为 GPU 上的着色器程序,即使只传入了一个颜色字符串。 - 光源 (Light):可选但关键。如果不添加光源,PBR 材质会呈现黑色(没有入射光参与计算)。
DirectionalLight、PointLight、SpotLight分别模拟平行光、点光源和聚光。 - 相机 (Camera):决定观察者的位置、方向与投影类型。
PerspectiveCamera模拟人眼的近大远小效果,需要指定视场角 (fov)、宽高比 (aspect) 和近/远裁切面 (near/far)。 - 渲染器 (WebGLRenderer):负责将相机视野内的场景绘制到
<canvas>上的引擎。它持有 WebGL 上下文,管理 back buffer,并按设置清除颜色和深度缓冲。
这些构件共同构成了一个 3D 世界的基础元素,但背后的执行路径远比搭积木复杂。
渲染管线概览
一个 Mesh 最终呈现在屏幕上,大致经过以下几个阶段:
数据准备:
BufferGeometry会将位置、法线等数据上传到 GPU 的顶点缓冲区,并通过顶点数组对象指定属性布局。材质会生成对应的着色器程序(顶点着色器 + 片元着色器),并把 uniform 值(颜色、光照方向、变换矩阵等)准备好。顶点处理(顶点着色器):GPU 调用顶点着色器,每次处理一个顶点。核心任务是将局部坐标经过模型矩阵、视图矩阵、投影矩阵变换,输出裁剪空间坐标。three.js 在内部维护这些矩阵,开发者只需操作对象的
position、rotation和scale。着色器还会将法线等数据传递给后续阶段。图元装配与光栅化:GPU 把变换后的顶点按索引缓冲区组织成三角形(或线),然后将这些三角形“浇铸”到屏幕坐标系的像素网格上,生成一系列片元(fragment)。每个片元对应一个潜在的像素,携带插值后的顶点属性(颜色、法线、UV 坐标等)。
片元处理(片元着色器):每个片元执行片元着色器,根据插值数据计算最终颜色。对于
MeshStandardMaterial,这一步会运行一段较复杂的 PBR 光照计算,采样法线、粗糙度、金属度、环境光照和阴影贴图。光栅化后的片元还需通过深度测试、混合等固定功能管线,才最终写入帧缓冲区。屏幕输出:渲染器在下一帧交换缓冲区,帧缓冲中的位图由
<canvas>显示出来。每一次renderer.render(scene, camera)调用都会触发上述整套流程,并且对每一帧都要重复执行。
开发者虽然没有直接编写 GLSL,但管线并未消失——three.js 替我们生成了对应的着色器程序,并在每帧遍历场景图、执行剔除、绑定状态和发出绘制命令。理解这些阶段的划分,对后续调整材质和定位渲染问题会有帮助。
基本用法:第一个立方体
以下是一个最小可运行的示例,展示了 Scene → Camera → Renderer 的完整协作。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>旋转立方体</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// 初始化场景、相机与渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(3, 3, 5);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xdddddd);
document.body.appendChild(renderer.domElement);
// 创建几何体、材质与光源,组装 Mesh
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x156289 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 2, 3);
scene.add(directionalLight);
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
// 渲染循环
function animate() {
requestAnimationFrame(animate);
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>将以上代码保存为 HTML 文件并在浏览器中打开,会看到一个浅灰色背景的页面,中央有一个蓝色旋转立方体,不同面因光照呈现出不同的明暗。
代码背后的关键流程如下:
scene.add(cube)将立方体 Mesh 挂载到场景树中,使其成为可被渲染的对象。- 在每一帧,
renderer.render(scene, camera)会遍历 scene 下的所有对象,更新世界矩阵,填充绘制列表,最终向 WebGL 发出drawElements等指令。 requestAnimationFrame驱动连续动画,保持与屏幕刷新率同步。同时cube.rotation.y += 0.01更新旋转变换,three.js 会在下一帧中自动利用新的矩阵数据完成渲染。
这个示例完全避开了直接的 WebGL 调用,但背后 renderer.render 每调用一次,就执行了一遍前面描述的完整管线。
注意点与后续学习
本章没有展开的内容对后续学习很重要:
- 没有编写 GLSL:示例中的着色器全部由 three.js 内部生成。如果需要自定义材质效果(例如卡通着色),就涉及下一步要学习的 GLSL 知识。实际工程中 PBR 材质已能覆盖多数需求,但特定效果仍需要手动编写着色器。
- 没有拆解矩阵运算:所有空间变换和投影矩阵的计算,three.js 已经完成。但这不意味着矩阵不重要——理解模型、视图、投影变换的关系,是定位模型“不显示”这类问题的基本前提。下一篇会专门处理坐标系统。
- 没有涉及阴影、纹理与后期处理:这些是提升画面表现力的重要手段,将在后续章节中逐步引入。
- 单场景与多画布限制:浏览器对 WebGL 上下文数量有硬性限制(Chrome 和 Safari 约 16 个,Firefox 约 200 个)。如果应用需要在同一页面上渲染多个独立 3D 视图,就需要注意这个上限。three.js 默认会为每个
WebGLRenderer创建一个上下文。 - 版本:示例基于 three.js r128,近几年的 API 相对稳定,但更早版本(如 r79)在模块导入、材质行为和几何体定义方面差异较大。接手旧项目时要留意 API 差异。
学习路径上,建议先借助本章示例感受“数据—场景—渲染”的闭环,再阅读下一篇关于坐标系和着色器的基础细节。three.js 把底层复杂性很好地封装了起来,但要对渲染有足够的掌控力,迟早需要回头学习 WebGL 本身。
参考链接
- [1] https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API
- [2] https://developer.mozilla.org/zh-CN/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Three.js
- [3] https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js
- [7] https://developer.chrome.com/docs/web-platform/webgpu/from-webgl-to-webgpu?hl=zh-cn
- [9] https://developer.aliyun.com/article/1697250
