一、MSAA 简介

关于锯齿的产生原因以及主流抗锯齿技术 MSAA 网上的资料很多,凡是游戏开发也多多少少都有了解,因此这里就不多赘述,有兴趣可以直接参考以下几篇文章:

  1. 现代图形 API 的 MSAA、UE4 MSAA & depth
  2. 知乎上关于 MASS 和抗锯齿的问题 & 解答
  3. 最简单的 OpenGL 抗锯齿、主流抗锯齿方案详解

拿随意一个游戏举例,MSAA N samples 效果对照如下:

1.1 移动平台上的 MSAA

前面介绍了锯齿的产生原因以及 MSAA 解决方案,这里主要是介绍 MSAA 每一步是在哪个时机,那一块地方做的,简单描述下性能上的问题,并且主要考虑移动平台的 TBRD 架构

一样可以先参阅文章:

  1. 深入剖析 MSAA
  2. 针对移动端 TBDR 架构 GPU 特性的渲染优化、TBDR 架构基础

1.1.1 关于流程

直入主题:MSAA 先在光栅化阶段生成覆盖信息,然后计算像素颜色,根据覆盖信息和深度信息决定是否来写入子采样点,整个完成后再通过某个过滤器进行降采样得到最终的图像,大体流程如下图:

极大部分情况下片上的 FrameBuffer 是 NxMSAA 格式,而我们只需要最后 MSAA resolve / 降采样的结果:此时硬件及 API 就会最直接在片上就完成 resolve 操作,这是最理想情况,也是 On-Chip MSAA 的规操:只要你当前的 RenderTarget 是单一采样格式

//像 Unity 中我们自己写的 RenderPass,需要 or 写入的 RT 都是不开 MSAA 的
rtDescriptor = new RenderTextureDescriptor(width, height, format, depthBufferBits)
{dimension = TextureDimension.Tex2D,msaaSamples = 1,sRGB = false
};

Unity FrameDebug 也可以跟踪到每次 MSAA resolve 的时机:

1.1.2 看上去硬件包办了,但还远没有这么简单

通过前面的流程也能知道:因为有硬件支持,多 Samples 的纹理存储以及 resolve 操作都是在片上做的(On-Chip),也就是红色箭头的部分,因此和 Depth Test、Alpha Test 类似,MSAA 只需要跟片上缓存交互即可,这样直接避免了 GPU 内存的直接读写,降低了带宽消耗

由于 GPU 的片上缓存的存储空间非常有限,因此渲染完成一个 Tile 之后,需要将结果复制到FrameBuffer 中(#Unity RenderBufferStoreAction),同理如果一帧内需要修改 RenderTarget 多遍渲染时,在对 Tile 进行写入的时候可能还需要从 FrameBuffer 中将对应 Tile 中旧的数据读取到片上缓存(#Unity RenderBufferLoadAction,对于 Tile 是 restore)

但上面都是理想情况,MSAA 的性能和各显卡平台支持程度都不容乐观,其中一点就是:像 4xMSAA 就需要四倍的块缓冲内存,考虑到芯片上的块缓冲内存很最贵,所以显卡会通过减少块的大小来消除这个问题,举个例子,假设默认的 tile 渲染大小是 32x32,如果你开启了 2xMSAA,如果没到内存瓶颈还好,一旦超过了片上内存能能接受的上限,一个 tile 就只能渲染 16x16 的区域了

不但如此,由于大型游戏后续效果处理对 depthTexture 的依赖,引擎底层 / 图形 API 支持不足,导致硬件 MSAA 没法在 On-Chip 上一口气做完,我们不得不手动进行额外的 Load/resolve 操作从而产生更多的额外开销,这块没明白没关系,后面在解决 MSAA depth resolve 问题时会具体提到

1.2 Unity URP 开启 MSAA

其实很简单,就是一个设置:

然后想办法把它做成游戏时可以动态配置的形式:

static URPAssetRuntimeParams assetRuntimeParams;
public UniversalRenderPipeline(UniversalRenderPipelineAsset asset)
{//修改下 URP 源码……UniversalRenderPipeline.assetRuntimeParams.Init(asset);
}
static void InitializeStackedCameraData(Camera baseCamera, UniversalAdditionalCameraData baseAdditionalCameraData, ref CameraData cameraData)
{var assetRuntimeParams = UniversalRenderPipeline.assetRuntimeParams;if (baseCamera.allowMSAA && assetRuntimeParams.msaaSampleCount > 1)msaaSamples = (baseCamera.targetTexture != null) ? baseCamera.targetTexture.antiAliasing : assetRuntimeParams.msaaSampleCount;
}public bool IsOpenMSAA
{get { return _isOpenMSAA; }set{_isOpenMSAA = value;UserDataManager.SetData(IS_OPEN_MSAA, GLOBAL_SETING_GROUP, value);if (IsOpenMSAA)UniversalRenderPipeline.assetRuntimeParams.msaaSampleCount = 2;elseUniversalRenderPipeline.assetRuntimeParams.msaaSampleCount = 1;}
}

但这只是开始,如果你是大型项目的话,很有可能会不得不面对三个问题:

  1. 所有用到 depthTexture 的渲染全部出错,例如后处理描边等
  2. Win + D3D11 是好的,但是各种手机/平台不能很好的支持 MSAA ?
  3. 性能?能更省一点嘛?

1.3 AlphaToCoverage 及 HDR resolve

参考文章:

  1. 知乎:为什么 Alpha to coverage 方法不需要排序
  2. 关于风格化云渲染的一些尝试
  3. Alpha To Coverage

对于 Alpha to coverage:如果像草和树等 AlphaTest 的物体本身 Texture 就做过边缘柔滑,再加上也都不是特别细(<1pixel),就没有必要开启 Alpha to coverage

Unity 中可以在 shader 中添加如下标签以开启 AtoC:

AlphaToMask On

对于 HDR resolve:如果不存在爆亮区域需要解决锯齿问题,就也可以不做考虑

二、URP MSAA 及其 Depth resolve 问题

无脑开启 MSAA 带来的一个必然结果是,凡是用到 depthTexture 的 Shading,可能无一例外全坏:比如基于深度检测的后处理描边等等

2.1 MSAA resolve

为什么会这种情况,要先从 MSAA resolve 的算法说起,对于颜色而言,resolve 算法必定是对多采样点进行平均:不然就不可能得到抗锯齿的效果

但是归限于硬件的一个因素:同一 RT 下 MSAA 时 Depth Stencil 也必须是 MultiSample 的,并且与 Color 的 Sample 数量相同,这样深度格式就也必须是 NxMSAA 的,尽管深度完全不需要抗锯齿

但是这不是导致问题的关键,关键在于:depthTexture 也采取了和 colorTexture 一样的 resolve 算法也就是平均,从而使得边缘深度信息完全出错(并且这样做还没有任何意义),这也是导致上面问题的主要原因

知道了这点之后,其实想解决就很简单:第一个想到的方法必然是修改 depthTexture 的 resolve 算法:由平均改为取最值,但很可惜,前面说过这块是硬件帮我们处理的,因而我们想要修改 resolve 算法,第一步只能从硬件平台 API 上下手

2.1.1 硬件 resolve 支持

先吹一波这篇文章,其实已经讲得很好了:从一个小 BUG 看 MSAA depth resolve,大致总结下:像 IOS metal 直接就支持我们使用自定义的 resolve 算法,并且使用起来非常简单,但除此之外特别是主流的 Andriod OpenGLES 3.1/3.0 都不能很好的支持,但也有解,比如使用 framebuffer fetch 扩展等等,但无一例外都需要动到引擎源码,能不能搞定还有另说(特别是 Unity)

2.1.2 软件 resolve

这样看来,在不动源码的前提下,我们能考虑的也是最简单的:就是跳过硬件 resolve:这其实很好办,如果理解了前面文章里的内容,就很容易想到其中一个方案:不进行解析,直接将渲染纹理绑定为着色器中的多采样纹理

if (m_ActiveCameraDepthAttachment != RenderTargetHandle.CameraTarget)
{var depthDescriptor = descriptor;depthDescriptor.colorFormat = RenderTextureFormat.Depth;depthDescriptor.depthBufferBits = k_DepthStencilBufferBits;depthDescriptor.bindMS = msaaSamples > 1 && (SystemInfo.supportsMultisampledTextures != 0);cmd.GetTemporaryRT(m_ActiveCameraDepthAttachment.id, depthDescriptor, FilterMode.Point);
}

这样我们就可以保证我们拿到的 depthTexture 都是 resolve 前的,可以直接进行深度采样,或是干脆自己 Copy 并手动 resolve(以下代码来源于 URP CopyDepthPass):这和 OpenGLES 3.1 及以上使用 texelFetch 函数指定 sampleIndex 获取对应 sample 的 color,然后进行自定义 Resolving 的操作一样,都是软 resolve 解决方案

 #define DEPTH_TEXTURE_MS(name, samples) Texture2DMS<float, samples> name#if MSAA_SAMPLES == 1DEPTH_TEXTURE(_CameraDepthAttachment);SAMPLER(sampler_CameraDepthAttachment);
#elseDEPTH_TEXTURE_MS(_CameraDepthAttachment, MSAA_SAMPLES);float4 _CameraDepthAttachment_TexelSize;
#endiffloat SampleDepth(float2 uv)
{#if MSAA_SAMPLES == 1return SAMPLE(uv);#elseint2 coord = int2(uv * _CameraDepthAttachment_TexelSize.zw);float outDepth = DEPTH_DEFAULT_VALUE;UNITY_UNROLLfor (int i = 0; i < MSAA_SAMPLES; ++i)outDepth = DEPTH_OP(LOAD(coord, i), outDepth);return outDepth;#endif
}

好了到此 depth resolve 问题就应该可以解决了,但软件 resolve 的代价呢?必然是有的:首先就是 nxMSAA 多倍的内存占用,这次直接给 load 到了内存中,光带宽问题就不小,毕竟你放弃了硬件的 resolve,同时也就放弃了 On-Chip MSAA

2.2 Unity-URP 是怎么解决的

URP MSAA 及两个 Depth Pass

一个前提是:如果你需要在 shader 里用到 depthTexture,那么就需要先勾选 DepthTexture 配置项:这样 URP 内部才会通过 CopyDepth 的方式获取当前摄像机的 depthTexture

参考对应的源码:

  • createDepthTexture 决定是否创建深度纹理,否则直接使用摄像机的 Target
  • 会有一个专门的 CopyDepthPass 负责深度的拷贝,并设置全局的 _CameraDepthTexture 以供 shader 使用
if (cameraData.renderType == CameraRenderType.Base)
{m_ActiveCameraColorAttachment = (createColorTexture) ? m_CameraColorAttachment : RenderTargetHandle.CameraTarget;m_ActiveCameraDepthAttachment = (createDepthTexture) ? m_CameraDepthAttachment : RenderTargetHandle.CameraTarget;bool intermediateRenderTexture = createColorTexture || createDepthTexture;// Doesn't create texture for Overlay cameras as they are already overlaying on top of created textures.bool createTextures = intermediateRenderTexture;if (createTextures)CreateCameraRenderTarget(context, ref renderingData.cameraData);
}
if (!requiresDepthPrepass && renderingData.cameraData.requiresDepthTexture && createDepthTexture)
{m_CopyDepthPass.Setup(m_ActiveCameraDepthAttachment, m_DepthTexture);EnqueuePass(m_CopyDepthPass);
}

2.2.1 当开启 MSAA 之后,URP 这么处理深度

目前的 URP 7.5.1 版本,如果开启了 MSAA,内部反而不会进行 DepthCopy,取而代之的是 DepthPrePass,也就是预渲染深度:

这个 pass 会在所有的物体真正绘制之前,先画一遍不透明物体,但是只会写入深度值(DepthOnly),并且目标纹理不会开启 MSAA,也和摄相机渲染目标完全无关,就是一个自己的 RT,这张 RT 就作为 DepthTexture 作为后续使用


// If camera requires depth and there's no depth pre-pass we create a depth texture that can be read later by effect requiring it.
bool createDepthTexture = cameraData.requiresDepthTexture && !requiresDepthPrepass;
createDepthTexture |= (cameraData.renderType == CameraRenderType.Base && !cameraData.resolveFinalTarget);
if (requiresDepthPrepass)
{m_DepthPrepass.Setup(cameraTargetDescriptor, m_DepthTexture);EnqueuePass(m_DepthPrepass);
}

很明显这样做不会出现 MSAA depth resolve 的问题,毕竟写入的 RT 压根不开启多倍 MSAA sampler,但是它需要你对所有不透明物体都多加一个 DepthOnly 的 Pass,这意味着会多出大量额外的 drawcall,尽管这个 pass 里面不需要任何计算

综上这个只能说是可行的方法之一:以更多 drawcall 的代价解决 MSAA 深度的问题,同时也可以顺带做下软件 earlyZ,不过这只是目前 URP 的做法,但显然不是唯一解

2.2.1 还有别的方案嘛

当然可以,不过要略微修改下 URP 的源码:那就是在渲染不透明物体后,执行 DepthCopyPass 时直接软件 resolve 深度

首当其冲:把最下面判断 DepthCopyPass 是否启用的逻辑中的 msaa 判断加回来:

bool CanCopyDepth(ref CameraData cameraData)
{bool msaaEnabledForCamera = cameraData.cameraTargetDescriptor.msaaSamples > 1;bool supportsTextureCopy = SystemInfo.copyTextureSupport != CopyTextureSupport.None;bool supportsDepthTarget = RenderingUtils.SupportsRenderTextureFormat(RenderTextureFormat.Depth);bool supportsDepthCopy = !msaaEnabledForCamera && (supportsDepthTarget || supportsTextureCopy);// TODO:  We don't have support to highp Texture2DMS currently and this breaks depth precision.// currently disabling it until shader changes kick in.depthDescriptor.bindMS = msaaSamples > 1 && (SystemInfo.supportsMultisampledTextures != 0) && !SystemInfo.supportsMultisampleAutoResolve;//bool msaaDepthResolve = false;return supportsDepthCopy || msaaDepthResolve;
}

但是只改这个的话,进游戏会出错,原因(报错内容)是:A non-multisampled texture being bound to a multisampled sampler. Disabling in order to avoid undefined behavior. Please enable the bindMS flag on the texture

字面意思:采样纹理格式和采样器对不上,因此还需要设置渲染纹理的 bindMS 属性,以确保 m_ActiveCameraDepthAttachment 不解析(resolve)采样纹理,维持其多 Samples 的格式与内容

depthDescriptor.bindMS = msaaSamples > 1 && (SystemInfo.supportsMultisampledTextures != 0);

好了搞定,这下 DepthCopyPass 设置输入的 DepthTexture 就是多 Samples 的 Texture,你就可以在 shader 里进行自定义 resolve,这块 URP 已经帮我们做了

这一部分的内容其实就是上面软件 resolve 的流程,代码也可以直接参考

注意这里有一个坑:DepyCopyPass 帮你 resolve 的深度是多采样求最值,这个算法没问题,但是由于采样点位置不同,因此不能保证它和你后续直接采样纹理中心点的结果相同,它们存在小小的误差,后续如果要在这张 copy 后的 Texture 中继续写入单采样的深度结果,就最好不要用等于(Equal 或者 LessEqual)判断深度

既然这也是一个可行的 MSAA 方案,URP 为什么没有这么去做?

其实原先 URP 有这么做的,只不过后面为了解决部分 OpenGL 设备黑屏的问题,又把对应的功能给阉割了,具体可以参考 CHANGE.LOG 文件和对应 git 提交,也可以参考这篇链接

因此不排除我们实现了 MSAA,原先部分设备黑屏的问题还是会出现,因此如果采取该方案,我们还需要对机型做一个筛选:或仅对部分高配机型做 MSAA 的支持

三、硬件平台与性能

3.1 硬件 RenderTexture.ResolveAA 打点

大部分的 MSAA 解析都是硬件做的,而 Unity3D FrameDebug 可以跟踪解析,是因为在底层打了点,但是这有一个前提:就是当前驱动是否关闭了 MSAA 的自动解析,否则 Unity3D 本身是无法跟踪的

这就导致可能 PC D3D11 平台 MSAA 不会有什么问题,但只要切换 PC + OpenGLES3.0 就会发现跟踪不到 resolveAA 了,但是不用怕其实你的 MSAA 依旧生效,可以直接方法游戏画面以确认,阅读 URP / Unity 源码也可以略知一二,一个确定是否自动解析了多重采样纹理的属性就是 SystemInfo.supportsMultisampleAutoResolve:

bool PlatformRequiresExplicitMsaaResolve()
{return !SystemInfo.supportsMultisampleAutoResolve &&SystemInfo.graphicsDeviceType != GraphicsDeviceType.Metal;
}

但如果关闭了 MSAA 的自动解析,就需要引擎底层去手动触发,良心的 Unity 还是把对应的接口给了我们的:RenderTexture.ResolveAntiAliasedSurface,必然底层也帮我们做了这件事


其次就是 ResolveAA 的时机与次数,当然下面内容讨论的前提是 SystemInfo.supportsMultisampleAutoResolve = false

Unity 底层触发硬件解析的方式比较暴力:即每次切换 renderTarget 都会对切换出去的 renderTarget 强制 resolveAA,这操作在某些情况下其实是冗余的,比如两次 renderTarget 不同,但是作为 shader Resources 的 MSTexture 确是同一张,很显然此时你只需要对该 MSTexture 进行一次 resolveAA 就够了,可事实上会有两次

优化方案必然有,网上已经有一个很好的例子了,可以直接参考 github:大致思路就是一个 targetHandle 直接持有至多两张 Texture,其中一张是 MSTexture 格式的,一张是 resolve 后的,然后对于改写了原先的 Get/SetTemporary 和 Idfentifier 方法:

  • GetTemporaryRT 时直接支持直接用 RenderTexture 创建 renderTarget
  • resolve 专门用一个 PASS 去做,并且只在非透明物体渲染后,深度拷贝前做一次,这也意味着在此之后所有渲染都不带 AA
  • 重写 Identifier,支持只获取 MSTexture,或根据是否 resolve 来直接获取结果

整个过程看上去是增加了一个 renderTexture,但就算 unity 本身关闭 bindMS,内部也会有两个renderTexture Handle,所以概念上是等同的

3.2 Set Memoryless

这节可以算作这一节的扩展:片上 MSAA 只省带宽,其实不省系统内存,即仍然会在系统内存中申请 与 MSTexture + Resolve Texture 同等大小的一块区域,直到下一次图元刷新时删除

但如果你只需要 resolve 的结果,那么这块 MSTexture 的内存就完全没必要申请,在 IOS metal 及 vulkan 平台上,支持你设置 RT 的存储模式为 Memoryless 以进一步减少内存开销,这也是苹果官方极力推荐的优化 MSAA 的手段

注意如果纹理是 Memoryless 的,那么这种纹理就是渲染过程中的临时资源,不能在渲染的开始加载纹理的内容,也不能在渲染的结束时保存其内容

3.3 并非所有机型都可以完美支持 MSAA

尽管可以通过获取硬件 Caps(参考 Unity 源码 GraphicsCaps 或者 QualitySetting)来首当其冲排除掉一些硬件上就不支持 MSAA 的设备,但是仍然不能保证所有查询硬件属性支持 MSAA 的设备,都能够不出错,其中就包括前面说的黑屏问题

举个例子,测试了多个 Iphone 设备,其中发现仅 iPad Air 2 开启 MSAA 会黑屏,尽管这个设备放现在基本属于低端机一列,不会通过高端机判定

目前 RO 是否开启 MSAA 是根据机型硬件指数来确定的,只有高端机支持开启 MSAA,理论上后续应该进行更全量的云测,以确保拿到一份覆盖大多市面主流设备/驱动的 MSAA 功能及性能相关的测试报告,以做进一步的筛选和判定

除此之外软件 resolve 深度到底使用哪一套方案(preDepth or DepthCopy)还有待商榷,目前来看不能确定哪个性能更优,设备支持更难说,这块只能说任重而道远

其它引用:

  • Catlikecoding 关于 URP MSAA
  • https://zhuanlan.zhihu.com/p/382063141
  • GPU 渲染管线和硬件架构浅谈
  • https://zhuanlan.zhihu.com/p/33033139?utm_source=wechat_session
  • https://mp.weixin.qq.com/s/eXHgNkF4k0ivcc7_thdbkw

大型项目中 MSAA 的方案参考相关推荐

  1. ROS教程(二十一):Roslaunch在大型项目中的使用技巧

    Roslaunch在大型项目中的使用技巧 Description:  本教程主要介绍roslaunch在大型项目中的使用技巧.重点关注如何构建launch文件使得它能够在不同的情况下重复利用.我们将使 ...

  2. 从实习经历中总结,项目中常见 Mock 方案

    文章目录 前言 项目中常见 Mock 方案 代码侵入 拦截 Ajax 请求 接口管理工具 Swagger YAPI RAP2-DELOS moco 总结 JSON Server 起飞教程 安装 启动服 ...

  3. python大型项目中的日志模块_Python中日志模块的使用

    前言 程序和脚本往往是无人值守运行的,一旦发生问题,就需要我们去追溯当时的情况来定位问题的原因. 这便需要我们在程序和脚本中引入日志的功能. 相比于print信息,使用logging日志有以下优点 可 ...

  4. 项目中的难点怎么克服_克服大型项目中的文档挑战

    项目中的难点怎么克服 鉴于最近熊猫( Pandas) ,NumPy和Matplotlib等开放源数据科学项目的普及Swift增长,人们对文档的兴趣日益浓厚 ,这不足为奇. 为了帮助您了解所面临的问题, ...

  5. 大型项目中需求分析人员与其他人员的分工协作

    我所在的项目为某省大型电子运维项目组(EOMS),当前项目总人数接近50人,分为业务保障组.系统组.开发组及其他几个组. 各组主要分工界面如下: 业务保障组主要负责需求调研.需求分析.需求引导.需求确 ...

  6. 记一些大型项目中所作的规划

    ---------------------------------------------------------------------------------- 需求分析 可行性分析 容量规划 架 ...

  7. C++大型项目中使用hpp和h文件代替cpp

    文章目录 1.hpp头文件与h头文件的区别: 2.msf中代码分析 2.1利用hpp实现 2.2利用h文件实现 1.hpp头文件与h头文件的区别: (1) hpp,其实质就是将.cpp的实现代码混入. ...

  8. 大型项目中会出现的一些问题:

    典型的一个案例就是服务血崩效应 我们来看一张图: 图是一条微服务调用链, 正常的情况我们就不必在讨论了, 我们来说一下非正常情况, 假设现在 微服务H 响应时间过长,或者微服务H直接down机了如图: ...

  9. iOS大型项目解耦方案有难度?BeeHive设计优化来帮助

    [https://yq.aliyun.com/articles/71685?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ ...

最新文章

  1. re2正则表达式匹配引擎的c接口版本cre2的中文使用手册
  2. Vue使用v-bind绑定动态数据
  3. springcloud20---Config加入eureka
  4. 母婴企业上云 实现线上线下互动营销、一体化管理服务
  5. Linux 命令(10)—— split 命令
  6. cygwin-1.7 离线安装包_【软件安装管家】ArcGIS 10.7 软件安装包+安装教程
  7. 如何利用Caffe训练ImageNet分类网络
  8. Vue 将字符串保存成 TXT 文件保存到电脑
  9. st语言 数组的常用方法_ST语言入门基础
  10. neo4j 入门例子
  11. 浅谈Python中的type()、dtype()、astype()的区别
  12. 电脑怎么找到tomcat端口_查看tomcat端口号(怎么看tomcat的端口号)
  13. 如何编译Linux内核文件
  14. python中isin函数_python中Isin函数是什么
  15. 数学建模理论自制笔记1:微分方程及其模型
  16. mac地址储存在计算机的内存,mac地址通常存在计算机的
  17. [SWPUCTF 2021 新生赛]PseudoProtocols
  18. java之HeapByteBufferDirectByteBuffer以及回收DirectByteBuffer
  19. python求一元二次方程实根_Python编程实现数学运算求一元二次方程的实根算法示例...
  20. Unity使用leancloud开发弱数据联网游戏(注册、登录和云端数据存读)

热门文章

  1. 红米k20 android版本,红米k20pro入手哪个版本好 红米k20pro哪个更值得入手
  2. pwm驱动 pca9685 代码简析
  3. I2C协议及PCA9685控制芯片
  4. Ubuntu使用Wine安装钉钉、微信、QQ等Windows软件
  5. python的matmul_关于tf.matmul() 和tf.multiply() 的区别说明
  6. JSP webshell免杀——JSP的基础
  7. 交换机背板与容量计算
  8. com 关于CLSID
  9. 使用OpenCV对图像进行两种平移操作(图像的尺寸变化与图像的尺寸不变)
  10. 踩坑日记之SQLSERVER 亿级数据备份以及归档