你有没有想过,那些在网页上酷炫的 3D 汽车展示、沉浸式的产品定制,或是让人欲罢不能的网页游戏,它们背后的魔法是什么?答案,往往就指向 WebGL。
欢迎来到我们的 WebGL 系列教程!今天,我们将一起踏出最重要,也最激动人心的第一步。忘掉复杂的 3D 模型和华丽的光影,我们的目标很"小"------在浏览器里,画一个彩色的三角形。
你可能会笑,一个三角形?是的。在 3D 图形的世界里,三角形就是创世的"原子"。万物,皆由无数个微小的三角形构成。搞定了它,你就拿到了开启 3D 世界的钥匙。
WebGL 是什么?它和 Canvas、Three.js 是亲戚吗?
在动手之前,我们得先理清几个概念,免得在路上迷路。
- Canvas: 把它想象成一块画板。HTML5 给了我们
<canvas>
标签,你可以在上面进行 2D 绘图,就像用画笔一样。 - WebGL (Web Graphics Library): 这不是一支画笔,而是一套直接跟"显卡"(GPU)对话的"遥控器"。它让你能在 Canvas 这块画板上,以极高的性能绘制 2D 和 3D 图形。它非常底层,所以也很灵活,但代码会显得有些"啰嗦"。
- Three.js: 如果说 WebGL 是手动挡的赛车,那 Three.js 就是自动挡的豪华轿车。它是一个强大的 3D 库,封装了 WebGL 复杂的细节,让你能用更简洁、更面向对象的代码快速创建 3D 场景。
"那我为什么不直接学 Three.js?" 好问题!直接学 Three.js 当然可以快速出成果。但了解底层的 WebGL,能让你在遇到性能瓶颈或者需要实现高度定制化的效果时,不再是一个只会"调 API"的"司机",而是一个能"修引擎"的"工程师"。懂底层,才能走得更远。
核心思想:渲染管线与着色器
WebGL 的工作方式,可以比作一条"图形渲染流水线" (Rendering Pipeline)。你把一堆"原材料"(顶点坐标)扔进流水线的一端,最终在另一端出来一张"成品图片"。
在这条流水线上,有两个岗位需要我们亲自编写代码 来"培训员工",这两个特殊的"员工",就叫做着色器 (Shader)。
- 顶点着色器 (Vertex Shader): 它的任务是处理每个顶点。它会接收你传入的原始坐标,然后计算出这个顶点最终应该出现在屏幕的哪个位置。想让物体移动、旋转、缩放?都是在这里做文章。
- 片元着色器 (Fragment Shader): 当顶点着色器确定了图形的轮廓后,片元着色器登场。它的任务更纯粹:决定图形覆盖的每一个像素点,应该是什么颜色。物体的颜色、光照、纹理,都由它说了算。
这两个着色器,使用一种叫 GLSL (OpenGL Shading Language) 的类 C 语言编写。别怕,入门很简单。
好了,理论到此为止,我们亮代码!
第 1 步:准备 HTML 画板
创建一个 HTML 文件,内容非常简单,只需要一个 <canvas>
元素。
html
<!DOCTYPE html>
<html>
<head>
<title>我的第一个 WebGL 三角形</title>
</head>
<body onload="main()">
<canvas id="webgl-canvas" width="500" height="500"></canvas>
<!-- 我们将在这里写所有的代码 -->
</body>
</html>
第 2 步:编写你的第一个着色器 (GLSL)
我们把 GLSL 代码直接写在 HTML 的 <script>
标签里,方便管理。
-
顶点着色器 (
vertexShaderSource
)attribute vec2 a_position;
:这是我们从 JavaScript "喂"给顶点着色器的数据。attribute
表示这是顶点数据,vec2
表示它是一个二维向量(包含 x, y 坐标),a_position
是我们给它起的名字。void main() { ... }
:GLSL 程序的入口函数。gl_Position = vec4(a_position, 0.0, 1.0);
:这是最关键的一行!gl_Position
是一个内置的特殊变量,我们必须给它赋值。它代表顶点最终的位置。它是个vec4
(四维向量),所以我们把二维的a_position
扩展一下,Z 轴设为 0,W 分量设为 1.0(现在你只需要记住要这么做就行)。
-
片元着色器 (
fragmentShaderSource
)precision mediump float;
:设置浮点数的精度,这基本是个模板语句,先不用深究。void main() { ... }
:入口函数。gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0);
:这是片元着色器的内置输出变量,代表最终的像素颜色。它也是一个vec4
,四个分量分别代表红®、绿(G)、蓝(B)和透明度(A),取值范围都是 0.0 到 1.0。这里我们设置了一个不那么刺眼的紫红色。
第 3 步:万事俱备,只欠 JavaScript
接下来是整个流程的"胶水"代码,负责设置环境、编译着色器、传递数据,并最终发出绘制命令。我会把详细的注释写在代码里。
我将把所有代码整合到一个完整的 HTML 文件中,你可以直接复制保存为 triangle.html
,然后用浏览器打开它。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebGL 教程 1:第一个三角形</title>
<style>
body { background-color: #333; color: #eee; text-align: center; }
canvas { background-color: #000; border: 1px solid #555; }
</style>
</head>
<body onload="main()">
<h1>我的第一个 WebGL 三角形</h1>
<canvas id="webgl-canvas" width="500" height="500"></canvas>
<!-- 顶点着色器代码 -->
<script id="vertex-shader" type="x-shader/x-vertex">
// an attribute will receive data from a buffer
// 'a_position' 是我们将要从JS传入的顶点位置数据
attribute vec2 a_position;
// all shaders have a main function
void main() {
// gl_Position is a special variable a vertex shader
// is responsible for setting.
// 我们需要一个 vec4,所以把二维的 a_position 扩展成四维
// z = 0.0: 我们在 2D 平面上
// w = 1.0: 一个必须的值,暂时不用关心为什么
gl_Position = vec4(a_position, 0.0, 1.0);
}
</script>
<!-- 片元着色器代码 -->
<script id="fragment-shader" type="x-shader/x-fragment">
// fragment shaders don't have a default precision so we need
// to pick one. mediump is a good default.
precision mediump float;
void main() {
// gl_FragColor is a special variable a fragment shader
// is responsible for setting.
// 设置颜色为紫红色 (R=1, G=0, B=0.5, A=1)
gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0);
}
</script>
<script>
function main() {
// 1. 获取 Canvas 和 WebGL 上下文
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert(' দুঃখিত, আপনার ব্রাউজার WebGL সমর্থন করে না।');
return;
}
// 2. 创建并编译着色器
// 从 script 标签中获取 GLSL 源码
const vertexShaderSource = document.getElementById('vertex-shader').text;
const fragmentShaderSource = document.getElementById('fragment-shader').text;
// 创建、编译着色器的辅助函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
// 如果编译失败,打印错误信息
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
// 3. 创建着色器程序并链接
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
// 如果链接失败,打印错误信息
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
const program = createProgram(gl, vertexShader, fragmentShader);
// 4. 找到着色器中 a_position 的位置(内存地址)
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// 5. 创建 Buffer,并向其中存入顶点数据
const positionBuffer = gl.createBuffer();
// 绑定Buffer到 ARRAY_BUFFER,后续操作都会针对此 buffer
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// WebGL 的坐标系是 -1.0 到 1.0
// 这是三角形的三个顶点坐标 (x, y)
const positions = [
0, 0.5, // 顶点1
-0.5, -0.5, // 顶点2
0.5, -0.5 // 顶点3
];
// 将JS数组转换为强类型数组,并传入 buffer
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 6. 清空画布并设置视口
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.1, 0.1, 0.1, 1.0); // 设置背景色为深灰色
gl.clear(gl.COLOR_BUFFER_BIT);
// 7. 启用着色器程序
gl.useProgram(program);
// 8. 告诉 WebGL 如何从 Buffer 中读取数据给 attribute
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 再次绑定,确保我们用的是正确的 buffer
// vertexAttribPointer 会告诉WebGL该如何解析buffer中的数据
const size = 2; // 每次迭代读取 2 个单位的数据 (x, y)
const type = gl.FLOAT; // 数据是 32bit 浮点型
const normalize = false; // 不要归一化
const stride = 0; // 0 = 移动 size * sizeof(type) 字节以获取下一个位置
const offset = 0; // 从 buffer 的开头开始读取
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
// 9. 绘制!
const primitiveType = gl.TRIANGLES; // 我们要画三角形
const drawOffset = 0; // 从第0个顶点开始
const count = 3; // 一共画3个顶点
gl.drawArrays(primitiveType, drawOffset, count);
}
</script>
</body>
</html>
总结与展望
恭喜你!如果一切顺利,你现在应该能在浏览器中看到一个静静躺在黑色画布中央的紫红色三角形。
别看它简单,我们今天可是完成了一次完整的 WebGL 渲染流程:
- 创建了画板 (
<canvas>
) - 编写了"大脑" (顶点和片元着色器)
- 准备了"原料" (顶点坐标数据)
- 用 JS 作为"总指挥",将所有部分组装起来,并命令 GPU 进行绘制。
这其中涉及的 Buffer
、Attribute
、Program
等概念,是 WebGL 的核心。现在可能觉得有点繁琐,但别担心,多接触几次,它们就会成为你武器库里最称手的工具。
在下一篇中,我们将让这个单调的三角形变得五彩斑斓。我们将探索如何从 JavaScript 向着色器传递更多类型的数据(比如颜色),并让每个顶点拥有不同的颜色,从而创造出平滑的色彩渐变。
敬请期待 《第 2 篇:为世界添彩 - WebGL 中的颜色与着色器变量》!