【Unity3D Shader】利用GrabPass模拟玻璃效果
本文将介绍和实现Unity Shader中的玻璃效果。实现效果如下:
一、效果分析和实现原理
试想下生活中的玻璃杯,它能反射周围的环境,同时如果我们将一颗小球放入杯中,我们能够透过玻璃杯看到小球,而且如果玻璃杯表面凹凸不平的话,我们还会看到小球好像被扭曲了,这便是光线在玻璃表面反射和折射的效果。
反射/折射
下面来讲讲在Unity Shader中反射和折射的实现:
反射/折射实现的方法其实很类似,首先获得用于环境映射的立方体纹理,然后通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。
1.创建用于环境映射的立方体纹理
在Unity 5中,创建用于环境映射的立方体纹理的方法有三种,前两种方法都需要我们提前准备好立方体纹理的图像,然后对其进行一些处理得到Cubemap,这里不做赘述,而着重介绍第三种方法:利用脚本和Unity提供的 Camera.RenderToCubemap 函数来实现,它可以把从任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理,该方法可能根据物体在场景中位置的不同,生成它们各自不同的立方体纹理,因此灵活性较高。
void OnWizardCreate ()
{
//创建用于渲染立方体纹理的的临时摄像机
GameObject go = new GameObject( "CubemapCamera");
go.AddComponent<Camera>();
//将摄像机放置在指定位置
go.transform.position = renderFromPosition.position;
//渲染出立方体纹理
go.GetComponent<Camera>().RenderToCubemap(cubemap);
//销毁临时创建的摄像机
DestroyImmediate( go );
}
2.计算反射/折射方向
CG提供了reflect/refract函数来计算反射/折射方向
观察上图,物体反射到摄像机中的光线方向,可以由光路可逆的原则来反向求得,即
reflDir = reflect(-viewDir, normal);
对于折射方向,我们还需要一个属性ratio,它代表不同介质的透射比,另外需要注意的是第一、二个参数都必须是归一化后的参数,即:
refrDir = refract(-normalize(viewDir), normalize(normal), ratio);
GrabPass
在本例中,我们还使用了 GrabPass ,GrabPass 通常用来实现玻璃等透明材质的模拟,与使用简单的透明混合不同,使用 GrabPass 可以让我们对该物体后面的图像进行更复杂的处理,例如本例中的使用法线来模拟折射效果,而不再是简单的和原屏幕颜色进行混合。
需要注意的是,在使用 GrabPass 的时候,我们需要额外小心物体的渲染队列设置。正如之前所说,GrabPass 通常用于渲染透明物体,尽管代码里并不包括混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(即 "Queue"="Transparent")。这样才可以保证当渲染该物体时,所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。
为了更好的理解 GrabPass 的作用,我编写了一个测试Shader,并赋予一个Panel来显示 GrabPass 抓取的屏幕图像。
Shader "测试/TestShader" {
SubShader {
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
//抓取屏幕图像并存储在名为_GrabTex的纹理中
GrabPass { "_GrabTex" }
pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _GrabTex;
float4 _GrabTex_ST;
struct a2v {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _GrabTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 color = tex2D(_GrabTex, i.uv).rgb;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
如图,在右边的Panel中我们可以看到 GrabPass 抓取到的屏幕图像,由于我们把Queue设置成了Transparent,因此该Shader在渲染时,其他所有不透明物体都已经被渲染到屏幕上了,也就正确地抓取到了“透过玻璃看到的图像”。
在上面的实现中,我们在 GrabPass 使用一个字符串指明了被抓取的屏幕图像将会存储在哪个名称的纹理中。实际上,GrabPass 支持两种形式。
- 直接使用 GrabPass { },然后再后续的Pass中直接使用GrabTexture来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色。
- 使用 GrabPass { "TextureName" },我们可以在后续的Pass中使用TextureName来访问屏幕图像。使用这种方法同样可以抓取屏幕,但Unity只会在每一帧时为第一个使用名为TextureName的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其它Pass中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都只会执行一次抓取工作,但这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。
在了解了上面提到的知识点后,我们便可以模拟一个玻璃效果了,我们首先使用一张法线纹理来修改模型的法线信息,然后通过一个Cubemap来模拟玻璃的反射,而在模拟折射时,则使用了 GrabPass 获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果。
二、编写Shader模拟玻璃效果
Shader "渲染纹理/GlassRefraction" {
Properties {
_MainTex ("Main Tex", 2D) = "white"{} //基础纹理
_BumpMap ("Normal Map", 2D) = "bump"{} //法线纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox"{} //立方体纹理
_Distortion ("Distortion", Range(0, 1000)) = 100 //控制模拟折射时图像的扭曲程度
_RefractAmount ("Refraction Amount", Range(0, 1)) = 1 //控制折射程度
}
SubShader {
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
GrabPass { "_RefractionTex" }
pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
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, _BumpMap);
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;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
//对屏幕图像的采样坐标进行偏移
//选择使用切线空间下的法线方向来进行偏移是因为该空间下的法线可以反映顶点局部空间下的法线方向
fixed2 offset = bump * _Distortion * _RefractionTex_TexelSize;
//对scrPos偏移后再透视除法得到真正的屏幕坐标
i.scrPos.xy = (offset + i.scrPos.xy) / i.scrPos.w;
//折射颜色
fixed3 refrColor = tex2D(_RefractionTex, i.scrPos.xy).rgb;
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
//计算反射方向
fixed3 reflDir = reflect(-worldViewDir, bump);
//基础纹理颜色
fixed3 texColor = tex2D(_MainTex, i.uv.xy).rgb;
//反射颜色
fixed3 reflColor = texCUBE(_Cubemap, reflDir).rgb * texColor;
//使用_RefractAmount混合反射颜色和折射颜色
fixed3 finalColor = lerp(reflColor, refrColor, _RefractAmount);
return fixed4(finalColor, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
三、实现效果
RefractionAmount为0(仅反射)
RefractionAmount为1(仅折射)
RefractionAmount为0.5(反射+折射)
参考:Unity Shader入门精要