简介
请先阅读 Three.js 简介,因为它是 本文档的基础。
我想讨论的是着色器。WebGL 非常出色,正如我之前所说,Three.js(和其他库)在为您抽象出困难方面做得非常出色。但有时您可能想要实现特定效果,或者想要深入了解屏幕上显示的精彩内容,而着色器几乎肯定是其中的一部分。您可能想要从上一个教程中的基础知识入手,然后学习一些更棘手的内容。
我将以您使用 Three.js 为基础,因为它在着色器启动方面为我们做了很多工作。 您将首先了解着色器的上下文,然后在本文档的后面部分学习高级步骤。
1. 我们的两个着色器
WebGL 不提供使用固定流水线,这是一种简写方式,表示它不会为您提供任何开箱即用的渲染方式。它提供的是可编程流水线,这种流水线功能更强大,但理解和使用起来也更困难。简而言之,可编程流水线意味着作为程序员,您需要负责将顶点等渲染到屏幕上。着色器是此流水线的一部分,共有两种类型:
- 顶点着色器
- 片段着色器
我相信您一定会同意,这两种着色器本身没有任何意义。您应该了解的是,它们都在显卡的 GPU 上完全运行。这意味着我们希望尽可能将所有内容都卸载到它们,让 CPU 执行其他工作。 现代 GPU 针对着色器所需的功能进行了大量优化,因此能够使用它非常棒。
2. 顶点着色器
以标准原始形状(例如球体)为例。它由顶点组成,对吧? 系统会依次向顶点着色器提供每个顶点,并且可以随意处理这些顶点。 顶点着色器可以自行决定如何处理每个顶点,但它有一项责任:它必须在某个时间点设置一个名为 gl_Position 的内容,这是一个 4D 浮点向量,即顶点在屏幕上的最终位置。就其本身而言,这是一个非常有趣的过程,因为我们实际上是在讨论如何将 3D 位置(具有 x、y、z 的顶点)放置或投影到 2D 屏幕上。幸运的是,如果我们使用 Three.js 之类的工具,我们可以使用简写方式设置 gl_Position ,而无需进行过于繁琐的操作。
3. 片段着色器
因此,我们有了包含顶点的对象,并且已将这些顶点投影到 2D 屏幕上,但我们使用的颜色呢?纹理和光照呢?这正是片段着色器的用途。 与顶点着色器非常相似,片段着色器也只有一项必须完成的工作:它必须设置或舍弃 gl_FragColor 变量(另一个 4D 浮点向量),即片段的最终颜色。
但什么是片段?想象一下构成三角形的三个顶点。需要绘制出该三角形内的每个像素。片段是这三个顶点提供的数据,用于绘制该三角形中的每个像素。因此,片段会从其组成顶点接收插值。如果一个顶点是红色,而其相邻顶点是蓝色,我们会看到颜色值从红色插值到紫色,再到蓝色。
4. 着色器变量
您可以进行三种变量声明:Uniforms、 Attributes 和 Varyings。这些变量可能会让人感到困惑,因为它们与任何其他常见语言都不匹配。您可以这样理解它们:
Uniforms 会发送到顶点着色器和片段着色器两者 ,并且包含在整个渲染帧中保持不变的值。一个很好的例子可能是光源的位置。
Attributes 是应用于各个顶点的值。属性仅 适用于顶点着色器。例如,每个顶点都有不同的颜色。属性与顶点具有一对一关系。
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);
}
下面是片段着色器的 Hello World:
/**
* 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 和属性。这是真的。
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。我们还将添加一个 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. 做成动画
我们应该让它动起来。我们该怎么做?我们需要完成两件事:
- 一个 uniform,用于为每个帧设置动画,以确定应应用多少位移。我们可以使用正弦或余弦,因为它们的值介于 -1 到 1 之间
- JS 中的动画循环
我们将 uniform 添加到 MeshShaderMaterial 和顶点着色器。首先是顶点着色器:
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);
总结
这样就大功告成了!您现在可以看到它以一种奇怪(且略微令人迷惑)的脉动方式进行动画。
关于着色器这个主题,我们还有很多内容可以介绍,希望您觉得这篇简介很有帮助。现在,您应该能够在看到着色器时理解它们,并且有信心创建一些精彩的着色器。