WebGL 和 Three.js 核心对比

1,397 阅读10分钟

引言

Three.js 和 WebGL 是现代 Web 3D 图形渲染的两项重要技术。虽然它们密切相关,但它们在设计目标、使用方法和应用场景上有明显的区别。要理解它们的核心原理,我们可以从以下几个方面进行对比和分析:

一、WebGL 的核心原理

WebGL(Web Graphics Library)是一种底层的 JavaScript API,用于在浏览器中直接操作 GPU 进行 3D 图形渲染。它基于 OpenGL ES 2.0,允许开发者在浏览器中构建复杂的 2D 和 3D 场景。

1. 直接与 GPU 交互

WebGL 允许开发者直接与 GPU 交互,这意味着开发者可以控制渲染的每一个细节,如顶点处理、片段着色、纹理处理等。这种低级控制赋予了 WebGL 极高的灵活性和性能,但也要求开发者具备较高的图形编程知识。

2. 着色器(Shaders)

WebGL 使用 GLSL(OpenGL Shading Language)编写着色器程序。着色器是运行在 GPU 上的小程序,用于处理顶点和像素数据。WebGL 的图形渲染分为两个主要阶段:顶点着色器处理几何形状的顶点,片段着色器决定最终像素的颜色。

着色器提供了高度自定义的能力,使开发者可以实现复杂的视觉效果,如光照模型、纹理映射、阴影处理等。

3. 渲染管线

WebGL 的渲染管线是固定的,包括顶点处理、图元装配、光栅化、片段处理等步骤。每个步骤都可以通过着色器程序进行定制,从而实现特定的渲染需求。

4. 低级别 API

WebGL 是一个非常低级别的 API,开发者需要手动管理图形渲染中的很多细节,如创建缓冲区、加载纹理、编译着色器、处理深度缓冲区等。虽然这赋予了极大的控制权,但也增加了开发的复杂性。

二、Three.js 的核心原理

Three.js 是一个基于 WebGL 的高级 JavaScript 库,旨在简化 3D 图形的创建和渲染。它为开发者提供了一组抽象层,使得即使不熟悉图形编程的开发者也能轻松创建 3D 场景。等。

1. 抽象与封装

Three.js 对 WebGL 的底层操作进行了大量封装,提供了更高层次的抽象。例如,在 Three.js 中,几何体、材质、相机和灯光等都是现成的对象,开发者只需要调用相关 API 即可快速创建 3D 场景,而不必处理 WebGL 中繁琐的底层细节。

2. 场景图(Scene Graph)

Three.js主要有几个核心组件,分别为场景(Scene)、相机(Camera)、灯光(Light)、物体(Object3D)、几何体(Geometry)、材质(Material)。

Three.js 使用场景图的概念来组织 3D 对象。所有 3D 对象(如几何体、灯光、相机等)都是 Object3D 的实例,它们可以被添加到 Scene 中。场景图允许开发者以层次化的方式管理和操作这些对象,使得复杂场景的构建变得更加直观。

3. 即用型组件

Three.js 提供了大量即用型组件,如几何体(BoxGeometrySphereGeometry 等)、材质(MeshBasicMaterialMeshPhongMaterial 等)、灯光(DirectionalLightPointLight 等)、相机(PerspectiveCameraOrthographicCamera 等)。这些组件大大降低了开发复杂 3D 场景的门槛。

4. 着色器的简化使用

虽然 Three.js 封装了 WebGL 的很多细节,但它仍然保留了对自定义着色器的支持。开发者可以使用 ShaderMaterial 来定义自己的着色器程序,同时 Three.js 还提供了一些简单的工具和库来简化着色器的编写和管理。

5. 渲染循环与动画

Three.js 提供了一个简单易用的渲染循环,通过 requestAnimationFrame 方法,开发者可以轻松实现动画效果。Three.js 还内置了多种动画和过渡效果,使得开发者能够快速为场景添加动态元素。

三、WebGL 与 Three.js 的对比

主要通过抽象层级和渲染方式两方面进行对比,最后通过具体例子给大家展示两者的区别。

1. 抽象层次

1.1. 开发难度

WebGL:较难,需要深入了解图形渲染原理和 GPU 编程。适合那些需要精细控制渲染过程或需要实现高度自定义效果的场景。

Three.js:简单,它降低了开发门槛,通过封装常见的 3D 渲染任务,使得即使是不熟悉图形编程的开发者也能创建复杂的 3D 场景。

1.2. 灵活性

WebGL:灵活度相当高,提供了对渲染管线的完全控制,可以实现极为复杂和特定的渲染效果。

Three.js:灵活性相对较弱,虽然封装了大部分 WebGL 功能,但仍然允许开发者在需要时直接操作底层 WebGL API,提供了在易用性与灵活性之间的平衡。

1.3. 应用场景

WebGL:适合高性能要求的应用,如科学计算、工程模拟、需要自定义渲染管线的高端 3D 应用。

Three.js:适合快速开发 3D 网站、数据可视化、游戏原型、虚拟现实体验等场景。

2. 渲染对比

2.1. WebGL 渲染流程

image.png

WebGL渲染流程主要包含六大步骤,分别为:初始化 WebGL 上下文编写和编译着色器设置缓冲区配置渲染状态绘制图形重复渲染

具体详细流程如下:

首先,通过创建 canvas 元素并获取 WebGL 上下文.

编写顶点着色器和片段着色器的 GLSL 代码。

然后,创建顶点缓冲区createBuffer,绑定缓冲区bindBuffer,上传顶点数据bufferData。

设置视口viewport,配置深度测试、混合等状态进行渲染。

绑定缓冲区bindBuffer和着色器useProgram;传递 uniform 数据;执行绘制命令drawElements。

最后,使用 requestAnimationFrame 循环更新动画并重新绘制场景。

2.2. Three.js 渲染流程

image.png

Threejs 渲染流程主要包含创建场景创建相机创建渲染器创建几何体和材质绑定几何体到场景渲染场景更新和重绘场景

具体实现如下:

首先,创建一个 THREE.Scene 对象,用于容纳所有 3D 对象、光源和相机。

创建一个相机对象(如 THREE.PerspectiveCamera),设置视角和位置。

创建 THREE.WebGLRenderer 对象,绑定到canvas元素并设置渲染器的大小和其他属性。

创建几何体、材质和网格等对象,将几何体和材质组合在一起。

然后,使用 scene.add()方法将网格对象、光源等添加到场景中。

最后,使用 render 渲染场景。并使用requestAnimationFrame() 更新动画并重绘场景。


从WebGL和Three.js的渲染流程,我们可以总结出它们之间最大的区别:

WebGL 提供了对 GPU 的直接控制,允许开发者进行细粒度的图形编程,但需要处理许多底层细节。

Three.js封装了 WebGL,提供了更高层次的抽象,简化了 3D 场景的创建和管理,适合快速开发和原型设计。


3. 实例

下面让我们看一个最简单的例子,使用 WebGL和 Three.js 分别创建一个立方体,更直观地感受两者的区别。

例子地址:codesandbox.io/p/sandbox/w…

WebGL例子

// WebGL创建一个绿色旋转立方体
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rotating Cube with WebGL</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<canvas id="glCanvas" width="640" height="480"></canvas>
<script>
    // 顶点着色器源码
    const vertexShaderSource = `
        attribute vec4 aVertexPosition; // 输入顶点位置
        uniform mat4 uModelViewMatrix;  // 模型视图矩阵
        uniform mat4 uProjectionMatrix; // 投影矩阵

        void main(void) {
            // 计算顶点位置
            gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
        }
    `;

    // 片段着色器源码
    const fragmentShaderSource = `
        void main(void) {
            gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);  // 输出绿色
        }
    `;

    // 初始化着色器程序
    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)) {
            console.error('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)) {
            console.error('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 vertices = new Float32Array([
            // Front face
            -1.0, -1.0,  1.0,
             1.0, -1.0,  1.0,
             1.0,  1.0,  1.0,
            -1.0,  1.0,  1.0,

            // Back face
            -1.0, -1.0, -1.0,
            -1.0,  1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0, -1.0, -1.0,

            // Top face
            -1.0,  1.0, -1.0,
            -1.0,  1.0,  1.0,
             1.0,  1.0,  1.0,
             1.0,  1.0, -1.0,

            // Bottom face
            -1.0, -1.0, -1.0,
             1.0, -1.0, -1.0,
             1.0, -1.0,  1.0,
            -1.0, -1.0,  1.0,

            // Right face
             1.0, -1.0, -1.0,
             1.0,  1.0, -1.0,
             1.0,  1.0,  1.0,
             1.0, -1.0,  1.0,

            // Left face
            -1.0, -1.0, -1.0,
            -1.0, -1.0,  1.0,
            -1.0,  1.0,  1.0,
            -1.0,  1.0, -1.0,
        ]);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 将顶点数据传递给缓冲区

        // 创建并绑定索引缓冲区对象
        const indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

        // 定义立方体的索引数据,表示每个面由哪些顶点组成
        const indices = new Uint16Array([
            0, 1, 2,    0, 2, 3,    // Front face
            4, 5, 6,    4, 6, 7,    // Back face
            8, 9, 10,   8, 10, 11,  // Top face
            12, 13, 14, 12, 14, 15, // Bottom face
            16, 17, 18, 16, 18, 19, // Right face
            20, 21, 22, 20, 22, 23  // Left face
        ]);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); // 将索引数据传递给缓冲区

        return {
            position: positionBuffer,
            indices: indexBuffer,
        };
    }

    // 全局变量,用于跟踪上一次动画帧的时间
    let then = 0;

    // 绘制场景函数
    function drawScene(gl, programInfo, buffers, rotation) {
        // 清除画布
        gl.clearColor(0.0, 0.0, 0.0, 1.0);  // 黑色背景
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 清除颜色缓冲区和深度缓冲区
        gl.enable(gl.DEPTH_TEST); // 启用深度测试

        // 设置顶点属性
        const numComponents = 3; // 每个顶点由三个分量(x, y, z)组成
        const type = gl.FLOAT;   // 数据类型为32位浮点数
        const normalize = false; // 不规范化数据
        const stride = 0;        // 每个顶点数据间隔为0,表示紧密排列
        const offset = 0;        // 从缓冲区的起始位置读取数据
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
        gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);

        gl.useProgram(programInfo.program); // 使用着色器程序

        // 创建并设置模型视图矩阵
        const modelViewMatrix = mat4.create();
        mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -6.0]); // 将立方体移向屏幕内
        mat4.rotate(modelViewMatrix, modelViewMatrix, rotation[0], [1, 0, 0]); // 绕x轴旋转
        mat4.rotate(modelViewMatrix, modelViewMatrix, rotation[1], [0, 1, 0]); // 绕y轴旋转

        // 将模型视图矩阵传递给着色器
        gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, modelViewMatrix);

        // 创建并设置投影矩阵
        const projectionMatrix = mat4.create();
        mat4.perspective(projectionMatrix, Math.PI / 4, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
        gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);

        // 绘制立方体
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
        gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); // 36个顶点组成12个三角形
    }

    // 动画函数,用于逐帧更新旋转角度并重绘立方体
    function animate(gl, programInfo, buffers, rotation) {
        requestAnimationFrame((now) => {
            // 计算时间增量
            const deltaTime = (now - then) / 1000;
            then = now; // 更新全局时间

            // 更新旋转角度
            rotation[0] += deltaTime;
            rotation[1] += deltaTime;

            // 绘制场景
            drawScene(gl, programInfo, buffers, rotation);

            // 继续动画循环
            animate(gl, programInfo, buffers, rotation);
        });
    }

    // 主函数,初始化WebGL上下文、着色器和缓冲区,并启动动画
    function main() {
        const canvas = document.querySelector("#glCanvas"); // 获取canvas元素
        const gl = canvas.getContext("webgl"); // 获取WebGL上下文

        // 检查WebGL是否可用
        if (!gl) {
            console.error("Unable to initialize WebGL. Your browser or machine may not support it.");
            return;
        }

        const shaderProgram = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource); // 初始化着色器程序
        const programInfo = {
            program: shaderProgram,
            attribLocations: {
                vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
            },
            uniformLocations: {
                projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
                modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
            },
        };

        const buffers = initBuffers(gl); // 初始化缓冲区

        const rotation = [0, 0]; // 初始化旋转角度

        animate(gl, programInfo, buffers, rotation); // 启动动画
    }

    window.onload = main; // 在页面加载后启动main函数
</script>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@2.8.1/dist/gl-matrix-min.js"></script>
</body>
</html>

image.png

Three.js例子

// Three.js创建一个绿色旋转立方体
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Three.js Cube</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
    // 创建场景
    const scene = new THREE.Scene();

    // 创建相机
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;

    // 创建渲染器
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 创建几何体和材质
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

    // 创建立方体
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // 渲染场景
    function animate() {
        requestAnimationFrame(animate);
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        renderer.render(scene, camera);
    }

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

两者都是创建一个相同的绿色旋转立方体,可见它们代码实现复杂度是不一样的。

四、总结

WebGL 和 Three.js 各自扮演着不同的角色。WebGL 是底层的、强大且灵活的 3D 渲染工具,适合那些需要完全控制渲染过程的开发者。而 Three.js 则是一个封装良好的高级库,通过简化 WebGL 的复杂性,使得更多开发者能够轻松进入 3D 开发的世界。

  • WebGL 提供了对 GPU 的直接控制,允许开发者进行细粒度的图形编程,但需要处理许多底层细节。

  • Three.js 封装了 WebGL,提供了更高层次的抽象,简化了 3D 场景的创建和管理,适合快速开发和原型设计

理解这两者的核心原理和区别,能够帮助开发者根据项目的需求选择合适的工具,并在此基础上创造出丰富多样的 3D 应用。