GLSL 着色器

着色器使用 GLSL(OpenGL 着色语言),这是一种语法类似于 C 的特殊 OpenGL 着色语言。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(红、绿、蓝、Alpha)颜色 — 每个像素调用一次片段着色器。片段着色器的目的是设置 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>
      html,
      body,
      canvas {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        font-size: 0;
      }
    </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 来设置 Three.js 将在页面中插入的<canvas> 元素的 widthheight,使其全屏显示。<head> 中的<script> 元素将 Three.js 库包含到页面中;我们将在 <body> 标签中将代码写入三个 script 标签。

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

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

立方体的源代码

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

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

顶点着色器代码

让我们继续编写一个简单的顶点着色器 — 将下面的代码添加到 body 的第一个 <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 空间中顶点位置的裁剪,但在本例中我们不需要。

纹理着色器代码

现在我们将纹理着色器添加到代码中 — 将下面的代码添加到 body 的第二个 <script> 标签中

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

这将设置一个 RGBA 颜色来重现当前浅蓝色 — 前三个浮点值(范围从 0.01.0)代表红色、绿色和蓝色通道,而第四个值是 Alpha 透明度(范围从 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,
});

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

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

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 上一些非常酷的示例以获取灵感并从其源代码中学习。

另见