Three 着色器简介
我之前已经给大家介绍了 Three.js。如果你还没有读过,你可能想要,因为它是我将在本文中构建的基础。
我想做的是讨论着色器。WebGL非常出色,正如我在 Three 之前所说 Three.js(和其他库)在为你抽象出困难方面做得非常出色。但有时你想要实现特定的效果,或者你会想更深入地挖掘那些令人惊叹的东西是如何出现在屏幕上的,而着色器几乎肯定会成为这个等式的一部分。另外,如果你像我一样,你可能很想从上一个教程中的基本内容变成更棘手的东西。我将基于您正在使用 Three.js 进行工作,因为它在启动着色器方面为我们做了很多驴子工作。
我还要先说,在开始时我将解释着色器的上下文,本教程的后半部分是我们将进入稍微高级的领域的地方。原因是着色器乍一看是不寻常的,需要一些解释。
1. 我们的两个着色器
WebGL不提供固定管道的使用,这是一种简写的方式,它没有给你任何开箱即用的方式。 does 然而,它提供的是可编程管道,它更强大,但也更难以理解和使用。简而言之,可编程管道意味着作为程序员,您负责将顶点等渲染到屏幕上。着色器是此管道的一部分,有两种类型:
- 顶点着色器
- 片段着色器
我相信你会同意,这两者本身绝对没有任何意义。您应该了解的是,它们都完全在显卡的GPU上运行。这意味着我们希望将所有可能的工作卸载给他们,让我们的 CPU 做其他工作。现代 GPU 针对着色器所需的功能进行了大量优化,因此能够使用它非常棒。
2. 顶点着色器
采用标准的原始形状,如球体。它由顶点组成,对吧?顶点着色器依次被赋予这些顶点中的每一个,并且可能会弄乱它们。顶点着色器实际如何处理每个顶点着色器,但它有一个责任:它必须在某个时候设置称为 gl_Position 的东西,即 4D 浮点向量,这是顶点在屏幕上的最终位置。就其本身而言,这是一个非常有趣的过程,因为我们实际上是在谈论将3D位置(带有x,y,z的顶点)投影到2D屏幕上或投影到2D屏幕上。值得庆幸的是,如果我们使用像 Three 这样的东西.js我们将有一种速记方法来设置 gl_Position 而不会变得太重。
3. 片段着色器
所以我们有我们的对象及其顶点,我们已经将它们投影到2D屏幕上,但是我们使用的颜色呢?纹理和照明呢?这正是片段着色器的用途。
与顶点着色器非常相似,片段着色器也只有一个必须完成的工作:它必须设置或丢弃 gl_FragColor 变量,另一个 4D 浮点向量,这是我们片段的最终颜色。但什么是碎片?想想三个构成三角形的顶点。需要绘制出该三角形中的每个像素。片段是这三个顶点提供的数据,用于绘制该三角形中的每个像素。因此,片段从其组成顶点接收插值。如果一个顶点为红色,其相邻顶点为蓝色,我们将看到颜色值从红色到紫色再到蓝色的插值。
4. 着色器变量
在谈论变量时,您可以进行三种声明: 制服 、 属性 和 变化 。当我第一次听说这三个人时,我感到非常困惑,因为它们与我曾经合作过的任何其他东西都不匹配。但以下是您可以想到它们的方式:
- Uniforms 被发送到 both 顶点着色器和片段着色器,并包含在整个渲染帧中保持不变的值。一个很好的例子可能是光源的位置。
- Attributes 是应用于各个折点的值。属性 仅适用于 顶点着色器。这可能类似于每个顶点具有不同的颜色。属性与折点具有一对一的关系。
- Varyings 是我们想要与片段着色器共享的顶点着色器中声明的变量。为此,我们确保在顶点着色器和片段着色器中声明相同类型和名称的可变变量。它的经典用法是顶点的法线,因为这可用于照明计算。
稍后我们将使用所有三种类型,以便您可以了解它们的实际应用方式。
现在我们已经讨论了顶点着色器和片段着色器以及它们处理的变量类型,现在值得看看我们可以创建的最简单的着色器。
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);
}
片段着色器也是如此:
/**
* 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向我们发送了几件制服。这两个制服是 4D 矩阵,称为模型视图矩阵和投影矩阵。你并不迫切需要确切地知道这些是如何工作的,尽管如果可以的话,最好了解事物是如何做到的。简而言之,它们是顶点的 3D 位置实际投影到屏幕上的最终 2D 位置的方式。
我实际上将它们排除在上面的代码片段之外,因为 Three.js 将它们添加到着色器代码本身的顶部,因此您无需担心这样做。说实话,它实际上增加了更多的东西,例如光数据、顶点颜色和顶点法线。如果你在没有Three的情况下这样做.js你必须自己创建和设置所有这些制服和属性。真实的故事。
6. 使用网格着色器材质
好的,我们已经设置了一个着色器,但是我们如何在 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,所以我认为您期望一定程度的复杂性。
我们实际上可以向网格着色器材质添加另外两个属性:制服和属性。它们都可以取向量、整数或浮点数,但正如我之前提到的,整个帧的统一是相同的,即对于所有顶点,所以它们往往是单一值。但是,属性是每个顶点的变量,因此它们应是一个数组。属性数组中的值数与网格中的顶点数之间应存在一对一的关系。
7. 后续步骤
现在我们将花一些时间添加动画循环、顶点属性和制服。我们还将添加一个可变变量,以便顶点着色器可以将一些数据发送到片段着色器。最终的结果是,我们粉红色的球体看起来会从上方和侧面被照亮,并且会脉动。这有点迷幻,但希望它能引导您很好地理解这三种变量类型以及它们之间的关系和底层几何图形。
8. 一盏假灯
让我们更新颜色,使其不是平面彩色对象。我们可以看看 Three.js 如何处理光照,但我相信你可以理解它比我们现在需要的更复杂,所以我们要假装它。你应该完全浏览 着色器 一下 Three.js 中 the ones 最近令人惊叹的WebGL项目中的 出色的着色器,以及Chris Milk和Google,Rome 。
回到我们的着色器。我们将更新顶点着色器,以提供垂直于片段着色器的每个顶点。我们这样做的方式各不相同:
// 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 colour
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);
}
它看起来如何?
真的没有太大区别!这是因为该属性尚未在网格着色器材质中设置,因此着色器实际上使用零值。它现在有点像占位符。一秒钟后,我们将属性添加到JavaScript中的MeshShaderMaterial中,Three.js将自动将两者绑定在一起。
同样值得注意的是,我必须将更新的位置分配给 新的 vec3 变量,因为原始属性与所有属性一样是只读的。
10. 更新网格着色器材质
让我们直接开始更新我们的网格着色器材质,使用为我们的位移提供动力所需的属性。提醒:属性是每个顶点的值,因此我们需要球体中的每个顶点一个值。喜欢这个:
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 运行到 1
- JS中的动画循环
我们将把制服添加到网格着色器材质和顶点着色器中。首先是顶点着色器:
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 来调用它。在那里,我们还需要更新制服的价值。
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. 结论
就是这样!你现在可以看到它以一种奇怪(而且有点迷幻)的脉动方式制作动画。关于着色器这个主题,我们可以介绍更多内容,但我希望这个介绍对你有所帮助。现在,当您看到着色器时,您应该能够理解它们,并有信心创建一些自己的惊人着色器!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 利用 HTML5 美化表单
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论