Unity-Shader读书笔记(14)

消融效果

        书中的消融效果是使用噪声纹理+透明度测试。我们使用对噪声纹理采样的结果和某个控制消融程度的阈值进行比较,小于阈值则使用clip函数进行裁剪。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
Shader "Learn/DissolveShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BurnMap ("Burn Map", 2D) = "white" {}

_BurnAmount ("Burn Amount", Range(0,1)) = 0
_LineWidth ("Burn Line Width", Range(0,0.2)) = 0.1

_BurnFirstColor("Burn First Color", Color) = (1,0,0,1)
_BurnSecondColor("Burn Second Color", Color) = (1,0,0,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags{"LightMode" = "ForwardBase"}

Cull Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct v2f
{
float2 uv : TEXCOORD0;
float2 uvBumpMap : TEXCOORD1;
float2 uvBurnMap : TEXCOORD2;
float3 lightDir : TEXCOORD3;
float3 worldPos : TEXCOORD4;

float4 pos : SV_POSITION;

SHADOW_COORDS(5)
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
sampler2D _BurnMap;
float4 _BurnMap_ST;

fixed _BurnAmount;
fixed _LineWidth;

float4 _BurnFirstColor;
float4 _BurnSecondColor;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uvBumpMap = TRANSFORM_TEX(v.uv, _BumpMap);
o.uvBurnMap = TRANSFORM_TEX(v.uv, _BurnMap);

TANGENT_SPACE_ROTATION;

o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;

TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;
clip(burn.r - _BurnAmount);

float3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap,i.uvBumpMap));

fixed3 albedo = tex2D(_MainTex,i.uv).rgb;

fixed ambient = UNITY_LIGHTMODEL_AMBIENT * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal,tangentLightDir));

fixed t = 1 - smoothstep(0,_LineWidth,burn.r - _BurnAmount);
fixed3 burnColor = lerp(_BurnFirstColor,_BurnSecondColor,t);
burnColor = pow(burnColor,5);

UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
fixed3 finalColor = lerp(ambient + diffuse * atten,burnColor,t * step(0.0001,_BurnAmount));

return fixed4(finalColor.rgb,1);
}
ENDCG
}

Pass
{
Tags { "LightMode" = "ShadowCaster" }

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_shadowcaster

#include "UnityCG.cginc"

fixed _BurnAmount;
sampler2D _BurnMap;
float4 _BurnMap_ST;

struct v2f {
V2F_SHADOW_CASTER;
float2 uvBurnMap : TEXCOORD1;
};

v2f vert(appdata_base v) {
v2f o;

TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)

o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;

clip(burn.r - _BurnAmount);

SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
FallBack "Diffuse"
}

        Shader使用了Pass,第一个Pass用来实现消融效果。而第二个Pass用来做阴影处理。

        在属性中,我们使用_BurnAmount来控制消融的程度,当值为0的时候就是正常效果,而值为1的时候物体就完全消融。_LineWidth属性用于控制烧焦效果时的线宽,它的值越大,火焰边缘的蔓延范围越广。_MainTex是物体的漫反射纹理,_BumpMap是法线(切线空间下)纹理,而_BurnMap就是噪声纹理。_BurnFirstColor和_BurnSecondColor对应了火焰边缘的两种颜色。

        为了得到正确的光照效果,我们要设置正确的LightMode和#pragma multi_compile_fwdbase的编译指令。为了正面和反面都被渲染,我们使用了Cull命名关闭了该Shader的面片剔除。这个就和透明度测试的操作一样。为了能得到正确的阴影效果,我们也使用了SHADOW_COORDS等获取到这个效果。

        这里我们使用了在切线空间下的法线纹理,和之前不同的是我们是将光线转换到切线空间中再进行光照处理。而Unity提供的TANGENT_SPACE_ROTATION可以让我们直接得到旋转矩阵,然后在顶点着色器中直接得到切线空间下的向量方向。但是我们要在顶点结构体中进行正确的设置,我们要有normal和tangent。如同代码中的appdata结构体。

        在片元着色器中,我们先对噪声纹理进行采样。这样我们就可以直接判断出这个地方是否要裁剪。接下来的一段就是正常的漫反射处理。我们想要在宽度为_LineWidthd的范围内模拟一个烧焦颜色burnColor。我们使用smoothstep函数来计算混合系数t,这个混合的值由采集的噪声纹理来控制。当t值为1时,表明该像素位于消融的边界处。当t值0时,表明该像素为正常的模型颜色,而中间插值则表示需要模拟一个烧焦效果。我们用t来混合_BurnFirstColor和_BurnSecondColor两种火焰颜色。为了让效果更加接近烧焦的痕迹,我们还使用pow函数对结果进行处理。然后我们再使用t来混合正常的光照颜色(这里只有漫反射+环境光,其实应该还有一个高光反射的)和烧焦痕迹。我们再使用step函数来保证_BurnAmount为0时,不显示任何的消融效果。

        第二个Pass是用来投射阴影的。而这个投射阴影和之前的透明度测试一样用我们规定的裁剪条件来进行操作。

水波效果

        之前我们使用时间和波函数实现了2D效果的水面波动。这回我们将使用噪声来模拟水面波动效果。此时噪声纹理通常会用作一个高度图来修改水面的法线方向,以此模拟水面波动。当然我们仍然要使用时间变量来控制我们采集纹理。(书中是想做出那种清澈的水被扰动的样子,所以书中又使用了之前镜子的做法)

        具体Shader实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
Shader "Learn/WaterWave"
{
Properties
{
_Color ("Main Color", Color) = (0,0.15,0.115,1)
_MainTex ("Texture", 2D) = "white" {}
_WaveMap ("Wave Map", 2D) = "bump" {}
_CubeMap ("Environment Cubemap", Cube) = "_Skybox"{}
_WaveXSpeed ("Wave Horizontal Speed", Range(-0.1,0.1)) = 0.01
_WaveYSpeed ("Wave Vertical Speed", Range(-0.1,0.1)) = 0.01
_Distortion ("Distortion", Range(0,100)) = 10
}
SubShader
{
Tags { "Queue"="Transparent" "RenderType"="Opaque" }

GrabPass{ "_RefractionTex" }

Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

#pragma multi_compile_fwdbase

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct v2f
{
float4 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _WaveMap;
float4 _WaveMap_ST;
samplerCUBE _CubeMap;
fixed _WaveXSpeed;
fixed _WaveYSpeed;
float _Distortion;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
fixed4 _Color;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv, _WaveMap);

float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldNormal(v.tangent);
fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;

o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float2 speed = _Time.y * float2(_WaveXSpeed,_WaveYSpeed);
fixed3 bump1 = UnpackNormal(tex2D(_WaveMap,i.uv.zw + speed)).rgb;
fixed3 bump2 = UnpackNormal(tex2D(_WaveMap,i.uv.zw - speed)).rgb;
fixed3 bump = normalize(bump1 + bump2);

float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
fixed3 refrCol = tex2D(_RefractionTex,i.scrPos.xy / i.scrPos.w).rgb;

bump = normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));
fixed4 texColor = tex2D(_MainTex,i.uv.xy + speed);
fixed3 reflDir = reflect(-viewDir,bump);
fixed3 reflCol = texCUBE(_CubeMap,reflDir).rgb * texColor.rgb * _Color.rgb;

fixed fresnel = pow(1 - saturate(dot(viewDir,bump)),4);
fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);

return fixed4(finalColor.rgb,1);
}
ENDCG
}
}
}

        _Color用于控制水面颜色。_MainTex水面波纹材质纹理。_WaveMap是一个由噪声纹理生成的法线纹理。_CubeMap则是我们用于模拟的立方体纹理。_Distortion则用于控制模拟折射时图像的扭曲程度。_WaveXSpeed和_WaveYSpeed分别用于控制法线纹理在x和y方向上的平移速度。

        我们要设置好渲染队列,以保证所有的不透明物体会在此之前被渲染完毕。这样我们才能得到“透过水面看到图像”的效果。而RenderType设置是为了在使用着色器替换的时候,物体可以被正确的渲染出来。这个通常发生在我们需要得到摄像机的深度和法线纹理的时候。接下来,我们使用GrabPass抓取屏幕图像并存储在我们设定的纹理中。即在第二个Pass中,我们使用_RefractionTex纹理。关于这些内容大家可以返回看看Unity-Shader读书笔记(9)

        在顶点着色器中,我们需计算出屏幕坐标。我们使用专门的函数ComputeGrabScreenPos来得到这个屏幕坐标值(这里需要注意的是,ComputeGrabScreenPos所用的是裁剪空间下的坐标。内置着色器 helper 函数 - Unity 手册 这里也有专门的提醒)。

        在片元着色器中,我们使用设定的_WaveXSpeed和_WaveYSpeed变量与时间变量作用来得到偏移值。接下来我们对法线纹理进行双重采样。书中是说利用该值来模拟两层交叉的水面波动(到底为什么这样就可以做到这个效果,我是没有理解到了)。然后对两次结果相加并归一化得到切线空间下的法线。后面的操作都是和之前说的玻璃效果一致的。

再谈全局雾效

        之前我们也做过全局雾效。我们使用的方法是屏幕后处理+深度纹理(这里我们还使用了射线技术来更快的得到屏幕像素点对应的世界坐标),然后以高度来进行展示雾效。之前实现的效果,我们只能得到均匀的雾效。而接下来要实现的是不均匀,同时让雾不断飘动,使雾看起来更加缥缈。这里我们仍然要使用屏幕后处理技术。

C#实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
public class FogWithNoise : ScreenPostEffectsBase
{
public Shader shader;
private Material m_Material;
public Material M_Material
{
get
{
m_Material = CheckShaderAndCreateMaterial(shader, m_Material);
return m_Material;
}
}

// 把你自己要的摄像机拖入其中
public Camera myCamera;

private void OnEnable()
{
// 这里做一个预防
if (myCamera == null)
{
myCamera = Camera.main;
}

myCamera.depthTextureMode |= DepthTextureMode.Depth;
}

[Range(0.1f, 3.0f)]
public float fogDensity = 1;

public Color fogColor = Color.white;

public float fogStart = 0;
public float fogEnd = 2;

public Texture noiseTexture;

[Range(-0.5f, 0.5f)]
public float fogXSpeed = 0.1f;

[Range(-0.5f, 0.5f)]
public float fogYSpeed = 0.1f;

[Range(-0.5f, 0.5f)]
public float noiseAmount = 1f;

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
Matrix4x4 frustumCorners = Matrix4x4.identity;

float fov = myCamera.fieldOfView;
float near = myCamera.nearClipPlane;
float aspect = myCamera.aspect;

float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = myCamera.transform.right * halfHeight * aspect;
Vector3 toTop = myCamera.transform.up * halfHeight;

Vector3 topLeft = myCamera.transform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;

topLeft.Normalize();
topLeft *= scale;

Vector3 topRight = myCamera.transform.forward * near + toTop + toRight;
topRight.Normalize();
topRight *= scale;

Vector3 bottomLeft = myCamera.transform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;

Vector3 bottomRight = myCamera.transform.forward * near - toTop + toRight;
bottomRight.Normalize();
bottomRight *= scale;

frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);

m_Material.SetMatrix("_FrustumCornersRay", frustumCorners);

m_Material.SetFloat("_FogDensity", fogDensity);
m_Material.SetColor("_FogColor", fogColor);
m_Material.SetFloat("_FogStart", fogStart);
m_Material.SetFloat("_FogEnd", fogEnd);

m_Material.SetTexture("_NoiseTex", noiseTexture);
m_Material.SetFloat("_FogXSpeed", fogXSpeed);
m_Material.SetFloat("_FogYSpeed", fogYSpeed);
m_Material.SetFloat("_NoiseAmount", noiseAmount);

Graphics.Blit(source, destination, m_Material);
}
else
Graphics.Blit(source, destination);
}
}
}

        这里的实现也和之前的雾效脚本差不多,但是为了能使用噪声纹理等效果我们还是多加了些变量使用。之前变量的意思,大家可以去看看屏幕后处理那一章。这里现在只解释新变量的意思。fogXSpeed和fogYSpeed对应噪声纹理在X和Y方向上的移动速度,我们就是用这个来模拟雾的飘动效果。noiseAmount用来控制噪声程度,当noiseAmount为0时不应用任何噪声。这时候我们得到的就是之前的效果。noiseTexture就是我们要传递给Shader的噪声纹理。

具体的Shader实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Shader "Learn/FogWithNoise"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_FogDensity ("FogDensity",Float) = 1.0
_FogColor ("FogColor",Color) = (1,1,1,1)
_FogStart ("FogStart",Float) = 0.0
_FogEnd ("FogEnd",Float) = 1.0

_NoiseTex ("Texture", 2D) = "white" {}
_FogXSpeed ("FogXSpeed",Float) = 0.1
_FogYSpeed ("FogYSpeed",Float) = 0.
_NoiseAmount ("NoiseAmount",Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

CGINCLUDE
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
float4 vertex : SV_POSITION;
float4 interpolatedRay : TEXCOORD2;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
sampler2D _NoiseTex;
float4 _NoiseTex_ST;
float4 _FogColor;
float4x4 _FrustumCornersRay;
float _FogDensity;
float _FogStart;
float _FogEnd;
float _FogXSpeed;
float _FogYSpeed;
float _NoiseAmount;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.uv_depth = v.uv;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif

int index = 0;
if(v.uv.x < 0.5 && v.uv.y < 0.5)
index = 0;
else if(v.uv.x > 0.5 && v.uv.y < 0.5)
index = 1;
else if(v.uv.x > 0.5 && v.uv.y > 0.5)
index = 2;
else
index = 3;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif

o.interpolatedRay = _FrustumCornersRay[index];

return o;
}

fixed4 frag(v2f i) : SV_Target
{
// 获取每个像素对应的深度缓冲值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth);
float linearDepth = LinearEyeDepth(d);
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

float2 speed = float2(_FogXSpeed,_FogYSpeed) * _Time.y;

float noise = (tex2D(_NoiseTex,i.uv + speed).r - 0.5) * _NoiseAmount;

float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));

fixed4 finalColor = tex2D(_MainTex,i.uv);
finalColor.rgb = lerp(finalColor.rgb,_FogColor,fogDensity);
return fixed4(finalColor.rgb, 1);
}

ENDCG


Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}

FallBack Off
}

        在顶点函数部分,我们直接使用之前的雾效中的顶点函数。我们的重点在于片元函数的实现。

        在之前的实现中,我们直接使用计算出来的世界坐标进行判断来生成雾效。而现在我们是使用uv坐标对噪声纹理进行采集(书中并没有说为什么要把的出来的值减去0.5)。然后我们再乘上我们设定的_NoiseAmount来控制,噪声纹理的效果。而我们使用_FogXSpeed、_FogYSpeed和时间变量来做出雾飘动的效果。最后在设定fogDensity,我们再用noise去控制其大小。接下来就和之前的雾效一样了。