
Web上有很多有关使用Three.js的基础知识的介绍,但是您可能会注意到缺少更高级主题的资料。 这些主题之一是将着色器和场景与三维模型相结合。 在许多新手开发人员看来,这些似乎是来自不同领域的不兼容的事物。 今天,我们将通过一个简单的“等离子球体”示例,了解ShaderMaterial是什么,被什么吃,效果效果是什么以及对渲染场景进行后处理的速度有多快。
假定读者熟悉使用Three.js的基础知识,并了解着色器的工作原理。 如果您以前从未遇到过,那么我强烈建议您首先阅读以下内容:
但是让我们开始吧...
ShaderMaterial-什么?
我们已经看到了如何使用平面纹理以及如何将其拉伸到三维对象上。 因为这种纹理是一张普通的图片。 当我们检查片段着色器的编写时,那里的一切也都平坦。 因此:如果我们可以使用着色器生成平面图像,那么为什么不将其用作纹理呢?
正是这种想法构成了着色器材质的基础。 为三维对象创建材质时,我们为其指定着色器而不是纹理。 在其基本形式中,它看起来像这样:
const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
片段着色器将用于创建材质的纹理,您当然会问,顶点着色器会做什么? 他会再次平凡地重新计算坐标吗? 是的,我们将从这个简单的选项开始,但是我们也可以为三维对象的每个顶点设置偏移量或执行其他操作-现在在平面上没有任何限制。 但是最好以一个例子来看所有这些。 换句话说,了解得很少。 创建一个场景并在中心制作一个球体。

作为球体的材料,我们将使用ShaderMaterial:
const geometry = new THREE.SphereBufferGeometry(30, 64, 64); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
顶点着色器将是中性的:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
请注意,Three.js传递其统一变量。 他们暗示,我们不必做任何事情。 它们本身包含了我们已经可以从JS访问的各种矩阵以及相机的位置。 想象一下,在着色器本身的开头会插入一些内容:
另外,几个属性变量被传递到顶点着色器:
attribute vec3 position; attribute vec3 normal; attribute vec2 uv;
通过名称可以清楚地知道它是什么-当前顶点的位置,此点表面的法线以及该顶点所对应的纹理上的坐标。
传统上,空间中的坐标指定为(x,y,z),纹理平面上的坐标指定为(u,v)。 因此,变量的名称。 您经常会在各种例子中遇到他。 理论上,我们需要将这些坐标传递到片段着色器,以便在那里使用它们。 我们会做到的。
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
首先,片段着色器应该是这样的:
#define EPSILON 0.02 varying vec2 vUv; void main() { if ((fract(vUv.x * 10.0) < EPSILON) || (fract(vUv.y * 10.0) < EPSILON)) { gl_FragColor = vec4(vec3(0.0), 1.0); } else { gl_FragColor = vec4(1.0); } }
只需创建一个网格即可。 如果您想一想,那么在平面上它将只是一个正方形网格,但是由于我们将其叠加在一个球体上,因此它会扭曲,变成一个球体。 维基百科上有一张很好的图片,说明正在发生的事情:

也就是说,在片段着色器中,我们制作了平坦的纹理,如本图的中心所示,然后Three.js将其拉到球体上。 很舒服
当然,对于更复杂的模型,扫描将更加复杂。 但是通常在创建各种设计网站时,我们会使用简单的几何形状,并且很容易想象一下您的头部会扫一扫。
好吧,你能做什么?
主要特征是着色器材质可以随时间变化。 这不是我们一次绘制并忘记的静态事物,我们可以对其进行动画处理。 此外,颜色(在片段着色器中)和形状(在顶点中)均如此。 这是一个非常强大的工具。
在我们的示例中,我们将包围球体着火。 将有两个球体-一个普通的球(内部),第二个球来自着色器材质(外部的球体较大)。 添加其他领域将不会发表评论。

首先,为材质中的着色器添加时间作为统一变量。 没有时间没有地方。 我们已经在纯JS中做到了这一点,但是在Three.js中,它非常简单。 将着色器中的时间称为uTime,并将其存储在TIME变量中:
function updateUniforms() { SCENE.traverse((child) => { if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') { child.material.uniforms.uTime.value = TIME; child.material.needsUpdate = true; } }); }
每次调用animate函数时,我们都会更新所有内容:
function animate() { requestAnimationFrame(animate); TIME += 0.005; updateUniforms(); render(); }
着火
实质上,生火与生成景观非常相似,但不是高度而是颜色。 或透明度,例如我们的案例。
我们已经看到了随机性和噪声的功能,我们将不对其进行详细分析。 我们需要做的就是在不同的频率上发出噪音以增加变化,并使每种噪音以不同的速度运动。 您会得到类似火焰的信息,大的火焰移动缓慢,小的火焰移动更快:
uniform float uTime; varying vec2 vUv; float rand(vec2); float noise(vec2); void main() { vec2 position1 = vec2(vUv.x * 4.0, vUv.y - uTime); vec2 position2 = vec2(vUv.x * 4.0, vUv.y - uTime * 2.0); vec2 position3 = vec2(vUv.x * 4.0, vUv.y - uTime * 3.0); float color = ( noise(position1 * 5.0) + noise(position2 * 10.0) + noise(position3 * 15.0)) / 3.0; gl_FragColor = vec4(0.0, 0.0, 0.0, color - smoothstep(0.1, 1.3, vUv.y)); } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 position) { vec2 blockPosition = floor(position); float topLeftValue = rand(blockPosition); float topRightValue = rand(blockPosition + vec2(1.0, 0.0)); float bottomLeftValue = rand(blockPosition + vec2(0.0, 1.0)); float bottomRightValue = rand(blockPosition + vec2(1.0, 1.0)); vec2 computedValue = smoothstep(0.0, 1.0, fract(position)); return mix(topLeftValue, topRightValue, computedValue.x) + (bottomLeftValue - topLeftValue) * computedValue.y * (1.0 - computedValue.x) + (bottomRightValue - topRightValue) * computedValue.x * computedValue.y; }
为了使火焰不会覆盖整个球体,我们使用第四个颜色参数-透明度-并将其绑定到y坐标。 在我们的情况下,此选项非常方便。 一般而言,我们对噪声应用具有透明度的渐变。
在这样的时候,记住平稳的步伐很有用
通常,使用着色器创建火焰的这种方法很经典。 您会经常在各个地方见到他。 玩魔术数字会很有用-在示例中它们是随机设置的,等离子的外观取决于它们。
为了使射击更加有趣,让我们继续进行顶点着色器和一个萨满祭司...
如何使火焰稍微“倾泻”在太空中? 对于初学者来说,尽管很简单,但是这个问题可能会造成很大的困难。 我看到了解决此问题的非常复杂的方法,但从本质上讲,我们需要沿“从其中心”沿线平滑移动球体上的顶点。 来回,来回。 Three.js已经将顶点和法线的当前位置传递给了我们-我们将使用它们。 对于“来回”,一些有限的功能将适用,例如正弦。 您当然可以试验,但正弦为默认选项。
不知道要拿什么-正弦。 更好的是,具有不同频率的正弦之和。
我们将坐标垂直于获得的值移动,并根据先前已知的公式重新计算。
uniform float uTime; varying vec2 vUv; void main() { vUv = uv; vec3 delta = normal * sin(position.x * position.y * uTime / 10.0); vec3 newPosition = position + delta; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
我们得到的不再是一个领域。 这个...我什至不知道这个名字。 但是,再次提醒您,别忘了把握机会-它们影响很大。 在创建这种效果时,通常会通过反复试验来选择某些东西,这对自己形成“数学直觉”非常有用-能够或多或少地想象一个函数的行为,它如何依赖于哪些变量。
在这个阶段,我们有一个有趣但有点笨拙的图像。 因此,首先让我们看一下后处理,然后再来看一个生动的例子。
后处理
使用渲染的Three.js图像执行某些操作的能力是非常有用的,而在许多系列的课程中我们都不应忘记。 从技术上讲,这是按以下方式实现的:渲染器提供给我们的图像被发送到EffectComposer(只要它是一个黑匣子),该工具会对其本身进行萨满处理并在画布上显示最终图像。 也就是说,在渲染器之后,又添加了一个模块。 我们将参数传输到此作曲家-处理收到的图像。 这样的参数之一称为通过。 从某种意义上说,作曲家的工作方式就像Gulp一样-它什么也没做,我们给它提供已经完成工作的插件。 这么说也许并不完全正确,但是这个想法应该很清楚。
我们将进一步使用的所有内容均未包含在Three.js的基本结构中,因此我们连接了一些依赖项和依赖项本身的依赖项:
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script>
请记住,这些脚本包含在三个程序包中,您可以使用webpack或类似程序将所有这些脚本放入一个捆绑包中。
在其基本形式中,作曲家是这样创建的:
COMPOSER = new THREE.EffectComposer(RENDERER); COMPOSER.setSize(window.innerWidth, window.innerHeight); const renderPass = new THREE.RenderPass(SCENE, CAMERA); renderPass.renderToScreen = true; COMPOSER.addPass(renderPass);
RenderPass实际上并没有做任何新的事情。 它只是渲染我们从常规渲染器获得的内容。 实际上,如果您查看RenderPass的源代码,则可以在其中找到标准渲染器。 由于现在正在此处进行渲染,因此我们需要在脚本中用composer替换渲染器:
function render() {
在使用EffectComposer时,这种将RenderPass用作第一遍的方法是标准做法。 通常,我们首先需要获取场景的渲染图像,然后再对其进行处理。
在Three.js的示例中的后处理部分中,您可以找到一个叫做UnrealBloomPass的东西。 这是来自虚幻引擎的移植脚本。 它增加了一点光晕,可用于创建更美丽的照明。 通常,这将是改善图像的第一步。
const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 1, 0.1); bloomPass.renderToScreen = true; COMPOSER.addPass(bloomPass);
请注意:renderToScreen选项仅设置为我们传递给作曲家的最后一个Pass。
但是,让我们已经看看bloomPass给我们带来了什么样的光辉,以及它如何与球体融为一体:
同意,这比球体和普通光源要有趣得多,因为它们通常在Three.js的初始课程中显示。
但我们将走得更远...
着色器神更多的着色器!

使用console.log并查看作曲家的结构非常有用。 在其中,您可以找到一些名称为renderTarget1,renderTarget2等的元素,其中的数字与传递的通行证的索引相对应。 然后很清楚为什么要这样调用EffectComposer。 它根据SVG中的过滤器原理工作。 记住,您可以在其中使用执行某些过滤器的结果吗? 在这里同样的事情-您可以组合效果。
使用console.log了解Three.js对象和许多其他库的内部结构非常有用。 经常使用此方法可以更好地了解什么是什么。
添加另一个通行证。 这次将是ShaderPass。
const shader = { uniforms: { uRender: { value: COMPOSER.renderTarget2 }, uTime: { value: TIME } }, vertexShader: document.getElementById('postprocessing-vertex-shader').textContent, fragmentShader: document.getElementById('postprocessing-fragment-shader').textContent }; const shaderPass = new THREE.ShaderPass(shader); shaderPass.renderToScreen = true; COMPOSER.addPass(shaderPass);
RenderTarget2包含上一遍的结果-bloomPass(它是连续第二遍),我们将其用作纹理(本质上是平面渲染的图像),并将其作为统一变量传递给新的着色器。
也许值得制动并在这里实现所有魔力...
接下来,创建一个简单的顶点着色器。 在大多数情况下,在此阶段,我们不需要对顶点做任何事情,只需将坐标(u,v)传递给片段着色器:
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
零碎的我们可以品尝到我们的口味和颜色。 例如,我们可以添加一个光干扰效果,使所有内容变为黑白,并以亮度/对比度播放:
uniform sampler2D uRender; uniform float uTime; varying vec2 vUv; float rand(vec2); void main() { float randomValue = rand(vec2(floor(vUv.y * 7.0), uTime / 1.0)); vec4 color; if (randomValue < 0.02) { color = texture2D(uRender, vec2(vUv.x + randomValue - 0.01, vUv.y)); } else { color = texture2D(uRender, vUv); } float lightness = (color.r + color.g + color.b) / 3.0; color.rgb = vec3(smoothstep(0.02, 0.7, lightness)); gl_FragColor = color; } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
让我们看一下结果:
如您所见,滤镜叠加在球体上。 它仍然是三维的,没有任何损坏,但是在画布上我们有经过处理的图像。
结论
Three.js中的着色器材料和后期处理是两个非常小但非常强大的工具,绝对值得使用。 有很多选择可供使用-一切都受您的想象力限制。 即使是最简单的场景,在它们的帮助下,也可能无法识别。