一、消融效果
实现的原理就是“噪声纹理+透明度测试”——使用对噪声纹理的采样结果 和某个控制消融程度的阈值 进行比较,小于阈值的就用 clip 函数把对应的像素裁剪掉 。烧焦效果则是将两种颜色混合 ,再使用 pow 函数 处理后,与原纹理颜色混合后的结果
Properties {
_BurnAmount ( "Burn Amount" , Range ( 0.0 , 1.0 ) ) = 0.0
_LineWidth ( "Burn Line Width" , Range ( 0.0 , 0.2 ) ) = 0.1
_MainTex ( "Base (RGB)" , 2 D) = "white" { }
_BumpMap ( "Normal Map" , 2 D) = "bump" { }
_BurnFirstColor ( "Burn First Color" , Color) = ( 1 , 0 , 0 , 1 )
_BurnSecondColor ( "Burn Second Color" , Color) = ( 1 , 0 , 0 , 1 )
_BurnMap ( "Burn Map" , 2 D) = "white" { }
}
_BurnAmount 控制消融程度,0时为物体正常效果,1时物体会完全消融
_LineWidth 控制模拟烧焦时的线宽,值越大,火焰边缘的蔓延范围越广
_BumpMap 对应法线纹理
_BurnFirstColor 和 _BurnSecondColor 对应了火焰两种颜色
_BurnMap 放噪声纹理
struct v2f {
float4 pos : SV_POSITION;
float2 uvMainTex : TEXCOORD0;
float2 uvBumpMap : TEXCOORD1;
float2 uvBurnMap : TEXCOORD2;
float3 lightDir : TEXCOORD3;
float3 worldPos : TEXCOORD4;
SHADOW_COORDS ( 5 )
} ;
v2f vert ( a2v v) {
v2f o;
o. pos = UnityObjectToClipPos ( v. vertex) ;
o. uvMainTex = TRANSFORM_TEX ( v. texcoord, _MainTex) ;
o. uvBumpMap = TRANSFORM_TEX ( v. texcoord, _BumpMap) ;
o. uvBurnMap = TRANSFORM_TEX ( v. texcoord, _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;
}
使用了 TRANSFORM_TEX 计算了三张纹理坐标
把光源方向从模型空间变换到了切线空间,在切线空间中处理光照可以更容易地利用法线贴图
TRANSFER_SHADOW(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. uvMainTex) . rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT. xyz * albedo;
fixed3 diffuse = _LightColor0. rgb * albedo * max ( 0 , dot ( tangentNormal, tangentLightDir) ) ;
fixed t = 1 - smoothstep ( 0.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, 1 ) ;
}
先对噪声纹理进行采样,并将采样结果和_BumpAmount 相减,传递给 clip 函数,当结果小于0时,像素就会被剔除 (红色通道(R)通常被用来存储这种灰度信息,因为它足以表示从完全不透明(白色,即1.0)到完全透明(黑色,即0.0)的连续变化)
fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount);
:(min, max, value),当value在min和max之间,就返回一个插值。当burn.r - _BurnAmount小于0时,t接近1,表示像素远离溶解边缘;当burn.r - _BurnAmount大于_LineWidth时,t接近0,表示像素接近或处于溶解边缘
fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);
:当t接近1时,burnColor接近_BurnFirstColor;当t接近0时,burnColor接近_BurnSecondColor
burnColor = pow(burnColor, 5);
:pow函数将burnColor的颜色值提升到5次幂,这通常用于增强颜色的饱和度和对比度,使溶解边缘更加鲜明
finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));
:
step函数是一个阶跃函数,如果_BurnAmount大于0.0001,则返回1,否则返回0。只有当溶解效果被启用时(即_BurnAmount不为0),溶解颜色才会被混合
如果t接近1且_BurnAmount大于0.0001,则finalColor接近burnColor;如果t接近0或_BurnAmount等于0,则finalColor接近光照颜色
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
}
使用透明度测试的阴影需要特别处理
v2f结构体定义中,V2F_SHADOW_CASTER; 可以定义阴影投射所需要定义的变量
TRANSFER_SHADOW_CASTER_NORMALOFFSET 来填充 V2F_SHADOW_CASTER 中声明的变量
在顶点着色器中,只需要关注自定义的部分——计算噪声纹理的采样坐标
在片元着色器中,先使用噪声纹理的采样结果来剔除片元
再 SHADOW_CASTER_FRAGMENT(i) 完成阴影投射的部分
二、水波效果
模拟水面,此时噪声纹理会用作高度图,不断修改水面的法线方向
使用时间相关的变量来对噪声纹理进行采样
得到法线信息后,再进行折射反射计算
模拟反射、折射等效果
使用立方体贴图Cubemap 作为环境纹理,模拟反射
使用 GrabPass来获取当前屏幕的渲染纹理,并使用切线空间下的法线方向对屏幕坐标进偏移,再使用该坐标对渲染纹理进行屏幕采样,模拟折射
菲涅尔系数来动态决定混合系数: f r e s n e l = p o w ( 1 − m a x ( 0 , v ⋅ n ) , 4 ) fresnel = pow(1-max(0, v \cdot n),4) f res n e l = p o w ( 1 − ma x ( 0 , v ⋅ n ) , 4 )
v 和 n 分别对应了视角方向和法线方向,夹角越小,cos值越大,fresnel值越小,反射越弱,折射越强
WaterWaveShader
_Color ( "Color" , Color) = ( 1 , 1 , 1 , 1 )
_MainTex ( "Albedo (RGB)" , 2 D) = "white" { }
_WaveMap ( "Wave Map" , 2 D) = "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
_WaveMap 是噪声纹理生成的法线纹理
_Cubemap 是模拟反射的立方体纹理
_Distortion 是控制模拟折射时图像的扭曲程度
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
GrabPass { "_RefractionTex" }
使用 GrabPass 来获取屏幕图像,被存到 _RefractionTex 中
把Queue 设置为 Transparent 可以确保半透明
设置RenderType 是为了在使用“着色器替换”时,该物体可以被正确渲染(通常发生在需要得到摄像机深度和法线纹理时)
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
} ;
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
} ;
v2f vert ( a2v v) {
v2f o;
o. pos = UnityObjectToClipPos ( v. vertex) ;
o. scrPos = ComputeGrabScreenPos ( o. pos) ;
o. uv. xy = TRANSFORM_TEX ( v. texcoord, _MainTex) ;
o. uv. zw = TRANSFORM_TEX ( v. texcoord, _WaveMap) ;
float3 worldPos = mul ( unity_ObjectToWorld, v. vertex) . xyz;
fixed3 worldNormal = UnityObjectToWorldNormal ( v. normal) ;
fixed3 worldTangent = UnityObjectToWorldDir ( v. tangent. xyz) ;
fixed3 worldBinormal = cross ( worldNormal, worldTangent) * v. tangent. w;
o. TtoW0 = float4 ( worldTangent. x, worldBinormal. x, worldNormal. x, worldPos. x) ;
o. TtoW1 = float4 ( worldTangent. y, worldBinormal. y, worldNormal. y, worldPos. y) ;
o. TtoW2 = float4 ( worldTangent. z, worldBinormal. z, worldNormal. z, worldPos. z) ;
return o;
}
通过调用 ComputeGrabScreenPos(o.pos); 来获取被抓取的屏幕图像的采样坐标
将 _MainTex 和 _WaveMap 的采样坐标存在 uv的四个分量中
因为需要对Cubemap进行采样,所以需要 切线空间到世界空间的转换坐标 TBN(切线、副切线、法线),存在o.TtoW中,按列组成一个变换矩阵
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, 1 ) ;
}
折射
使用内置的_Time.y 变量和速度变量计算了法线纹理的偏移量 speed,并使用speed 对_WaveMap 分别进行两个方向的采样,最后相加归一化得到切线空间下的法线方向
再使用该值与 _Distortion 和 _RefractionTex_TexelSize 对屏幕采样坐标进行偏移,模拟折射 (使用切线空间下的法线坐标是因为该空间下的法线可以反应顶点局部空间下的法线方向)
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
:×上i.scrPos.z 是因为模拟深度越大,折射程度越大
refrCol = tex2D( _RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
:对scrPos进行了透视除法,再使用该坐标对抓取的屏幕图像进行采样,得到折射颜色
反射
texColor = tex2D(_MainTex, i.uv.xy + speed);
:对主纹理进行了纹理动画,模拟水波的效果
把法线方向从切线空间转换到世界空间(用TBN矩阵的每一行分别与法线方向相乘)
据此得到反射方向
再用反射方向对Cubemap进行采样,并与主纹理颜色进行相乘
菲涅尔系数
fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);
finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
:混合反射和折射得到最终颜色
三、再谈全局雾效
在13节中实现的雾效效果是基于高度的均匀雾效,在同一个高度上,浓度是相同的
用噪声纹理可以实现不均匀的雾效,且可以让雾看起来更加飘渺
1.FogWithNoise.cs
private void OnRenderImage ( RenderTexture src, RenderTexture dest)
{
if ( material != null)
{
Matrix4x4 frustumCorners = Matrix4x4. identity;
float fov = camera. fieldOfView;
float near = camera. nearClipPlane;
float aspect = camera. aspect;
float halfHeight = near * Mathf. Tan ( fov * 0.5f * Mathf. Deg2Rad) ;
Vector3 toRight = cameraTransform. right * halfHeight * aspect;
Vector3 toTop = cameraTransform. up * halfHeight;
Vector3 topLeft = cameraTransform. forward * near + toTop - toRight;
float scale = topLeft. magnitude / near;
topLeft. Normalize ( ) ;
topLeft *= scale;
Vector3 topRight = cameraTransform. forward * near + toRight + toTop;
topRight. Normalize ( ) ;
topRight *= scale;
Vector3 bottomLeft = cameraTransform. forward * near - toTop - toRight;
bottomLeft. Normalize ( ) ;
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform. forward * near + toRight - toTop;
bottomRight. Normalize ( ) ;
bottomRight *= scale;
frustumCorners. SetRow ( 0 , bottomLeft) ;
frustumCorners. SetRow ( 1 , bottomRight) ;
frustumCorners. SetRow ( 2 , topRight) ;
frustumCorners. SetRow ( 3 , topLeft) ;
material. SetMatrix ( "_FrustumCornersRay" , frustumCorners) ;
material. SetFloat ( "_FogDensity" , fogDensity) ;
material. SetColor ( "_FogColor" , fogColor) ;
material. SetFloat ( "_FogStart" , fogStart) ;
material. SetFloat ( "_FogEnd" , fogEnd) ;
material. SetTexture ( "_NoiseTex" , noiseTexture) ;
material. SetFloat ( "_FogXSpeed" , fogXSpeed) ;
material. SetFloat ( "_FogYSpeed" , fogYSpeed) ;
material. SetFloat ( "_NoiseAmount" , noiseAmount) ;
Graphics. Blit ( src, dest, material) ;
}
else
{
Graphics. Blit ( src, dest) ;
}
}
2.FogWithNoiseShader
fixed4 frag ( v2f i) : SV_Target {
float linearDepth = LinearEyeDepth ( SAMPLE_DEPTH_TEXTURE ( _CameraDepthTexture, i. uv_depth) ) ;
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i. interpolatedRay. xyz;
float2 speed = _Time. y * float2 ( _FogXSpeed, _FogXSpeed) ;
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. rgb, fogDensity) ;
return finalColor;
}
利用内置变量_Time.y和_FogXSpeed、_FogXSpeed属性计算出当前噪声纹理的偏移量,并对噪声纹理进行采样,得到噪声值
把噪声值添加到雾的浓度计算中
再将雾颜色与原始颜色混合输出