JavaScript内核与高级编程之:`JavaScript` 的 `WebGL`:其在 `JavaScript` 中的 `GPU` 编程。

咳咳,大家好!今天咱们聊点儿刺激的,直接上手,聊聊JavaScript里的“显卡超频”——WebGL。

一、 啥是WebGL?—— 浏览器里的硬件加速器

WebGL,全称Web Graphics Library,直译过来就是“网页图形库”。它可不是什么新奇玩意儿,它本质上是OpenGL ES 2.0/3.0的JavaScript binding(绑定)。这意味着啥?意味着你可以直接在浏览器里,用JavaScript来调用GPU的强大计算能力,搞出各种炫酷的3D图形效果,甚至做一些复杂的计算任务。

想想看,以前只能在桌面应用里看到的3D游戏、数据可视化、科学模拟等等,现在都能在浏览器里跑起来,是不是有点小激动?

二、 WebGL的工作原理:流水线的故事

要把3D世界搬到浏览器里,WebGL可不是简单地画几个三角形就完事儿的。它背后有一套复杂的渲染流程,我们通常称之为“渲染管线”(Rendering Pipeline)。这个管线就像一个工厂的流水线,把原始的3D数据一步步加工成最终的图像。

我们来简单地拆解一下这个流水线:

  1. 顶点数据(Vertex Data): 这是所有故事的起点。它包含了3D模型的所有顶点信息,比如每个顶点的坐标、颜色、法向量等等。

  2. 顶点着色器(Vertex Shader): 这是第一个“工人”,负责对顶点数据进行处理。它可以进行坐标变换(比如把模型坐标转换到世界坐标、再转换到相机坐标)、光照计算等等。它的输入是顶点数据,输出是变换后的顶点数据。

  3. 图元装配(Primitive Assembly): 这个阶段把顶点组装成一个个的图元,比如三角形、线段、点等等。

  4. 光栅化(Rasterization): 这个阶段把图元转换成像素。简单来说,就是确定哪些像素需要被绘制,以及每个像素的颜色和深度值。

  5. 片元着色器(Fragment Shader): 这是第二个“工人”,负责对每个像素进行着色。它可以进行更复杂的光照计算、纹理贴图等等。它的输入是光栅化阶段产生的像素信息,输出是最终的像素颜色。

  6. 测试与混合(Tests and Blending): 这个阶段进行深度测试、模板测试、颜色混合等等,最终把像素颜色写入帧缓冲区(Frame Buffer)。

  7. 帧缓冲区(Frame Buffer): 存储最终图像的缓冲区。

代码示例:一个简单的三角形

光说不练假把式,咱们来写一个简单的WebGL程序,画一个彩色的三角形。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebGL Triangle</title>
    <style>
        body { margin: 0; }
        canvas { width: 100%; height: 100%; display: block; }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        const canvas = document.getElementById("glCanvas");
        const gl = canvas.getContext("webgl");

        if (!gl) {
            alert("WebGL not supported!");
        }

        // 顶点着色器代码
        const vsSource = `
            attribute vec4 aVertexPosition;
            attribute vec4 aVertexColor;
            varying lowp vec4 vColor;

            void main() {
                gl_Position = aVertexPosition;
                vColor = aVertexColor;
            }
        `;

        // 片元着色器代码
        const fsSource = `
            varying lowp vec4 vColor;

            void main() {
                gl_FragColor = vColor;
            }
        `;

        // 初始化着色器程序
        function initShaderProgram(gl, vsSource, fsSource) {
            const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
            const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

            const shaderProgram = gl.createProgram();
            gl.attachShader(shaderProgram, vertexShader);
            gl.attachShader(shaderProgram, fragmentShader);
            gl.linkProgram(shaderProgram);

            if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
                alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
                return null;
            }

            return shaderProgram;
        }

        // 加载着色器
        function loadShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }

            return shader;
        }

        // 初始化缓冲区
        function initBuffers(gl) {
            const positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

            const positions = [
                0.0,  0.5,
                -0.5, -0.5,
                0.5, -0.5,
            ];

            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

            const colorBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

            const colors = [
                1.0,  0.0,  0.0,  1.0,    // 红色
                0.0,  1.0,  0.0,  1.0,    // 绿色
                0.0,  0.0,  1.0,  1.0,    // 蓝色
            ];

            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

            return {
                position: positionBuffer,
                color: colorBuffer,
            };
        }

        // 绘制场景
        function drawScene(gl, programInfo, buffers) {
            gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
            gl.clearDepth(1.0);                 // Clear everything
            gl.enable(gl.DEPTH_TEST);           // Enable depth testing
            gl.depthFunc(gl.LEQUAL);            // Near things obscure far things

            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

            // 设置顶点属性指针
            gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
            gl.vertexAttribPointer(
                programInfo.attribLocations.vertexPosition,
                2,                  // 每个顶点属性由2个值组成
                gl.FLOAT,           // 数据类型是浮点数
                false,              // 不进行归一化
                0,                  // 步长为0
                0                   // 偏移量为0
            );
            gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);

            // 设置颜色属性指针
            gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
            gl.vertexAttribPointer(
                programInfo.attribLocations.vertexColor,
                4,                  // 每个顶点属性由4个值组成
                gl.FLOAT,           // 数据类型是浮点数
                false,              // 不进行归一化
                0,                  // 步长为0
                0                   // 偏移量为0
            );
            gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);

            // 设置着色器程序
            gl.useProgram(programInfo.program);

            // 绘制三角形
            gl.drawArrays(gl.TRIANGLES, 0, 3);
        }

        // 程序入口
        function main() {
            const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

            const programInfo = {
                program: shaderProgram,
                attribLocations: {
                    vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
                    vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
                },
            };

            const buffers = initBuffers(gl);

            drawScene(gl, programInfo, buffers);
        }

        main();
    </script>
</body>
</html>

这个代码有点长,但别害怕,咱们一步步来解释:

  • HTML: 创建了一个 <canvas> 元素,这是WebGL绘制的地方。
  • JavaScript:
    • 获取WebGL上下文: canvas.getContext("webgl") 获取WebGL的上下文对象。
    • 着色器代码: vsSourcefsSource 分别是顶点着色器和片元着色器的代码。它们是用GLSL(OpenGL Shading Language)写的,这是一种专门用于GPU编程的语言。
    • 创建和编译着色器: loadShader 函数负责加载、编译着色器代码。
    • 创建着色器程序: initShaderProgram 函数负责把顶点着色器和片元着色器链接成一个着色器程序。
    • 创建缓冲区: initBuffers 函数负责创建顶点缓冲区和颜色缓冲区,并把数据上传到GPU。
    • 绘制场景: drawScene 函数负责设置WebGL的状态,并调用gl.drawArrays函数来绘制三角形。
    • 程序入口: main 函数是程序的入口,它负责初始化WebGL,并调用drawScene函数来绘制场景。

把这段代码保存成一个HTML文件,用浏览器打开,你就能看到一个彩色的三角形了。

三、 GLSL:GPU的语言

刚才我们看到了,顶点着色器和片元着色器是用GLSL写的。GLSL是一种类C的语言,专门用于GPU编程。它有很多内置的函数和数据类型,可以方便地进行图形计算。

GLSL有一些特殊的变量,需要特别注意:

变量名 类型 说明
attribute 变量修饰符 用于顶点着色器,声明从JavaScript传递过来的顶点属性。只能在顶点着色器中使用。
uniform 变量修饰符 用于声明全局变量,可以从JavaScript传递过来。可以在顶点着色器和片元着色器中使用。
varying 变量修饰符 用于声明从顶点着色器传递到片元着色器的变量。在顶点着色器中赋值,在片元着色器中接收。
gl_Position vec4 顶点着色器的内置变量,用于指定顶点的最终位置。必须在顶点着色器中赋值。
gl_FragColor vec4 片元着色器的内置变量,用于指定像素的最终颜色。必须在片元着色器中赋值。

四、 WebGL的进阶:矩阵变换、纹理贴图、光照模型

画一个三角形只是WebGL的入门。要做出更炫酷的效果,还需要掌握一些更高级的技术。

  • 矩阵变换: 通过矩阵变换,可以对3D模型进行平移、旋转、缩放等操作。常用的矩阵变换包括模型矩阵(Model Matrix)、视图矩阵(View Matrix)、投影矩阵(Projection Matrix)。

  • 纹理贴图: 把图像贴到3D模型上,可以增加模型的细节和真实感。

  • 光照模型: 模拟光照效果,可以使3D场景更加逼真。常用的光照模型包括环境光、漫反射光、镜面反射光。

代码示例:矩阵变换

我们来修改一下之前的代码,用矩阵变换来旋转三角形。

// 顶点着色器代码 (修改后)
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aVertexColor;
    uniform mat4 uModelViewMatrix;  // 模型视图矩阵
    uniform mat4 uProjectionMatrix; // 投影矩阵
    varying lowp vec4 vColor;

    void main() {
        gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
        vColor = aVertexColor;
    }
`;

// 片元着色器代码 (不变)
const fsSource = `
    varying lowp vec4 vColor;

    void main() {
        gl_FragColor = vColor;
    }
`;

// 初始化着色器程序 (修改后)
function initShaderProgram(gl, vsSource, fsSource) {
    // ... (省略) ...
    return shaderProgram;
}

// 绘制场景 (修改后)
function drawScene(gl, programInfo, buffers, deltaTime) {
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // 创建透视投影矩阵
    const fieldOfView = 45 * Math.PI / 180;   // in radians
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const zNear = 0.1;
    const zFar = 100.0;
    const projectionMatrix = mat4.create();  // 使用gl-matrix库

    mat4.perspective(projectionMatrix,
                     fieldOfView,
                     aspect,
                     zNear,
                     zFar);

    // 创建模型视图矩阵
    const modelViewMatrix = mat4.create();

    // 将模型视图矩阵移动到绘制正方形的起始位置
    mat4.translate(modelViewMatrix,     // destination matrix
                     modelViewMatrix,     // matrix to translate
                     [-0.0, 0.0, -6.0]);  // amount to translate

    // 旋转模型视图矩阵
    mat4.rotate(modelViewMatrix,  // destination matrix
                modelViewMatrix,  // matrix to rotate
                deltaTime,        // amount to rotate in radians
                [0, 0, 1]);       // axis to rotate around (Z)

    // 设置顶点属性指针 (不变)
    // ... (省略) ...

    // 设置着色器程序
    gl.useProgram(programInfo.program);

    // 设置 uniform 变量
    gl.uniformMatrix4fv(
        programInfo.uniformLocations.projectionMatrix,
        false,
        projectionMatrix);
    gl.uniformMatrix4fv(
        programInfo.uniformLocations.modelViewMatrix,
        false,
        modelViewMatrix);

    // 绘制三角形
    gl.drawArrays(gl.TRIANGLES, 0, 3);
}

// 程序入口 (修改后)
function main() {
    const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

    const programInfo = {
        program: shaderProgram,
        attribLocations: {
            vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
            vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
        },
        uniformLocations: {
            projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
            modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
        },
    };

    const buffers = initBuffers(gl);

    let then = 0;
    // 绘制场景
    function render(now) {
        now *= 0.001;  // convert to seconds
        const deltaTime = now - then;
        then = now;

        drawScene(gl, programInfo, buffers, deltaTime);

        requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
}

main();

这个代码主要做了以下修改:

  • 顶点着色器: 增加了 uModelViewMatrixuProjectionMatrix 两个 uniform 变量,分别表示模型视图矩阵和投影矩阵。在main函数中,将顶点位置乘以这两个矩阵,得到最终的顶点位置。
  • 绘制场景: 增加了 projectionMatrixmodelViewMatrix 两个矩阵,分别表示投影矩阵和模型视图矩阵。使用 mat4.perspective 函数创建透视投影矩阵,使用 mat4.translatemat4.rotate 函数创建模型视图矩阵。然后,通过 gl.uniformMatrix4fv 函数把这两个矩阵传递给顶点着色器。
  • 动画循环: 使用 requestAnimationFrame 创建动画循环,使三角形不断旋转。

注意: 这个代码使用了 gl-matrix 库来进行矩阵运算。你需要在HTML文件中引入这个库。你可以从https://glmatrix.net/下载它。

五、 WebGL的用途:不仅仅是游戏

虽然WebGL在游戏开发中应用广泛,但它的用途远不止于此。

  • 数据可视化: 可以用WebGL来创建各种炫酷的数据可视化图表,比如3D散点图、3D柱状图等等。
  • 科学模拟: 可以用WebGL来模拟各种科学现象,比如流体模拟、粒子模拟等等。
  • 虚拟现实/增强现实: WebGL是WebVR/WebAR的基础技术,可以用它来创建各种虚拟现实/增强现实应用。
  • 图像处理: 可以用WebGL来进行图像处理,比如滤镜、特效等等。

六、 WebGL的挑战:性能优化、兼容性

WebGL虽然强大,但也面临一些挑战。

  • 性能优化: WebGL程序通常需要处理大量的图形数据,因此性能优化非常重要。常用的优化方法包括减少绘制调用、使用顶点缓冲区对象(VBO)、使用纹理贴图集(Texture Atlas)等等。
  • 兼容性: WebGL的兼容性受到浏览器和显卡驱动的影响。为了保证程序的兼容性,需要进行充分的测试,并针对不同的平台进行优化。

七、 总结:WebGL,开启无限可能

WebGL是JavaScript里的一把利器,它让开发者能够直接利用GPU的强大计算能力,创造出各种令人惊艳的图形效果和应用。虽然学习WebGL需要掌握一些新的概念和技术,但只要你肯努力,就能开启无限可能。

今天的分享就到这里。希望大家能从中学到一些东西,并开始尝试用WebGL创造属于自己的精彩世界。下次有机会再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注