Notifications
Article
使用Unity制作起雾的窗户效果着色器
Published a month ago
1.6 K
8
本教程将介绍如何制作着色器以实现起雾的窗户效果。
本教程主要分为三部分内容:
  • 高斯模糊效
  • 读写纹理
  • 根据纹理修改模糊效果
我们在每个部分的最后会提供可以使用的着色器,你可以从中学习方法,以便在制作其它着色器时重用或改写。
获取实现水雾窗户效果的着色器代码:https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/window.shader

高斯模糊

窗户的起雾效果通过高斯模糊和往上面添加轻微的着色来实现。
我们会通过使用GrabPass标签,获取已经在摄像机渲染的窗户后面的像素,然后对这些像素应用高斯模糊算法。
很多文章讲解过高斯模糊的实现原理,我使用了《GLSL代码的高斯模糊教程》来编写自定义的着色器。
使用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
Tags:
Unity China
674
Comments
GUI
17 days ago
工程师
高斯模糊?
0
Oscar
a month ago
可以评论?
0
Liming Zhang
Staff
a month ago
Lead Field Engineer
funkiso用grabpass会不会导致窗户前面的非透明物体也获取进去了呢之前发生过这种情况
如果窗户前面有不透明物体,它会直接挡住这块窗户,也就是不会存在上面说的情况
0
funkiso
a month ago
用grabpass会不会导致窗户前面的非透明物体也获取进去了呢之前发生过这种情况
0
Liangzheng
a month ago
codeman
厉害! 谢谢分享!
1