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 结构。
<!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> 元素的 width 和 height,使其全屏显示。<head> 中的<script> 元素将 Three.js 库包含到页面中;我们将在 <body> 标签中将代码写入三个 script 标签。
- 第一个将包含顶点着色器。
- 第二个将包含片段着色器。
- 第三个将包含生成场景的实际 JavaScript 代码。
在继续阅读之前,请将此代码复制到一个新的文本文件中,并将其保存在您的工作目录中,文件名为 index.html。我们将在此文件中创建一个展示简单立方体的场景,以解释着色器的工作原理。
立方体的源代码
我们不必从头开始创建所有内容,而是可以重用使用 Three.js 构建基本演示中的立方体源代码。大多数组件,如渲染器、相机和灯光将保持不变,但我们将使用着色器来设置立方体的颜色和位置,而不是基本材质。
转到 GitHub 上的cube.html 文件,复制第二个<script> 元素内的所有 JavaScript 代码,然后粘贴到当前示例的第三个 <script> 元素中。保存并使用浏览器加载 index.html — 您应该会看到一个蓝色的立方体。
顶点着色器代码
让我们继续编写一个简单的顶点着色器 — 将下面的代码添加到 body 的第一个 <script> 标签中
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0);
}
计算出的 gl_Position 是通过将模型视图和投影矩阵分别乘以每个向量来获得的,以获得每个向量的最终顶点坐标。
注意:您可以从顶点处理段落中了解更多关于模型、视图和投影变换的信息,您也可以查看本文末尾的链接以了解更多相关信息。
projectionMatrix 和 modelViewMatrix 均由 Three.js 提供,向量通过新的 3D 位置传递,这会导致原始立方体沿着 x 轴移动 10 个单位,沿着 z 轴移动 5 个单位,通过着色器进行转换。我们可以忽略第四个参数,并将其保留为默认的 1.0 值;它用于操纵 3D 空间中顶点位置的裁剪,但在本例中我们不需要。
纹理着色器代码
现在我们将纹理着色器添加到代码中 — 将下面的代码添加到 body 的第二个 <script> 标签中
void main() {
gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0);
}
这将设置一个 RGBA 颜色来重现当前浅蓝色 — 前三个浮点值(范围从 0.0 到 1.0)代表红色、绿色和蓝色通道,而第四个值是 Alpha 透明度(范围从 0.0 — 完全透明 — 到 1.0 — 完全不透明)。
应用着色器
要将新创建的着色器实际应用到立方体上,请先注释掉 basicMaterial 的定义
// const basicMaterial = new THREE.MeshBasicMaterial({color: 0x0095DD});
然后,创建shaderMaterial
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: document.getElementById("vertexShader").textContent,
fragmentShader: document.getElementById("fragmentShader").textContent,
});
此着色器材质从脚本中获取代码,并将其应用于分配给它的对象。
然后,在定义立方体的行中,我们需要将 basicMaterial 替换为新创建的 shaderMaterial
// const cube = new THREE.Mesh(boxGeometry, basicMaterial);
const cube = new THREE.Mesh(boxGeometry, shaderMaterial);
Three.js 会编译并运行附加到分配了此材质的网格的着色器。在我们的例子中,立方体将同时应用顶点和纹理着色器。就是这样 — 您刚刚创建了最简单的着色器,恭喜您!立方体应该看起来像这样

它看起来与 Three.js 立方体演示完全相同,但稍微不同的位置和相同的蓝色都是通过着色器实现的。
最终代码
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
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
body {
margin: 0;
padding: 0;
font-size: 0;
}
canvas {
width: 100%;
height: 100%;
}
结果
总结
本文教授了着色器的基础知识。我们的示例功能不多,但您可以使用着色器做许多很酷的事情 — 查看 ShaderToy 上一些非常酷的示例以获取灵感并从其源代码中学习。
另见
- Learning WebGL — 获取通用的 WebGL 知识
- WebGL Shaders and GLSL at WebGL Fundamentals — 获取 GLSL 特定信息