JavaScript内核与高级编程之:`JavaScript` 的 `Three.js`:其在 `WebGL` 渲染中的底层实现。

各位靓仔靓女,很高兴今天能跟大家聊聊 Three.js 这玩意儿。别看它用起来好像搭积木一样简单,但背后可藏着不少 WebGL 的硬核知识呢!今天咱们就来扒一扒 Three.js 到底是怎么把 WebGL 玩转的,保证让你听完之后,也能自信地说一句:“WebGL,我熟!”

第一部分:WebGL,那张画布

首先,得明确一点:Three.js 并不是一个独立的渲染引擎,它其实是 WebGL 的一个封装库。你可以把 WebGL 想象成一块空白的画布,你得告诉它在哪儿画什么、怎么画,它才会老老实实地给你呈现出来。

WebGL 本身非常底层,你需要用 JavaScript 来控制它,但你需要用 GLSL(OpenGL Shading Language)写着色器程序(shaders)。着色器程序运行在你的显卡(GPU)上,负责处理顶点和像素的渲染。

1.1 WebGL 的基本流程

WebGL 的渲染流程大概是这样的:

  1. 创建 WebGL 上下文(Context): 就像你要画画,得先准备好画布一样。

    const canvas = document.getElementById('myCanvas');
    const gl = canvas.getContext('webgl'); // 或者 'webgl2'
    if (!gl) {
      alert('你的浏览器不支持 WebGL!');
    }
  2. 定义顶点数据(Vertex Data): 告诉 WebGL 你要画什么形状,它的每个顶点坐标是什么。

    const vertices = [
      -0.5, -0.5, 0.0, // 第一个顶点
       0.5, -0.5, 0.0, // 第二个顶点
       0.0,  0.5, 0.0  // 第三个顶点
    ];
  3. 创建缓冲区对象(Buffer Object): 把顶点数据放到显卡里,让 GPU 可以快速访问。

    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  4. 编写并编译着色器程序(Shaders): 顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)是 WebGL 的核心。顶点着色器负责处理顶点的位置,片元着色器负责处理像素的颜色。

    // 顶点着色器
    const vertexShaderSource = `
      attribute vec3 aVertexPosition;
      void main() {
        gl_Position = vec4(aVertexPosition, 1.0);
      }
    `;
    
    // 片元着色器
    const fragmentShaderSource = `
      void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
      }
    `;

    编译着色器:

    function createShader(gl, type, source) {
      const shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('着色器编译出错:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
      }
      return shader;
    }
    
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  5. 创建并链接着色器程序(Program): 把顶点着色器和片元着色器组合成一个完整的程序。

    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);
    
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      console.error('着色器程序链接出错:', gl.getProgramInfoLog(shaderProgram));
      gl.deleteProgram(shaderProgram);
      return null;
    }
    
    gl.useProgram(shaderProgram);
  6. 绑定顶点数据到着色器属性(Attribute): 告诉 WebGL 顶点数据对应着色器中的哪个属性。

    const vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
    gl.enableVertexAttribArray(vertexPositionAttribute);
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
  7. 绘制(Draw): 终于可以开始画了!

    gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置背景颜色
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3); // 画一个三角形

1.2 GLSL 着色器语言

GLSL 是一种专门为 GPU 设计的编程语言,它的语法和 C 语言很像,但有一些针对图形渲染的特性。

  • attribute:用于从顶点缓冲区接收数据,只能在顶点着色器中使用。
  • uniform:用于从 JavaScript 传递数据到着色器,可以被顶点着色器和片元着色器共享。
  • varying:用于从顶点着色器向片元着色器传递数据。

第二部分:Three.js 的底层实现

Three.js 的核心思想就是把 WebGL 的这些底层操作封装起来,让你用更简洁、更面向对象的方式来创建 3D 场景。

2.1 Three.js 的核心概念

  • Scene(场景): 场景是所有物体的容器,你可以把所有的 3D 模型、光源、相机都添加到场景中。
  • Camera(相机): 相机决定了你从哪个角度观察场景。Three.js 提供了多种相机类型,比如透视相机(PerspectiveCamera)和平行投影相机(OrthographicCamera)。
  • Renderer(渲染器): 渲染器负责把场景中的物体绘制到屏幕上。Three.js 提供了多种渲染器,比如 WebGLRenderer、CanvasRenderer 和 SVGRenderer。
  • Mesh(网格): 网格是 3D 模型的基本单元,由几何体(Geometry)和材质(Material)组成。
  • Geometry(几何体): 几何体定义了 3D 模型的形状,比如立方体、球体、圆锥体等。
  • Material(材质): 材质定义了 3D 模型的外观,比如颜色、纹理、光泽度等。
  • Light(光源): 光源照亮场景中的物体,让它们看起来更有立体感。Three.js 提供了多种光源类型,比如环境光(AmbientLight)、方向光(DirectionalLight)、点光源(PointLight)等。

2.2 Three.js 的渲染流程

Three.js 的渲染流程可以简化为以下几个步骤:

  1. 创建场景、相机和渲染器。
  2. 创建 3D 模型(Mesh),并添加到场景中。
  3. 设置相机的位置和方向。
  4. 调用渲染器的 render() 方法,把场景绘制到屏幕上。

2.3 Three.js 如何使用 WebGL

Three.js 在底层大量使用了 WebGL 的 API,但它把这些 API 封装成了更易于使用的对象和方法。

  • Geometry -> WebGL Buffer: Three.js 的 Geometry 对象会被转换成 WebGL 的 Buffer 对象,存储在显卡中。
  • Material -> WebGL Shader: Three.js 的 Material 对象会被转换成 WebGL 的 Shader 程序,用于控制物体的外观。
  • Mesh -> Draw Call: Three.js 的 Mesh 对象会被转换成 WebGL 的 Draw Call,用于绘制 3D 模型。

代码示例:使用 Three.js 创建一个简单的场景

<!DOCTYPE html>
<html>
<head>
  <title>Three.js Example</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
  <script>
    // 1. 创建场景、相机和渲染器
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 2. 创建一个立方体
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // 绿色
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // 3. 设置相机的位置
    camera.position.z = 5;

    // 4. 渲染循环
    function animate() {
      requestAnimationFrame(animate);

      // 让立方体旋转
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;

      renderer.render(scene, camera);
    }

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

这段代码做了什么?

  1. 引入 Three.js 库: cdn.jsdelivr.net 提供了一个方便的方式来引入 Three.js。
  2. 创建场景、相机和渲染器: THREE.Scene, THREE.PerspectiveCamera, THREE.WebGLRenderer 是 Three.js 的核心类。
  3. 创建一个立方体: THREE.BoxGeometry 创建了一个立方体的几何体, THREE.MeshBasicMaterial 创建了一个简单的绿色材质, THREE.Mesh 把几何体和材质组合成一个网格。
  4. 设置相机的位置: 把相机放在 Z 轴的 5 个单位处,以便可以看到立方体。
  5. 渲染循环: requestAnimationFrame 创建了一个动画循环,不断地更新立方体的旋转角度,并重新渲染场景。

2.4 Three.js 的 ShaderMaterial

如果你想更深入地控制物体的外观,可以使用 Three.js 的 ShaderMaterialShaderMaterial 允许你直接编写 WebGL 的 Shader 程序,从而实现各种自定义的渲染效果。

const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    void main() {
      gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
    }
  `
});

const mesh = new THREE.Mesh(geometry, shaderMaterial);
scene.add(mesh);

在这个例子中,我们直接在 JavaScript 中编写了顶点着色器和片元着色器,并把它们传递给 ShaderMaterial

2.5 Three.js 的 GLSL Program Cache

Three.js 为了提高渲染性能,使用了 GLSL Program Cache。当 Three.js 第一次遇到某个特定的着色器程序时,它会把这个程序编译并缓存起来。下次再遇到相同的着色器程序时,Three.js 就可以直接从缓存中加载,而不需要重新编译,从而节省了大量的 CPU 时间。

第三部分:Three.js 的优化技巧

了解了 Three.js 的底层实现之后,我们就可以更好地优化 Three.js 的性能。

  • 减少 Draw Call: Draw Call 是 CPU 和 GPU 之间的一次通信,每次 Draw Call 都会消耗一定的性能。尽量把多个物体合并成一个物体,或者使用 Instanced Rendering 技术来减少 Draw Call。
  • 优化 Shader 程序: Shader 程序的性能直接影响渲染速度。尽量简化 Shader 程序,避免使用复杂的计算和纹理查找。
  • 使用 LOD(Level of Detail): 对于远处的物体,可以使用更低精度的模型,从而减少渲染的顶点数量。
  • 使用纹理压缩: 纹理压缩可以减少纹理占用的内存空间,并提高纹理的加载速度。
  • 使用 Frustum Culling: Frustum Culling 可以剔除掉不在相机视野内的物体,从而减少渲染的物体数量。

表格:Three.js 和 WebGL 的对应关系

Three.js 对象 WebGL 对象/概念 描述
THREE.Geometry Buffer Object 存储顶点数据、法线数据、纹理坐标等。
THREE.Material Shader Program 定义物体的外观,包含顶点着色器和片元着色器。
THREE.Texture Texture Object 存储纹理图像。
THREE.Mesh Draw Call 表示一个 3D 模型,包含几何体和材质。渲染时,Three.js 会把 Mesh 对象转换成 WebGL 的 Draw Call。
THREE.Scene 场景是所有物体的容器,WebGL 没有直接对应的概念,但场景中的物体最终都会被转换成 WebGL 的对象。
THREE.Camera View Matrix, Projection Matrix 相机定义了观察场景的角度,WebGL 使用 View Matrix 和 Projection Matrix 来表示相机的变换。
THREE.WebGLRenderer WebGL Context 渲染器负责把场景绘制到屏幕上,WebGL Context 是 WebGL 的上下文,提供了访问 WebGL API 的入口。
THREE.ShaderMaterial Custom Shader Program 允许用户自定义着色器程序,提供了更大的灵活性。

总结

今天我们一起深入了解了 Three.js 的底层实现,知道了它其实是 WebGL 的一个封装库。通过 Three.js,我们可以更方便地创建 3D 场景,而不需要直接操作 WebGL 的底层 API。当然,了解 WebGL 的原理可以帮助我们更好地理解 Three.js,并优化 Three.js 的性能。

希望今天的分享对你有所帮助! 如果你觉得还不过瘾,可以尝试自己编写一些 WebGL 的代码,或者阅读 Three.js 的源码,相信你会收获更多!

发表回复

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