游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2166|回复: 0

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

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2019-9-20 14:19:27 | 显示全部楼层 |阅读模式
本教程由游戏开发者Linden Reid介绍如何制作着色器实现起雾的窗户效果,主要分为三部分内容:

  • 高斯模糊效果
  • 读写纹理
  • 根据纹理修改模糊效果


1.gif

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

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

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标签。

  1. SubShader
  2. {
  3.    //绘制透明窗户也很重要
  4.    Tags
  5.    {
  6.       "Queue" = "Transparent"
  7.    }

  8.    //将对象后的屏幕内容抓取到_BGTex中
  9.    GrabPass
  10.    {
  11.       "_BGTex"
  12.    }
  13.    // … 其它着色器代码…
复制代码

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

  1. #include "UnityCG.cginc"
复制代码

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

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

  1. vertexOutput output;
  2. output.pos = UnityObjectToClipPos(input.vertex);
  3. output.grabPos = ComputeGrabScreenPos(output.pos);
复制代码

应用模糊效果

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

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

  1. float4 gaussianBlur(
  2.    float2 dir,
  3.    float4 grabPos,
  4.    float res,
  5.    sampler2D tex,
  6.    float radius
  7. )

  8. {
  9. //模糊算法在此编写
  10. }
复制代码

该算法会获取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。

  1. //属性
  2. //在材质设置
  3. uniform float4 _FogColor;
  4. uniform float _BlurRadius;

  5. //获取通道
  6. uniform sampler2D _BGTex;
  7. uniform float4 _BGTex_TexelSize;
复制代码


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

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

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

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

  1. float4 blurX = gaussianBlur(float2(1,0),
  2.                             input.grabPos,
  3.                             _BGTex_TexelSize.z,
  4.                             _BGTex,
  5.                             _BlurRadius);

  6. float4 blurY = gaussianBlur(float2(0,1),
  7.                             input.grabPos,
  8.                             _BGTex_TexelSize.w,
  9.                             _BGTex,
  10.                             _BlurRadius);

  11. return (blurX + blurY) * _FogColor;
复制代码

效果

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

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

2.png

读写纹理

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

从着色器读取纹理

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

  1. uniform sampler2D _MouseMap;
复制代码

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

  1. float4 mouseSample=tex2D(_MouseMap,input.texCoord.xy);
复制代码

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

640.png

使用C#代码写入纹理

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

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

  1. void OnMouseDrag ()
  2. {
  3.    //从鼠标位置向屏幕创建光线
  4.    //然后测试对纹理的碰撞效果
  5.    Ray ray = cam.ScreenPointToRay(Input.mousePosition);
  6.    RaycastHit hit;

  7.    if(Physics.Raycast(ray, out hit, 100))
  8.    {
  9.       Color color = new Color(1, 0, 0, 1);

  10.       //把纹理坐标转换为像素坐标
  11.       int x = (int)(hit.textureCoord.x * texture.width);
  12.       int y = (int)(hit.textureCoord.y * texture.height);

  13.       //写入被碰到的像素
  14.       texture.SetPixel(x, y, color);

  15.      //写入Radius范围内的相邻像素
  16.       for (int i = 0; i < texture.height; i++)
  17.       {
  18.          for (int j = 0; j < texture.width; j++)
  19.          {
  20.             float dist = Vector2.Distance(new Vector2(i,j),
  21.                                           new Vector2(x,y)
  22.             );

  23.             if(dist <= Radius)
  24.                texture.SetPixel(i, j, color);
  25.             }
  26.       }

  27.       //应用改动并告知着色器
  28.       texture.Apply();
  29.       destinationRenderer.material.SetTexture("_MouseMap", texture);
  30.    }
  31. }

复制代码

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

写入鼠标位置

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


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

5.gif

根据纹理修改模糊效果

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

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

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

我们要进行以下乘法。

  1. _BlurRadius * (1 - red channel)
复制代码

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

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

  1. // r = 1表示鼠标点击
  2. // r = 0表示没有鼠标操作
  3. float blurRadius = _BlurRadius * (1-mouseSample.r);
  4. float4 color = mouseSample.r*_ClearColor + (1.0-mouseSample.r)*_FogColor;

  5. float4 blurX = gaussianBlur(float2(1,0),
  6.                             input.grabPos,
  7.                             _BGTex_TexelSize.z,
  8.                             _BGTex,
  9.                             blurRadius);

  10. float4 blurY = gaussianBlur(float2(0,1),
  11.                             input.grabPos,
  12.                             _BGTex_TexelSize.w,
  13.                             _BGTex,
  14.                             blurRadius);

  15. return (blurX + blurY) * color;
复制代码

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

6.gif

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

时间算法

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

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

算法如下所示。

  1. age = current time - time drawn
  2. percent max age = age / max age
复制代码

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

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

  1. blur radius = max radius * percent max age
  2. tint = (1 - percent max age)*(clear color) + (percent max age)*(fog color)
复制代码

应用时间

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

  1. Color color = new Color(Time.timeSinceLevelLoad, 0, 0, 1);
复制代码

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

  1. //从鼠标点击纹理获取像素绘制的时间
  2. float timeDrawn = tex2D(_MouseMap, input.texCoord.xy).r;

  3. //时间 = 当前时间 - 绘制时间
  4. float age = clamp(_Time.y - timeDrawn, 0.0001, _Time.y);

  5. //百分比最大时间 = 时间/最大时间
  6. float percentMaxAge = saturate(age / _MaxAge);
复制代码

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

  1. // 时间越长表示百分比最大时间越大,从而有更大的模糊效果
  2. float blurRadius = _BlurRadius * percentMaxAge;
  3. float4 color = (1-percentMaxAge)*_ClearColor + percentMaxAge*_FogColor;
复制代码

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

7.gif

结语

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

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

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

作者:Linden Reid  
来源:Unity官方平台
原地址:https://mp.weixin.qq.com/s/cLyCGg6h9WGkIk5Xj4KU1w

4.png
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-12-23 02:11

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表