为网络创建卡通水着色器。 第三部分

图片

第二部分中,我们检查了浮力和泡沫线。 在最后一部分中,我们将水下变形用作后处理效果。

折射和后处理效果


我们的目标是从视觉上传达水的折射。 我们已经讨论过如何在2D场景的片段着色器中创建这种类型的失真。 唯一的区别是我们需要了解屏幕的哪个区域在水下,并且仅对其应用失真。

后处理


在一般情况下,后处理效果是渲染后应用于整个场景的任何效果,例如,阴影或旧的CRT屏幕效果 。 我们首先将其渲染到缓冲区或纹理,而不是直接将场景渲染到屏幕,然后将场景通过着色器,然后渲染到屏幕。

在PlayCanvas中,您可以通过创建新脚本来自定义此后处理效果。 让我们将其称为Refraction.js并将此模板复制为空白:

//---------------   ------------------------// pc.extend(pc, function () { //  -      var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //      this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; //      pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //    ,    , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //---------------  ------------------------// var Refraction = pc.createScript('refraction'); Refraction.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Refraction.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' }); //  initialize       Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); //     postEffects var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; //       this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; }; Refraction.prototype.update = function(){ if(this.savedFS != this.fs.resource || this.savedVS != this.vs.resource){ this.swap(this); } }; Refraction.prototype.swap = function(old){ this.entity.camera.postEffects.removeEffect(old.effect); this.initialize(); }; 

这类似于常规脚本,但是我们定义了可应用于相机的RefractionPostEffect类。 为了进行渲染,它需要顶点和片段着色器。 属性已经配置完毕 ,因此让我们创建具有以下内容的Refraction.frag

 precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; } 

以及带有基本顶点着色器的Refraction.vert

 attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; } 

现在,将Refraction.js脚本附加到相机,并将适当的属性分配给着色器。 开始游戏时,您将以与以前相同的方式看到场景。 这是一个空的后效果,仅重新渲染场景即可。 为了确保它能正常工作,让我们尝试给场景添加红色调。

尝试将红色分量设置为1.0,而不是简单地将颜色返回到Refraction.frag,这将为图像提供如下所示的图像。


失真着色器


要创建动画变形,我们需要添加一个统一的时间变量,因此让我们在Refraction.js的此后效果构造函数中创建它:

 var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>>    this.time = 0; }; 

现在在render函数中,将其传递给着色器以增加它:

 RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //      , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>>    uniform-  scope.resolve("uTime").setValue(this.time); this.time += 0.1; //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); 

现在,我们可以使用水失真教程中相同的着色器代码,将完整的片段着色器转换为以下内容:

 precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying vec2 vUv0; void main() { vec2 pos = vUv0; float X = pos.x*15.+uTime*0.5; float Y = pos.y*15.+uTime*0.5; pos.y += cos(X+Y)*0.01*cos(Y); pos.x += sin(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; } 

如果一切都正确完成,则整个图片应该看起来好像完全在水下。


任务1:确保失真仅适用于屏幕底部。

相机口罩


我们快完成了。 我们仍然需要将此失真效果应用于屏幕的水下部分。 我想到的最简单的方法是用纯白色的水渲染表面重新渲染场景,如下图所示。


它将渲染为用作遮罩的纹理。 然后,我们将该纹理传输到折射着色器,仅当蒙版中的相应像素为白色时,变形着色器才会使完成图像中的像素变形。

让我们向水表面添加一个布尔属性,以了解是否将其用作遮罩。 将以下内容添加到Water.js中:

 Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"}); 

然后,像往常一样,我们可以使用material.setParameter('isMask',this.isMask);将其传递给着色器material.setParameter('isMask',this.isMask); 。 然后在Water.frag中对其进行声明,如果属性为true,则将像素着色为白色。

 //    uniform uniform bool isMask; //      ,    //    true if(isMask){ color = vec4(1.0); } 

通过打开“ Is Mask?”属性来确保此方法有效。 在编辑器中,然后重新启动游戏。 如上图所示,它应该看起来是白色的。

现在,要重新渲染场景,我们需要第二台摄像机。 在编辑器中创建一个新相机,并将其命名为CameraMask 。 我们还将在编辑器中复制Water实体,并将其命名为WaterMask 。 确保实体“水是口罩?” 为假,WaterMask为真。

要订购新相机以渲染到纹理而不是屏幕上,请创建新的CameraMask.js脚本并将其附加到新相机。 我们创建一个RenderTarget来捕获此相机的输出:

 //  initialize       CameraMask.prototype.initialize = function() { //  512x512x24-      var colorBuffer = new pc.Texture(this.app.graphicsDevice, { width: 512, height: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true }); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = new pc.RenderTarget(this.app.graphicsDevice, colorBuffer, { depth: true }); this.entity.camera.renderTarget = renderTarget; }; 

现在,在启动应用程序之后,您将看到该摄像机不再渲染到屏幕上。 我们可以在Refraction.js中获得其目标渲染的输出,如下所示:

 Refraction.prototype.initialize = function() { var cameraMask = this.app.root.findByName('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer); // ... //     ,    }; 

请注意,我将此蒙版纹理作为参数传递给后效果构造函数。 我们需要在构造函数中创建一个指向它的链接,因此它将如下所示:

 ////       var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<<    this.buffer = buffer; }; 

最后,在render函数中,我们将缓冲区传递给着色器:

 scope.resolve("uMaskBuffer").setValue(this.buffer); 

现在,为了确保所有这些工作正常,我将把它作为任务留给您。

任务2:在屏幕上渲染uMaskBuffer,以确保它是第二台摄像机的输出。

应该考虑以下几点:目标渲染是在CameraMask.js脚本的初始化中配置的,并且应在调用Refraction.js时准备好。 如果脚本的工作方式不同,那么我们会收到错误消息。 为确保它们以正确的顺序工作,请将CameraMask拖到编辑器中实体列表的顶部,如下所示。


第二个摄像机应该始终与原始摄像机具有相同的视图,因此让我们始终在更新中遵循CameraMask.js脚本的位置和旋转:

 CameraMask.prototype.update = function(dt) { var pos = this.CameraToFollow.getPosition(); var rot = this.CameraToFollow.getRotation(); this.entity.setPosition(pos.x,pos.y,pos.z); this.entity.setRotation(rot); }; 

在初始化时,定义CameraToFollow

 this.CameraToFollow = this.app.root.findByName('Camera'); 

剪贴蒙版


现在两个摄像机都渲染相同的东西。 我们希望蒙版相机渲染除真实水以外的所有东西,而真实相机则渲染除蒙版水以外的所有东西。

为此,我们可以使用相机的位截取蒙版。 它的工作原理类似于碰撞口罩 。 如果对象的蒙版和相机蒙版之间按位AND的结果为1. AND则该对象将被裁剪(即,不渲染)。

假设Water有位2,WaterMask有位3,则对于真实相机应设置除3以外的所有位,对于遮罩相机应设置除2以外的所有位,最简单的说法是“除N以外的所有位”如下方式:

 ~(1 << N) >>> 0 

在此处阅读有关按位运算的更多信息。

要配置相机剪贴蒙版,我们可以在CameraMask.js脚本的初始化底部插入以下内容:

  //   ,  2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; //   ,  3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; //      ,   : // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2)); 

现在在Water.js中,我们将设置Water网格的蒙版的第2位,并将其蒙版版本设置在第3位:

 //      initialize  Water.js //    var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit); 

现在,一种将使用白开水,而第二种将使用纯白水。 左图显示了原始相机的视图,右图显示了遮罩相机的视图。


面膜应用


现在是最后一步! 我们知道水下区域标有白色像素。 我们只需要检查我们是否在白色像素中,否则,请关闭Refraction.frag中的失真:

 //   ,      vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); //     ? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ //      pos = vUv0; } 

这应该可以解决我们的问题!

还值得注意的是,由于遮罩的纹理是在启动时初始化的,因此当您在运行时调整窗口大小时,它将不再与屏幕大小相对应。

平滑处理


您可能会注意到场景的边缘现在看起来有点锐利。 发生这种情况是因为在应用了后期效果后,我们失去了平滑度。

我们可以在效果之上应用其他平滑,作为另一个后效果。 幸运的是,我们可以使用PlayCanvas存储中的另一个变量。 转到脚本资产页面 ,单击绿色的大下载按钮,然后从出现的列表中选择您的项目。 该脚本将作为posteffect-fxaa.js出现在“资产”窗口的根目录中。 只需将其附加到Camera实体,您的场景就会看起来好多了!

结论思想


如果到达这里,您可以称赞自己! 在本教程中,我们介绍了很多技术。 现在,在使用顶点着色器,纹理渲染,应用后处理效果,对象的选择性剪切,使用深度缓冲区以及使用混合和透明度时,您必须充满信心。 尽管我们已经在PlayCanvas中实现了所有这些功能,但是您可以在任何平台上以一种或另一种形式满足所有这些计算机图形通用概念。

所有这些技术也适用于许多其他效果。 我在Abzu图上报告中发现了一个特别有趣的顶点着色器应用程序,开发人员在其中解释了他们如何使用顶点着色器为屏幕上成千上万的鱼设置有效的动画。

现在,您可以在游戏中应用漂亮的水效果! 您可以自定义它并添加自己的详细信息。 用水可以做更多的事情(我什至没有提到任何反射类型)。 以下是一些想法。

噪音波


您可以对纹理进行采样,而不是仅用余弦和正弦组合为波设置动画,从而使波看起来更自然,更不可预测。

动态泡沫痕迹


您可以在移动对象时创建纹理的动态痕迹,而不是在表面上完全静止的水线,而可以绘制纹理。 这可以通过许多不同的方式完成,因此该任务本身可以成为一个项目。

源代码


可以在这里找到完成的PlayCanvas项目。 我们的存储库还在Three.js下有一个项目端口

Source: https://habr.com/ru/post/zh-CN417091/


All Articles