第 3 篇:让图形动起来 - WebGL 2D 变换

到目前为止,我们已经创造了一个有形状、有颜色的三角形。但要让它在画布上移动,我们现在唯一的办法就是......回去修改 JavaScript 里的顶点坐标数组。

javascript 复制代码
// 如果想让三角形向右移动一点...
const positionsAndColors = [
     0.1,  0.5,    1.0, 0.0, 0.0, // 把 x 从 0.0 改成 0.1
    -0.4, -0.5,    0.0, 1.0, 0.0, // 把 x 从 -0.5 改成 -0.4
     0.6, -0.5,    0.0, 0.0, 1.0  // 把 x 从 0.5 改成 0.6
];
// 然后重新上传整个 buffer 的数据...
gl.bufferData(...);

这太笨拙了!如果要让它旋转呢?天哪,那我们得拿出三角函数来手算每个点的新坐标。这种方式不仅效率低下,而且完全违背了 GPU 的设计初衷。

GPU,这位图形处理专家,最擅长的工作就是------疯狂地进行数学运算 ,尤其是矩阵运算。而变换 (Transformation),无论是平移、旋转还是缩放,本质上都是数学运算。

魔法咒语:矩阵 (Matrix)

想象一下,你有一个点的坐标 (x, y)。现在,我给你一个神奇的"咒语"------矩阵 ,你用这个点的坐标去乘以这个咒语,就能得到一个新的坐标 (x', y')

  • 如果这个咒语是"平移咒语",新坐标就是点平移后的位置。
  • 如果这个咒语是"旋转咒语",新坐标就是点围绕原点旋转后的位置。
  • 如果这个咒语是"缩放咒语",新坐标就是点缩放后的位置。

更神奇的是,这些咒语可以叠加!你可以先旋转,再平移,只需要把两个咒语矩阵先乘起来,得到一个更复杂的"复合咒语",然后用这个终极咒语去处理所有的点,一步到位。

这就是 2D 变换的核心思想:定义一个物体的形状一次(比如一个以原点为中心的三角形),然后通过向 GPU 发送不同的"变换咒语"(矩阵),来控制它在屏幕上的最终状态。

新伙伴登场:uniform 变量

我们如何把这个"咒语矩阵"发送给 GPU 呢?

回顾一下,attribute 是给每个顶点都不同的数据。但变换矩阵对于一个物体的所有顶点来说,都是一样 的,是统一 的。因此,我们需要 GLSL 变量家族的最后一位成员:uniform

uniform 变量就像一个全局指令,从 JavaScript 发出,顶点着色器(有时片元着色器也会用)里的每一次执行都能接收到这份完全相同的数据。

开始施法:代码改造

1. 顶点着色器:接收并使用矩阵

我们的顶点着色器需要一个 uniform 变量来接收 2D 变换矩阵(它是一个 3x3 的矩阵,所以类型是 mat3)。然后,在计算 gl_Position 之前,用它来变换顶点的位置。

ini 复制代码
attribute vec2 a_position;
attribute vec4 a_color;

uniform mat3 u_transformMatrix; // 新增:接收变换矩阵

varying vec4 v_color;

void main() {
  // 核心改动:
  // 1. 把 a_position (vec2) 扩展成 vec3,因为 mat3 需要和 vec3 相乘。
  //    我们加一个 1.0,这在图形学中称为齐次坐标,是矩阵运算的要求。
  // 2. 将位置向量与变换矩阵相乘,得到变换后的新位置。
  // 3. 取新位置的 x, y 分量,构建最终的 gl_Position。
  vec2 transformedPosition = (u_transformMatrix * vec3(a_position, 1.0)).xy;
  
  gl_Position = vec4(transformedPosition, 0.0, 1.0);
  v_color = a_color;
}

2. 片元着色器:无需改动!

颜色逻辑和变换无关,所以我们的片元着色器保持原样。

3. JavaScript:动画循环与矩阵计算

JavaScript 的部分将是改动最大的。我们需要:

  • 恢复顶点数据 :让三角形的坐标变回最原始、最简单的状态,比如以(0,0)为中心。所有变换都交给矩阵。
  • 添加矩阵数学函数 :在现实项目中,你通常会使用一个成熟的库(如 gl-matrix)。但为了教学,我们手动实现几个简单的函数来创建平移、旋转和缩放矩阵。这样能让你更清楚地看到底层发生了什么。
  • 获取 uniform 的位置 :就像 getAttribLocation 一样,我们需要用 getUniformLocation 来找到着色器中 uniform 变量的地址。
  • 创建动画循环 :使用 requestAnimationFrame,这是一个浏览器提供的 API,能让我们以最优的帧率来重复执行绘制函数,从而形成流畅的动画。在每一帧中,我们都会更新变换(比如改变旋转角度),计算新的矩阵,并重新绘制。

下面是集成了所有改动的完整代码。这次,你会看到一个在原地不停旋转的彩色三角形!

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebGL 教程 3:动态变换</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 2D 变换</h1>
    <canvas id="webgl-canvas" width="500" height="500"></canvas>

    <!-- 顶点着色器代码 (已更新) -->
    <script id="vertex-shader" type="x-shader/x-vertex">
        attribute vec2 a_position;
        attribute vec4 a_color;

        // 新增:接收一个 3x3 的变换矩阵
        uniform mat3 u_transformMatrix;

        varying vec4 v_color;

        void main() {
            // 将 2D 位置乘以 3x3 矩阵
            // a_position 是 vec2,需要扩展成 vec3 (x, y, 1) 才能与 mat3 相乘
            vec2 transformedPosition = (u_transformMatrix * vec3(a_position, 1.0)).xy;

            gl_Position = vec4(transformedPosition, 0.0, 1.0);
            v_color = a_color;
        }
    </script>

    <!-- 片元着色器代码 (无变化) -->
    <script id="fragment-shader" type="x-shader/x-fragment">
        precision mediump float;
        varying vec4 v_color;
        void main() {
            gl_FragColor = v_color;
        }
    </script>

    <script>
        // --- 简单的矩阵数学帮助函数 ---
        // 在实际项目中,你会使用像 gl-matrix 这样的库
        const matrixUtils = {
            createIdentity: () => [
                1, 0, 0,
                0, 1, 0,
                0, 0, 1
            ],
            createTranslation: (tx, ty) => [
                1, 0, 0,
                0, 1, 0,
                tx, ty, 1
            ],
            createRotation: (angleInRadians) => {
                const c = Math.cos(angleInRadians);
                const s = Math.sin(angleInRadians);
                return [
                    c, -s, 0,
                    s,  c, 0,
                    0,  0, 1
                ];
            },
            createScale: (sx, sy) => [
                sx, 0,  0,
                0, sy,  0,
                0,  0,  1
            ],

            // 矩阵乘法 (matA * matB)
            multiply: (matA, matB) => {
                const a00 = matA, a01 = matA, a02 = matA;
                const a10 = matA, a11 = matA, a12 = matA;
                const a20 = matA, a21 = matA, a22 = matA;
                const b00 = matB, b01 = matB, b02 = matB;
                const b10 = matB, b11 = matB, b12 = matB;
                const b20 = matB, b21 = matB, b22 = matB;
                return [
                    b00 * a00 + b01 * a10 + b02 * a20,
                    b00 * a01 + b01 * a11 + b02 * a21,
                    b00 * a02 + b01 * a12 + b02 * a22,
                    b10 * a00 + b11 * a10 + b12 * a20,
                    b10 * a01 + b11 * a11 + b12 * a21,
                    b10 * a02 + b11 * a12 + b12 * a22,
                    b20 * a00 + b21 * a10 + b22 * a20,
                    b20 * a01 + b21 * a11 + b22 * a21,
                    b20 * a02 + b21 * a12 + b22 * a22
                ];
            }
        };

        function main() {
            const canvas = document.getElementById('webgl-canvas');
            const gl = canvas.getContext('webgl');
            if (!gl) { alert('WebGL not supported!'); return; }

            const vertexShaderSource = document.getElementById('vertex-shader').text;
            const fragmentShaderSource = document.getElementById('fragment-shader').text;
            const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
            const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
            const program = createProgram(gl, vertexShader, fragmentShader);

            const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
            const colorAttributeLocation = gl.getAttribLocation(program, "a_color");
            // 新增: 获取 uniform 变量的位置
            const matrixUniformLocation = gl.getUniformLocation(program, "u_transformMatrix");

            const positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            
            // 顶点数据恢复到以原点为中心的简单版本
            const positionsAndColors = [
                 0.0,  0.25,   1.0, 0.0, 0.0,
                -0.25,-0.25,   0.0, 1.0, 0.0,
                 0.25,-0.25,   0.0, 0.0, 1.0
            ];
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positionsAndColors), gl.STATIC_DRAW);

            gl.useProgram(program);

            gl.enableVertexAttribArray(positionAttributeLocation);
            gl.enableVertexAttribArray(colorAttributeLocation);
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

            const FSIZE = (new Float32Array()).BYTES_PER_ELEMENT;
            const STRIDE = 5 * FSIZE;
            gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, STRIDE, 0);
            gl.vertexAttribPointer(colorAttributeLocation, 3, gl.FLOAT, false, STRIDE, 2 * FSIZE);

            let currentAngle = 0;
            
            // 动画循环
            function animate() {
                // 1. 更新状态 (例如:角度)
                currentAngle += 0.02;

                // 2. 计算矩阵
                let matrix = matrixUtils.createIdentity();
                // 注意乘法顺序:先缩放,再旋转,最后平移
                // matrix = matrixUtils.multiply(matrix, matrixUtils.createScale(1.0, 1.0));
                matrix = matrixUtils.multiply(matrix, matrixUtils.createRotation(currentAngle));
                matrix = matrixUtils.multiply(matrix, matrixUtils.createTranslation(0.5, 0.0));

                // 3. 绘制
                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);

                // 将计算好的矩阵发送给顶点着色器
                // gl.uniformMatrix3fv(location, transpose, value)
                // transpose 必须是 false
                gl.uniformMatrix3fv(matrixUniformLocation, false, matrix);

                gl.drawArrays(gl.TRIANGLES, 0, 3);
                
                // 4. 请求下一帧
                requestAnimationFrame(animate);
            }

            // 启动动画!
            animate();
        }

        // --- 辅助函数 (与之前相同) ---
        function createShader(gl, type, source) { /* ... */ }
        function createProgram(gl, vertexShader, fragmentShader) { /* ... */ }
        // For brevity, I'm hiding the unchanged helper functions here.
        // They are the same as in the previous article.
        function createShader(gl, type, source) {const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader; console.error("Shader compile error:", gl.getShaderInfoLog(shader)); gl.deleteShader(shader);}
        function createProgram(gl, vertexShader, fragmentShader) {const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program; console.error("Program link error:", gl.getProgramInfoLog(program)); gl.deleteProgram(program); }

    </script>
</body>
</html>

总结与展望

太棒了!我们成功地让三角形"活"了起来。它先是围绕自己的中心旋转,然后作为一个整体平移到了画布的右半边。

今天我们解锁了 WebGL 中极为强大的能力:

  • 理解了变换的意义:将形状定义与位置状态分离,让控制更灵活。
  • 认识了矩阵:它是实现平移、旋转、缩放的数学工具,是 GPU 的"母语"。
  • 掌握了 uniform 变量:学会了如何向着色器传递对所有顶点都一致的全局数据。
  • 创建了动画循环 :使用 requestAnimationFrame 让我们的场景动了起来。

现在,我们的 2D 图形已经相当完备了。但 WebGL 的魅力远不止于此。在下一篇中,我们将为我们的图形穿上华丽的"外衣"------纹理贴图。我们将学习如何加载一张图片,并把它精准地"贴"到我们的三角形表面上,让它不再是简单的颜色渐变。

准备好进入更丰富多彩的视觉世界了吗?敬请期待 《第 4 篇:赋予表面生命 - WebGL 纹理贴图》

相关推荐
胖鱼罐头3 小时前
Android-尺寸单位换算全解析
前端
不一样的少年_3 小时前
别再无脑装插件了!你的浏览器扩展可能正在“偷家”
前端·安全·浏览器
Linsk3 小时前
如何实现TypeScript级的polyfill自动引入
前端·typescript·前端工程化
林希_Rachel_傻希希3 小时前
一文搞懂 JavaScript 数组非破坏性方法:slice、indexOf、join 你都懂了吗?
前端·javascript
_AaronWong3 小时前
分享一个平常用的工具包:前端开发实用工具函数集合
前端·javascript·vue.js
我是天龙_绍3 小时前
vue2数据响应式
前端
猪哥帅过吴彦祖3 小时前
Flutter 系列教程:Dart 语言快速入门 (下)
前端·flutter·ios
Keepreal4963 小时前
浏览器事件循环
javascript·浏览器
西瓜啵啵奶茶3 小时前
Siderbar和Navbar
前端