着色器简介

Paul Lewis

简介

我之前已经为您介绍过 Three.js 简介。如果您还没有读过,您可能需要这样做,因为这是我在本文中构建的基础。

我想讨论的是着色器。WebGL 非常强大,正如我之前所说的 Three.js(和其他库)可以很好地为您消除困难。但有时,您想要实现特定的效果,或者需要更深入地探究这些令人惊叹的内容在屏幕上的显示方式,而着色器几乎肯定是该等式中的一部分。另外,如果您像我一样 建议您从上一个教程的基本内容入手 了解稍微复杂一点的知识我将在您使用 Three.js 的基础上进行练习,因为它在着色器的运行方面为我们完成了很多工作。此外,我还要提前说明,在开始时,我会说明着色器的上下文,在本教程的后半部分,我们将介绍稍微高级一些的领域。这是因为着色器乍看起来并不常见,需要花点时间解释一下。

1. 我们的两种着色器

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

  1. 顶点着色器
  2. fragment 着色器

我相信你会认同,这两者本身完全没有任何意义。您需要了解的是,它们都完全在显卡的 GPU 上运行。这意味着我们希望将我们所能的全部分流给它们,让 CPU 去做其他工作。 新型 GPU 针对着色器所需的功能进行了优化,因此能够很好地使用它。

2. 顶点着色器

选取标准的基本形状,例如球体。它由顶点组成,对吧? 系统会依次向每一个顶点赋予一个顶点着色器,这些顶点可能会打乱这些顶点。顶点着色器的实际作用取决于顶点着色器,但它有一项责任:它必须在某个点设置一个名为 gl_Position 的元素,这是一个 4D 浮点矢量,即顶点在屏幕上的最终位置。gl_Position这本身就是一个很有趣的过程,因为我们实际上讨论的是将 3D 位置(一个具有 x,y,z 的顶点)投放到或投影到 2D 屏幕上。幸运的是,如果我们使用的是 Three.js 这样的代码,那么我们提供了一种设置 gl_Position 的简写方法,并且不会变得过于沉重。

3. fragment 着色器

我们设置了带有顶点的对象,将其投影到 2D 屏幕上,但我们使用的色彩呢?那么,纹理化和光照效果呢?这正是 fragment 着色器的作用。 与顶点着色器非常相似,片段着色器也只有一项必须完成的作业:它必须设置或舍弃 gl_FragColor 变量,即另一个 4D 浮点向量(片段的最终颜色)。但什么是 fragment?想一想三个顶点组成了一个三角形。该三角形内的每个像素都需要绘制出来。片段是这三个顶点提供的数据,用于在该三角形中绘制每个像素。因此,Fragment 会从其组成顶点接收内插值。如果一个顶点的颜色为红色,而其邻点为蓝色,我们会看到颜色值从红色、紫色到蓝色插值。

4. 着色器变量

在讨论变量时,您可以做出三种声明:制服属性转换。当我第一次听说这三个人时,我感到非常困惑,因为他们跟我合作过的其他产品都不具备。但您可以这样理解:

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

  2. 属性是应用于各个顶点的值。属性适用于顶点着色器。每个顶点的颜色可能各不相同。属性与顶点之间存在一对一关系。

  3. 转换是指在顶点着色器中声明的变量,我们想与 fragment 着色器共享这些变量。为此,我们要确保在顶点着色器和 fragment 着色器中声明相同类型和名称的不同变量。顶点的法线就是一个典型用途,因为它可用于光照计算。

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

现在,我们已经讨论了顶点着色器和 fragment 着色器以及它们处理的变量类型,现在有必要了解我们可以创建的最简单的着色器。

5. 邦焦诺世界

下面是顶点着色器的 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);
}   

以下是对 Fragment 着色器的相同说明:

/**
* Set the colour 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 和属性。真实故事。

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 添加两个属性:uniform 和属性。它们都可以接受矢量、整数或浮点数,但正如我在前面提到的,整个帧的 uniform 都是一样的,即所有顶点,因此它们往往是单个值。不过,属性是逐顶点变量,因此它们应为数组。属性数组中值的数量与网格中的顶点数之间应该是一对一的关系。

7. 后续步骤

现在,我们要花点时间来添加动画循环、顶点属性和 uniform。我们还将添加一个不同的变量,以便顶点着色器可以将一些数据发送到 fragment 着色器。最终的结果就是,粉色球面看起来会由上到侧发光,并闪烁。这有点迷幻,但希望您能充分了解这三种变量类型,以及它们之间的关系以及底层几何图形。

8. 假灯

我们来更新配色,使其不是平面着色对象。我们来看一下 Three.js 如何处理光照,但我确信您可以理解,它比我们现在需要的更复杂,因此我们将进行模拟。您应该全面了解 Three.js 中的炫酷着色器,以及 Chris Milk 和 Google 罗马最近一个令人惊叹的 WebGL 项目中的那些着色器。回到着色器。我们将更新顶点着色器,为 fragment 着色器提供法线的每个顶点。我们通过不同的方式来完成此操作:

// 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);
}

在 Fragment 着色器中,我们将设置相同的变量名称,然后使用顶点法线的点积与一个矢量(表示从球体上方和右侧照射的光)。这最终会产生一种类似于 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 colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

点积之所以能起作用,是因为如果给定的两个向量,输出一个数字,表示两个向量的“相似度”。对于归一化向量,如果它们指向完全相同的方向,则值 1 为 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);

12. 总结

这样就大功告成了!现在,你可以看到它在以奇怪(并且有点迷幻)的方式闪烁。

关于着色器这个主题,我们还介绍了许多内容,但希望这个介绍能对您有所帮助。现在,您在看到着色器时应该能够理解它们,并且能够信心十足地自行创建一些出色的着色器!