接上文 :渲染纹理(上)之截屏功能实现
四、GrabPass 抓取图象
GrabPass 是 UnityShader 中的一个特殊 Pass,它的目的很简单:立刻获取当前的屏幕图象
代码也非常简单:
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
GrabPass { "_RefractionTex" }
LOD 200
PASS
{
//……
sampler2D _RefractionTex; //来自GrabPass
float4 _RefractionTex_TexelSize; //同样来自GrabPass,该纹理每一像素的大小
//……核心逻辑
}
//……
是的,最多只需要在里面填上一个纹理的名字就好了,其它什么都不需要
GrabPass 根据里面的内容,有两种形式:
- 如果有提供纹理名,就像上面的例子,Unity 只会在每一帧为第一个使用名为 "_RefractionTex" 的纹理的物体进行一次屏幕抓取操作,它可以在其它 Pass 中被访问,但必然都会是同一张纹理图像
- 没有提供纹理名,Pass 内留空,此时对于每一个使用了 GrabPass 的物体,都会进行一次屏幕抓取操作,内部使用 "_GrabTexture" 来访问屏幕图像
GrabPass 不同于后处理,它比后处理要更加暴力和强硬:只要我想要当前的屏幕图像,随时就要给我,而不是说所有渲染的结果绘制在一张纹理上,再进行处理
总体来讲,GrabPass 方便,但性能低下,特别是对于移动设备
五、GrabPass 的一个简单例子:粗糙玻璃效果
除了 GrabPass,其它都是之前了解过的内容了:
Shader "Jaihk662/Glass1"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {} //玻璃材质纹理
_NormalMap ("Normal Map", 2D) = "white" {} //玻璃发现
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} //模拟环境映射
_Distortion ("Distortion", Range(0, 100)) = 10 //模拟折射时,图像扭曲程度
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0 //折射程度,为0只反射,为1只折射
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
//Queue设置为Transparent可以保证该物体渲染时,所有的不透明物体都已经被渲染到屏幕上了
GrabPass { "_RefractionTex" }
LOD 200
PASS
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
sampler2D _NormalMap;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
sampler2D _RefractionTex; //来自GrabPass
float4 _RefractionTex_TexelSize; //同样来自GrabPass,该纹理每一像素的大小
float4 _NormalMap_ST;
float4 _MainTex_ST;
struct _2vert
{
float4 vertex: POSITION;
float3 normal: NORMAL;
float4 tangent: TANGENT;
float2 texcoord: TEXCOORD0;
};
struct vert2frag
{
float4 pos: SV_POSITION;
float4 scrPos: TEXCOORD0;
float4 uv: TEXCOORD1;
float4 TtoW1: TEXCOORD2;
float4 TtoW2: TEXCOORD3;
float4 TtoW3: TEXCOORD4;
};
vert2frag vert(_2vert v)
{
vert2frag o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos); //获得屏幕图像的采样坐标,考虑过平台差异,输入裁剪空间坐标,输出齐次坐标系下的屏幕坐标值(就是屏幕坐标乘上w,只计算xy,zw不变)
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _NormalMap);
float3 wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 wNormal = UnityObjectToWorldNormal(v.normal);
float3 wTangent = UnityObjectToWorldDir(v.tangent);
float3 wBinormal = cross(wNormal, wTangent) * v.tangent.w;
o.TtoW1 = float4(wTangent.x, wBinormal.x, wNormal.x, wPos.x);
o.TtoW2 = float4(wTangent.y, wBinormal.y, wNormal.y, wPos.y);
o.TtoW3 = float4(wTangent.z, wBinormal.z, wNormal.z, wPos.z);
return o;
}
fixed4 frag(vert2frag i): SV_Target
{
float3 wPos = float3(i.TtoW1.w, i.TtoW2.w, i.TtoW3.w);
fixed3 wViewDir = normalize(UnityWorldSpaceViewDir(wPos));
fixed3 normal = UnpackNormal(tex2D(_NormalMap, i.uv.zw));
//计算折射,考虑玻璃材质
float2 offest = normal.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = i.scrPos.xy + offest;
fixed3 refractCol = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb; //根据偏移后的坐标进行采样,得到折射颜色
//正常计算反射
normal = normalize(half3(dot(i.TtoW1.xyz, normal), dot(i.TtoW2.xyz, normal), dot(i.TtoW3.xyz, normal)));
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
fixed3 reflectionDir = reflect(-wViewDir, normal);
fixed3 reflectionCol = texCUBE(_Cubemap, reflectionDir).rgb * texColor.rgb;
fixed3 finalColor = reflectionCol * (1 - _RefractAmount) + refractCol * _RefractAmount;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
其中需要注意的几个点是:
- 小心物体的渲染队列:假设场景中有 ABCDE 五个物体,它们的渲染顺序是 ABCDE,此时如果 C 物体的 Shader 中应用了 GrabPass,那么该 shader 拿到的屏幕图象必然是没有 DE 物体的,因为它们还没有渲染,因此,很多时候我们需要设置 Tag 为 "Queue" = "Transparent",让对应的物体最后被渲染
- ComputeGrabScreenPos(o.pos):输入顶点的裁剪空间坐标,输出对应齐次坐标系下的屏幕坐标值,也就是说,若不考虑平台差异,一般获取屏幕空间坐标的公式是
,而 ComputeGrabScreenPos 方法得到的结果是
- ComputeGrabScreenPos 和 ComputeGrabScreenPos 的差别:前者考虑了平台差异,在一些情况下做了 y 轴的翻转
六、扩展:渲染流派 IMR、TBR 及 TBDR
这里主要解释为什么 GrabPass 尽量不要在手机端上使用的原因,内容会尽可能的好理解
IMR 立即渲染模式(Immediate Mode Rendering):
IMR 是一般 PC 端 GPU 的工作原理,每当你提交一个物体,这个物体将会就会立刻被渲染,应用几何光栅一气呵成,然后其渲染结果就会被放入到帧缓冲区,紧接着再渲染下一个物体
- 优点:简单省事,对复杂物体的容忍度高,GrabPass逻辑非常的简单,直接从当前的帧缓冲中取一份就好
- 缺点:需要的显存带宽、位宽高,性能相对于另外两种模式更耗
TBR 基于瓦块的渲染(Tile Based Rendering):
TBR 是一般手机端 GPU 的工作原理,对于手机端,显存和 CPU 内存往往共用一块区域,整体硬件能力远远不如 PC 端,因此优化的手段是很有必要的
TBR 的本质是将一整个屏幕分成多个区块(Tile)渲染(大小8x8像素到64x64像素不等),每当一个物体提交的时候,立刻执行完毕所有几何阶段,并且将几何阶段执行完毕的数据缓存到一个结构当中(对应图中的 Primitive List 和 Vertex Data),这样最后可以得到一个个区块,对于这一个个区块再进行光栅化放入帧缓存(如果中间有万不得已的情况,也会提前对当前所有区块强制更新到帧缓存)
- 优点:更优的性能,需要的显存、位宽不高
- 缺点:遇到 GrabPass 等操作傻眼,需要强制先更新一遍帧缓存,然后再继续缓存几何数据,除非 GrabPass 的时机完美
TBDR 基于瓦块的延迟渲染(Tile Based Deferred Rendering):
TBR 的优化,和 TBR 类似,略,有兴趣的话可以参考专门的资料,继续讲跑题了
参考资料:
- https://zhuanlan.zhihu.com/p/75813429
- https://zhuanlan.zhihu.com/p/145400372
- 《UnityShader入门精要》