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


上半部分1.0倍率下半,新鲜的Demo

视频为使用Unity 2021.3 使用 NativeRendering Plugin 在URP 管线下在 RenderFeature中 使用 Apple Variable Rasterization Rates(可变光栅化率)技术进行渲染

Apple是这么介绍这个技术的
“在复杂的3D应用程序中,每个像素都需要执行大量的计算来输出图像,以生成高质量的结果。 然而一旦你的渲染画面随着屏幕分辨率变大,那么渲染更多高品质像素的代价也越大”
“一种通用解决方案是对画面进行局部降分辨率,对画面中不太好注意到的地方,或者可接受的范围进行降采样可以节省大量的消耗”
试想一款赛车游戏,在画面中的玩家赛车保持1.0倍的缩放渲染,周围运动模糊的部分进行“直接”降分辨率渲染。
为什么要强调 直接 ?因为传统的运动模糊做法需要先对屏幕进行一次截取(Blit),然后通过Stencil,Depth等各种不同的遮罩方案区分出玩家赛车和背景,然后将截取出的画面进行模糊后和原始画面进行合并。这会触发一次RenderPass切换,并伴随着全屏幕的Load/Store,这在移动端上会大量的带宽占用,结局就是设备发热。 而可变速率渲染/光栅化 在drawcall绘制到某个tile时就直接降分辨率了,因此不需要额外的后处理。
在写这个文章的时候发现B站一个妹子也做了这个而技术的介绍视频,可以结合参考下

这个技术并不是苹果的专利
在PC上 DX12 PC/移动端上 Vulkan上也都有各自的实现,甚至是加强版,他们把这个技术称为 变分辨率渲染 VRS
他们的技术思想是对于原本点对点的像素采样,现在可以将周围像素组合起来仅仅采样1次,目前支持的组合方式有 1×1,2×2,4×4,2×1,1×2,4×2,2×4,4×4
Vulkan 支持 PerImage, PerPrimitive, PerDrawll级别的可变分辨率渲染
顾名思义就是支持整张画面将分辨率(后处理),PerPrimitive(三角形), PerDrawllCall(单一绘制)
而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技术后,都不需要使用了。
目前主流的高通/联发科芯片产商也直接在驱动中集成了这项技术,相信之后这个技术普及后能见到更多高质量画质,但是省电不发热的游戏 😀
目前高通支持这个技术的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
1.直接在Asset/Plugin/IOS下创建基于Objective-c的文件
2.实现CommandBuffer支持的Native Callback函数
有 EventWithData(携带数据) 和 Event(仅发送事件ID int形) 两种
对用CommandBuffer调用的API为
#if (UNITY_IOS && !UNITY_EDITOR) [DllImport (“__Internal”)] #endif private static extern IntPtr GetRenderEventFunc(); #if (UNITY_IOS && !UNITY_EDITOR) [DllImport (“__Internal”)] #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 SomeFunction
3.实现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进行一起传递
[StructLayout(LayoutKind.Sequential)] 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 , 在一帧结束后,调用 [MTLCommandBuffer commit] 进行提交到GPU
创建Ratemap的代码很简单,官方给出的范例
MTLRasterizationRateMapDescriptor *descriptor = [[MTLRasterizationRateMapDescriptor alloc] init]; descriptor.label = @”My rate map”; descriptor.screenSize = destinationMetalLayer.drawableSize; MTLSize zoneCounts = MTLSizeMake(8, 4, 1);MTLRasterizationRateLayerDescriptor *layerDescriptor = [[MTLRasterizationRateLayerDescriptor alloc] initWithSampleCount:zoneCounts];
8,4,1 表示将屏幕分为横向8块,纵向4块, 1是占位值,Ratemap仅需要两个值,但是因为参数MTLSize构造函数需要3个值,所以第3个值填1即可,实际上用不到第3个值。
for (int row = 0; row < zoneCounts.height; row++) { layerDescriptor.verticalSampleStorage[row] = 1.0; } for (int column = 0; column < zoneCounts.width; column++) { layerDescriptor.horizontalSampleStorage[column] = 1.0; }
1.0表示使用 原生分辨率*1.0倍
layerDescriptor.horizontalSampleStorage[0] = 0.5; layerDescriptor.horizontalSampleStorage[7] = 0.5; layerDescriptor.verticalSampleStorage[0] = 0.5; layerDescriptor.verticalSampleStorage[3] = 0.5
对4个边角设置为0.5倍
[descriptor setLayer:layerDescriptor atIndex:0]; id<MTLRasterizationRateMap> rateMap = [_device newRasterizationRateMapWithDescriptor: descriptor];
最后使用Descriptor创建出真正的rateMap
firstPassDescriptor.rasterizationRateMap = _rateMap;
将rateMap赋值给 RenderPassDescriptor
id<MTLRenderCommandEncoder> commandEncoder = [MTLCommandBuffer renderCommandEncoderWithDescriptor:firstPassDescriptor];
通过RenderPassDescriptor 创建出这一帧用来接受渲染指令的RenderCommandEncoder
之后把让Unity的 RenderFeature进行正常渲染
渲染结束后,进行一个BlitPass 上采样输出一张ColorTexture贴合设备分辨率
准备工作(NativePlugin侧)
MTLSizeAndAlign rateMapParamSize = _rateMap.parameterBufferSizeAndAlign; _rateMapData = [_device newBufferWithLength: rateMapParamSize.size options:MTLResourceStorageModeShared]; [_rateMap copyParameterDataToBuffer:_rateMapData offset:0];
1.BlitPass 的Shader需要知道当前屏幕哪些部分被设置了什么样的倍数数据,因此我们要创建一个buffer来存储他们
[renderEncoder setFragmentBuffer:_rateMapData offset:0 atIndex:0];
我们将获取到数据塞入 Metal FragmentShader Buffer中,索引为0
typedef struct { float4 position [[position]]; } PassThroughVertexOutput; fragment float4 transformMappedToScreenFragments( PassThroughVertexOutput in [[stage_in]], constant rasterization_rate_map_data &data [[buffer(0)]], 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 [[buffer(0)]] 索引位置必须和[renderEncoder setFragmentBuffer:_rateMapData offset:0 atIndex:0]; 索引一致
constexpr sampler s(coord::pixel, address::clamp_to_edge, filter::linear); 中使用 coord::pixel 而不是 coord::normalized 表示我们需要使用真实纹理尺寸进行采样,而不是[0-1]的归一化uv坐标
之后在NativePlugin中进行类似Unity的操作 Blit(sourceTex, targetTex, Material)的操作即可
这里涉及到 MTLRenderPipelineDescriptor 的相关操作。 之后回到Unity就能得到正确的结果。
最终补充一个技术点,通过GPUFrameCapture可以发现,图中的白色渲染部分实际上仅占用原始RenderTarget对象(黑色区域) 很小的一个部分,我们可以通过 [Ratemap physicalSizeForLayer:AtIndex] 来获取到白色部分的真实大小,这样我们在创建这张RenderTarget时可以直接使用真实的缩放后的尺寸创建,减少多余内存的浪费。
由于本文不是MetalAPI教程,且假定你是熟悉对应API的同学,因此这里不展开介绍。 本文只是为Unity扩展可变速率渲染功能可行性的一种探索,及技术普及
相关项目文件之后会在github中放出,因为写代码时用了太多粗鄙词汇用在变量上,因此还在整理中。
 
引用