GLSL 着色器

着色器使用 GLSL (OpenGL 着色语言),这是一种特殊的 OpenGL 着色语言,其语法类似于 C。GLSL 由图形管道直接执行。有 多种类型的着色器,但通常使用两种着色器在 Web 上创建图形:顶点着色器和片段 (像素) 着色器。顶点着色器将形状位置转换为 3D 绘图坐标。片段着色器计算形状颜色和其他属性的渲染。

GLSL 不像 JavaScript 那样直观。GLSL 是强类型化的,并且涉及向量和矩阵的许多数学运算。它可以很快变得非常复杂。在本文中,我们将创建一个简单的代码示例来渲染一个立方体。为了加快后台代码,我们将使用 Three.js API。

您可能还记得 基本理论 文章,顶点是 3D 坐标系中的一个点。顶点可能并且通常具有附加属性。3D 坐标系定义空间,而顶点有助于在该空间中定义形状。

着色器类型

着色器本质上是在屏幕上绘制某些内容所需的函数。着色器在 GPU (图形处理单元) 上运行,GPU 已针对此类操作进行了优化。使用 GPU 处理着色器会将一些数字运算从 CPU 中卸载。这使 CPU 可以专注于其他任务的处理能力,例如执行代码。

顶点着色器

顶点着色器操作 3D 空间中的坐标,并且每个顶点调用一次。顶点着色器的目的是设置 gl_Position 变量,这是一个特殊的、全局的、内置的 GLSL 变量。gl_Position 用于存储当前顶点的坐标。

void main() 函数是定义 gl_Position 变量的标准方法。void main() 内部的所有内容都将由顶点着色器执行。顶点着色器会产生一个变量,其中包含有关如何在 3D 空间中将顶点的坐标投影到 2D 屏幕上的信息。

片段着色器

片段 (或纹理) 着色器为正在处理的每个像素定义 RGBA (红、绿、蓝、阿尔法) 颜色,每个像素调用一次片段着色器。片段着色器的目的是设置 gl_FragColor 变量。gl_FragColor 是一个内置的 GLSL 变量,类似于 gl_Position

计算结果会产生一个变量,其中包含有关 RGBA 颜色的信息。

演示

让我们构建一个简单的演示来解释这些着色器在实际中的作用。请务必先阅读 Three.js 教程,以掌握场景、其对象和材质的概念。

注意:请记住,您不必使用 Three.js 或任何其他库来编写着色器,纯 WebGL (Web 图形库) 就足够了。我们在示例中使用 Three.js 只是为了使后台代码更简单、更易于理解,以便您只需专注于着色器代码。Three.js 和其他 3D 库会为您抽象许多内容,如果您想使用原始 WebGL 创建这样的示例,则需要编写更多额外的代码才能使其正常工作。

环境设置

要开始使用 WebGL 着色器,请按照 使用 Three.js 构建基本演示 中描述的环境设置步骤操作,以确保 Three.js 按预期工作。

HTML 结构

下面是我们将使用的 HTML 结构。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>MDN Games: Shaders demo</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        font-size: 0;
      }
      canvas {
        width: 100%;
        height: 100%;
      }
    </style>
    <script src="three.min.js"></script>
  </head>
  <body>
    <script id="vertexShader" type="x-shader/x-vertex">
      // vertex shader's code goes here
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      // fragment shader's code goes here
    </script>
    <script>
      // scene setup goes here
    </script>
  </body>
</html>

它包含一些基本信息,例如文档 <title>,以及一些 CSS 用于设置 <canvas> 元素的 widthheight,Three.js 将插入该元素以使页面全屏显示。<script> 元素位于 <head> 中,它将 Three.js 库包含在页面中;我们将把代码写入 <body> 标签中的三个脚本标签中

  1. 第一个将包含顶点着色器。
  2. 第二个将包含片段着色器。
  3. 第三个将包含生成场景的实际 JavaScript 代码。

在继续阅读之前,请将此代码复制到一个新的文本文件中,并将其保存在您的工作目录中,命名为 index.html。我们将在该文件中创建一个包含简单立方体的场景,以解释着色器的运作方式。

立方体的源代码

我们可以重复使用 使用 Three.js 构建基本演示 中立方体的源代码,而不是从头开始创建所有内容。渲染器、相机和灯光等大多数组件将保持不变,但我们将使用着色器来设置立方体的颜色和位置,而不是使用基本材质。

转到 GitHub 上的 cube.html 文件,复制第二个 <script> 元素内部的所有 JavaScript 代码,并将其粘贴到当前示例的第三个 <script> 元素中。保存并加载浏览器中的 index.html,您应该会看到一个蓝色的立方体。

顶点着色器代码

接下来,让我们编写一个简单的顶点着色器,将以下代码添加到正文中的第一个 <script> 标签中

glsl
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
}

通过将模型视图矩阵和投影矩阵分别乘以每个向量来计算 gl_Position,从而获得最终的顶点坐标。

注意:您可以从 顶点处理段落 中了解有关模型视图投影变换的更多信息,您也可以查看本文末尾的链接以了解更多信息。

projectionMatrixmodelViewMatrix 都由 Three.js 提供,并且向量与新的 3D 坐标一起传递,这会导致原始立方体沿 x 轴移动 10 个单位,沿 z 轴移动 5 个单位,通过着色器进行平移。我们可以忽略第四个参数,并将其保留为默认值 1.0;这用于在 3D 空间中操作顶点坐标的裁剪,但我们现在不需要。

纹理着色器代码

现在,我们将纹理着色器添加到代码中,将以下代码添加到正文中的第二个 <script> 标签中

glsl
void main() {
  gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}

这将设置一个 RGBA 颜色来重新创建当前的浅蓝色,前三个浮点值 (范围为 0.01.0) 代表红、绿、蓝通道,而第四个值是阿尔法透明度 (范围为 0.0 - 全透明 - 到 1.0 - 全不透明)。

应用着色器

要将新创建的着色器实际应用到立方体,请先注释掉 basicMaterial 定义

js
// const basicMaterial = new THREE.MeshBasicMaterial({color: 0x0095DD});

然后,创建 shaderMaterial

js
const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertexShader").textContent,
  fragmentShader: document.getElementById("fragmentShader").textContent,
});

此着色器材质会从脚本中获取代码并将其应用到分配给该材质的对象。

然后,在定义立方体的行中,我们需要用新创建的 shaderMaterial 替换 basicMaterial

js
// const cube = new THREE.Mesh(boxGeometry, basicMaterial);
const cube = new THREE.Mesh(boxGeometry, shaderMaterial);

Three.js 会编译并运行附加到网格的着色器,该网格将使用此材质。在本例中,立方体将应用顶点着色器和纹理着色器。就是这样,您刚刚创建了最简单的着色器,恭喜!下面是立方体的显示效果

Three.js blue cube demo

它看起来与 Three.js 立方体演示完全相同,但略微不同的位置和相同的蓝色都是使用着色器实现的。

最终代码

HTML

html
<script src="https://end3r.github.io/MDN-Games-3D/Shaders/js/three.min.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
  void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
  }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
  void main() {
      gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
  }
</script>

JavaScript

js
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(WIDTH, HEIGHT);
renderer.setClearColor(0xdddddd, 1);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(70, WIDTH / HEIGHT);
camera.position.z = 50;
scene.add(camera);

const boxGeometry = new THREE.BoxGeometry(10, 10, 10);

const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader: document.getElementById("vertexShader").textContent,
  fragmentShader: document.getElementById("fragmentShader").textContent,
});

const cube = new THREE.Mesh(boxGeometry, shaderMaterial);
scene.add(cube);
cube.rotation.set(0.4, 0.2, 0);

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
render();

CSS

css
body {
  margin: 0;
  padding: 0;
  font-size: 0;
}
canvas {
  width: 100%;
  height: 100%;
}

结果

结论

本文介绍了着色器的基础知识。我们的示例没有做太多的事情,但是你可以用着色器做更多更酷的事情——在 ShaderToy 上查看一些非常酷的着色器,以获取灵感并学习它们的源代码。

另请参见