fwalker 发表于 2022-12-3 16:17

移动端 可变[速率/光栅化]渲染/VRR/VRS 在Unity下实现

前言

一不小心真的变成年更博主了,主要是真的挺忙的,还有目前在腾讯一直做虚幻引擎,很多程序方案真的不太容易分享,不像效果类的科普,教教大家工具的使用就好,程序类的需要源码的资产的组合展示,而虚幻这块资产导出受制于版本号,在海量源码中修修改改又不好抽出成Unity类似的package包,所以没时间整理这些。
索性把一些通用技术类的移植到Unity上来科普来的更有效率


上半部分1.0倍率下半,新鲜的Demo
https://www.zhihu.com/video/1579802008000970752
视频为使用Unity 2021.3 使用 NativeRendering Plugin 在URP 管线下在 RenderFeature中 使用 Apple Variable Rasterization Rates(可变光栅化率)技术进行渲染
Apple是这么介绍这个技术的

“在复杂的3D应用程序中,每个像素都需要执行大量的计算来输出图像,以生成高质量的结果。 然而一旦你的渲染画面随着屏幕分辨率变大,那么渲染更多高品质像素的代价也越大"
"一种通用解决方案是对画面进行局部降分辨率,对画面中不太好注意到的地方,或者可接受的范围进行降采样可以节省大量的消耗"
Apple Developer Documentation

试想一款赛车游戏,在画面中的玩家赛车保持1.0倍的缩放渲染,周围运动模糊的部分进行“直接”降分辨率渲染。
为什么要强调 直接 ?因为传统的运动模糊做法需要先对屏幕进行一次截取(Blit),然后通过Stencil,Depth等各种不同的遮罩方案区分出玩家赛车和背景,然后将截取出的画面进行模糊后和原始画面进行合并。这会触发一次RenderPass切换,并伴随着全屏幕的Load/Store,这在移动端上会大量的带宽占用,结局就是设备发热。 而可变速率渲染/光栅化 在drawcall绘制到某个tile时就直接降分辨率了,因此不需要额外的后处理。
这个技术并不是苹果的专利
在PC上 DX12 PC/移动端上 Vulkan上也都有各自的实现,甚至是加强版,他们把这个技术称为 变分辨率渲染 VRS
他们的技术思想是对于原本点对点的像素采样,现在可以将周围像素组合起来仅仅采样1次,目前支持的组合方式有 1x1,2x2,4x4,2x1,1x2,4x2,2x4,4x4



https://developer.nvidia.com/vrworks/graphics/variablerateshading

Vulkan 支持 PerImage, PerPrimitive, PerDrawCall级别的可变分辨率渲染

顾名思义就是支持整张画面将分辨率(后处理),PerPrimitive(三角形), PerDrawCall(单一绘制)
说到这个,刚好看一个B站小姐姐做了这个技术科普,可以结合起来一起看


而PerPrimitive方案特别适合大世界植被渲染,对于大范围的植被,我们不但可以做LOD级别的切换,还可以在同一个Instancing DrawCall中根据距离切换渲染分辨率,这样可以让玩家不那么明显的感知到LOD的突兀的三段式模型




对马岛之鬼

对于这项技术的另一个使用,就是全面推翻了之前在手游中普遍使用的 “离屏软粒子”技术, 这项技术我最早实在Gpu Gems3 中看到的

这个技术目前在各大游戏厂商的手游中都普遍用到了



https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-23-high-speed-screen-particles

它通过将代价很大的半透明渲染,比如特效,粒子渲染到一个分辨率为原生分辨率 1/4,8/1的 RenderTarget上,用以减少Pixel计算数量,最后回贴到Surface/FrameBuffer上的一种技术。 但是这个技术的代价是需要一张相同降分辨率的Depth/Stencil Texture,切换RenderPass 等涉及到bandwidth的操作,前面提到的后处理模糊处理类似的开销,这些在使用 VRR/VRS技术后,都不需要使用了。
目前主流的高通/联发科芯片产商也直接在驱动中集成了这项技术,相信之后这个技术普及后能见到更多高质量画质,但是省电不发热的游戏 :D
目前高通支持这个技术的GPU为 Adreno 660及之后,MTK天玑系列最新芯片也支持。 但是因为我的安卓手机还没有如此现金,所以使用Iphone 来制作这个Demo, Iphone从 IOS13 起就支持 PerImage/PerRegion级别的 VRR了。
说了这么多,开始说下
制作流程

首先假定你
熟悉C++编程
熟悉Metal Shader Language
熟悉Metal渲染API
熟悉Objective-C语法
熟悉Unity URP/RenderFeature开发
熟悉Unity NativePlugin流程
渲染流程大纲

1.Unity中创建RenderFeature,使用该Feature注册任意一个事件用以开启RatemapRenderPass, 比如以 AfterOpaqueEvent
2.RenderFeature中使用CommandBuffer 调用NativePlugin创建一个原生RenderPass,并将Unity的ColorTarget,DepthTarget提供过去
3.RenderFeature中使用CommandBuffer进行任意渲染
4.RenderFeature中使用CommandBuffer 调用NativePlugin结束原生RenderPass并提交GPU
5.RenderFeature中使用CommandBuffer 调用NativePlugin创建一个BlitPass对降采样过的ColorTarget进行UpScale,并将结果返回给Unity
相关技术点

可以参考官方NativeRendering demo
GitHub - Unity-Technologies/NativeRenderingPlugin: C++ Rendering Plugin example for Unity1.直接在Asset/Plugin/IOS下创建基于Objective-c的文件


2.实现CommandBuffer支持的Native Callback函数


有 EventWithData(携带数据) 和 Event(仅发送事件ID int形) 两种
对用CommandBuffer调用的API为
#if (UNITY_IOS && !UNITY_EDITOR)
          
#endif
      private static extern IntPtr GetRenderEventFunc();

#if (UNITY_IOS && !UNITY_EDITOR)
          
#endif
      private static extern IntPtr GetRenderEventAndDataFunc();

CommandBuffer.IssuePluginEventAndData(GetRenderEventAndDataFunc(), (int)EventID, void* data);
CommandBuffer.IssuePluginEvent(GetRenderEvent(), (int)EventID);不能在CommandBuffer中调用如下声明的Native Function,那是在代码中直接调用,无法被CommandBuffer识别的
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SomeFunction3.实现CommandBuffer和NativePlugin数据传递
Unity的资源比如 IndexBuffer,VertexBuffer,RenderTexture 实际上是上层抽象的资源,并非实际资源,但是Unity给了我们一个获取RHI资源的方式
RenderTexture.colorBuffer.GetNativeRenderBufferPtr()
RenderTexture.depthBuffer.GetNativeRenderBufferPtr()
Texture2D.GetNativeTexturePtr()
Mesh.GetNativeIndexBufferPtr();
Mesh.GetNativeVertexBufferPtr(int SubMeshIndex);这些数据被转换为一个指针,当我们传入NativePlugin时,可以用当前手机对应的渲染底层的资源类型去转换它
id<MTLBuffer> vertexBuffer = (__bridge id<MTLBuffer>)(vertexHandle);
id<MTLBuffer> indexBuffer = (__bridge id<MTLBuffer>)(indexHandle);
id<MTLTexture> srvTexture = (__bridge id<MTLTexture>)texturehandle;
id<MTLBuffer> uvBuffer = (__bridge id<MTLBuffer>)uvHandle;对了,在传递数据前,最后加上 GCHandle.Alloc 来保证这个数据所占用的内存不会被Unity这边回收
所以流程要多一小步
1.intPtr textureHandle = GCHandle.Alloc(RenderTexture.colorBuffer.GetNativeRenderBufferPtr(),AddrOfPinnedObject());
2.id<MTLTexture> srvTexture = (__bridge id<MTLTexture>)texturehandle;
因为IssuePluginEventAndData 每次只能传递一个 void* data
那么如果你有很多数据要一起传过去就需要调用很多次这个API, 实际上有可以将数据打包成一个Struct进行一起传递

struct VRRPassData
{
   public IntPtr colorTex;
   public IntPtr outputTex;
   public int validatedata;
}
GCHandle.Alloc(new VRRPassData(),AddrOfPinnedObject());
记住,一定要记得加上标签LayoutKind.Sequential 表示这是一段连续内存
然后我们在NativePlugin侧申明一个同结构的结构体进行转换
typedef struct
{
    void* srcTexture;
    void* dstTexture;
    int validatedata;
}BlitPassData;

static void UNITY_INTERFACE_API OnRenderEventAndData(int eventID, void* data)
g_BlitData = (BlitPassData*)data;好了,NativeRendering 大致需要注意的知识点就这些
现在说说Metal VRR用到的API

1.MTLRenderPassDescriptor 用来描述一个Pass,包括ColorTarget,DepthTarget,以及是否使用VRR(通过赋值rasterizationRateMap属性的方式)
2.MTLRasterizationRateMapDescriptor 用来描述一个rasterizationRateMap
   2.1 MTLRasterizationRateLayerDescriptor 用来描述Ratemap中层信息,以及划分区域,各个区域的渲染分辨率倍数
3.MTLRenderCommandEncoder Metal通过累计一帧中所有的渲染指令到Encoder后,最后调用EndEncoding进行指令编码为GPU可以理解的语言
4.MTLCommandBuffer 用来生成 MTLRenderCommandEncoder , 在一帧结束后,调用 进行提交到GPU
创建Ratemap的代码很简单,官方给出的范例
MTLRasterizationRateMapDescriptor *descriptor = [ init];
descriptor.label = @"My rate map";
descriptor.screenSize = destinationMetalLayer.drawableSize;
MTLSize zoneCounts = MTLSizeMake(8, 4, 1);MTLRasterizationRateLayerDescriptor *layerDescriptor = [ initWithSampleCount:zoneCounts];8,4,1 表示将屏幕分为横向8块,纵向4块, 1是占位值,Ratemap仅需要两个值,但是因为参数MTLSize构造函数需要3个值,所以第3个值填1即可,实际上用不到第3个值。
for (int row = 0; row < zoneCounts.height; row++)
{
    layerDescriptor.verticalSampleStorage = 1.0;   
}
for (int column = 0; column < zoneCounts.width; column++)
{
    layerDescriptor.horizontalSampleStorage = 1.0;
}1.0表示使用 原生分辨率*1.0倍
layerDescriptor.horizontalSampleStorage = 0.5;
layerDescriptor.horizontalSampleStorage = 0.5;
layerDescriptor.verticalSampleStorage = 0.5;
layerDescriptor.verticalSampleStorage = 0.5对4个边角设置为0.5倍
;
id<MTLRasterizationRateMap> rateMap = ;最后使用Descriptor创建出真正的rateMap
firstPassDescriptor.rasterizationRateMap = _rateMap;将rateMap赋值给 RenderPassDescriptor
id<MTLRenderCommandEncoder> commandEncoder = ;通过RenderPassDescriptor 创建出这一帧用来接受渲染指令的RenderCommandEncoder
之后把让Unity的 RenderFeature进行正常渲染
渲染结束后,进行一个BlitPass 上采样输出一张ColorTexture贴合设备分辨率
准备工作(NativePlugin侧)
MTLSizeAndAlign rateMapParamSize = _rateMap.parameterBufferSizeAndAlign;
_rateMapData = [_device newBufferWithLength: rateMapParamSize.size
                                    options:MTLResourceStorageModeShared];
;1.BlitPass 的Shader需要知道当前屏幕哪些部分被设置了什么样的倍数数据,因此我们要创建一个buffer来存储他们
;我们将获取到数据塞入 Metal FragmentShader Buffer中,索引为0
typedef struct
{
    float4 position [];
} PassThroughVertexOutput;

fragment float4 transformMappedToScreenFragments(
      PassThroughVertexOutput in [],
      constant rasterization_rate_map_data &data [],
      texture2d<half> intermediateColorMap    [[ texture(0) ]])
                                                
{
    constexpr sampler s(coord::pixel, address::clamp_to_edge, filter::linear);

    rasterization_rate_map_decoder map(data);
    float2 physCoords = map.map_screen_to_physical_coordinates(in.position.xy);
   
    return float4(intermediateColorMap.sample(s, physCoords));
   
}准备一个fragment shader, 其中constant rasterization_rate_map_data &data [] 索引位置必须和atIndex:0]; 索引一致
constexpr sampler s(coord::pixel, address::clamp_to_edge, filter::linear); 中使用 coord::pixel 而不是 coord::normalized 表示我们需要使用真实纹理尺寸进行采样,而不是的归一化uv坐标
之后在NativePlugin中进行类似Unity的操作 Blit(sourceTex, targetTex, Material)的操作即可
这里涉及到 MTLRenderPipelineDescriptor 的相关操作。 之后回到Unity就能得到正确的结果。

最终效果
https://www.zhihu.com/video/1579830611535224832




XCode GPU FrameCapture

最终补充一个技术点,通过GPUFrameCapture可以发现,图中的白色渲染部分实际上仅占用原始RenderTarget对象(黑色区域) 很小的一个部分,我们可以通过 来获取到白色部分的真实大小,这样我们在创建这张RenderTarget时可以直接使用真实的缩放后的尺寸创建,减少多余内存的浪费。
由于本文不是MetalAPI教程,且假定你是熟悉对应API的同学,因此这里不展开介绍。 本文只是为Unity扩展可变速率渲染功能可行性的一种探索,及技术普及
相关项目文件之后会在github中放出,因为写代码时用了太多粗鄙词汇用在变量上,因此还在整理中。

引用
Apple Developer Documentation
https://www.khronos.org/blog/khronos-vulkan-working-group-releases-shading-rate-extension-to-increase-rendering-performance-and-quality
VRWorks - Variable Rate Shading (VRS)
GCHandle.Alloc 方法 (System.Runtime.InteropServices)
GitHub - Unity-Technologies/NativeRenderingPlugin: C++ Rendering Plugin example for Unity

rustum 发表于 2022-12-3 16:23

[蹲]

zt3ff3n 发表于 2022-12-3 16:26

高手高手

JamesB 发表于 2022-12-3 16:28

你太牛逼了勒

TheLudGamer 发表于 2022-12-3 16:37

您太过谦了,巨佬
页: [1]
查看完整版本: 移动端 可变[速率/光栅化]渲染/VRR/VRS 在Unity下实现