着色器简介

Paul Lewis

简介

请先阅读 Three.js 简介,因为它是本文档的基础。

我想讨论的是着色器。WebGL 非常出色,正如我之前所说,Three.js(和其他库)在为您消除困难方面做得非常出色。但有时您可能希望实现特定效果,或者希望更深入地了解这些令人惊叹的内容是如何出现在屏幕上的,而着色器几乎肯定会成为其中的一部分。您可能希望从上一个教程中的基础内容过渡到稍微复杂一些的内容。

我将基于您使用 Three.js 的情况进行讲解,因为在着色器运行方面,它为我们做了很多工作。 您将首先了解着色器的背景信息,然后在本文件的后续部分中了解高级步骤。

1. 我们的两个着色器

WebGL 不提供固定流水线,也就是说,它不提供任何开箱即用的渲染方式。它提供的是可编程流水线,功能更强大,但也更难理解和使用。简而言之,可编程流水线意味着作为程序员,您需要负责将顶点等渲染到屏幕上。着色器是此流水线的一部分,分为两种类型:

  1. 顶点着色器
  2. 片段着色器

相信您也会同意,这两个数字本身没有任何意义。您应该了解的是,它们都完全在显卡的 GPU 上运行。这意味着,我们希望尽可能将所有工作都分流给它们,让 CPU 能够执行其他工作。现代 GPU 针对着色器所需的功能进行了深度优化,因此能够使用它非常棒。

2. 顶点着色器

以一个标准的基本形状(例如球体)为例。它由顶点组成,对吧? 顶点着色器会依次接收每个顶点,并可以对它们进行处理。 顶点着色器对每个顶点的实际操作取决于它本身,但它有一个职责:它必须在某个时刻设置一个名为 gl_Position 的 4D 浮点向量,这是顶点在屏幕上的最终位置。这本身就是一个非常有趣的过程,因为我们实际上是在讨论将 3D 位置(具有 x、y、z 坐标的顶点)投影到 2D 屏幕上。幸运的是,如果我们使用 Three.js 之类的工具,则可以采用一种简便的方法来设置 gl_Position,而不会让事情变得太繁重。

3. 片段着色器

现在,我们已经有了包含顶点的对象,并且已将这些顶点投影到 2D 屏幕上,但我们使用的颜色呢?纹理和光照呢?这正是片元着色器的用途。 与顶点着色器非常相似,片段着色器也只有一项必须完成的工作:它必须设置或舍弃 gl_FragColor 变量,这是另一个 4D 浮点向量,它是片段的最终颜色。

但什么是 fragment?想象一下构成三角形的三个顶点。该三角形内的每个像素都需要绘制出来。片段是这三个顶点提供的数据,用于绘制该三角形中的每个像素。因此,片段会从其组成顶点接收插值。如果一个顶点是红色,其相邻顶点是蓝色 我们会看到颜色值从红色插值到紫色,再到蓝色

4. 着色器变量

您可以进行三种变量声明:Uniform、Attribute 和 Varying。这些语言可能令人困惑,因为它们与其他任何常见语言都不匹配。您可以这样理解:

  1. Uniform 会发送到顶点着色器和片段着色器,并且包含在整个渲染帧中保持不变的值。一个很好的例子就是灯光的位置。

  2. 属性是应用于各个顶点的值。属性仅可用于顶点着色器。这可能是每个顶点都有不同的颜色。属性与顶点具有一对一关系。

  3. Varying 是在顶点着色器中声明的变量,我们希望与片段着色器共享这些变量。为此,我们需要确保在顶点着色器和片段着色器中声明一个类型和名称相同的 varying 变量。一个典型的应用场景是顶点的法线,因为这可以用于光照计算。

稍后,我们将使用所有这三种类型,以便您了解它们的实际应用方式。

不,我们来看一个最简单的着色器。

5. Bonjourno World

下面是顶点着色器的 Hello World:

/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}   

以下是片段着色器的相同代码:

/**
* Set the color to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

不过,这并不太复杂,对吧?

在顶点着色器中,Three.js 会向我们发送几个 uniform。这两个 uniform 是 4D 矩阵,分别称为模型视图矩阵和投影矩阵。您无需确切了解这些机制的工作原理,但如果可以,最好还是了解一下。简而言之,它们是顶点的 3D 位置实际投影到屏幕上最终 2D 位置的方式。

我实际上已将它们从示例代码段中省略,因为 Three.js 会将它们添加到您着色器代码本身的顶部,因此您无需担心这样做。事实上,它添加的内容远不止这些,例如光照数据、顶点颜色和顶点法线。如果不使用 Three.js,您必须自行创建和设置所有这些 uniform 和 attribute。真实故事。

6. 使用 MeshShaderMaterial

好了,我们已经设置好着色器,但如何将其与 Three.js 搭配使用呢?

/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader:   $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});

然后,Three.js 会编译并运行附加到您为其指定该材质的网格的着色器。没有比这更简单的了。可能确实如此,但我们讨论的是在浏览器中运行 3D,因此我认为您应该预料到会有一定的复杂性。

我们实际上可以向 MeshShaderMaterial 添加另外两个属性:uniforms 和 attributes。它们都可以接受向量、整数或浮点数,但正如我之前提到的,uniform 变量在整个帧或所有顶点中都是相同的,因此它们往往是单个值。不过,属性是每个顶点的变量,因此应为数组。属性数组中的值数量与网格中的顶点数量之间应存在一对一关系。

7. 后续步骤

现在,我们将花一些时间添加动画循环、顶点属性和 uniform。我们还会添加一个 varying 变量,以便顶点着色器可以向片段着色器发送一些数据。 最终结果是,原本是粉色的球体现在看起来像是从上方和侧面被照亮,并且会不断跳动。这有点像幻觉,但希望它能帮助您很好地理解这三种变量类型,以及它们如何相互关联和底层几何图形。

8. 假光

现在,更新着色,使其不再是单色对象。我们可以看看 Three.js 如何处理光照,但正如您所知,它比我们需要的更复杂,所以我们要伪造它。您应该仔细查看 Three.js 中的这些精彩着色器,以及 Chris Milk 和 Google 合作的 WebGL 项目《Rome》中的这些着色器。 回到着色器。我们将更新顶点着色器,以便为片段着色器提供每个顶点法线。我们通过 varying 变量来实现此目的:

// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;

void main() {

// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}

在片段着色器中,我们将设置相同的变量名称,然后使用顶点法线与一个向量的点积,该向量表示从球体上方和右侧照射的光线。最终结果是,我们得到了一个类似于 3D 软件包中定向光的效果。

// same name and type as VS
varying vec3 vNormal;

void main() {

// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
    
// ensure it's normalized
light = normalize(light);

// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));

// feed into our frag color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

点积之所以有效,是因为给定两个向量,它会得出一个数字,告诉你这两个向量有多“相似”。对于归一化向量,如果它们指向完全相同的方向,则值为 1。如果它们指向相反的方向,则会得到 -1。我们要做的是将该数字应用于灯光。因此,右上角的顶点的值将接近或等于 1(完全照亮),而侧面的顶点的值将接近 0,背面的顶点的值将为 -1。我们将负值限制为 0,但当您代入数字时,最终会得到我们看到的基本光照。

接下来该怎么做?如果能尝试调整一些顶点位置, 那就太好了。

9. 属性

现在,我希望我们使用一个属性为每个顶点附加一个随机号码。我们将使用此数字沿顶点的法线方向将其向外推。最终结果将是某种奇怪的尖刺球,每次刷新页面时都会发生变化。它还不会动(这是下一步要做的事),但刷新几次页面后,您会发现它会随机变化。

首先,在顶点着色器中添加属性:

attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
                    normal *
                    vec3(displacement);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

效果如何?

其实差别不大!这是因为该属性尚未在 MeshShaderMaterial 中设置,因此着色器实际上会改用零值。目前它就像一个占位符。稍后,我们将在 JavaScript 中向 MeshShaderMaterial 添加该属性,Three.js 会自动将两者关联在一起。

另请注意,我必须将更新后的位置分配给新的 vec3 变量,因为原始属性(与所有属性一样)是只读的。

10. 更新 MeshShaderMaterial

直接开始更新 MeshShaderMaterial,添加实现位移所需的属性。提醒一下:属性是每个顶点的值,因此我们需要为球体中的每个顶点提供一个值。示例如下:

var attributes = {
displacement: {
    type: 'f', // a float
    value: [] // an empty array
}
};

// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}

现在我们看到的是一个变形的球体,但很酷的是,所有位移都发生在 GPU 上。

11. 做成动画

我们完全应该让它动起来。我们是如何做到的?我们需要做好两件事:

  1. 一个 uniform,用于为每帧中应应用的位移量添加动画效果。我们可以使用正弦或余弦,因为它们的值在 -1 到 1 之间
  2. JS 中的动画循环

我们将向 MeshShaderMaterial 和顶点着色器添加 uniform。首先是顶点着色器:

uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
                    normal *
                    vec3(displacement *
                        amplitude);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

接下来,我们更新 MeshShaderMaterial:

// add a uniform for the amplitude
var uniforms = {
amplitude: {
    type: 'f', // a float
    value: 0
}
};

// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms:       uniforms,
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

着色器现已完成。但现在我们似乎倒退了一步。这主要是因为我们的振幅值为 0,而我们将其与位移相乘,所以看不到任何变化。我们也没有设置动画循环,因此我们永远不会看到 0 变为其他任何值。

在 JavaScript 中,我们现在需要将渲染调用封装到一个函数中,然后使用 requestAnimationFrame 调用该函数。在这里,我们还需要更新 uniform 的值。

var frame = 0;
function update() {

// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;

renderer.render(scene, camera);

// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);

总结

这样就大功告成了!现在,您可以看到它以一种奇怪(且略显迷幻)的脉动方式呈现动画效果。

关于着色器,我们还可以介绍很多内容,希望本简介对您有所帮助。现在,您应该能够理解所看到的着色器,并有信心创建一些属于自己的出色着色器。