Unity Shader GrabPass使用注意的问题(转)

最近项目中碰到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)下载(我收藏到网盘了)

原文地址

移动端高性能图形开发 – 移动端GPU架构探究(转)

提要

随着移动端上引擎的渲染特性如HDR,PBR,SSR,完整的后处理管线等等不断的增加,直接照搬端游方案很容易翻车,很多时候需要针对移动端做适配,这时候只有对移动端硬件底层做充分的了解才能得出最优解。 本文从引擎开发者角度详细阐述一下对于移动端GPU架构的理解。

从IMR说起

IMR常用于PC,主机,笔记本等设备。最大的特点就是Primitive提交之后直接进行绘制, 如下如所示

这个和RTR 上的Pipeline是一致的。

硬件层面的渲染流程见下图,

IMR中的内存访问 
首先确认的是,FrameBuffer是一直存储在SystemMemory上的,每个Primitive的渲染都会读取和写入DepthStencil,SceneColor,这些都是放在System Memory上的,读取System Memory(也叫Main Memory,也叫DDR,也叫DRAM)上的数据计算完之后又写入主存。

假设FrameBuffer是存储在主存上的一段连续内存,那么每个三角形在绘制的时候其实是一种随机的内存访问,这种随机即使在加入Cache机制之后还是会引起很大的性能消耗。

IMR的优势和劣势 
优势 – 简单顺畅的流程保证了在单个Primitive渲染流程中没有额外的中间结果存储。

劣势 – 带宽消耗。

假设渲染一个1440p的画面,使用32bit的SceneColor,32bit的Depth/stencil,那整体需要的内存大小为30m,如果整个FrameBuffer都要同时随机访问的话,即使有Cache机制,也会触发大量的从System Memory到Cache的读取以及从Cache到System Memory的写入。

移动端方案 – TBDR

移动端硬件设计的目的 相对于桌面gpu单纯追求更高的帧率,移动端的方案则需要同时考虑更低的发热,更小的功耗,更高的性能,并在他们之中寻找平衡点。所以移动端硬件设计方案最重要就是尽可能的减少带宽消耗。所以就有了TBDR(Tiled Based Deferred Rendering)。

整个FrameBuffer被划分成了多个块,称之为Tile,分Tile渲染,每次渲染一个Tile的时候,将当前Tile所影响的Primitive都取出来,进行对应的三角形变换,着色,深度测试后之后,将结果再写回主存,

AMD在早年的分享中做了一个实验,同样渲染两个三角形所产生带宽消耗对比

硬件上和PC有两个区别:

1、显存和系统内存共用,GPU和内存直接打交到代价很大。

2、GPU上封装了一块很小的高速存储空间,称之为On-chip memory(也叫Tile Memory,也叫 on-chip framebuffer, Tile Buffer之类的),这块内存在渲染的第二阶段就会使用这部分的内存,大小因不同的GPU设计而不同,最小可能只覆盖 16 × 16 pixel。

当然有些厂商还做了多级缓存机制,比如最新的apple芯片,除了每个Shader Core都拥有自己的L1 Cache之外,还增加了GPU Last Level Cache,进一步减小了GPU与主存传递数据的代价。

详解TBDR渲染流程

完整的pipeline可以参考下图


伪代码如下

# Phase one
for draw in renderPass:
    for primitive in draw:
        for vertex in primitive:
            execute_vertex_shader(vertex)
        if primitive not culled:
            append_tile_list(primitive)
# Phase two
for tile in renderPass:
    for primitive in tile:
        for fragment in primitive:
            execute_fragment_shader(fragment)

上面的流程可以分为两个Phase – Tiling Phase和Render Phase。

Tiling Phase

在绘制Tile的时候需要只要当前Tile有哪些Primitive参与绘制,这个需要提前算好,这个计算的过程就叫Tilling(也叫Binning)。

Tilling Phase做三件事

1、将当前ViewPort划分为多个Tile,

2、执行VertexShader,三角形都变换到屏幕空间

3、计算Primitive会影响到哪些Tile,并将结果存储到主存上去。

Tilling计算的结果会存储到主存去,称之为Intermediate store(也有叫 polygon lists,也有叫Frame Data,也有叫 parameter buffer, 也叫Geometry Work Set),后面在分Tile渲染的时候会从主存load这个数据,但是这个代价就小很多了,不过也要注意顶点数量,不要让这个buffer过于膨胀。

下面有个简单的示意动画

Rendering Phase

Tiling之后GPU就对依次渲染这些Tiles,每个GPU Core每次渲染一个Tile,Core越多,同时能渲染的Tile就越多。

几个重要的点单独拎出来说一下

硬件层面的RenderPass

简单的说,就是一次渲染管线的执行。一次RenderPass会将渲染结果存储到FBO的Attachments里面,每个Attachment都需要在Tile上初始化(Load Action),并且有可能需要写回到SystemMemory(SaveAction)。

Tile Memory Load Store Action

对于OnChip和System Memory之间内存的拷贝,我们通常称之为Resolve,甚至还有Light weight resolve(从OnChip到System Memory)和Heavy weight resolve(从System Memory到OnChip 再写回到System Memory)的说法,另外MSAA里面也有关于resove的说法,所以…

Resolve 的具体含义得根据上下文来确定。

不过现在多了一种说法就是Load Action 和Store Action。 如果是TileMemory到SysemMemory的内存写入,指的就是Store Action为Store。

Load Action 有三种,DontCare,Load, Clear。 Store Action 有三种, Store, DontCare,storeAndMultisampleResolve, MultisampleResolve. 每种Action 的详细说明请参考苹果的文档Load and Store Actions

如果想要优化流水线的性能,就一定要注意设置好每个RenderTarget的Load Action 和 Store Actions。

比如,深度和Stencil通常只有在Rasterize阶段才会使用,所以直接放到了Onchip上, Store Actions设置为DontCare这样就不用把结果写入主存,省下了大量的带宽。

Resource Storage Mode

通常用StorageMode来表示内存中的对象的被CPU和GPU的访问模式,通常有Shared,Private,Memoryless三种,如下图所示

Shared – CPU 和GPU都可以访问,这类资源通常由CPU创建并更新。

Private – 存在SystemMemory上,只有GPU可以访问,通常用于绘制 render targets,Compute Shader存储中间结果或者 texture streaming.

Memotyless – 存在TileMemory, 只有当前Tile可以访问,用完就会被刷新掉,比如Depth/Stencil Buffer 在 iOS 上对于所有的不需要 resolve 的 rt(或 store action 设置为 don’t care)都应该设置为 memoryless,比如上面说到的Depth和Stencil。

Tiled Base Deferred Render中的Defered

其实这里有两个Deferred,一个是指VS之后不立即进行PS着色,所有的厂商的移动端芯片都是如此。 另一个Deferred是由Power VR以及苹果的芯片独占,这个Deferd指的是shading操作是在所有像素都经过可见性检测之后才进行的,这个可见性判定的操作称之为HSR.下图是详细的HSR流程。

其实每个厂商都有一些相关的消除Overdraw的算法,比如Mali的Forward Pixel Kill (FPK),比如高通的A Low Resolution Z (LRZ) ,具体算法以及原理可以看最后的链接。

TBDR的优势和劣势

1、带宽上的节省

2、由于OnChip Memory的存在,一些不需要resolve出来的信息可以只存在OnChip中,可以节省System Memory。

3、分Tile 渲染对于Texture Cache 更加友好

1、屏幕尺寸越大,划分的Tile数量越多,生成的Geometry WorkSet越大。

2、Tilling Phase是完全多出来的,打断了流水线的执行,引入了latency。

Performance Tuning

主要介绍一下Profile工具,之前有写过一篇高级图形调试优化技巧 – XCode篇,有兴趣可以点开看看,不过最新版本的xcode又出了很多新的功能,最好是去wwdc的视频学习一下。

在XCode截帧之后,每个RenderPass都单独标记出来,并且每个Attachment的Load Action和Save Action)以及Memory Type都有标注出来,还可以看到当前pass的带宽消耗以及当前帧的总带宽消耗。

Android下可以使用Renderdoc,不过API需要是Vulkan,截帧之后就可以看到每个pass以及对应的load store action.

当然还有其他厂商比如Arm和高通提供的工具,总体来说,还是Xcode好用一些。

小结

移动端硬件发展至今,大的渲染框架还是TB(D)R这一套,但是随着硬件制程以及技术迭代,移动端硬件能提供的性能空间也在飞速发展,各种移动端的渲染技术也是层出不穷,如何合理地使用与分配这些性能空间算是引擎开发者的必备技能了。

另一方面,移动端和桌面端的硬件区别也将会越来越小,不用说几年前的switch掌机,还是去年苹果发布的arm架构的M1芯片,一些桌面端的GPU甚至也利用TBR来做性能优化了, 称之为partially-tile-based rendering

移动端也有在某些情况下切换IMR渲染的技术,比如高通的FlexRender。

后面还会继续针对移动端高性能图形开发写几篇东西,大家敬请期待。

Reference

Tile-Based Rendering https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/tile-based-rendering Performance Tuning for Tile-Based Architectures – OpenGL Insights Chapter.23 ARM’s

Mali Midgard Architecture Explored – https://www.anandtech.com/show/8234/arms-mali-midgard-architecture-explored/7

Next-Gen Tile-Based GPUs – http://developer.amd.com/wordpress/media/2013/01/gdc2008_ribble_maurice_TileBasedGpus.pdf

GPU Framebuffer Memory: Understanding Tiling – https://developer.samsung.com/galaxy-gamedev/resources/articles/gpu-framebuffer.html

Best Practices For Mobile

Principles of High Performance – https://developer.arm.com/solutions/graphics-and-gaming/developer-guides/learn-the-basics/principles-of-high-performance A New Software Based GPU Framework Evgeny Miretsky 2013

On NVIDIA’s Tile-Based Rendering – https://www.techpowerup.com/231129/on-nvidias-tile-based-rendering

Tile-based Rasterization in Nvidia GPUs – https://www.realworldtech.com/tile-based-rasterization-nvidia-gpus/

Understanding GPU Family 4 – https://developer.apple.com/documentation/metal/gpu_features/understanding_gpu_family_4

How GPU Works – https://cs184.eecs.berkeley.edu/sp19/lecture/23-51/how-gpus-work

PowerVR performance tips for Unreal Engine 4 – https://www.imaginationtech.com/blog/powervr-performance-tips-for-unreal-engine-4/

Load and Store Actions – https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/LoadandStoreActions.html

Killing Pixels – A New Optimization for Shading on ARM Mali GPUs – https://community.arm.com/developer/tools-software/graphics/b/blog/posts/killing-pixels—a-new-optimization-for-shading-on-arm-mali-gpus

Visibility processing – https://developer.qualcomm.com/docs/adreno-gpu/developer-guide/gpu/overview.html

原文地址