案例研究 - 在混合现实中创建星系

在 Microsoft HoloLens 发布之前,我们询问了我们的开发人员社区,他们希望经验丰富的内部团队为新设备生成什么样的应用程序。 开发人员分享了 5000 多个想法,经过 24 小时的 Twitter 投票,名为星系探索者的想法最终入选。

该项目的艺术主管 Andy Zibits 和团队的图形工程师 Karim Luccin 讨论了艺术与工程之间的协作工作,最终在星系探索者中创建了银河系的准确交互呈现。

技术

我们的团队 - 由两名设计师、三名开发人员、四名艺术家、一名制作人和一名测试人员组成 - 用六周时间生成了一个功能齐全的应用,让人们可以了解和探索浩瀚无垠的银河系的美景。

我们希望充分利用 HoloLens 的功能直接在生活空间中渲染 3D 对象,因此我们决定创建一个逼真的星系,让人们能够将其放大,看到每颗恒星在其自身上的轨道上运行。

在开发的第一周,我们为银河系的呈现提出了几个目标:需要有深度、运动和立体感 — 填满了可帮助我们创建银河系形状的恒星。

创建包括数十亿颗恒星的星系动画的难度在于,在每一帧中,需要更新的单种元素数量对于 HoloLens 而言太大,无法使用 CPU 进行动画处理。 我们的解决方案涉及到艺术和科学的复杂组合。

幕后

为了使人们能够探索每颗恒星,我们的第一步是确定一次性可以渲染多少个粒子。

渲染粒子

当前的 CPU 能够很好地同时处理串行任务以及少量的并行任务(取决于 CPU 的核心数),但 GPU 在并行处理数千个操作方面要高效得多。 但是,由于 GPU 通常不与 CPU 共享相同的内存,因此在 CPU 与 GPU 之间交换数据很快就会成为瓶颈。 我们的解决方案是在 GPU 上制作一个星系,它必须完全驻留于 GPU 上。

我们开始对数千个各种模式的点粒子进行压力测试。 这样,我们就可以将星系放到 HoloLens 上,看看哪些粒子是有效的,哪些是无效的。

创建恒星的位置

我们的一名团队成员已经编写了可以在初始位置生成恒星的 C# 代码。 恒星呈椭圆排布,其位置可以用 (curveOffset, ellipseSize, elevation) 来描述,其中 curveOffset 是呈椭圆排布的恒星,ellipseSize 是椭圆的 X 和 Z 轴维度,elevation 是恒星在星系中的正确仰角。 因此,我们可以创建一个使用每个恒星属性初始化的缓冲区(Unity 的 ComputeBuffer)并将其发送到 GPU 上,在余下的体验中,该缓冲区将一直驻留在 GPU 上。 为了绘制此缓冲区,我们使用了 Unity 的 DrawProcedural,这样,就可以在任意一组点上运行着色器(GPU 上的代码),而无需创建实际的网格来表示星系:

CPU

GraphicsDrawProcedural(MeshTopology.Points, starCount, 1);

GPU

v2g vert (uint index : SV_VertexID)
{

 // _Stars is the buffer we created that contains the initial state of the system
 StarDescriptor star = _Stars[index];
 …

}

我们从包含数千个粒子的原始圆形模式开始。 这可以证明,我们能够管理大量的粒子并以高效的速度运行,但我们对星系的整体形状并不满意。 为了改善形状,我们尝试了各种可旋转的模式和粒子系统。 这些系统最初让我们看到了希望,因为粒子数量和性能保持一致,但形状在中心附近分解,而恒星向外发射能量,这造成了不真实感。 我们需要一个发射组件,使我们能够操控时间并使粒子逼真地移动,越来越靠近银河系的中心。

We attempted various patterns and particle systems that rotated, like these.

我们尝试了各种可旋转的模式和粒子系统,如图中所示。

我们的团队对星系的运转方式做了一些研究,并专门为星系定制了一个粒子系统,以便可以根据“密度波理论”在椭圆上移动粒子。该理论认为星系的旋臂是密度较高但不断变化的区域,就像交通拥堵时的情况一样。 星系看似稳固,但当恒星沿其各自的椭圆运动时,它们实际上是在进入和离开旋臂。 在我们的系统中,粒子从来不是驻留在 CPU 上 — 我们生成了卡并将它们全部定位在 GPU 上,因此整个系统只是初始状态 + 时间。 系统推进工作如下:

Progression of particle system with GPU rendering

支持 GPU 渲染的粒子系统的推进

添加足够多的椭圆并将其设置为旋转后,星系就开始形成“旋臂”,汇集的恒星在其中运动。 恒星在每条椭圆路径上的间距被赋予了一定的随机性,为每颗恒星添加了一点位置随机性。 这使恒星运动和旋臂形状的外观分布要自然得多。 最后,我们添加了根据与中心的距离驱动颜色的功能。

建立恒星运动

若要动画显示一般的恒星运动,我们需要为每一帧添加一个恒定角,并使恒星以恒定的径向速度沿其椭圆运动。 这就是使用 curveOffset 的主要原因。 这在技术上是不正确的,因为恒星在沿着椭圆的长侧运动时速度更快,不过,总体运动感觉良好。

Stars move faster on the long arc, slower on the edges.

恒星在长弧上运动速度更快,在边上运动速度更慢。

这样,每颗恒星都由 (curveOffset, ellipseSize, elevation, Age) 完整描述,其中 Age是自加载场景以来经过的总累积时间

float3 ComputeStarPosition(StarDescriptor star)
{

  float curveOffset = star.curveOffset + Age;
  
  // this will be coded as a “sincos” on the hardware which will compute both sides
  float x = cos(curveOffset) * star.xRadii;
  float z = sin(curveOffset) * star.zRadii;
   
  return float3(x, star.elevation, z);
  
}

因此,我们能够在启动应用程序时生成数万颗恒星,然后沿着既定曲线对单独的一组恒星进行动画处理。 由于所有内容都在 GPU 上,因此系统可以在不占用 CPU 的情况下并行动画处理所有恒星。

Here’s what it looks like when drawing white quads.

这是绘制白色四边形时的情形。

为了使每个四边形朝向相机,我们使用了几何着色器将每个恒星位置转换为屏幕上包含恒星纹理的 2D 矩形。

Diamonds instead of quads.

菱形而不是四边形。

由于我们想尽可能地限制过度绘制(处理像素的次数),因此我们旋转了四边形以减少它们的重叠。

添加云团

可通过多种方法获得粒子的立体感 — 从光线在立体内部行进,到绘制尽可能多的粒子来模拟云团。 渲染实时光线的行进过于消耗资源且难以创作,因此我们首先尝试使用一种在游戏中渲染森林的方法生成了一个模仿的系统 — 将大量树木的 2D 图像朝向相机。 当我们在游戏中这样做时,可以从一部旋转的相机渲染树木的纹理,保存所有这些图像,并在运行时为每个标牌选择与视图方向匹配的图像。 如果图像是全息影像,则做不到这一点。 左眼和右眼之间的差异使得我们需要更高的分辨率,否则图像看起来会显得平坦、混叠或重复。

在第二次尝试中,我们尝试使用了尽可能多的粒子。 在将粒子添加到场景之前我们以加法方式绘制了粒子并将其模糊化,从而获得了最佳视觉效果。 这种方法的典型问题涉及到我们一次可以绘制多少粒子,以及在保持 60fps 的同时这些粒子可以覆盖多大的屏幕面积。 模糊化生成的图像以获得这种云团感觉通常是一项开销极高的操作。

Without texture, this is what the clouds would look like with 2% opacity.

这是在没有纹理、不透明度为 2% 的情况下的云团效果。

使用加法并绘制大量云团意味着将有多个四边形相互叠加,并反复着色同一像素。 在星系中心,同一个像素有数百个相互重叠的四边形,在全屏显示时,这会产生巨大的开销。

制作全屏云团并试图将其模糊化不是一个好主意,因此我们决定让硬件为我们完成这项任务。

首先提供一点上下文

在游戏中使用纹理时,纹理大小极少与我们想要使用它的区域相匹配,但我们可以使用不同类型的纹理过滤,让显卡从纹理的像素中内插所需的颜色(纹理过滤)。 我们感兴趣的过滤是双线性过滤,它使用 4 个最近的邻居计算任一像素的值。

Original before filtering

Result after filtering

使用此属性时,我们发现,每次尝试将纹理绘制成面积的两倍时,纹理就会变得模糊。

我们没有全屏渲染并失去我们可以花费在其他事情上的宝贵时间,而是在一块微小的屏幕上进行渲染。 然后,通过复制此纹理并以 2 为倍数将它拉伸几次,我们在模糊化内容的同时回到全屏。

x3 upscale back to full resolution.

3 倍放大后回到全分辨率。

这样,我们能够以原始开销的一小部分获得了云团部分。 我们没有按全分辨率添加云团,而是只绘制 1/64 的像素,然后将纹理拉伸回到全分辨率。

Left, with an upscale from 1/8th to full resolution; and right, with 3 upscale using power of 2.

左侧将纹理从 1/8 分辨率放大为全分辨率;右侧以 2 次方将纹理放大 3 倍。

请注意,尝试一次从 1/64 尺寸放大为全尺寸的结果看起来完全不同,因为显卡在我们的设置中仍会使用 4 个像素来着色更大的区域,并开始出现伪像。

然后,如果我们使用较小的卡添加全分辨率恒星,则会得到整个星系:

Near final result of galaxy rendering using full resolution stars

在获得形状的正确轨迹后,我们添加了一个云层,用我们在 Photoshop 中绘制的点替换了临时点,并添加了一些额外的颜色。 成果是让我们的艺术和工程团队都很满意的银河系,它满足了我们在深度、立体和运动方面的目标 — 所有这些工作都没有给 CPU 造成负担。

Our final Milky Way Galaxy in 3D.

最终的 3D 银河系。

进一步探索

我们已将星系探索者应用的代码开源并已在 GitHub 上提供,开发人员可以其于这些代码生成自己的应用。

想要更详细地了解星系探索者的开发过程? 请在 Microsoft HoloLens YouTube 频道上查看我们以往项目的所有最新信息。

关于作者

Picture of Karim Luccin at his desk Karim Luccin 是一名软件工程师,也是一名科幻视觉效果爱好者。 他曾经担任星系探索者的图形工程师。
Photo of art lead Andy Zibits Andy Zibits 是一名艺术主管和太空爱好者,他曾经管理星系探索者的 3D 建模团队,并为更多粒子渲染付出了大量努力

另请参阅