.wd-author__links {display: flex;}
我们的目标是将影院的丰富性与浏览器的技术能力相结合,打造一种有趣且沉浸式的体验,让用户能够与之建立强烈的联系。
这项工作太过繁杂,无法在本文中完整呈现,因此我们深入研究,从中抽出一些我们认为有趣的技术故事章节。在此过程中,我们提炼出了一些难度逐渐递增的专题教程。
但这类技术体验的魅力就在于其融合性。这也是主要挑战之一:如何在一个场景中将视觉效果和交互元素融合在一起,以打造一个一致的整体?这种视觉复杂性难以管理,让我们很难确定自己在开发的哪个阶段。
为了解决视觉效果和优化之间相互关联的问题,我们大量使用了控制台,该控制台会捕获我们当时正在审核的所有相关设置。用户可以在浏览器中实时调整场景的任何内容,从亮度到景深、伽玛等… 任何人都可以尝试调整体验中重要参数的值,并参与发现最佳效果的活动。
我们不会在此处介绍所有设置,但建议您进行实验:这些按键会在不同场景中显示不同的设置。在最后的暴风序列中,有一个额外的按键:Ctrl-A,您可以使用它来切换动画播放和四处飞行。在此场景中,如果您按 Esc(退出鼠标锁定功能),然后再次按 Ctrl-I,则可以访问特定于暴风场景的设置。环顾四周,拍摄一些像下面这样美丽的明信片风景。
在许多经典的迪士尼电影和动画中,场景的制作意味着将不同的图层组合在一起。其中包含多层实景、单格动画,甚至实体布景,以及在玻璃上绘制而成的顶层:一种称为“Matte-painting”的技术。
在许多方面,我们打造的体验结构都很相似;即使某些“层”远远不止是静态视觉效果。事实上,它们会根据更复杂的计算来影响物体的外观。不过,至少在宏观层面上,我们要处理的是堆叠在一起的视图。顶部显示的是界面层,下方是 3D 场景:该场景本身由不同的场景组件组成。
我们在界面中采用了一种有趣的优化技术,即将许多界面叠加图片合并到一个 PNG 中,以减少服务器请求。在此项目中,界面由 70 多张图片(不包括 3D 纹理)组成,这些图片会预先加载,以缩短网站的延迟时间。您可以在此处查看实时精灵图片表:
以下是一些提示,介绍了我们如何充分利用精灵贴图,以及如何将其用于视网膜设备,并使界面尽可能清晰整洁。
创建精灵贴图后,您应该会看到一个如下所示的 JSON 文件:
其中:
请注意,我们使用高密度图片创建了精灵贴片,然后只需将其调整为原始大小的一半,即可创建正常版本。
现在,一切就绪,我们只需一个 JavaScript 代码段即可使用它。
具体使用方法如下:
环境体验是在 WebGL 图层上设置的。在构思 3D 场景时,最棘手的问题之一是如何确保您可以创作出在建模、动画和特效方面能够最大限度发挥表现潜力的内容。在许多方面,这个问题的核心是内容流水线:为 3D 场景创建内容时遵循的约定流程。
我们希望打造一个令人惊叹的世界,因此需要一个可让 3D 艺术家创作出如此世界的实用流程。我们需要尽可能让他们在 3D 建模和动画软件中拥有最大的表现自由度;而我们则需要通过代码在屏幕上渲染这些内容。
我们已经在解决此类问题上花费了一些时间,因为过去每次创建 3D 网站时,我们都会发现可用的工具存在限制。因此,我们创建了一款名为 3D Librarian 的工具,这是一次内部研究。它已经可以应用于实际工作了。
此工具的历史悠久:它最初是用于 Flash 的,可让您将大型 Maya 场景作为一个经过优化以便在运行时解压缩的压缩文件导入。之所以是最佳选择,是因为它能够有效地将场景打包到与渲染和动画期间处理的数据结构基本相同的数据结构中。加载时,对文件进行的解析工作非常少。由于文件采用的是 Flash 可以原生解压缩的 AMF 格式,因此在 Flash 中解压缩非常快。在 WebGL 中使用相同的格式需要在 CPU 上执行更多工作。事实上,我们不得不重新创建一个数据解压缩 JavaScript 代码层,该层实际上会解压缩这些文件,并重新创建 WebGL 正常运行所需的数据结构。解压缩整个 3D 场景是一项 CPU 负载较轻的操作:在中高端机器上,解压缩 Find Your Way To Oz 中的场景 1 需要大约 2 秒。因此,此操作是在“场景设置”(实际启动场景之前)时使用 Web Worker 技术完成的,以免用户体验卡顿。
此实用工具可以导入大多数 3D 场景:模型、纹理、骨骼动画。您可以创建一个库文件,然后 3D 引擎可以加载该文件。您将场景中所需的所有模型塞入此库中,然后,瞧,它们就会生成在场景中。
不过,我们遇到了一个问题,那就是我们现在要处理 WebGL:这个新手。这是一个非常棘手的项目:它为基于浏览器的 3D 体验设定了标准。因此,我们创建了一个临时 JavaScript 层,该层会接受 3D 图书馆压缩的 3D 场景文件,并将其正确转换为 WebGL 能够理解的格式。
“Find Your Way To Oz” 中反复出现的一个主题是风。故事情节的线索结构为风的逐渐加大。
狂欢节的第一个场景相对平静。在浏览各种场景时,用户会体验到风力逐渐增强,最终达到暴风的场景。
因此,提供身临其境的强风效果至关重要。
为了实现这一点,我们在 3 个嘉年华场景中添加了柔软的物体,这些物体应该会受到风的影响,例如帐篷、旗帜、照片打印亭的表面和气球本身。
如今,桌面游戏通常以核心物理引擎为基础构建。因此,当需要在 3D 世界中模拟柔软对象时,系统会为其运行完整的物理模拟,从而创建逼真的柔软行为。
在 WebGL / JavaScript 中,我们还无法运行完整的物理模拟。因此,在《绿野仙踪》中,我们必须想办法在不实际模拟风的情况下营造风的效果。
我们在 3D 模型本身中嵌入了每个对象的“风敏感度”信息。3D 模型的每个顶点都有一个“风属性”,用于指定该顶点应受到风的影响程度。因此,这是 3D 对象的指定风敏感度。然后,我们需要创建风本身。
我们将在一个简单的“程序化草地”中创建风。
我们先创建场景。我们将创建一个简单的带纹理的平坦地形。然后,每根草都会用倒置的 3D 圆锥来表示。
initGrass 和 initTerrain 函数调用分别使用草地和地形填充场景:
在这里,我们将创建一个 15 x 15 个草地位元的网格。我们会为每个草地位置添加一些随机性,以免它们像士兵一样排列整齐,看起来很奇怪。
此地形只是一个水平平面,放置在草块的底部 (y = 2.5)。
目前没有什么特别的。
现在,我们开始添加风。首先,我们要将风敏感度信息嵌入到草地 3D 模型中。
我们将为草地 3D 模型的每个顶点嵌入此信息作为自定义属性。我们将使用以下规则:草模型的底端(圆锥的顶端)的灵敏度为零,因为它固定在地面上。草模型的顶部(圆锥底部)对风的敏感度最高,因为它离地面最远。
下面展示了如何重新编码 instanceGrass 函数,以将风敏感度添加为草地 3D 模型的自定义属性。
我们现在使用自定义材质 windMaterial,而不是之前使用的 MeshPhongMaterial。WindMaterial 封装了我们稍后将要看到的 WindMeshShader。
因此,instanceGrass 中的代码会循环遍历草地模型的所有顶点,并为每个顶点添加一个名为 windFactor 的自定义顶点属性。对于草模型的底端(应与地形接触的位置),此 windFactor 设为 0;对于草模型的顶端,此 windFactor 设为 1。
我们需要的另一个元素是向场景中添加实际的风。如前所述,我们将使用 Perlin 噪声来实现此目的。我们将程序化生成 Perlin 噪声纹理。
为方便起见,我们将此纹理分配给地形本身,而不是之前的绿色纹理。这样,您就可以更轻松地了解风的变化。
因此,此 Perlin 噪声纹理将在空间上覆盖地形的延伸部分,并且纹理的每个像素都将指定该像素所处地形区域的风速。地形矩形将成为我们的“风区”。
我们将使用此着色器将 Perlin 噪声渲染到纹理。这在 initNoiseShader 函数中完成。
如前所述,我们现在还将此纹理用作地形的主要渲染纹理。这对于风效本身的运作而言并不是必需的。不过,有这样的图表很不错,因为我们可以直观地了解风力发电的情况。
下面是经过改进的 initTerrain 函数,它使用 noiseMap 作为纹理:
现在,我们已经完成了风纹纹理,接下来我们来看看 WindMeshShader,它负责根据风来变形草地模型。
我们不会在此处复制整个着色器代码(您可以随时在源代码文件中查看该代码),因为其中大部分代码都是 MeshPhongMaterial 着色器的复制版本。不过,我们先来看看顶点着色器中经过修改的与风相关的部分。
因此,此着色器的操作是先根据顶点的 2D xz(水平)位置计算 windUV 纹理查找坐标。此 UV 坐标用于从 Perlin 噪声风纹理中查找风力 vWindForce。
此 vWindForce 值会与特定于顶点的 windFactor(上文中所述的自定义属性)合成,以计算顶点需要的变形量。我们还提供了一个全局 windScale 参数来控制风的总体强度,以及一个 windDirection 矢量,用于指定风变形需要朝向哪个方向。
这样,我们的草就会根据风向发生形变。不过,我们还没有完成。目前,这种变形是静态的,无法传达大风区域的效果。
为此,我们会将传递给 NoiseShader 的 vOffset 均匀值随时间推移。这是一个 vec2 参数,可让我们指定沿特定方向(风向)的噪声偏移。
我们可以在每个帧调用的 render 函数中执行此操作:
这样就大功告成了!我们刚刚创建了一个场景,其中包含受风影响的“程序化草地”。
现在,让我们为场景增添一些趣味。我们来添加一些飞舞的尘埃,让场景更有趣。
毕竟,灰尘应该会受到风的影响,因此在风景场景中让灰尘四处飞舞是完全合理的。
在 initDust 函数中,将尘埃设置为粒子系统。
此时,系统会创建 130 个尘埃粒子。请注意,每个粒子都配备了特殊的 WindParticleShader。
现在,我们将在每一帧中使用 CoffeeScript 让粒子稍微移动一下,不受风的影响。代码如下。
此外,我们还将根据风向偏移每个粒子的位置。这在 WindParticleShader 中完成。具体而言,是在顶点着色器中。
此顶点着色器与我们用于基于风的草地变形的着色器没有太大区别。它将 Perlin 噪声纹理作为输入,并根据尘埃世界位置在噪声纹理中查找 vWindForce 值。然后,它会使用此值来修改尘埃粒子的位移。
我们的 WebGL 场景中最具冒险精神的可能就是最后一个场景,您可以点击气球进入龙卷风的中心,在网站上完成旅程,并观看即将发布的独家视频。
在制作此场景时,我们就知道需要为用户提供一项具有强大影响力的核心体验。旋转的龙卷风将作为中心元素,其他内容层将塑造此元素,以产生戏剧性效果。为此,我们围绕这个奇怪的着色器构建了一个类似于电影制片厂的场景。
我们采用了混合方法来制作逼真的合成图像。有些是视觉特效,例如用光形状来制作镜头光晕效果,或者在您正在观看的场景上方以图层形式呈现动画效果的雨滴。在其他情况下,我们绘制了平面,使其看起来在移动,例如根据粒子系统代码移动的低空云层。而围绕龙卷风旋转的碎片则是 3D 场景中的各个层,这些层会按顺序在龙卷风前后移动。
我们之所以必须以这种方式构建场景,主要是为了确保我们有足够的 GPU 来处理龙卷风着色器,并与我们应用的其他效果保持平衡。最初,我们遇到了严重的 GPU 平衡问题,但后来此场景经过优化,比主要场景更轻量。
为了制作最终的暴风序列,我们结合使用了许多不同的技术,但这项工作的核心是看起来像龙卷风的自定义 GLSL 着色器。我们尝试了许多不同的技术,从顶点着色器(用于创建有趣的几何漩涡)到基于粒子的动画,甚至扭曲几何形状的 3D 动画。这些特效似乎都无法重现龙卷风的感觉,或者需要过多的处理。
我们发现脑细胞内部有点像龙卷风的漏斗。由于我们使用的是体积渲染技术,因此我们知道可以从空间中的所有方向查看此着色器。我们可以将着色器的渲染设置为与风暴场景相结合,尤其是在夹在云层之间且位于壮观背景上方时。
着色器技术涉及一种技巧,该技巧基本上使用单个 GLSL 着色器通过一种简化的渲染算法(称为“使用距离场进行光线漫游”渲染)渲染整个对象。在此技术中,系统会创建一个像素着色器,用于估算屏幕上每个点与表面的最短距离。
着色器的核心从 main 函数开始,该函数会设置相机转换并进入一个循环,该循环会反复评估与表面的距离。调用 RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) 是核心光线漫游计算发生的位置。
基本思想是,随着我们深入到龙卷风的形状中,我们会定期向像素的最终颜色值添加颜色贡献,以及向光线沿着不透明度贡献。这样可以为龙卷风的纹理营造出层次分明的柔软质感。
我们先创建一个与场景宽度和高度匹配的 renderTarget,以便使龙卷风着色器的分辨率与场景无关。然后,我们会根据获得的帧速率动态确定风暴着色器的分辨率下采样。
下一步优化需要深入了解算法。着色器中的驱动计算因素是对每个像素执行的迭代次数,用于尝试近似计算表面函数的距离:光线追踪循环的迭代次数。使用更大的步长,我们可以在云层外部通过更少的迭代次数估算龙卷风表面。在室内时,我们会减小步长以提高精度,并能够混合值以产生雾状效果。此外,创建一个边界圆柱以获取投射光线的深度估算值也带来了显著的速度提升。
问题的下一部分是确保此着色器能够在不同的显卡上运行。我们每次都会进行一些测试,并开始直观地了解我们可能会遇到的兼容性问题类型。之所以效果不如直觉,是因为我们无法总是获得有关错误的良好调试信息。典型场景只是 GPU 错误,没有其他问题,甚至系统崩溃!
跨视频板兼容性问题的解决方法类似:确保输入的静态常量为定义的精确数据类型,例如:0.0 表示浮点数,0 表示整数。编写较长函数时请务必小心;最好将内容拆分为多个更简单的函数和临时变量,因为编译器似乎无法正确处理某些情况。确保纹理都是 2 的幂,并且不太大。在循环中查找纹理数据时,请务必“谨慎”。
我们在兼容性方面遇到的最大问题是暴风的光效。我们使用了缠绕在龙卷风周围的预制纹理,以便为其漩涡着色。这是一个非常漂亮的效果,可以轻松将龙卷风融入场景颜色,但在尝试在其他平台上运行时花了好长时间。
移动版体验无法直接照搬桌面版,因为技术和处理要求太高。我们必须打造一款专门针对移动用户的新产品。
我们认为,将桌面版嘉年华打印照片亭作为移动 Web 应用(可使用用户的移动设备相机)会很酷。这是我们之前从未见过的。
在撰写本文时,我们认为有必要为您提供一些关于如何顺利进行移动开发流程的提示。下面是这些信息!快来看看您可以从中学习到什么!
引导加载程序是必需的,而不是应避免的。我们知道,有时会出现后一种情况。这主要是因为随着项目的发展,您需要不断维护预加载内容的列表。更糟糕的是,如果您同时拉取多个不同的资源,则不太清楚应如何计算加载进度。这时,我们自定义的非常通用的抽象类“Task”就派上用场了。其主要思想是允许无限嵌套的结构,其中任务可以有自己的子任务,子任务可以有自己的子任务,以此类推。此外,每个任务都会根据其子任务的进度(而非父任务的进度)计算自己的进度。将所有 MainPreloadTask、AssetPreloadTask 和 TemplatePreFetchTask 派生自 Task,我们创建了一个如下所示的结构:
您可以在此处查看实时演示(在 iPhone 或 Android 手机上运行):
其余功能在各个平台上运行都很顺畅。乐在其中!
鉴于“Find Your Way To Oz”的庞大规模以及涉及的各种不同技术,本文仅介绍了我们采用的部分方法。