最近项目中碰到GrabPass多次抓图的问题,经测试发现可以每帧只抓取一次图,然后共用。后来看到网上这篇文章总结得很好,所以收藏下。
文档
关于Unity Shader中的GrabPass说明文档:
官方的ShaderLab: GrabPass
CSDN其他博主翻译的ShaderLab: GrabPass
GrabPass有两种写法
GrapPass { }
GrabPass { “TheGrabTextureName” }
两种写法的去写在哪呢。
文档中有说明,但是可能说的还是不够清楚。
我用自己总结的:
GrabPass { } 是每次Drawcall中的Shader的GrabPass使用时都会中屏幕内容抓取一次绘制的内容,并保存在默认的命为_GrabTexture的纹理中
GrabPass { “TheGrabTextureName” } 是每一帧Drawcall中的Shader的GrabPass中,第一次调用该GrabPass抓取的内容,保存在TheGrabTextureName的纹理中,后面Drawcall或是pass调用的GrabPass { “TheGrabTextureName” }只要TheGrabTextureName纹理名字与之前的GrabPass { “TheGrabTextureName” }中的TheGrabTextureName相同,都不会再执行GrabPass的过程,而直接使用之前Grab好的纹理对象内容。
下面我在实际Unity测试项目中测试结果写下来。
区别
我写了个测试用的,类似毛玻璃的模糊效果的Shader,如下:
使用GrabPass { } 的方式
// jave.lin 2020.03.04
Shader "Custom/GrabTexBlur" {
Properties {
_Blur ("_Blur", Range(0, 1)) = 0
}
SubShader {
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
GrabPass { }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
float4 grabPos : TEXCOORD1;
float4 worldPos : TEXCOORD2;
};
sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
fixed _Blur;
fixed3 blur(float2 uv) {
fixed3 sum = 0;
const int blurSize = 4;
const int initV = blurSize / 2;
const int maxV = initV + 1;
for (int i = -initV; i < maxV; i++) {
for (int j = -initV; j < maxV; j++) {
sum += tex2D(_GrabTexture, uv + float2(i * _GrabTexture_TexelSize.x, j * _GrabTexture_TexelSize.y));
}
}
// sum /= (blurSize + 1) * (blurSize + 1); // 这句时正确的效果
sum /= blurSize * blurSize; // 这句时为了测试两个Sphere交集部分更亮的效果
return sum;
}
v2f vert (appdata v) {
v2f o;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.vertex = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
i.grabPos.xy /= i.grabPos.w;
fixed4 col = tex2D(_GrabTexture, i.grabPos.xy);
col.rgb = lerp(col.rgb, blur(i.grabPos.xy), _Blur);
return col;
}
ENDCG
}
}
}
应用
- 场景中新建一个Sphere球体。
- 创建Material材质,设置使用的Shader。
- 给球体设置材质。
效果如下图:
Profiler > Frame Debugger
打开Frame Debugger查看绘制过程,因为shader指定在transparent的queue绘制队列,所以直接看TransparentGeometry下绘制的内容就好
可以看到有一个Grab RenderTexture的绘制
这时再复制一个毛玻璃的Sphere,然后再看看绘制过程。
这次看到有两次Grab RenderTexture的绘制
这是GrabPass { } 的绘制过程。
看看运行效果,记住这个效果,与下面的其他方式是不一样的
使用GrabPass { “TheGrabTextureName” }方式
shader还是和上面的一样,都是毛玻璃的测试shader。
只不过,这次我们给GrabPass出来的屏幕内容的纹理定义了一个名字。
GrabPass { "_GrabTexture" }
注意,如果你起的名字与默认的_GrabTexture相同也是没问题的,只要用了这种给GrabPass出来的纹理起个名字,就和没使用名字的处理流程是不一样的
然后再看看绘制过程
可以看到,这次虽然绘制了两个Sphere(黄色框那),但是Grab RenderTexture只执行了一次,因为都使用了GrabPass {“Name”}的方式,并且Name是一样的。那么后续的GrabPass就直接使用之前的纹理。
效果上会也和没起名字的不一样
两图对比一下
首先,shader中有这么一句故意使用的:
// sum /= (blurSize + 1) * (blurSize + 1); // 这句时正确的效果
sum /= blurSize * blurSize; // 这句时为了测试两个Sphere交集部分更亮的效果
因为少除了一些采样数据均值权重,所以整体权重比之前大,就会更亮。
GrabPass{ }的方式,在两球体交集部分会比较亮,原因是:GrabPass { } 每次Drawcall时的都重新先取拿一次当前绘制屏幕内容的内容到一个纹理中,所以第一个球体对Grab出来的屏幕像素内容处理逻辑了,导致某些像素比较亮了。但是第二个球体再次Grab出来的屏幕像素时,有些与第一个球体的像素集合有交集的本身有处理过,所以亮度本身比较高了,那么再次处理亮度,就会亮上加亮。
而GrabPass { “Name” } 的方式,区别在于,第二个球再次GrabPass { “Name” }时发现这个”Name”的纹理之前有了,就不再重新抓取当前屏幕内容了。所以使用的还是第一个球体执行毛玻璃+亮度之前的原始屏幕内容,所以交集出的部分不会更亮。第一次球体绘制的与第二次球体绘制有交集的那部分内容,都给第二次球体绘制的覆盖了。
不同Shader中使用了一样的GrabPass {“Name”}
再添加另一个shader,此shader作用就是添加亮度的,如下
Shader "Custom/GrabTexBrightness" {
Properties {
_Brightness ("_Brightness", Range(1, 2)) = 1
}
SubShader {
Tags { "Queue"="Transparent" "RenderType"="Transparent" }
GrabPass { "_GrabTexture" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
float4 grabPos : TEXCOORD1;
};
sampler2D _GrabTexture;
half _Brightness;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return tex2Dproj(_GrabTexture, i.grabPos) * _Brightness;
}
ENDCG
}
}
}
可以看到使用了GrabPass { “_GrabTexture” },名字与之前的毛玻璃+亮度方式的shader的”_GrabTexture”是一直的名字。
那么看看Frame Debugger的绘制过程
三次Draw Mesh,但只有一次Grab RenderTexxture,而且Draw Mesh BrightnessSphere与后面两次BlurSphere的Shader是不一样的,只不过他们使用的GrabPass {“Name”}中的Name是一样的。
注意性能
从Frame Debugger中的GrabPass的处理过程,显示的是:Grab RenderTexture,注意后面是RenderTexture。
注意都是猜测,具体调用底层的渲染API这个需要使用一个分析工具才能确定
而这个过程是很费(耗费性能)的,所以我们尽量能用GrabPass {“Name”} 就不用GrabPass{ }
刚开始我以为是RT的方式,后来发现一篇文章,虽然不是将GrabPass的,是将一般后效使用的MonoBehaviour类的回调OnRenderImage的方法下的Graphics.Blit,也是挺卡的,在一般的手机上。
这篇文章中有讲解:在使用Mali系列的Android手机。他用Mali Graphics Debugger看到底层渲染API的调用。
图中可以看到Unity的Profiler中,有显示调用的是RenderTexture.GrabPixels
中MGD(Mali Graphics Debugger)中查看底层API
在glReadPixels的最后个参数不为空,则表示数据从显存传输到系统内存,从CPU到GPU的逆向传输,这是非常缓慢的过程,并且是阻塞模式。
2020.03.15 更新,在看到unity 的SIGGRAPH2011的某个文档有说明:
GrabPass {“name”}
• new in 3.4: only copies the color buffer once per frame (_Grab is shared)
所以确定,调用的就是glReadPixel来读取ColorBuffer的像素的。
在FORCE FIELD EFFECT那部分有说明,文档:SIGGRAPH2011 Special Effect with Depth.pdf
如果多年后,下载不了,链接无效了,可以点击这里(Passworld:cmte)下载(我收藏到网盘了)