查看原文
其他

使用Unity制作起雾的窗户效果着色器

Linden Reid Unity官方平台 2022-05-07

本教程由游戏开发者Linden Reid介绍如何制作着色器实现起雾的窗户效果,主要分为三部分内容:

  • 高斯模糊效果

  • 读写纹理

  • 根据纹理修改模糊效果



我们在每一部分的最后会提供可以使用的着色器,你可以从中学习方法,以便在制作其它着色器时重用或改写。


获取实现水雾窗户效果的着色器代码:

https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/window.shader


高斯模糊

窗户的起雾效果通过高斯模糊和往上面添加轻微的着色来实现。


我们会通过使用GrabPass标签,获取已经在摄像机渲染的窗户后面的像素,然后对这些像素应用高斯模糊算法。


很多文章讲解过高斯模糊的实现原理,本文使用了《GLSL代码的高斯模糊教程》来编写自定义的着色器:

https://github.com/mattdesl/lwjgl-basics/wiki/ShaderLesson5


 

使用GrabPass

我们需要获取窗户后面的像素,以便对其进行模糊处理,我们可以使用Unity的GrabPass。


GrabPass将在对象后渲染的像素绘制到着色器可以访问的纹理上。使用该渲染批次时,我们需要使用SubShader代码块中的GrabPass 标签。

SubShader

{

   //绘制透明窗户也很重要

   Tags

   {

      "Queue" = "Transparent"

   }


   //将对象后的屏幕内容抓取到_BGTex中

   GrabPass

   {

      "_BGTex"

   }

   // … 其它着色器代码…


接下来,在CGPROGRAM标签中,确保已经包含Unity.cginc文件,从而使用读取GrabPass的特别函数。

#include "UnityCG.cginc"


为了能够读取GrabPass纹理,我们需要合适的纹理坐标。


Unity通过ComputeScreenGrabPos函数非常简单的获得坐标,只要将剪辑空间顶点位置作为输入提供,该函数就能够给出合适的纹理坐标来读取GrabPass纹理。我们可以在顶点着色器中进行这项计算。

vertexOutput output;

output.pos = UnityObjectToClipPos(input.vertex);

output.grabPos = ComputeGrabScreenPos(output.pos);

应用模糊效果

现在,我们可以在片元着色器Fragment Shader中读取纹理,然后应用模糊效果。


我们通过以下参数编写模糊算法。

float4 gaussianBlur(

   float2 dir,

   float4 grabPos,

   float res,

   sampler2D tex,

   float radius

)


{

//模糊算法在此编写

}


该算法会获取GrabPass纹理,即“tex”,应用模糊效果,并返回float4类型的像素颜色。


下面介绍每个参数的含义:

  • float2 dir:模糊效果将应用于两个通道,所以我们需要“dir”即方向参数。效果会在X方向和Y方向各应用一次,因为我们使用(1, 0)表示X方向,(0, 1)表示Y方向,所以获得的是Float2类型。

  • float4 grabPos:grabPos变量表示模糊像素的纹理坐标。

  • float res:res变量表示X轴和Y轴上的纹理分辨率。

  • sampler2D tex:该变量表示要模糊的纹理。我们需要整个纹理,因为模糊算法会对原始像素附近的像素进行采样。

  • float radius:该变量表示从原始像素到模糊位置的距离。数值越大,模糊效果越强。


接下来,让我们定义控制模糊效果所需的参数。


我们需要一个浮点数定义模糊强度,我们将其定义为_BlurRadius ,并在着色器代码的开始将该变量公开给属性块的材质。


我们还需要GrabPass纹理,该纹理的名称要和GrabPass 标签中的名称相同,本示例中为_BGTex。我们可以通过创建_YourTextureName_TexelSize属性来获取需要纹理的大小信息。


我们给模糊效果加入了深蓝色着色,使效果更明显。如果想使用该颜色,请添加颜色到属性中,我们将其命名为_FogColor。

//属性

//在材质设置

uniform float4 _FogColor;

uniform float _BlurRadius;


//获取通道

uniform sampler2D _BGTex;

uniform float4 _BGTex_TexelSize;


现在,我们得到了将模糊效果应用到背景纹理的所需信息。


我们打算将模糊效果应用到两个渲染批次:一个在X方向,另一个在Y方向。通常,我们会让第二个模糊渲染批次处理第一个模糊的结果,而不是处理原始背景图像。但这样需要更多着色器,过程也会更复杂。


所以,我对模糊的处理比较简单,在两个方向模糊了原始图像并添加效果。该方法的缺点是:1、模糊的质量较低。2、模糊部分比原始图像更亮,因为添加了效果。


我们将模糊部分乘以着色颜色。请注意,_TexelSize 在.zw属性中包含纹理的xy大小。

float4 blurX = gaussianBlur(float2(1,0),

                            input.grabPos,

                            _BGTex_TexelSize.z,

                            _BGTex,

                            _BlurRadius);


float4 blurY = gaussianBlur(float2(0,1),

                            input.grabPos,

                            _BGTex_TexelSize.w,

                            _BGTex,

                            _BlurRadius);


return (blurX + blurY) * _FogColor;


 

效果

我们将该着色器应用到材质上,并将其附加到场景的一个平坦表面上,在着色器实现了基本的模糊效果。


下图是起雾窗户效果的预览。


读写纹理

为了按照鼠标交互改变着色器效果,我们需要将鼠标移动写入纹理,并在着色器读取该纹理。


 

从着色器读取纹理

首先,我们在着色器中创建名为_MouseMap 的sampler2D属性。

uniform sampler2D _MouseMap;


在片元着色器中,绘制该纹理以便调试。

float4 mouseSample = tex2D(_MouseMap, input.texCoord.xy);


以上就是片元着色器的功能,用于实现纹理的读写过程。对_MouseMap属性进行编写前,我们将得到不透明灰色平面,如下所示。

 

使用C#代码写入纹理

为了写入纹理,我们需要创建C#脚本,并将脚本附加到平面。


我们可以通过C#代码的Material.Set函数,设置着色器属性。只需要让属性的字符串名称对应在着色器的对应名称即可。

public class DrawOnTexture : MonoBehaviour {


    //在检视窗口设置

    public Renderer destinationRenderer;

    public int TextureSize;

    public Color BlurColor;


    private Texture2D texture;


    void Start ()

    {

        //新建Texture2D

        texture = new Texture2D(TextureSize, TextureSize, TextureFormat.RFloat, false, true);


        //将所有像素值设为默认颜色

        for (int i = 0; i < texture.height; i++)

        {

            for (int j = 0; j < texture.width; j++)

            {

                texture.SetPixel(i, j, BlurColor);

            }

        }


        //应用SetPixel的属性

        texture.Apply();


        //将纹理信息传到材质

        destinationRenderer.material.SetTexture("_MouseMap", texture);

    }

}


我们为BlurColor选取了黑色,所以运行时场景效果如下图所示。



 

写入鼠标位置

在,我们添加一个OnMouseDrag()函数,当玩家点击划动平面时,在鼠标位置周围绘制圆圈。请将MeshCollider组件附加到平面对象,使它接收OnMouseDrag()事件。

void OnMouseDrag ()

{

   //从鼠标位置向屏幕创建光线

   //然后测试对纹理的碰撞效果

   Ray ray = cam.ScreenPointToRay(Input.mousePosition);

   RaycastHit hit;


   if(Physics.Raycast(ray, out hit, 100))

   {

      Color color = new Color(1, 0, 0, 1);


      //把纹理坐标转换为像素坐标

      int x = (int)(hit.textureCoord.x * texture.width);

      int y = (int)(hit.textureCoord.y * texture.height);


      //写入被碰到的像素

      texture.SetPixel(x, y, color);


     //写入Radius范围内的相邻像素

      for (int i = 0; i < texture.height; i++)

      {

         for (int j = 0; j < texture.width; j++)

         {

            float dist = Vector2.Distance(new Vector2(i,j),

                                          new Vector2(x,y)

            );


            if(dist <= Radius)

               texture.SetPixel(i, j, color);

            }

      }


      //应用改动并告知着色器

      texture.Apply();

      destinationRenderer.material.SetTexture("_MouseMap", texture);

   }

}


现在运行游戏,我们应该能在纹理上使用鼠标进行绘图了。


根据纹理修改模糊效果

现在,我们可以根据刚创建的鼠标拖动纹理来改变模糊效果。


 

根据鼠标拖动纹理应用模糊效果

我们返回到着色器部分,根据从纹理读取的数值应用模糊部分。由于我们在鼠标点击的位置绘制了红色,而且纹理默认是黑色,因此我们可以根据红色通道修改模糊和着色量。


我们要进行以下乘法。

_BlurRadius * (1 - red channel)


由于红色通道的数值在0~1之间,因此红色数值越大,模糊的半径越小。这种情况下,红色通道会是0或1,所以它会在红色绘制的位置移除模糊效果。


着色颜色同理,只不过需要在未应用起雾效果的部分定义_ClearColor。

// r = 1表示鼠标点击

// r = 0表示没有鼠标操作

float blurRadius = _BlurRadius * (1-mouseSample.r);

float4 color = mouseSample.r*_ClearColor + (1.0-mouseSample.r)*_FogColor;


float4 blurX = gaussianBlur(float2(1,0),

                            input.grabPos,

                            _BGTex_TexelSize.z,

                            _BGTex,

                            blurRadius);


float4 blurY = gaussianBlur(float2(0,1),

                            input.grabPos,

                            _BGTex_TexelSize.w,

                            _BGTex,

                            blurRadius);


return (blurX + blurY) * color;


现在,我们可以在窗口进行绘制,点击的位置将消退模糊和着色效果。



我们已经得到了不错的窗户起雾着色器。但是为什么不做的更复杂一些呢?


 

时间算法

在处理着色器前,请思考一下算法的原理。基本上,我们需要根据点击指定像素的时间,来修改模糊量。像素时间值越小,表示它被点击的时间越近,因此起雾效果较小。


我们还需要最大持续时间来定义像素恢复起雾效果的速度。该值将用于把时间转换为标准化数值,即0~1,用于调整最小值和最大值之间的模糊量。


算法如下所示。

age = current time - time drawn

percent max age = age / max age


然后,我们将标准化的“percent max age”值应用到模糊半径和着色。像素时间值越小,百分比最大持续时间越小,从而使模糊强度越小。


类似地,我们会根据percent max age 值,使用较小的着色颜色量和较大的清晰颜色。

blur radius = max radius * percent max age

tint = (1 - percent max age)*(clear color) + (percent max age)*(fog color)


 

应用时间

为了将其应用于着色器,首先我们将像素绘制时间写入鼠标贴图纹理的r通道,而不是只写入1.0。

Color color = new Color(Time.timeSinceLevelLoad, 0, 0, 1);


接下来,在着色器应用之前的算法,获取percent max age值。

//从鼠标点击纹理获取像素绘制的时间

float timeDrawn = tex2D(_MouseMap, input.texCoord.xy).r;


//时间 = 当前时间 - 绘制时间

float age = clamp(_Time.y - timeDrawn, 0.0001, _Time.y);


//百分比最大时间 = 时间/最大时间

float percentMaxAge = saturate(age / _MaxAge);


最后,我们将percent max age值应用到模糊半径和着色颜色。

// 时间越长表示百分比最大时间越大,从而有更大的模糊效果

float blurRadius = _BlurRadius * percentMaxAge;

float4 color = (1-percentMaxAge)*_ClearColor + percentMaxAge*_FogColor;


现在,模糊效果会根据定义的最大持续时间进行恢复。如下图所示,我们将_MaxAge设为1秒,使模糊效果快速淡化。


结语

本教程介绍了如何将颜色之外的信息编码到纹理中,以及如何利用该方法实现不错的效果。


获取水雾窗户效果的着色器代码:

https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/window.shader


下载Unity Connect APP,请点击此处 观看部分Unity官方视频,请关注B站帐户:Unity官方


你可以访问Unity答疑专区留下你的问题,Unity社区和官方团队帮你解答:

Connect.unity.com/g/discussion


推荐阅读

在Unity 2019.2中扩展Shader Graph,实现自定义光照
使用Unity实现魔法火焰效果使用Visual Effect Graph提升FPS示例项目的视觉效果
Unity着色器教程 | 积雪效果
使用Unity开发沙盒游戏《The Serpent Rogue》
在Unity实现游戏命令模式
高级动画绑定功能:角色与物品的交互
使用Unity AR Foundation在增强现实中查看模型



喜欢本文,请点“在看”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存