☰
WebGL Fundamentals:二维图形缩放(2D Scale)实战详解
2026/10/13 8:17:24 网站建设 项目流程
  • 图形学

【免费下载链接】webgl-fundamentals

WebGL lessons that start with the basics

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载

本教程出自 webgl-fundamentals 开源仓库的 WebGL 基础系列(日语版见 webgl/lessons/ja/webgl-2d-scale.md,英文版见 webgl/lessons/webgl-2d-scale.md),讲解如何在 WebGL 中对二维图形进行放大与缩小。读完本文你将掌握:在顶点着色器中通过一个uniform vec2 u_scale与顶点坐标相乘实现缩放的核心思路、对应的 JavaScript 侧 uniform 状态管理完整代码,以及负倍率带来的镜像翻转现象,并为后续用矩阵统一三种变换打好基础。

教程定位:WebGL 基础系列中的「缩放」篇

缩放(Scale)是 webgl-fundamentals 二维变换三连击(平移 → 旋转 → 缩放)的收官篇。该系列自 WebGL 的入门基础 开始,前两篇分别是二维图形的平移与二维图形的旋转。本教程在此基础上继续演进,最终引出下一篇文章二维矩阵数学——用矩阵把平移、旋转、缩放三种变换统一成更简单通用的形式。

与前两篇一脉相承,本篇依然采用「在顶点着色器中变换顶点、在 JavaScript 中通过 uniform 传入参数」的架构:顶点数据只上传一次,每次绘制只需更新 uniform 值,无需在 CPU 侧重新计算顶点坐标。这正是平移教程末尾强调的核心思想——几何体哪怕有成千上万个顶点,JavaScript 每帧也只需设置几个 uniform 值,剩下的全部交给 GPU。

缩放的核心思想:把顶点坐标乘以倍率

缩放与平移同样简单,核心只有一句话:把顶点坐标乘以缩放倍率。

平移是对坐标做加法(a_position + u_translation),而缩放是对坐标做乘法(a_position * u_scale)。这里u_scale是一个vec2,分别代表 X 轴和 Y 轴的缩放倍率:

  • u_scale = (1, 1)时图形大小不变(乘以 1 等于原值);
  • u_scale = (2, 2)时图形在两个方向都放大一倍;
  • u_scale = (0.5, 0.5)时图形缩小为一半;
  • 两个分量可以不同,例如(2, 1)实现只在水平方向拉宽的「压扁」效果。

注意 GLSL 中两个vec2直接相乘是逐分量乘法(component-wise multiply),因此a_position * u_scale等价于(a_position.x * u_scale.x, a_position.y * u_scale.y),与矩阵乘法的含义不同。

缩放发生在像素空间

从仓库中的完整示例 webgl/webgl-2d-geometry-scale.html 可以看到,缩放是在顶点坐标尚未转换到裁剪空间(clip space)之前进行的。此时坐标仍处于「像素单位」的本地空间(即教程中u_resolution对应的坐标系),所以倍率 2 意味着图形在屏幕上占据两倍的像素尺寸。这一点与 3D 世界中通常以模型空间为单位、再叠加观察/投影变换的缩放有所不同,但在 2D 教程阶段它是最直观的。

顶点着色器的完整实现

在上一篇旋转示例的基础上,着色器的改动非常小:新增一个uniform vec2 u_scale,并在旋转之前先对坐标做缩放。仓库中 webgl/webgl-2d-geometry-scale.html 的完整顶点着色器如下:

<script id="vertex-shader-2d" type="x-shader/x-vertex"> attribute vec2 a_position; uniform vec2 u_resolution; uniform vec2 u_translation; uniform vec2 u_rotation; uniform vec2 u_scale; void main() { // 先缩放坐标 vec2 scaledPosition = a_position * u_scale; // 再旋转坐标 vec2 rotatedPosition = vec2( scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x, scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x); // 最后叠加平移 vec2 position = rotatedPosition + u_translation; // 像素坐标 → 0~1 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>

新增的只有两处(其余来自前两篇):

uniform vec2 u_scale; // Scale the position vec2 scaledPosition = a_position * u_scale;

变换顺序:缩放 → 旋转 → 平移

着色器内依次执行缩放、旋转、平移三个步骤,其中旋转的计算直接复用了scaledPosition而非原始a_position。这意味着缩放先于旋转发生:图形先在自身坐标轴上被拉伸,再整体旋转。如果交换顺序(先旋转再缩放),结果会完全不同——这正是矩阵篇中要重点讨论的「变换顺序决定结果」问题的雏形,详见二维矩阵数学中对「先缩放后旋转再平移」与「先平移后旋转再缩放」两种顺序的对比说明。

片段着色器保持不变

片段着色器无需任何改动,仍然只是输出一个uniform vec4 u_color的纯色:

precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }

JavaScript 端:三个增量改动

对应着色器新增的 uniform,JavaScript 侧需要完成三件事:查询 uniform 位置、维护缩放状态、每帧上传数值。

1. 查询 uniform 位置

在程序链接近成功后,通过gl.getUniformLocation拿到u_scale的地址:

var scaleLocation = gl.getUniformLocation(program, "u_scale");

2. 初始化缩放状态

默认值为[1, 1],表示初始不缩放(乘以 1 等于原值):

var translation = [100, 150]; var rotation = [0, 1]; var scale = [1, 1]; var color = [Math.random(), Math.random(), Math.random(), 1];

3. 每帧设置 uniform

在drawScene()中,与平移、旋转并列,调用gl.uniform2fv将两个浮点数一次性上传给vec2类型的u_scale。仓库中完整的drawScene如下(见 webgl/webgl-2d-geometry-scale.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); // 打开顶点属性数组 gl.enableVertexAttribArray(positionLocation); // 绑定顶点缓冲 gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // 告诉属性如何从缓冲区取数据 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.uniform2fv(translationLocation, translation); // 设置旋转 gl.uniform2fv(rotationLocation, rotation); // 设置缩放 ← 本篇新增 gl.uniform2fv(scaleLocation, scale); // 绘制几何体 var primitiveType = gl.TRIANGLES; var offset = 0; var count = 18; // 'F' 由 6 个三角形组成,每个三角形 3 个顶点 gl.drawArrays(primitiveType, offset, count); }

需要强调的是,setGeometry(gl)只在初始化时调用一次(把 'F' 的 18 个顶点上传到 GPU),drawScene中不再触碰顶点数据。每次拖动滑杆只是修改 JavaScript 数组、调用gl.uniform2fv重新上传 uniform,随后gl.drawArrays即可让 GPU 用新倍率重绘——这正是「顶点上传一次、变换全部在着色器完成」架构的优势。

交互示例与滑杆配置

教程配套的可运行示例是 webgl/webgl-2d-geometry-scale.html,页面描述为 "Drag sliders to translate, rotate, and scale.",可直接在浏览器中打开体验。它借助仓库工具 webgl/resources/webgl-lessons-ui.js 中的webglLessonsUI.setupSlider生成滑杆 UI:

webglLessonsUI.setupSlider("#x", {value: translation[0], slide: updatePosition(0), max: gl.canvas.width }); webglLessonsUI.setupSlider("#y", {value: translation[1], slide: updatePosition(1), max: gl.canvas.height}); webglLessonsUI.setupSlider("#angle", {slide: updateAngle, max: 360}); webglLessonsUI.setupSlider("#scaleX", {value: scale[0], slide: updateScale(0), min: -5, max: 5, step: 0.01, precision: 2}); webglLessonsUI.setupSlider("#scaleY", {value: scale[1], slide: updateScale(1), min: -5, max: 5, step: 0.01, precision: 2});

注意 X/Y 轴缩放滑杆的配置参数:

  • min: -5, max: 5:倍率取值范围为 -5 到 5,刻意放开到负数以便演示镜像翻转;
  • step: 0.01, precision: 2:步进 0.01、显示两位小数,允许精细调节;
  • 平移与旋转滑杆则分别限定在画布尺寸内(max: gl.canvas.width/height)和 0~360 度。

回调函数updateScale本质上是闭包,把滑杆值写入对应轴再触发重绘:

function updateScale(index) { return function(event, ui) { scale[index] = ui.value; drawScene(); }; }

负倍率:镜像翻转

教程特别提醒一个观察点:当缩放倍率为负数时,图形会发生镜像翻转。

例如u_scale = (-1, 1)会让图形沿 Y 轴左右镜像(X 坐标全部取反),u_scale = (1, -1)会上下翻转,两者都为负则相当于绕原点旋转 180°。从数学上看,scaledPosition = a_position * u_scale中负分量自然地把对应轴坐标取反,因此翻转是乘法的直接结果,无需任何额外代码。这也解释了为何示例滑杆范围允许取到 -5——你可以亲手拖动scaleX滑杆跨过 0 点,观察 'F' 在某一瞬间被压成一条线然后翻转到镜像位置的过程。

从手写缩放走向矩阵数学

本教程把缩放写死在着色器里,但正如系列所强调的:缩放、旋转、平移三种变换互相依赖顺序,每次调整顺序都要改写着色器,非常不便。下一篇二维矩阵数学将展示如何用 3×3 矩阵把三种变换统一起来——届时缩放矩阵的构建正是本篇思路的推广。

在矩阵体系中,缩放被表示为一个对角矩阵,仓库中的矩阵库 webgl/resources/m3.js 提供了现成实现(scaling函数,见该文件 L259-L273):

function scaling(sx, sy, dst) { dst = dst || new MatType(9); dst[0] = sx; dst[1] = 0; dst[2] = 0; dst[3] = 0; dst[4] = sy; dst[5] = 0; dst[6] = 0; dst[7] = 0; dst[8] = 1; return dst; }

其中sx、sy与本篇的u_scale.x、u_scale.y一一对应;配套的scale(m, sx, sy)函数则是「矩阵 × 缩放矩阵」的快捷方式(同文件 L284-L286)。在矩阵方案下,顶点着色器退化为一行gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);,缩放、旋转、平移全部由 JavaScript 侧按需组合矩阵实现,且调整变换顺序不再需要改写任何 GLSL 代码。

附注:为什么示例图形用字母 'F'?

教程的底栏补充了一个经典设计决策:为什么所有示例都用字母 'F'?原文(webgl/lessons/ja/webgl-2d-scale.md 底栏)给出的理由是:'F' 的朝向在任何方向下都能一眼辨认,这是调试变换效果的关键。

  • 用爱心 ❤ 或三角形 △,水平翻转后无法分辨(它们具有对称性);
  • 用圆形 ○ 更糟,旋转与否几乎看不出来;
  • 用四色矩形理论上可行,但必须记住哪个角是什么颜色,徒增记忆负担;
  • 'F' 的左竖、上横、中横结构使其在缩放、旋转、镜像后依然可辨识方向,方便即时验证变换是否正确。

配套的方向示意图为 webgl/resources/f-orientation.svg。教程作者表示,任何「方向可辨识」的图形都适用,只是自第一次见到有人用 'F' 做纹理示例后便一直沿用至今。

小结

二维缩放的本质极其简单——a_position * u_scale一个乘法即可完成;但它承载了两个重要观念:一是「顶点只上传一次、变换交给着色器 + uniform」的 GPU 友好架构,二是变换顺序对结果的显著影响。前者在本系列中一以贯之,后者则直接引出了下一站:用矩阵数学统一平移、旋转与缩放三种变换,为进入三维世界做好准备。若想继续深入,可以继续阅读二维矩阵数学,或回顾平移与旋转两篇建立完整认知。

  • 图形学

【免费下载链接】webgl-fundamentals

WebGL lessons that start with the basics

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载
上一篇:nodebestpractices 通用 Docker 最佳实践:从 COPY/ADD 取舍到镜像完整性校验的六条守则
下一篇:如何通过APK安装器在Windows上原生运行安卓应用?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询