|
前言
一不小心真的变成年更博主了,主要是真的挺忙的,还有目前在腾讯一直做虚幻引擎,很多程序方案真的不太容易分享,不像效果类的科普,教教大家工具的使用就好,程序类的需要源码的资产的组合展示,而虚幻这块资产导出受制于版本号,在海量源码中修修改改又不好抽出成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 Unity 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 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进行一起传递
[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 = @&#34;My rate map&#34;;
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就能得到正确的结果。
最终效果
https://www.zhihu.com/video/1579830611535224832
XCode GPU FrameCapture
最终补充一个技术点,通过GPUFrameCapture可以发现,图中的白色渲染部分实际上仅占用原始RenderTarget对象(黑色区域) 很小的一个部分,我们可以通过 [Ratemap physicalSizeForLayer:AtIndex] 来获取到白色部分的真实大小,这样我们在创建这张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 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|