UnityShader19.1:渲染纹理(下)之GrabPass

接上文 :渲染纹理(上)之截屏功能实现

四、GrabPass 抓取图象

GrabPass 是 UnityShader 中的一个特殊 Pass,它的目的很简单:立刻获取当前的屏幕图象

代码也非常简单:

Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
GrabPass { "_RefractionTex" }
LOD 200
PASS 
{
    //……
    sampler2D _RefractionTex;           //来自GrabPass
	float4 _RefractionTex_TexelSize;    //同样来自GrabPass,该纹理每一像素的大小
    //……核心逻辑
}
//……

是的,最多只需要在里面填上一个纹理的名字就好了,其它什么都不需要

GrabPass 根据里面的内容,有两种形式:

  1. 如果有提供纹理名,就像上面的例子,Unity 只会在每一帧为第一个使用名为 "_RefractionTex" 的纹理的物体进行一次屏幕抓取操作,它可以在其它 Pass 中被访问,但必然都会是同一张纹理图像
  2. 没有提供纹理名,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):输入顶点的裁剪空间坐标,输出对应齐次坐标系下的屏幕坐标值,也就是说,若不考虑平台差异,一般获取屏幕空间坐标的公式是 { viewport }_{x}=\frac{​{clip}_{x}}{2 \cdot {clip}_{w}}+\frac{1}{2},而 ComputeGrabScreenPos 方法得到的结果是 \frac{​{clip}_{x} + {clip}_{w}}{2 }
  • 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 类似,略,有兴趣的话可以参考专门的资料,继续讲跑题了

参考资料:

猜你喜欢

转载自blog.csdn.net/Jaihk662/article/details/113703795