各位靓仔靓女,很高兴今天能跟大家聊聊 Three.js 这玩意儿。别看它用起来好像搭积木一样简单,但背后可藏着不少 WebGL 的硬核知识呢!今天咱们就来扒一扒 Three.js 到底是怎么把 WebGL 玩转的,保证让你听完之后,也能自信地说一句:“WebGL,我熟!”
第一部分:WebGL,那张画布
首先,得明确一点:Three.js 并不是一个独立的渲染引擎,它其实是 WebGL 的一个封装库。你可以把 WebGL 想象成一块空白的画布,你得告诉它在哪儿画什么、怎么画,它才会老老实实地给你呈现出来。
WebGL 本身非常底层,你需要用 JavaScript 来控制它,但你需要用 GLSL(OpenGL Shading Language)写着色器程序(shaders)。着色器程序运行在你的显卡(GPU)上,负责处理顶点和像素的渲染。
1.1 WebGL 的基本流程
WebGL 的渲染流程大概是这样的:
-
创建 WebGL 上下文(Context): 就像你要画画,得先准备好画布一样。
const canvas = document.getElementById('myCanvas'); const gl = canvas.getContext('webgl'); // 或者 'webgl2' if (!gl) { alert('你的浏览器不支持 WebGL!'); }
-
定义顶点数据(Vertex Data): 告诉 WebGL 你要画什么形状,它的每个顶点坐标是什么。
const vertices = [ -0.5, -0.5, 0.0, // 第一个顶点 0.5, -0.5, 0.0, // 第二个顶点 0.0, 0.5, 0.0 // 第三个顶点 ];
-
创建缓冲区对象(Buffer Object): 把顶点数据放到显卡里,让 GPU 可以快速访问。
const vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
-
编写并编译着色器程序(Shaders): 顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)是 WebGL 的核心。顶点着色器负责处理顶点的位置,片元着色器负责处理像素的颜色。
// 顶点着色器 const vertexShaderSource = ` attribute vec3 aVertexPosition; void main() { gl_Position = vec4(aVertexPosition, 1.0); } `; // 片元着色器 const fragmentShaderSource = ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色 } `;
编译着色器:
function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('着色器编译出错:', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
-
创建并链接着色器程序(Program): 把顶点着色器和片元着色器组合成一个完整的程序。
const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { console.error('着色器程序链接出错:', gl.getProgramInfoLog(shaderProgram)); gl.deleteProgram(shaderProgram); return null; } gl.useProgram(shaderProgram);
-
绑定顶点数据到着色器属性(Attribute): 告诉 WebGL 顶点数据对应着色器中的哪个属性。
const vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition'); gl.enableVertexAttribArray(vertexPositionAttribute); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
-
绘制(Draw): 终于可以开始画了!
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置背景颜色 gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, 3); // 画一个三角形
1.2 GLSL 着色器语言
GLSL 是一种专门为 GPU 设计的编程语言,它的语法和 C 语言很像,但有一些针对图形渲染的特性。
attribute
:用于从顶点缓冲区接收数据,只能在顶点着色器中使用。uniform
:用于从 JavaScript 传递数据到着色器,可以被顶点着色器和片元着色器共享。varying
:用于从顶点着色器向片元着色器传递数据。
第二部分:Three.js 的底层实现
Three.js 的核心思想就是把 WebGL 的这些底层操作封装起来,让你用更简洁、更面向对象的方式来创建 3D 场景。
2.1 Three.js 的核心概念
- Scene(场景): 场景是所有物体的容器,你可以把所有的 3D 模型、光源、相机都添加到场景中。
- Camera(相机): 相机决定了你从哪个角度观察场景。Three.js 提供了多种相机类型,比如透视相机(PerspectiveCamera)和平行投影相机(OrthographicCamera)。
- Renderer(渲染器): 渲染器负责把场景中的物体绘制到屏幕上。Three.js 提供了多种渲染器,比如 WebGLRenderer、CanvasRenderer 和 SVGRenderer。
- Mesh(网格): 网格是 3D 模型的基本单元,由几何体(Geometry)和材质(Material)组成。
- Geometry(几何体): 几何体定义了 3D 模型的形状,比如立方体、球体、圆锥体等。
- Material(材质): 材质定义了 3D 模型的外观,比如颜色、纹理、光泽度等。
- Light(光源): 光源照亮场景中的物体,让它们看起来更有立体感。Three.js 提供了多种光源类型,比如环境光(AmbientLight)、方向光(DirectionalLight)、点光源(PointLight)等。
2.2 Three.js 的渲染流程
Three.js 的渲染流程可以简化为以下几个步骤:
- 创建场景、相机和渲染器。
- 创建 3D 模型(Mesh),并添加到场景中。
- 设置相机的位置和方向。
- 调用渲染器的
render()
方法,把场景绘制到屏幕上。
2.3 Three.js 如何使用 WebGL
Three.js 在底层大量使用了 WebGL 的 API,但它把这些 API 封装成了更易于使用的对象和方法。
- Geometry -> WebGL Buffer: Three.js 的 Geometry 对象会被转换成 WebGL 的 Buffer 对象,存储在显卡中。
- Material -> WebGL Shader: Three.js 的 Material 对象会被转换成 WebGL 的 Shader 程序,用于控制物体的外观。
- Mesh -> Draw Call: Three.js 的 Mesh 对象会被转换成 WebGL 的 Draw Call,用于绘制 3D 模型。
代码示例:使用 Three.js 创建一个简单的场景
<!DOCTYPE html>
<html>
<head>
<title>Three.js Example</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script>
// 1. 创建场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 2. 创建一个立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // 绿色
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 3. 设置相机的位置
camera.position.z = 5;
// 4. 渲染循环
function animate() {
requestAnimationFrame(animate);
// 让立方体旋转
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
这段代码做了什么?
- 引入 Three.js 库:
cdn.jsdelivr.net
提供了一个方便的方式来引入 Three.js。 - 创建场景、相机和渲染器:
THREE.Scene
,THREE.PerspectiveCamera
,THREE.WebGLRenderer
是 Three.js 的核心类。 - 创建一个立方体:
THREE.BoxGeometry
创建了一个立方体的几何体,THREE.MeshBasicMaterial
创建了一个简单的绿色材质,THREE.Mesh
把几何体和材质组合成一个网格。 - 设置相机的位置: 把相机放在 Z 轴的 5 个单位处,以便可以看到立方体。
- 渲染循环:
requestAnimationFrame
创建了一个动画循环,不断地更新立方体的旋转角度,并重新渲染场景。
2.4 Three.js 的 ShaderMaterial
如果你想更深入地控制物体的外观,可以使用 Three.js 的 ShaderMaterial
。ShaderMaterial
允许你直接编写 WebGL 的 Shader 程序,从而实现各种自定义的渲染效果。
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}
`
});
const mesh = new THREE.Mesh(geometry, shaderMaterial);
scene.add(mesh);
在这个例子中,我们直接在 JavaScript 中编写了顶点着色器和片元着色器,并把它们传递给 ShaderMaterial
。
2.5 Three.js 的 GLSL Program Cache
Three.js 为了提高渲染性能,使用了 GLSL Program Cache。当 Three.js 第一次遇到某个特定的着色器程序时,它会把这个程序编译并缓存起来。下次再遇到相同的着色器程序时,Three.js 就可以直接从缓存中加载,而不需要重新编译,从而节省了大量的 CPU 时间。
第三部分:Three.js 的优化技巧
了解了 Three.js 的底层实现之后,我们就可以更好地优化 Three.js 的性能。
- 减少 Draw Call: Draw Call 是 CPU 和 GPU 之间的一次通信,每次 Draw Call 都会消耗一定的性能。尽量把多个物体合并成一个物体,或者使用 Instanced Rendering 技术来减少 Draw Call。
- 优化 Shader 程序: Shader 程序的性能直接影响渲染速度。尽量简化 Shader 程序,避免使用复杂的计算和纹理查找。
- 使用 LOD(Level of Detail): 对于远处的物体,可以使用更低精度的模型,从而减少渲染的顶点数量。
- 使用纹理压缩: 纹理压缩可以减少纹理占用的内存空间,并提高纹理的加载速度。
- 使用 Frustum Culling: Frustum Culling 可以剔除掉不在相机视野内的物体,从而减少渲染的物体数量。
表格:Three.js 和 WebGL 的对应关系
Three.js 对象 | WebGL 对象/概念 | 描述 |
---|---|---|
THREE.Geometry |
Buffer Object | 存储顶点数据、法线数据、纹理坐标等。 |
THREE.Material |
Shader Program | 定义物体的外观,包含顶点着色器和片元着色器。 |
THREE.Texture |
Texture Object | 存储纹理图像。 |
THREE.Mesh |
Draw Call | 表示一个 3D 模型,包含几何体和材质。渲染时,Three.js 会把 Mesh 对象转换成 WebGL 的 Draw Call。 |
THREE.Scene |
– | 场景是所有物体的容器,WebGL 没有直接对应的概念,但场景中的物体最终都会被转换成 WebGL 的对象。 |
THREE.Camera |
View Matrix, Projection Matrix | 相机定义了观察场景的角度,WebGL 使用 View Matrix 和 Projection Matrix 来表示相机的变换。 |
THREE.WebGLRenderer |
WebGL Context | 渲染器负责把场景绘制到屏幕上,WebGL Context 是 WebGL 的上下文,提供了访问 WebGL API 的入口。 |
THREE.ShaderMaterial |
Custom Shader Program | 允许用户自定义着色器程序,提供了更大的灵活性。 |
总结
今天我们一起深入了解了 Three.js 的底层实现,知道了它其实是 WebGL 的一个封装库。通过 Three.js,我们可以更方便地创建 3D 场景,而不需要直接操作 WebGL 的底层 API。当然,了解 WebGL 的原理可以帮助我们更好地理解 Three.js,并优化 Three.js 的性能。
希望今天的分享对你有所帮助! 如果你觉得还不过瘾,可以尝试自己编写一些 WebGL 的代码,或者阅读 Three.js 的源码,相信你会收获更多!