大多数浏览器和
Developer App 均支持流媒体播放。
-
Metal 助力光线追踪
Metal Performance Shaders (MPS) 能够驾驭 GPU 的大规模并行计算能力,因而可以显著提升现代光线追踪和光线投射技术的核心计算速度。了解 MPS 如何提升动态场景的计算速度,并通过实际例子深入了解如何实施柔和阴影、环境光遮蔽和全局照明。了解如何实现混合渲染应用,并探索将 app 扩展到多种 GPU 的新技巧。
资源
相关视频
WWDC19
-
下载
(用METAL进行光线追踪)
大家早上好 我叫Sean 我是Apple GPU软件团队的一名软件工程师
在这场演讲中我们要讲光线追踪 让我们先回顾一下什么是光线追踪
光线追踪app基于追踪 光线与场景进行交互时所采取的路径
因此光线追踪的app有渲染、 音频、物理模拟等等
特别是光线追踪经常用于 离线渲染app中 用于模拟单条光线在场景中的反弹
这就允许这些app渲染照片的 真实反射、 折射、阴影、全局照明等等
最近光线追踪已经开始 在实时app中使用了 比如游戏 这实际上引入了一些新需求
首先在实时app中物体要四处移动 因此我们要既支持摄像头 又支持物体移动
第二性能现在变得更加至关重要了 (实时光线追踪) 这意味着光线交叉自身 必须尽可能的高效 并且我们还要高效地使用 有限的光线预算
因此我们需要当心采样策略、 随机数生成等等
最后即使使用这些技术 我们也不能投射足够的光线 来消除所有的噪声 因此我们需要一个复杂的 噪声消减策略
幸运的是Metal内嵌 有对光线追踪和降噪的支持 这样就容易多了
首先让我们回顾一下光线追踪 在Metal中是如何运作的 然后我们继续了解一些更先进的话题
如果你看一下典型的光线追踪app 它们都遵循一个大致相同的大纲
首先我们生成一些光线 每条光线都由它的起点和方向矢量 决定
然后那些光线在场景中的几何图形上 交叉贯穿 这通常会构成三角形
交叉数据可以是到交叉点的距离 但一般包含额外数据 比如被击中的三角的索引 以及交叉点的中心坐标
下一步将使用交叉点结果 比如在渲染app中 这一般是着色过程 输出一张图片 这个步骤可能也生成额外光线 然后我们就重复这个过程 一直重复好多次 直到我们完成
app的每一帧的场景中 一般都交叉无数条光线
并且这个核心交叉步骤对于所有 光线追踪app来说很常见 因此我们在Metal中加速了这个 交叉步骤
去年我们引入了 MPSRayIntersector API 它是Metal性能着色器框架的 一部分 这个API在我们所有的 Mac和iOS设备上 加速GPU上的光线交叉
我们在去年的演讲中 具体介绍了这个API 我们推荐你们参考去年的这场演讲 获取更多信息
从高层级上说API通过Metal 缓冲区获取成批的光线 它查找每条光线上最近的交叉点 并把结果返回到另一个缓冲区中
所有这些操作都被编码到 Metal commandBuffer中 就是你在app中 执行交叉测试的地方
许多加速都来自于创建一种 叫做加速结构的数据结构 这种数据结构在空间中递归地 划分三角
从而我们可以在交叉搜索过程中 迅速消除不可能与给定光线 进行交叉的三角
Metal会替你创建这种数据结构 你所要做的就是指定 何时创建加速结构 再把它传给intersector 用于交叉测试
现在创建这种数据结构一般是 在app启动时的固定消耗
在去年的API版本中 总是在CPU上创建这种数据结构 今年我们把加速结构的创建 移到了GPU上 这样可以极大地减少启动消耗 更好的是 无论何时只要可能的话 就自动使用GPU 因此你不需要做任何事 就能在app中看到这种加速
现在让我们回顾一下典型的 光线追踪app 看看我们需要执行哪些操作才能把它 转换到Metal光线追踪app中
正如我所说过的 我们先生成光线 这一般是使用计算内核完成 但也可以从片段着色器中实现 或可以写入Metal缓冲区的 任意机制
然后我们把光线缓冲区传给 intersector
它会查找交叉 并把结果返回到我们的交叉缓冲区中 请记住 要使用 intersector 我们需要提供一个加速结构 我们通常只需要创建一次 然后多次重复使用
最后我们要启动最后一个计算内核 它将使用交叉数据 把着色的图片写入一个纹理中
并且在迭代的app中 这个计算内核还会把额外光线 重新写入到光线缓冲区
让我们在一个实际的app中看一下 这是如何运作的
在这个例子中 我们要讲 如何在AR Quick Look 中使用光线追踪 AR Quick Look 是去年引入的 可以让你在增强现实中预览3D资产
我们在今早的演讲中具体讲了 AR Quick Look 我也推荐你们观看那场演讲 在本场演讲中 我们主要讲 AR Quick Look 如何使用光线追踪来渲染 环境光遮蔽效果
我们稍后再具体讲环境光遮蔽 但现在 你需要了解环境光遮蔽 计算一个近似值 即有多少光可以 到达场景中的每一个点 这导致机器人模型下的地面变暗 以及机器人的腿和地面之间的 软接触阴影
效果有点微不足道 但如果我们把它关掉 我们可以看到它实际上花了很久 才把机器人放在场景中的地面上 这对于AR app来说非常重要 可以防止物体看起来像是漂浮在 地面上一样
在去年的 AR Quick Look版本中 阴影实际上是预先计算的 因此它们不会随着物体的移动而移动
今年我们使用了 Metal对动态场景的支持 对这些阴影进行实时渲染 因此现在随着物体的移动 它们的阴影也随着它们移动
这甚至对于变形的物体也起作用 比如蒙皮模型 我们可以看到阴影 随着鱼在场景中的游动而移动
正在发生的有三种类型的动画 如果我们只使用光栅化程序 我们可以在三角的新位置上 对三角进行光栅化 (动态场景) 但因为我们使用了光线追踪 我们需要维持加速结构
因此动画的第一种类型是简单的 摄像头移动 这种移动是由于 只移动iPad造成的
我们不需要更新加速结构 仅仅因为摄像头移动了 因此我们实际上是免费获得了 这种类型的动画 我们可以开始从新摄像头的位置上 发射光线
其它两种动画类型 都要求更新加速结构
第一种是顶点动画 这可以是蒙皮模型 比如鱼 但也可以是风中摇曳的植物、 衣服或其它变形类型 Metal包含一种特殊的加速结构 更新机制 针对这样的类进行了优化
最后一种动画类型是刚体动画 物体可以移动、旋转和缩放 但完全维持它们的形状 因此一大部分加速结构实际上 仍然有效 Metal还包含一种特殊机制 用于重新利用 部分没有发生变更的加速结构
首先让我们讲一下顶点动画 (顶点动画) 随着几何图形的变更 我们需要更新加速结构 我们要给每一帧从头开始 重新创建加速结构 但我们实际上可以做的更好
在顶点动画用例中 物体往往保持大体形状 比如角色的手将保持与胳膊相连接 他们的胳膊将保持与身体相连接等等 因此编码到加速结构中的空间谱系 大部分仍然有效 只需要调整到新几何图形即可 让我们看一个例子
这是我们之前看到过的加速结构
如果三角移动 我们可以看到边界区域 不再与三角对齐了 但树形结构自身绝大部分仍然有意义 因此我们与其从头重新创建加速结构 不如把边界区域对齐到三角的新位置 从底部到顶部
我们把这个操作叫做再安置 我们可以看到 这仍然是有效的加速结构 但比从头开始创建要快多了 因为我们可以重新使用现有的 树形结构
这也是完全在GPU上运行的 从而让这个过程变得更快了 但也意味着我们可以安全地编码 再安置操作 在比如说计算内核更新顶点之后
坏消息是我们不能添加或移除 任何几何图形 因为树形结构仍将编码 对旧几何图形的引用
这还潜在地降低了加速结构的品质 那可能会影响光线追踪的性能 这是因为三角形最初是使用一套 未来学进行划分的 在三角移动之后不会进行加速
影响通常都很微小 但极端情况下 比如远距离传送 几何图形可能会导致性能问题 尽管如此 这对于典型的变形用例 和角色蒙皮用例来说非常棒 让我们看一下如何在代码中进行设置
首先在我们创建加速结构之前 我们需要先启动对重新安置的支持 请注意仅仅启动重新安置 就已经可能会降低加速结构的品质了 因此如果你真的需要再安置加速结构 请一定要开启这个功能 (再安置) 然后我们把encodeRefit调用到 Metal commandBuffer中
对于顶点动画来说 这是我们所需要做的全部操作 接下来我们讲刚体动画
正如它的名字所暗示的那样 在这种动画中 物体可以移动、 旋转和缩放 但要完全维持它们的形状 在右侧的例子中 即使机器人看起来正在变形 实际上它所有的关节都在僵硬地移动 这也是一个刚体动画的例子
在一个典型场景中 大部分几何图形很可能只是 僵硬地移动 事实上大部分几何图形很可能 不发生移动
我们还可以在场景中多次复制 同一个物体 在加速结构中多次复制这些物体 消耗很大 仅仅因为几何图形的一部分 发生了移动 就再安置或重建整个加速结构 效率也很低
因此为了解决这两个问题 我们可以使用一种叫做 二级加速结构的东西
我们要做的就是首先 给场景中的每个唯一物体 创建一个高品质的三角加速结构 我们只需要在app启动时 做一次就可以
然后我们要使用第二加速结构 两次复制那些三角加速结构 每次复制 都是原始三角加速结构的一个实例
每个实例都与一个转换矩阵相关联 描述要把它放在场景中的哪个位置 我们使用两个缓冲区来实现 每个缓冲区都包含场景中 每个实例的一个输入
第一个缓冲区包含 所有实例的转换矩阵
第二个缓冲区包含 对三角加速结构数组的索引 描述每个实例使用哪个加速结构
然后我们要在场景中的实例上 创建第二加速结构
然后随着物体的移动 我们只需要快速重建实例的 加速结构即可 让我们看一下如何进行设置
首先我们要创建一个 AccelerationStructureGroup 实例的等级中的所有加速结构 都必须属于同一个群组 这就允许它们在内部分享资源
接下来我们要创建一个数组 容纳我们的三角加速结构 最后我们要循环场景中的 全部唯一物体 给每个唯一物体都创建一个 三角加速结构 并在结束时把它们添加到数组中
我们现在已经准备好创建 二级加速结构了 我们通过使用NPSInstance AccelerationStructure类实现 先附加我们的三角加速结构数组 以及我之前讲过的两个缓冲区 最后我们要指定场景中的实例的编号
然后无论何时当物体移动时 或场景中增加或移除一个物体时 我们可以使用实例加速结构 来进行重建
一般来说这种加速结构 比三角加速结构要小多了 我们可以给每一帧都执行这种操作 请注意 与再安置类似 当使用实例时有一些消耗 因此如果你的场景仅有一个物体 或只有少量物体 或特别是如果没有物体发生移动 可能值得把那些物体放到单个 三角加速结构中 这将增加你的内存占用 但也会回馈给你更好的性能 因此你需要进行试验来找到 对于你的app来说最好的方案
那么这就是动态场景 我们讲了如何使用再安置 支持顶点动画和蒙皮动画 以及如何使用二级加速结构 支持刚体动画 接下来我们谈谈降噪
目前我们所看到的所有图片 都没有噪声 那是因为它们都使用了降噪过滤器 如果我们把它关掉 我们可以看到如果没有降噪程序 看起来会是什么样子 我们可以看到这些图片如果用在 真正的app中太嘈杂了 那是因为我们仅对每个像素使用了 少量样本
通常我们会随时间 一起平均更多的样本来解决 但如果摄像头或物体正在移动 就没有那么简单了 幸运的是Metal现在包含 一个复杂的降噪过滤器 让我们看看它是如何运作的
理想情况是我们可以轻松地 使用渲染器输出噪声图像 通过降噪器运行它并返回一张 干净的图片 在实践中 降噪器需要关于场景的一些信息
我们从提供直接可见的 几何图形的深度和法线开始 许多渲染器周围都有这样的纹理 如果没有 那也很容易生成
然后降噪器将运行一堆图像处理操作 并输入一个较干净的图片 但因为我们的起点是每个像素仅有的 少量样本 因此最后得到的图片仍会有一些噪声 因此我们要重新查看在多个帧上 合并样本的想法
我们首先要留出干净的图片 从而在下一帧中重复使用
我们还要留出深度和法线 从而我们可以与下一帧中的 深度和法线进行对比
最后我们要提供一个动作矢量纹理 描述每个像素在帧之间移动了多少
在下一帧中 降噪器将穿过 所有纹理并产生一张更好的图片 这个图片将随时间变得更好 即使摄像头或物体移动了也一样
降噪器将使用深度和法线来检测 像素的历史记录是否 由于物体移动或物体被阻碍 而变得无效
这些都要使用MPSSVGF类家族 来实施
这是对很流行的 MPSSVGF降噪算法的实施 这个算法在高品质 和实时性能之间做出很好的权衡
因此降噪的整个过程 都由MPSSVGFDenoiser类调节
与此同时还使用MPSSVGF类 提供低级控制
这个类提供单一计算内核 供降噪器使用 并暴露许多参数 你可以用于调整 app中的降噪处理 你只需要直接调用这个类的方法 就可以创建一个自定义降噪器
降噪器 在降噪过程中 创建并销毁相当多的临时纹理 因此MPSSVGF纹理分配器协议 充当了这些内存分配的缓存
你可以使用默认实施 或自己实施这个协议 与你自己的app共享内存
一般来说 我们会对所有 Mac和iOS设备优化这些类
降噪器可以同时处理两张独立的图片 比如你可能想把直接和间接照明 分到不同的纹理中 还有单个通道纹理的快速路径 比如环境光遮蔽或阴影纹理 这比降噪一张完整的 RBG图片速度快
让我们看看该如何创建快速路径
首先我们要创建MPSSVGF物体 并配置其属性 我们所要提供的就是 用于降噪的Metal设备
接下来我们要创建 TextureAllocator 在这个例子中我们仅使用默认实施
最后我们要创建高级降噪器对象 管理降噪过程
现在我们已经准备好执行降噪了 我们先给降噪器附加全部的输入纹理 现在我们把整个降噪进程编码到 Metal commandBuffer中 最后我们可以从降噪器中取回 干净的图片
这就是给app启动降噪过程 所需要做的全部操作
目前我们讲了Metal中 用于光线追踪和降噪的所有基本知识 我们回顾了如何使用 MPS Ray Intersector API 执行基本的光线/三角交叉测试 然后我们讲了如何使用再安置 和二级加速结构 把这个测试扩展到动态场景中 最后我们讲了如何使用 MPSSVGF类 移除图片中的噪声
不要担心信息量太大 我们编写了一个样本 演示如何使用所有这些概念 这个样本在线可用
我之前提到过我们需要注意性能 特别是在实时情况下 因此接下来我要邀请我同事 Wayne上台 介绍如何在满足 实际性能预算的情况下 在实际设备上实现这些功能
大家好 在这部分的演讲中 我要讲的是如何使用 Metal中的光线追踪功能 在你的app中实施一些不同的 渲染技巧
特别是我主要讲软硬阴影、 环境光遮蔽和全局照明
先从硬阴影开始讲
我们用光线追踪 创建硬阴影模型的方式是 让表面上的点 朝着太阳的方向形成光线
如果某条光线击中了某个东西 那么相关联的点就处于阴影中 反之就处于太阳光下
要把硬阴影集成到现有app中 我假设你已经有了这样一个东西 你已经栅格化一个G-Buffer 并对照明运行了计算过程 它的输出就是你最终得到的 着色的图片
要利用光线追踪 在这里我们先获取G-Buffer 然后运行一个计算着色器 生成一些光线
然后我们把那些光线 传递给Metal 以一种加速结构进行交叉 Metal将把结果输出到一个 交叉缓冲区中
现在你可以在着色内核中使用 这个缓冲区了 用于决定表面的点是否处于阴影中
我想让你们重点关注的是光线生成 让我们快速回顾一下Metal中 是如何描述光线的
Metal提供一些不同的光线结构 至少包含光线原点 和光线方向的字段
你只需要给你想要追踪的每条光线 填充其中一种结构 并把这种结构写入光线缓冲区即可
你在光线缓冲区中安排光线的方式 对性能有影响
通常你可能像这样开始 我们以线性次序调用这行
这里的问题是随着Metal 以自己的方式处理这些光线 光线可能会击中 Metal用于加速光线遍历的 内部数据结构中不同的节点
这反过来可能会刷新底层硬件缓存
因此更好的方式是使用像素块 线性次序
因此来自屏幕上相邻像素的光线 可以击中加速结构的同一个部分 并像这样存储光线 让Metal更高效地驱动硬件
在这里的可视化中我要给你演示 尺寸为4乘4的像素块 在实践中我们发现8乘8尺寸 非常非常适合
因此优化光线存储 是改善性能的一个不错的方式 但如果可能的话 更好的方式是完全不发射光线
在阴影情境中 你想要这样做的原因可能是 并不是所有的像素都需要阴影光线 比如背景上的像素 天空盒上的像素 或背向太阳的表面上的像素
你的光线缓冲区很可能包含一种 光线结构 应用于屏幕上的每一个像素 我们需要的是一种方法 告诉Metal跳过我们毫不在乎的 像素的光线发射操作
有几种方法可以实现 我在这里要给你们展示的方式是 把光线结构中的 maxDistance字段 设为一个负值
这就是你需要了解的 关于硬阴影的主要信息 你可以看到 光线追踪能够产生非常好的效果 阴影非常清晰并且非常精确
但在现实中 阴影是由于太阳投射而产生 看起来不那么尖锐… 它们看起来可能更像是这样的效果 它们的边界比较柔和 并且柔和度与距离有关
你可以在左侧看到一个很棒的例子 灯柱的阴影在底座处是硬的 随着到地面距离的增加 阴影逐渐变柔和
要使用光线追踪 创建这个灯柱阴影的模型 我们不采用我刚才展示过的平行光线 而是延伸椎体 从表面点一直延伸到太阳
然后在这个椎体内随机生成一些 光线方向
现在你可以看到一些光线交叉 形成几何图形 而一些不交叉 就是这个比率控制着阴影的柔和度
看起来就像是这样
我在这里所展示的是 原生直接光照的概念 通过每个像素一条光线来追踪光线
在这个图片中 所有其它效果 比如反射和全局照明 都被禁用了 因此我们可以只关注阴影
你可以看到结果是非常非常嘈杂的 一张图片
为了解决这个问题 我们可以发射越来越多的光线 因为这是我们要在实时app中 很想避免的事 我们可以使用降噪器来替换这种方式 Sean之前给我们介绍过降噪器
这就是使用降噪器之后的效果
绝大多数噪声都被过滤掉了 我们可以获得看起来很不错的软阴影 每个像素只有一条光线
稍后我会在现场演示中为你实际演示 这个操作
现在让我们讲讲环境光遮蔽
从根本上说这是一个近似值 是关于有多少环境光可以到达 表面的近似值 正如你之前在我们的AR Quick Look 演示中所看到的那样 在环境中把物体放在地面上 是一个很不错的技巧
因此让我们使用光线追踪 把这个操作可视化
屏幕中间有一个表面点 在右侧有一个蓝块 它的作用是遮光板
我们在表面点周围定义一个 假想的半球 然后我们发射一些光线
如果某条光线击中了某些东西 我们就会发现那个物体阻挡了 环境光到达表面
正如我几分钟之前所提到的那样 在实时app中 我们真的很想避免把我们自己局限于 每个像素只有一条或两条光线
因此我们需要尽可能有效地使用 这些光线
其中一种方式就是重要性采样 这里的总体思路是以我们认为 最有利于最终图片效果的方向 发射光线
通过环境光遮蔽 最重要的光线就是距法线较近的光线
因此我们不像你在这里看到的那样 在半球均匀地发射光线 而是使用余弦采样
这在地平线周围分配了较少的光线 并在表面法线周围分配了较多的光线 这很棒 这正是我们所需要的光线分配
除角衰减之外 环境光遮蔽还有一个距离的概念 因此距离表面近的物体会遮挡 绝大部分光照
那里通常还存在一个脱落函数 与距离的平方成比例
现在有意思的是 我们可以把那个脱落函数直接放到 光线分配自身中
我们的实现方式是 发射不同长度的光线
你可以在这里看到 因为我告诉过你的那个 距离平方脱落函数 绝大多数光线都非常非常短 这对于性能来说很棒 短光线更易于 Metal在加速结构中进行追踪
我已经提过几次了 关于生成不同形状的光线 以及不同的分配 比如我们针对软阴影所使用的椎体 以及我们针对环境光遮蔽 所使用的半球
在实践中 我们先在2D参数空间生成点 然后我们通过你想使用的任何一种 光线分配来映射那个空间
现在参数空间中的那些点的位置 对图片品质有重大影响
如果你随机选择点 往往会出现采样点聚集在一起的区域
这就导致我们 发射方向基本相同的光线 而那些光线是多余的
你还会得到完全没有采样点的区域
这会影响图片品质 因为我们这些区域中的场景欠采样
生成采样点的一种更好的方式是 使用一种叫做低差异序列的东西 我正在屏幕上展示的这个 是Halton 2 3序列
你可以看到以这种方式生成的采样点 更均匀地覆盖了这个空间 并且我们可以通过放大和欠采样 来消除空隙
这就是对单个像素 生成良好光线的方式 现在我们需要做的就是把这种方式 应用到屏幕上所有的像素点上 从而使所有像素点都生成良好的光线
我们的实现方式是 获取我刚刚所展示的 其中一个低差异采样点 然后给每个像素应用 一个随机delta
所产生的效果是 每个像素仍以低差异序列运行 但采样点的具体位置发生了偏移 不是屏幕上相邻的像素
还有几种不同的方式可以生成 这些deltas 其中一种方式是采样一个完全是 随机数的RG纹理
但我们之前看到过 随机数并不是用于光线追踪的 好的选择 对于环境光遮蔽来说 有一个不错的替代方案 即蓝噪声
你可以在右侧看到 但蓝噪声纹理中的随机性 它的分布更加均匀 这对于图片品质来说很棒 特别是当我们局限于每个像素 只发射几条光线的情况下时
让我们看一下这些 对我们尝试要生成的 环境光遮蔽结果的影响
我们要从这儿开始 这是对全部像素使用半球采样 和随机deltas
这是使用我讲过的椎体采样 和蓝噪声所得到的结果
我可以在这两张图片之间切换 这样你就能看到其中的区别
现在这两张图片都是通过每个像素 仅发射两条光线而生成的
但你可以看到根据我们选择使用 那些光线的方式 噪声的数量很明显地有所下降 我们努力捕捉更多的表面细节
如果我们持续发射光线 最终这两种方法会产生 完全相同的结果 但使用重要性采样会让我们速度更快
那么这就是阴影和环境光遮蔽 对于这些效果 我们仅对光线 是否击中或是否错过某些东西感兴趣
对于我们通常与光线追踪相关联的 许多其它效果来说 比如全局照明 你需要随着光线在场景中的反弹 而创建光线模型 为了让你们了解更多相关信息 我要邀请我同事Matt上台来
(全局照明)
谢谢Wayne
在这部分中 我们要讲几个话题 先简要概述一下全局照明 然后看一些关于内存和光线管理的 最佳实践
最后讲 调试光线追踪app的一些策略
那么什么是全局照明?
从概念上说非常简单 光进入场景并直接照亮 它所击中的表面 并光栅化 这一般是渲染过程的结束 但在现实世界中 那些物体会吸收一些光 然后光线会弹开并继续在场景中穿梭 随着光线四处反弹会浮现一些 很有趣的视觉效果
当光反弹一次之后 我们就会在镜像表面上看到镜面反射 就像右边的球和墙一样
你还可以看到随着物体和阴影获取 由附近表面所反射的光 它们变得更明亮了
在光反弹两次之后 我们会在镜像表面之间看到反射 最终一些光线一直折射穿过透明物体 它们显示除了它们背后的表面 为我们提供了盒子的玻璃效果
如果我们尝试给场景周围所有的 光反弹都创建模型 只有一小部分光能反弹回摄像头 那样效率很低 所以我们要从反方向进行处理 从摄像头到光源
我们从摄像头向图片中的像素 投射光线
那些光线的交叉点会告诉我们 哪些物体可见 但我们还需要计算有多少光到达了 那些物体 以便了解它们在最终图片中的颜色
之前Wayne 描述了如何计算软阴影 在这里我们要实施完全相同的过程
我们从交叉点 向场景中的光源投射阴影光线 从而估计有多少光到达了光源
这将用作最终图片的光照效果
接下来我们从交叉点 向随机方向再次投射光线
我们使用Metal来了解那些光线 击中了什么 然后投射阴影光线以确定它们的 直接照明 然后使用直接光照给最终图片 添加光照 重复这个过程 我们可以模拟光在房间中的反弹 我们在去年的演讲中做了具体的描述 我推荐你参看去年的演讲获取如何 实施这个过程的具体信息
在这个过程中使用的管道看起来 与我们目前所见过的混合管道 稍微有点不一样
首先我们发射光线并使用Metal 找到它们在场景中的交叉点
然后我们编写一个着色器来处理 那些交叉测试的结果 从而了解我们击中了哪个表面
然后我们生成阴影光线 从那些交叉点 向场景中的光源生成阴影光线 我编写了一个着色器 用于了解哪些光线 击中了光源 然后把它们的光添加到 最终图片中
最后我们使用所击中的表面 作为下一组光线的发起点 我们一次又一次地重复这个过程 直到我们创建了我们想要创建的 大量光线反弹模型
这就是全局照明的运作方式
现在我们要讨论 为这个模型提供内存的一些最佳实践
因为任意光线都在场景中发生反弹 光线的状态随着 光与它所击中的物体的交互而改变 比如 如果光线击中了一种红色材料 那个红色表面会吸收除红光以外的 所有光 因此从那个表面上反射的二级光 将只包含红光
因此我们要持续追踪那个信息 以便把它传给管道的下一次迭代 那意味着我们得分配一些资源 用于持续追踪光线和场景属性
在右侧我列出了 你可能想要追踪的 场景属性的一些例子
要重定位所有这些新缓冲区 我们将占用大量内存 对于一张4K图片来说 仅光线缓冲区就有250MB 在我们的一个演示中 每条光线几乎占用了80字节 这种方式可以迅速超出可用的 GPU内存量
其中一种解决方案就是把光线分批 放到较小的群组或平铺中 通过限制你同时发射的光线的数量 你可以极大地减少资源的内存占用
因为这些缓冲区中的数据 将在管道迭代之间传递
把那个数据存在外面 然后再读取进来 在下一次迭代时 这将成为主要的受限因素
对于4K图片来说 我们大约要产生800万条光线 对于这个数字 每次迭代要读写大约5GB的数据
每个带宽问题都没有任何解决方案 但我们可以给你提供一些 我们觉得很好的最佳实践
第一 不要随机索引到数据缓冲区中
如果你按线程ID索引可能效率更高 从而编译器可以合并全部加载和存储 因为线程要访问的内存 位于相邻的缓冲区中
这真的会改善你的缓存一致性
下一个 对于不需要完全精确度的变量 尽可能使用较小的数据类型 如果可以的话 对于光线和场景以及材料属性 请试着用half替代 float数据类型
最后如果可能的话 请分离结构 以避免加载或存储你不会用到的数据 比如我们有一个结构包含材料属性 我们通过去除透明度变量得到了 很好的性能提升 因为并不是每条光线都会击中 透明的表面 我们不希望那些没有击中 透明表面的光线 为了读写透明度数据而付出代价 我们稍后会在GPU调试部分 看到一个更具体的例子
分配自己的缓冲区 存储起点和方向数据可能有违常规 但这样更有效率 而不是重复使用 Metal光线缓冲区结构
这是因为Metal光线缓冲区 可能包含你不希望对可能获取光线的 每一个着色器加载和存储的额外数据
为了最大化GPU的使用 你需要注意着色器占用 占用是一个很大的话题 我们不会在这里深入地介绍 但如果你存在占用问题 最简单的改善方式就是减少注册压力
请注意着色器中 同时活跃的变量数量 请注意循环计数器、函数调用 如果可以避免 请不要紧抓住完整结构不放
当我们处理光线交叉点时 我们需要评估 光线所击中的任何物体的表面属性 图形app一般会在纹理中 保存大量材料属性 这里的问题在于 因为着色器可能需要访问 物体所引用的任意纹理 我们不能提前了解光线会击中 哪个物体 潜在地 我们可能需要访问每一个 场景中的每一个纹理 这样很快就会脱离我们的掌控 如常用Sponza Atrium 场景有76个纹理 比可用的捆绑插槽的数量多两倍还多 因此我们很快就会用完全部的 捆绑位置
其中一种解决方式是使用 Metal参数缓冲区 Metal参数缓冲区代表一组资源 可以作为单个参数被集体分配到 着色器中
我们在两年前的WWDC做了一个 相关演讲 我推荐你参考那个演讲获取 更具体的信息
假设每个基元有一个纹理 参数缓冲区将是 包含对纹理的引用的一个结构
在这里我们创建了一个结构 叫做材料 它包含一个纹理引用 和我们想要访问的其它信息
接下来我们把参数缓冲区绑定到 计算内核 它将显示为材料结构的一个数组
我们从交叉缓冲区中读取它 从而了解光线击中了哪个基元
然后我们就使用那个索引 把基元索引到参数缓冲区中 这可以让我们访问 每个基元的唯一纹理
这就是我们今天要讲的 与内存相关的内容
现在我们要讨论管理光线的生命周期
随着光线在场景中的反弹 光线可能由于各种原因 不再影响最终的图片品质
第一 它可能完全离开了场景 与现实世界不同 你的场景占用的空间有限 如果光线退出了 就没办法再把它找回来了
如果发生那种情况 我们一般会估计一个环境贴图 来获取背景颜色 但那个光线实际上 已经从场景中消失了
第二 随着光线的反弹 它所携带的光 会被它与之相交互的表面衰减
如果光线丢失了足够多的光 它可能就不会 再对最终图片的品质产生 可衡量的影响了
最后对于透明表面 有一些光可能会被困在某些位置上 这样它们就永远都不能返回到摄像头
光线变得不活跃的速度有多快? 取决于场景类型 速度可能非常快
比如这个场景有个开放的世界 大量光线很快就会因为击中环境贴图 而迅速退出 在右侧我们展示的是 完全活跃的光线缓冲区 随着它在管道的第一次迭代中的 退出的一个简化表示 我们就是在这个步骤中从摄像头 向场景中投射光线
其中一些光线会击中环境贴图 并变得不活跃 在这里我们把不活跃的光线 表示为黄色 我们已经把它们从光线缓冲区中移除 在第一次迭代之后 只有57%的光线仍然活跃
我们让光线继续穿梭 最初击中地面的一些光线 发生反弹并击中环境贴图 现在余下的活跃光线降到了43%
其中有些光线一直穿过了透明物体 并最终退出了场景 我们的活跃光线只剩下三分之一了
当然了 我们迭代的次数越多 变得不活跃的光线越多
在这个例子中 我们知道光线缓冲区中的大量光线 都将变得不活跃 我们用于处理 这些不活跃的光线的时间是一种浪费 但因为我们不能提前了解到 哪些光线会变得不活跃 处理这个结果的 Metal Ray Intersector 以及全部着色器 仍要在全部光线上运行 那意味着我们得分配线程组内存 编译器可能会预取数据 我们可能要添加控制流语句 剔除不活跃的光线
我们的占用保持不变 但线程组的利用率却很低 我们浪费了所有的处理器容量
我们的解决方案是压缩光线缓冲区
对于每次迭代 我们仅向下一个 迭代的光线缓冲区中添加活跃光线 这增加了一些消耗 但却会提高缓存线和线程组的利用率 因此处理浪费少了 所需的带宽也少了
还有一个重点要注意 这个方案对于阴影光线也同样适用 有些光线会击中背光的表面 或可能会击中背景 因此我们不想缓存 这些光线的阴影光线
这个方法的缺点是 因为我们在光线缓冲区内 调整光线位置 我们的光线缓冲区中的索引 不再映射到恒定的像素位置上 因此我们需要分配一个缓冲区 用于沿着每条光线追踪像素坐标
虽然我们使用了额外缓冲区 但我们实际上所使用的内存变少了 如果我们把所有不需要处理的光线 都考虑进去的话
当我们压缩光线时 我们不希望两个着色器 都尝试向新光线缓冲区中的 同一个位置添加光线 因此我们需要在光线缓冲区中 为每一条在迭代之间保持活跃的光线 生产一个唯一索引
我们使用原子计数器来实现 在这里原子整数 outgoingRayCount 在新光线缓冲区中包含光线的 当前数量
我们使用atomic_fetch_add 明确地获取 流出光线的当前值并增加一
我们把这个值用作 对流出光线缓冲区的索引 以确保不会发生冲突 它还有一个额外的好处 即在流出光线计数中保留 仍然活跃的光线的数量
如果你不能限制你所启动的线程数 光线压缩也不会提供太多的帮助 我们刚生产的流出光线计数缓冲区 包含流出光线缓冲区中的 活跃光线的总数
我们可以用这个数来填充 MTLDispatch ThreadGroups IndirectArguments对象
那正好指明了 即将用于分派的启动维度
然后通过对indirectBuffer对象 使用IndirectDispatch 我们可以限制 我们要启动的线程的数量 使线程仅处理保持活跃的光线
还有一个与此相对应的 光线交叉函数
这里的重点是我们可以通过缓冲区 传递光线计数 从而我们可以把结果填充到 光线压缩步骤中 作为要启动的线程的数量
在压缩光线之后 在这个场景中我们获得了大约 15%的性能提升 但当然了 这个结果取决于 你所使用的场景的复杂程度 以及光线反弹的次数
那么这就是光线的生命周期 和光线的剔除
现在我们要讲使用Xcode 调试app
众所周知 在GPU上调试非常困难 特别是对于光线追踪来说尤其困难 每条光线可能会多次调用 你所做的任何修改 并且你可能需要在算法的不同阶段 编写大量代码来卸载缓冲区和纹理 从而了解哪里产生了报错 Xcode的帧捕捉工具 对于调试这些问题来说非常好用 它是个很强大的工具 并且可以节约很多时间
因此我要和你们一起调试 我们遇到的一个实际问题 当我们在光线追踪器中实施 超级采样时遇到了这个问题
我们对每帧都实施了对单个像素 多次采样的功能 然后突然 我们的光线追踪器开始 生产放大图片
第一步是在app运行时执行帧捕捉
这会记录在一帧的过程中 每个API调用 和着色器的GPU状态
通过选择任意着色器 我们可以检验绑定到它的资源 从而我们可以非常快速地减少 着色器漏掉的资源 通过选择写入帧缓冲区的 所有着色器来实现 并可以直接检验帧缓冲区的内容 在这里我们可以看到第一张图片很亮 第二张照片褪色很严重 第三张照片几乎是白色的
但在这里我们要选择输出 最亮的图片的着色器 并查看用于计算 帧缓冲区的两个输入缓冲区 第一个缓冲区仅包含 一条光线所积聚的光的总量
第二个缓冲区包含我们的新变量 也就是我们对指定像素的采样次数
这两个缓冲区看起来都包含有效数据 因此我们可以直接进入着色器调试器 来检验我们的着色器会如何处理 这个数据
我们的颜色计算只是 为指定像素发射的全部光线的亮度
当每个像素仅有一条光线时 这个计算没有任何问题 但现在我们无法补偿这一事实 即我们需要对每个像素进行多次采样
因此我们要在着色器调试器中 修改相应的代码 将总亮度除以输入样本数
我们直接在着色器调试器中进行 重新评估 我们可以立即看到我们的输出图像 已经被修正了 就是那么简单
(用Xcode进行性能调整) 我们频繁遇到的另一个问题是 我们尝试了解我们所做的修改 对性能产生的影响 Xcode帧捕捉工具 也把这个问题变简单了
这是一个在光线反弹时 追踪表面特征的结构的示例 在我们的场景中 并不是每个表面都使用透明表面 最终的两个值 透射和折射率 对于某些光线来说没有用 但因为我们把那种数据 整个打包到了单个结构中 没有击中透明表面的光线 仍需要在迭代中 读写那些字段
在这里我们把折射变量 重构到它们自己的结构中 仅在结构中保留击中透明表面的光线 这必须存在折射率数据
但我们仍可以做得更好 现在我们把变量的数据类型 修改为half 从而节约更多的空间 我们已经把内存占用从40字节 减少到了20字节 没有击中透明物体的光线 只需要12字节
我们该如何了解对性能所产生的 这种影响呢? 在这里我们使用帧捕捉工具 分别在修改前后抓取GPU追踪
我们可以做的最基本的性能分析 就发生在这个阶段 通过比较 修改前后的着色器计时 我们可以隔离性能发生变化的着色器 在这里我们可以看到 我们所标记为采样表面的着色器 计时从5.5毫秒降低到了4毫秒 这对于其中消耗较大的着色器来说 几乎节约了30%
如果我们想量化具体为何提升了性能 当Xcode执行帧捕捉时 它可以为我们提供 它所插入的所有性能计数器的结果 因为我们有兴趣了解我们如何影响 内存占用 我们可以看一下纹理单位统计 我们可以看到我们的纹理单位 平均暂停时间 已经从70%降到了54% 并且我们已经把L2吞吐量减少了 几乎三分之二
更有帮助的是Xcode 将自己做一些分析 并把潜在的问题报告给你 在这里它告诉我们说我们的初始版本 存在很大的内存问题 而新版本的性能好多了
还有一个技巧你可能会用得上 就是计算管道状态也有一些 有意思的遥测
请看 MaxTotalThreadsForThreadgroup 这是着色器占用情况的指示 你应该以1024为最大目标 小于1024说明可能存在占用问题 你需要修复
那么这就是Xcode中的调试 这使在Mac平台上开发光线追踪 和全局照明算法变得异常简单 现在Wayne会为大家现场演示 我们今天所讲的内容
谢谢Matt
你可能认出这个场景来了 来自我们平台本周稍早些时候的 《极限特工》演讲
为了在这里对它进行渲染 我要使用MacBook Pro 和四个外部GPU
你在屏幕上看到的一切都是 实时的光线追踪 我可以拿着摄像头在场景中四处移动
让我们从这里开始吧 你可以看到我们通过光线追踪 所获得的非常棒的阴影 它们在接触点上是硬阴影 并随着到地面的距离增加而变得 越来越柔和
请记住 对于这些阴影 我们仅对每个像素发射了一条光线 然后我们使用了 刚才Sean所讲过的降噪器 才最终得到了这个 非常棒的滤过的效果
这些都是动态地计算的 我可以四处移动灯
我可以立即看到这种效果
这里也有一个很棒的效果 如果我们飞过去从电车窗户向里看 你实际上可以看到我们影子的反光 再一次 你可以看到 影子跟着灯的移动而移动
如果我们现在前往场景的这部分 这里也有一个很棒的反射效果
如果我们看最左侧的电车 你可以看到后面有电车的倒影
但你还可以在我们后面的 电车的挡风玻璃上看到反光 因此这里的反光内又有反光 我们需要对每个像素模拟几次 光线反弹 才能达到那种效果
我要稍微把这里缩小一些 当然了在这个场景中并不是只有 摄像头能移动 灯也可以移动 Sean之前讲过Metal的 二级加速结构 我们在这里使用二级加速结构 使电车在场景中移动
然而我现在想给你展示的是 我们在房顶上实现的这个极好的 照明效果
如果我们注意右侧的墙壁 你可以看到目前 它主要由直接的太阳光点亮
但随着我控制太阳的旋转 你可以看到墙壁陷入了阴影中 现在它由这个非常棒的 非直接照明来点亮
这里所发生的就是 太阳光击中了左侧的屋顶 它反弹并照亮了右侧的墙壁 从而为我们提供了这个非常棒的 渗色效果
如果我继续旋转太阳 你可以逐渐看到 这些非常引人注目的阴影 它们穿过了屋顶的表面
如果我稍微旋转一下摄像头 你实际上可以看到 反射同样也击中了左侧的屋顶
因此这是——我真的很喜欢这个拍摄 同时在这里还有许多光线追踪的效果 我们有非直接照明 我们有阴影 我们有反射 这些都由Metal和多GPU进行 实时的光线追踪
现在我们要回到我们的演讲 我要稍微讲一下多GPU
对于我们刚才看到的演示 我们实施多GPU的方式 是通过把屏幕分成几个小的平铺 然后把这些小平铺映射到 不同的GPU上
在这个可视化中 我使用不同的颜色 来演示这些平铺是如何分配的 一个GPU渲染红色平铺 另一个渲染黄色平铺 以此类推
在所有GPU完成之后 我们只需要把结果合成到一起 从而构成我们的最终图片
如果我们后退一步 看看这里有什么 有两样东西很显眼
第一 在左侧的图片中 我们给GPU分配平铺的方式 看起来有点奇怪 我们为什么那样做呢? (交错平铺) 第二 对于那些小平铺 所隐含的是每个GPU将在那里 渲染一个像素块 然后再在别处渲染一个像素块 那感觉就像对光线一致性、 缓存击中率这样的东西不利一样 所有诸如此类的东西 让我们依次来处理这些问题 (负载平衡) 假如我们有四个GPU
执行多GPU的最简单的方式就是 把屏幕分为四块
现在问题是场景中的某些部分 可能比其它部分更容易渲染
如果我们假设左侧的街道和建筑物 比右侧的电车要更容易渲染
这就表明了为什么红色和黄色GPU 会比绿色和紫色GPU早完成的原因
我们可以通过把屏幕分成较小的平铺 来解决这个问题
然后我们可以把每个小平铺再分成 更小的平铺 以此类推 直到我们达到某个最小的平铺尺寸
这就产生了在GPU上 工作分配均匀的效果
如果屏幕的某一部分特别难以渲染 也没有关系 因为将从那部分屏幕中给每个GPU 分配平铺
在实践中你在这里看到的这个常规的 平铺模式 很可能不适用 因为你可能会遇到这种情况 平铺与场景中的几何图形对齐
因此我们稍微随机化一点儿 我马上会与大家分享一些 具体的实现细节 (负载平衡)
关于这个方法 其中一件最有意思的事情是 平铺到GPU的映射 并没有发生改变
同一个GPU将处理每帧中 同样的平铺
这很棒 你只需要在app初始化时 或你重新调整窗口尺寸时 计算那个映射即可 就这么简单 你再也不用考虑多GPU负载平衡了 因为没什么可监控的 在app的主循环中不需要重新计算
如果我们知道小平铺会更均匀地 分配工作 为什么不把它推到极端 让它成为一个像素呢?
那么问题是我们需要为 每个GPU提供 很好的连贯像素块
你需要在 均匀地平衡负载和确保每个GPU 都尽可能高效地运行之间做出权衡
为了更好的了解这个权衡 我们做了一个简单的实验 我们采用了一台新Mac Pro 以及一对Vega II Duo GPU 这样一共有四个GPU了 我们试着以各种平铺尺寸 渲染同一个场景 从而了解平铺尺寸如何影响性能
当然了 每个人状况可能不同 但我们发现性能窗口实际上很宽
因此如果平铺很小 或如果平铺非常非常大 效率都会降低
但中间的部分非常接近性能峰值
(平铺分配)
现在我们已经确定了平铺尺寸 我们接下来要做的是 把它们分配到不同的GPU上
为此 我们先给每个平铺 生成一个随机编号 然后把这些随机编号与一组临界值 进行比较
随机编号无论处于哪个范围内 该范围就为我们提供 该平铺的GPU使用
举个例子 如果平铺编号是.4 我们把它分配给GPU 1
如果编号是.55 我们把它分配给GPU 2 以此类推
一旦我们分配好每个平铺 所得到的输出就是 每个GPU需要渲染的平铺的列表
你可以在底部看到 我们为每个GPU所使用的范围 是相等的
当向GPU分配平铺时 它们被选中的几率是相等的
但在实践中 你当然不希望这样
比如你可能需要在其中一个GPU上 为非光线追踪任务保留容量 比如诊断或色调映射
或你可能使用具有不同性能的GPU 在那种情况下 你可能希望 给更强大的GPU分配更多的平铺
你可以非常轻松地解决这个问题 只需要调整范围即可
现在如果我们继续并重新分配 我们之前用过的同样的平铺 你可以看到GPU 2 现在分担了更多的工作
对于实际实施 在本周稍早些时候Pro app中 的Metal演讲中 有许多非常有用的信息 因此我就不再这里赘述了
但强调几个 对性能有很大影响的方面 是非常有用的
首先你很可能想 在驱动显示屏的GPU上 把平铺合成到一起
重要的是找到具体是哪个GPU 然后反向找到高效地把数据 放到该GPU上的方式
如果GPU属于同一个对等组 你可以使用我们的新对等组APIs 直接进行复制
否则你就需要通过CPU实现
其次 在GPU之间复制数据通常需要 数毫秒 我们一定不希望停滞并等待 完成数据迁移
这里有一个例子展示了 我们处理这个问题的方式 我们在这里有两个GPU 并且我们使用了我刚谈到的平铺机制 用于在两个GPU上展开渲染
在GPU 0中 在顶部有两个队列 其中一个只是简单地进行 背对背的光线追踪 然后第二个队列 异步地把已完成的平铺 复制到GPU 1上
现在我们假设底部的GPU 1 是驱动显示屏的那个GPU 这里的情况有点不同
这个GPU也分担了一部分 帧0的光线追踪 但我们不能继续并呈现那个帧 除非其余的平铺都已经 从其它GPU上复制过来了
因此与其等待 我们不如开始处理下一帧
稍后当我们收到来自其它GPU的 全部平铺之后 我们就可以继续 并把一切合成在一起了
我再给你们看一次
你可以看到我们最终处于这种 稳定状态 我们正在渲染帧N 然后我们合成了帧N减一
因此从根本上说 我们正在这里实现的是延迟隐藏
这与我刚展示给你们的平铺一起 在GPU之间负载平衡 这就在多GPU系统上针对光线追踪 为我们提供了非常棒的性能
就这样了 我们的演讲结束了
我们开始先快速回顾了光线追踪 是如何在Metal中运作的
然后我们介绍了 MPSRayIntersector的几个功能 可以帮助我们处理动态场景 也就是二级加速结构 以及由GPU加速的重建和再安置
我们还介绍了新Metal降噪器 然后我们讲了几个光线追踪的用例 比如阴影、环境光遮蔽和全局照明
然后演示了如何使用Xcode 调试并分析 光线追踪 最后我们又稍微提了一下 如何在你的光线追踪app中 利用多GPU 要获取更多信息 请访问 developer.apple.com 你还可以在网站上找到一些新样本 演示我们今天所讲到的功能
如果你不熟悉光线追踪 请一定要参看我们去年的演讲 最后 接下来中午12点 有我们的实验室活动 希望大家能加入我们
非常感谢你们的参与 稍后在实验室见
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。