【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
项目地址:
https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看 免费下载
本篇技术指南基于 webgl-fundamentals 项目官方教程体系中的 2D 平移(translation)章节(关联文档位于 webgl/lessons/fr/webgl-2d-translation.md,英文原版见 webgl/lessons/webgl-2d-translation.md),系统讲解 WebGL 2D 场景中"平移"几何体的两种实现路径:先展示在 JavaScript 中逐顶点重算数据的直观做法,再过渡到把平移计算移入顶点着色器的推荐方案。读完本文,你将掌握 uniform 平移参数的完整用法、两种方案各自的适用场景与性能差异,并能直接复刻仓库中对应的可运行示例。
一、前置知识:本系列从哪里开始
"平移"(translation)是数学上一个听起来很高深的术语,本质上就是"移动"某样东西。在本教程的语境下,它指的是移动几何体(顶点坐标),而不是翻译文字。
本篇文章是 webgl-fundamentals 教程系列的一部分,前一篇 WebGL 基础篇 建立了绘制一个矩形的完整管线:获取 WebGL 上下文、编译链接着色器程序、创建顶点缓冲、设置 attribute/uniform 并调用 gl.drawArrays。如果你还没有读过基础篇,建议先读完它再回到这里,否则下面的代码上下文会不太完整。
仓库中与该章节配套的可运行示例包括:
- 矩形平移示例(滑块交互)
- Shader 内平移的"F"字母示例
- 逐顶点更新的"F"字母对比示例
二、方法一:在 JavaScript 中逐顶点平移
2.1 用变量保存平移状态
沿用基础篇的代码,我们可以非常直观地平移矩形:把平移量、宽度、高度、颜色分别存入变量,例如:
var translation = [0, 0]; // 平移量:x 与 y
var width = 100; // 矩形宽度(像素)
var height = 30; // 矩形高度(像素)
var color = [Math.random(), Math.random(), Math.random(), 1]; // 随机颜色 RGBA
随后编写一个"重绘整个场景"的函数。每次更新 translation 后调用它即可。在仓库的 webgl-2d-rectangle-translate.html 中,这个函数是这样实现的:
function drawScene() {
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// 告诉 WebGL 如何从裁剪空间映射到像素
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 清空画布
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用我们的着色器程序(一对着色器)
gl.useProgram(program);
// 启用 attribute
gl.enableVertexAttribArray(positionLocation);
// 绑定位置缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 设置矩形(把平移量直接算进顶点坐标)
setRectangle(gl, translation[0], translation[1], width, height);
// 告诉 attribute 如何从 positionBuffer (ARRAY_BUFFER) 中取数据
var size = 2; // 每次迭代取 2 个分量
var type = gl.FLOAT; // 数据是 32 位浮点数
var normalize = false; // 不做归一化
var stride = 0; // 0 = 每次按 size * sizeof(type) 前进
var offset = 0; // 从缓冲开头开始
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);
// 设置分辨率
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// 设置颜色
gl.uniform4fv(colorLocation, color);
// 绘制矩形
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
其中的 setRectangle 把矩形四个角折算成两个三角形(6 个顶点),并把平移量 x、y 直接加进顶点坐标:
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]),
gl.STATIC_DRAW);
}
2.2 用滑块驱动平移
仓库示例在画布旁边挂了两条滑块,分别更新 translation[0] 和 translation[1],并在每次变化时调用 drawScene() 重绘,使用的是仓库自带的 UI 辅助库 webgl/resources/webgl-lessons-ui.js:
webglLessonsUI.setupSlider("#x", {slide: updatePosition(0), max: gl.canvas.width});
webglLessonsUI.setupSlider("#y", {slide: updatePosition(1), max: gl.canvas.height});
function updatePosition(index) {
return function(event, ui) {
translation[index] = ui.value;
drawScene();
};
}
拖动滑块,矩形就会在画布内移动。对矩形这种简单形状来说,这个方案完全够用——但它的局限很快就暴露了。
三、复杂形状的挑战:一个由 6 个三角形组成的 "F"
假设我们要画一个稍复杂的字母 "F",由 6 个三角形构成,形状示意如下:
沿用当前的思路,我们需要把 setRectangle 改造成一个接收 x、y 参数的 setGeometry(gl, x, y),在生成顶点数据时把平移量直接写入每个坐标。仓库 webgl-2d-geometry-translate.html 中给出了完整的顶点布局:F 由左立柱(left column)、上横梁(top rung)、中间横梁(middle rung)三部分构成,宽 100、高 150、厚度 30:
// 用定义字母 'F' 的顶点数据填充缓冲
function setGeometry(gl, x, y) {
var width = 100;
var height = 150;
var thickness = 30;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// 左立柱
x, y,
x + thickness, y,
x, y + height,
x, y + height,
x + thickness, y,
x + thickness, y + height,
// 上横梁
x + thickness, y,
x + width, y,
x + thickness, y + thickness,
x + thickness, y + thickness,
x + width, y,
x + width, y + thickness,
// 中间横梁
x + thickness, y + thickness * 2,
x + width * 2 / 3, y + thickness * 2,
x + thickness, y + thickness * 3,
x + thickness, y + thickness * 3,
x + width * 2 / 3, y + thickness * 2,
x + width * 2 / 3, y + thickness * 3,
]),
gl.STATIC_DRAW);
}
虽然这段代码可以工作,但你应当能看出它很难扩展:如果我们要绘制由成百上千条线组成的复杂几何体,就需要编写相当复杂的代码;更糟的是,每次绘制时 JavaScript 都必须重新计算并上传所有顶点数据——gl.bufferData 会重新分配并填充缓冲区,场景越复杂,每一帧的 CPU 开销就越大。
四、方法二:把平移计算搬进 Shader
既然问题出在"每次都要重算顶点",那更聪明的做法是:几何体只上传一次,把"移动多少"这件事交给 GPU 去算。
4.1 改造顶点着色器
在基础篇的顶点着色器中加入一个 u_translation uniform,在顶点位置进入坐标变换之前先加上平移量:
<script id="vertex-shader-2d" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation; // 新增:平移量
void main() {
// 加上平移量
vec2 position = a_position + u_translation;
// 把像素坐标转换到 0.0 ~ 1.0
vec2 zeroToOne = position / u_resolution;
// 从 0->1 转换到 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// 从 0->2 转换到 -1->+1(裁剪空间)
vec2 clipSpace = zeroToTwo – 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
</script>
这一小步的意义重大:a_position + u_translation 由 GPU 在每个顶点上并行完成,JavaScript 侧从此不再触碰顶点数据。
4.2 几何体只上传一次
对应地,setGeometry 不再需要 x、y 参数——顶点数据以原点(0,0)为基准定义即可,平移交给 uniform:
// 用定义字母 'F' 的顶点数据填充缓冲(只调用一次)
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// 左立柱
0, 0,
30, 0,
0, 150,
0, 150,
30, 0,
30, 150,
// 上横梁
30, 0,
100, 0,
30, 30,
30, 30,
100, 0,
100, 30,
// 中间横梁
30, 60,
67, 60,
30, 90,
30, 90,
67, 60,
67, 90,
]),
gl.STATIC_DRAW);
}
注意 setGeometry 只在程序初始化时调用一次,它不再出现在 drawScene 内部。缓冲的创建与数据上传发生在主流程中:
// 创建用于存放位置的缓冲
var positionBuffer = gl.createBuffer();
// 绑定到 ARRAY_BUFFER(可以把 ARRAY_BUFFER 理解成 positionBuffer 的别名)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 上传几何体数据
setGeometry(gl);
4.3 通过 uniform 下发平移量
在初始化阶段,先用 gl.getUniformLocation 拿到 u_translation 的位置:
var translationLocation = gl.getUniformLocation(program, "u_translation");
然后在 drawScene 中、绘制调用之前,用 gl.uniform2fv 把平移数组传给着色器:
function drawScene() {
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// 设置分辨率(uniform2f:传两个标量)
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// 设置颜色
gl.uniform4fv(colorLocation, color);
// 设置平移(uniform2fv:传 vec2 数组/向量)
gl.uniform2fv(translationLocation, translation);
// 绘制几何体:6 个三角形 × 3 个顶点 = 18 个顶点
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
几个值得注意的细节:
- gl.uniform2f 与 gl.uniform2fv 的差别:前者接受两个独立标量(如分辨率宽高),后者接受长度为 2 的数组或 Float32Array(如平移量 translation)。仓库源码 webgl/webgl-2d-geometry-translate-better.html 中两种用法各司其职。
- drawArrays 的 count 从矩形的 6 变成了 18:F 由 6 个三角形组成,每个三角形 3 个顶点,共 18 个顶点。着色器按顺序消费缓冲中每 3 个顶点生成一个三角形。
- 滑块交互逻辑与矩形版本完全一致:拖拽滑块 → 更新 translation[index] → 调用 drawScene() → uniform2fv 把新平移量送入 GPU → 顶点着色器完成 a_position + u_translation。
五、两种方案的对比与选型
| 顶点数据 | 每次渲染调用带 x,y 的 setGeometry(gl, x, y) 重算并重新 bufferData 上传 | 初始化时上传一次,之后不再改动 |
| 平移计算 | JavaScript 逐顶点做加法 | GPU 顶点着色器并行完成 a_position + u_translation |
| 每帧 JS 工作量 | 随顶点数量线性增长 | 恒定:只调一次 uniform2fv + drawArrays |
| 代码复杂度 | 几何越复杂,生成顶点数据的 JS 越难维护 | 与几何复杂度基本无关 |
| 适用场景 | 顶点极少、便于演示理解的教学场景 | 任意规模几何体,动画、实时交互的常规做法 |
这正是原文强调的核心结论:当我们开始绘制时,WebGL 几乎包办了所有工作,我们唯一要做的就是设置一个平移值并请求绘制。即使几何体拥有数万个顶点,主流程代码也保持不变——顶点数目的增长只会增加 GPU 的负担,而不会拖累 JavaScript 每帧必须执行的代码量。
从源码结构看,这套"几何体只上传一次 + 变换交给 Shader"的思路是整个 webgl-fundamentals 教程后续内容的基础:它下一篇文章就顺势引入了旋转(见 旋转篇),并最终发展为矩阵运算与 3D 变换。平移只是这条演进路径上的第一步。
六、小结与动手建议
本文围绕 webgl-fundamentals 的 2D 平移章节,完整走了一遍从"JS 逐顶点重算"到"Shader 内平移"的优化过程:
你可以直接在浏览器中打开仓库里的两个对比示例亲身体验:先打开 逐顶点更新版本,再打开 Shader 内平移版本,拖动滑块观察两者视觉效果相同、而实现路径截然不同。理解了这一点,你就掌握了 WebGL 中"让变换发生在 GPU 侧"的核心思想,这是通向旋转、缩放乃至矩阵变换的必经之路。
赞
【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
项目地址:
https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看 免费下载
相关推荐
3D抽奖系统:3步打造企业级互动抽奖体验
7种WAF绕过技术实战秘籍:渗透测试专家进阶指南
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

