Unity Shader实现水体的折射效果
水是透明物体,站在清澈的水边,我们可以透过水看到水中的物体。且随着水波晃动,水下的景象会随着水波扭曲。
前面我们实现了水的环境映射、水的反射效果,现在我们来做水的折射效果。
效果展示
透明/半透明物体折射效果的实现
原理
先把透明物体隐藏起来,实时获取摄像机渲染的图像,然后对图像进行扭曲处理,然后再把水的模型显示出来,把处理好的图像用屏幕uv的方式作为贴图贴上去
实现要点
1. GrabPass捕捉屏幕纹理
Unity提供了一个ShaderLab命令— GrabPass,用来实现一种特殊的Pass以获取屏幕图像。
GrabPass支持两种形式:
《Unity Shader入门精要》
直接使用GrabPass{},然后在后续的Pass中直接使用_GrabTexture来访问屏幕图像。但是当场景中有多个物体都使用了这种形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取动作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列以及它们渲染时当前的屏幕缓冲区中的颜色。
使用GrabPass{ “TextureName” },我们可以在后续的Pass中使用TextureName来访问屏幕图像。使用这种方法同样可以抓取屏幕,但Unity只会在每一帧时为第一个使用名为TextureName的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其他Pass中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都只会执行一次抓取操作,但这也意味着所有物体都会使用同一张屏幕图像。
GrabPass示例
新建一个3D Object(Plane)放在屏幕的左上角的位置,将GrabPass采集的纹理作为Plane的纹理,效果如下所示:
GrabPass示例shader代码
Shader "Hidden/TestGrabPass"
{
Properties{}
SubShader
{
GrabPass{ "_GrabTex" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
sampler2D _GrabTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_GrabTex, 1-i.uv);
// just invert the colors
// col = 1 - col;
return col;
}
ENDCG
}
}
}
为什么GrabPass的uv是反的?
这是坐标系差异所致:
Unity3D从左下角为(0,0)开始,往上递增。
D3D从左上角为(0,0)开始,往下递增
深入了解Unity3D中处理平台差异的细节可参考:浅谈后处理技术中的各种坑
2. 使用屏幕uv
裁剪空间到屏幕位置uv坐标的转换
裁剪空间的范围是[-1,1],也就是在经过MVP矩阵后,o.pos.x/ o.pos.w 以及o.pos.y/ o.pos.w 的范围都是[-1,1] 故可以将裁剪空间坐标转换为 相对屏幕位置的uv坐标,如下
o.screenUV = float2(( o.pos.x/o.pos.w+1)*0.5,(o.pos.y/o.pos.w+1)*0.5);
屏幕uv示例
左边的模型用普通uv对纹理采样,右边的模型用屏幕uv对纹理进行采样,可以看出使用屏幕uv采样时的效果就像是将纹理铺满整个屏幕但我们只取用模型部分的纹理一样。
屏幕uv示例shader代码
Shader "Hidden/TestScreenUV"
{
Properties{
_MainTex("Tex", 2D) = ""
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float2 screenUV : TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 计算屏幕UV
o.screenUV = float2(1,_ProjectionParams.x)*(o.vertex.xy/o.vertex.w+1)*0.5;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.screenUV);
// just invert the colors
// col = 1 - col;
return col;
}
ENDCG
}
}
}
3. 扭曲效果
对一张噪声图采样,将采样值来作为uv的偏移值,是实现的扭曲效果
扭曲效果示例
扭曲效果示例shader代码
Shader "Hidden/TestDistort"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {}
_DistortTex("Distort Tex", 2D) = "white" {}
_DistortScale("Distort Scale", Range(0.0, 0.5)) = 0.1
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
sampler2D _DistortTex;
fixed _DistortScale;
fixed4 frag (v2f i) : SV_Target
{
fixed4 distortCol = tex2D(_DistortTex, i.uv);
fixed uvOffset = distortCol.r* _DistortScale;
fixed4 col = tex2D(_MainTex, i.uv+uvOffset);
// just invert the colors
// col = 1 - col;
return col;
}
ENDCG
}
}
}
实现水的折射效果
综合上面的3个要点,我们实时获取摄像机渲染的图像,然后对图像进行扭曲处理,然后再把水的模型显示出来,把处理好的图像用屏幕uv的方式作为贴图贴上去,最后再将之前实现的环境映射效果加上。
水的折射shader代码
Shader "WaterEffect/Refract"
{
Properties
{
_CubeMap("CubeMap", CUBE) = ""{}
_RefractRatio("Refract Ratio", Float) = 0.5
_FresnelScale("Fresnel Scale", Float) = 0.5
_DistortTex("Distort Texture", 2D) = "" {}
_DistortScale("Distort Scale", Float) = 0.05
}
SubShader
{
GrabPass{ "_GrabTex" }
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldReflect : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float3 worldViewDir : TEXCOORD3;
float4 vertexLocal : TEXCOORD4;
float3 worldRefract : TEXCOORD5;
float2 uv : TEXCOORD6;
float2 screenUV : TEXCOORD7;
};
float _RefractRatio;
v2f vert (appdata v)
{
v2f o;
o.vertexLocal = v.vertex;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldNormal = mul(unity_ObjectToWorld, v.normal);
o.worldViewDir = normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
o.worldReflect = reflect(-o.worldViewDir,normalize(o.worldNormal));
o.worldRefract = refract(-o.worldViewDir,normalize(o.worldNormal),_RefractRatio);
o.uv = v.uv;
o.screenUV = (o.vertex.xy / o.vertex.w + 1)*0.5;
return o;
}
sampler2D _GrabTex;
float _FresnelScale;
samplerCUBE _CubeMap;
sampler2D _DistortTex;
float _DistortScale;
fixed4 frag (v2f i) : SV_Target
{
// 水的反射
float4 fresnelReflectFactor = _FresnelScale + (1 - _FresnelScale)*pow(1-dot(i.worldViewDir,i.worldNormal), 5);
fixed4 colReflect = texCUBE(_CubeMap, normalize(i.worldReflect));
// 水的折射
fixed4 colDistort = tex2D(_DistortTex, i.uv);
float uvOffset = colDistort.r*_DistortScale;
float2 sceneUV = i.screenUV.xy;//*0.5+0.5;
sceneUV.y = 1-sceneUV.y;
fixed4 colRefract = tex2D(_GrabTex,float2(sceneUV.x,sceneUV.y)+uvOffset);
// 综合水的反射和水的折射
fixed4 col = fresnelReflectFactor * colReflect + (1-fresnelReflectFactor) * colRefract;
return float4(col.xyz,1);
}
ENDCG
}
}
}
效果展示
至此,我们就完成了水的折射效果!~ 接下来我们还要完成水的波动、水和水中物体的交界检测(用来生成泡沫),以及水的颜色随着深度逐渐加深的效果。