上周,我在 10 天内为 Newgrounds 主办的游戏果酱制作了一款名为 Death's Clutch 的游戏。制作它很有趣,因为它涉及许多自定义渲染,例如制作以 3D 移动的 DOOM 风格的 2D 广告牌,或者破解 PlayCanvas 的材质系统以获得风格化的照明。我希望这篇“它是如何制作的”文章对寻求风格化 PlayCanvas 游戏的其他人有用,我很好奇是否有读者知道实现这些效果的更好方法(特别是想与其他引擎/工作流程进行比较)。我想记录这个过程的另一个原因是提醒自己美丽的东西是如何制作的——一层一层。当您滚动浏览此部分时,您将看到添加到下面实时演示中的每种效果及其说明。来回滚动以切换效果并查看其影响。我们将从一个基本的 PlayCanvas 场景开始,然后从那里构建恐怖游戏的美感。这个演示是互动的!单击右下角的锁定图标以启用轨道控制。用鼠标滚轮或捏合缩放。通过单击并拖动或触摸并拖动来旋转)。我们做的第一件事是在相机上安装聚光灯。每个游戏资产都作为应用于平面的 2D 纹理添加。接下来,我们使这些平面始终旋转以面向相机。一些物体,如附着在墙上的鸡蛋,旋转偏离其原始角度不超过 30 度。
我们还应用了不透明纹理,使它们看起来不再像在平面上绘制的图像。接下来,我们通过将其颜色设置为黑色来关闭环境光。这将所有不在聚光灯范围内的东西完全变黑。尝试放大和缩小使光线离墙壁和物体更近或更远。即使将环境光设置为黑色,场景仍然不会像预期的那样在灯光范围之外的所有地方都漆黑一片。这是因为相机的默认透明颜色不是黑色。所以任何间隙/空白空间都会有这种深灰色。这里默认照明的一个大问题是,当光线直接照射到它时,所有东西看起来都褪色了。当光线直射墙壁或大怪物时,这一点最为明显。发生这种情况是因为我们使用了基于物理的光照模型,但我们的材质都是这些简单的 2D 图像,只有漫反射颜色。预期的结果是这些对象应始终使用其原始颜色进行渲染,而不管有多少光线到达它们。这通常是通过创建一个“未点亮”的材料来完成的。但我们也希望它们在没有光线到达的情况下不可见。
为了实现这一点,我们创建了一个自定义材质,它的行为类似于标准的基于物理的材质,但只是根据接收到的光量在原始颜色和黑色之间进行插值。这消除了我们不想要的任何逼真的照明效果,如反射和镜面反射。接下来,我们将发光纹理应用于每个对象,以定义哪些区域应该在黑暗中发光,例如墙壁上的灯光或怪物的眼睛。这有助于玩家在灯光关闭的情况下也能看到一点,并且它创造了一种非常酷的效果,在完整的怪物出现之前,您只能看到眼睛在黑暗中发光。尝试将光线移近和移开鸡蛋,看看它在黑暗中的发光效果。使用自发光纹理比向所有这些对象添加灯光要高效得多。无论是否有光线到达,发光区域都会表现得好像有更多的光到达它们一样。这确实产生了一个问题,即使在很长的黑暗走廊的尽头,玩家也可以看到发光的物体,这破坏了惊喜。为了通过自发光解决这个问题并限制玩家可以看到发光物体的距离,我们添加了雾。
雾会根据它们与相机的距离使颜色变暗,无论它们是否自发光。重要的是聚光灯的最大范围小于雾让您看到的最大范围。这种差异使得发光的眼睛在接近你时,在完整的怪物出现之前会出现片刻。迷雾对于制造悬念也很重要。玩家在黑暗中移动,不知道前方是什么,这让关卡感觉比实际更大。创建的最具挑战性的效果是这种对光线有反应但主要保持原始图像颜色的自定义材质。 PlayCanvas 确实提供了创建自定义材质的方法,请参阅 BasicMaterial,您可以在其中创建完全自定义的着色器。任何材质还具有“customFragmentShader”属性,可以覆盖现有材质的着色器。如果您想保留所有现有的输入(例如附加的纹理),您需要使用全新的材质重新设置这些输入,这很好。我们的问题是我们需要保留标准材质的所有复杂功能(因此它可能会受到雾、自发光和我们将来添加的任何效果的影响),但只需调整它计算最终颜色的方式。我们不能只是复制它并使用 customFragmentShader,因为着色器是在运行时生成的。例如,它会根据场景中的灯光数量或雾是否处于活动状态而变化。这是 PlayCanvas 源代码中的一个示例,其中仅在使用雾时才在运行时注入雾的着色器代码。我最终在最终生成的着色器上进行了字符串替换以对其进行自定义。这非常脆弱,因为:
我不得不等待编译正确版本的着色器。当应用程序首次加载时,某些对象可能具有纯白色材料,直到加载纹理。在着色器的第一个版本中,我要替换的代码不存在。当您使用 customFragmentShader 替换着色器时,它会将您的代码插入到现有着色器定义的常量/统一等之下。但是我正在复制的着色器已经包含所有这些常量!所以我需要弄清楚这些定义在哪一行结束,所以我不会复制它们(这只是在运行时知道)。更好的解决方案是制作 StandardMaterial 类的副本并对其进行调整,但对我而言,如果不修改引擎会变得多么容易,我并不明显。一个更好的解决方案是我在写这篇文章时才了解到的,是传入一个自定义块对象来覆盖任何默认的材料块。这样你就可以调整照明系统的任何部分,而不必重写整个材质,也不必担心它如何与所有其他功能交互。这是我第一次使用 PlayCanvas 完成的游戏,总体上给我留下了深刻的印象!能够直接在浏览器中与我的队友协作真是太好了。他们以前从未使用过编辑器,但很容易为他们设置一个沙箱,艺术家可以在其中调整材料设置和位置,音乐家可以看到所有位置音效和背景音乐层次在他处理它们时听起来如何,而无需我每次他们想在游戏中看到他们的更新时更新游戏的版本。所以它感觉以这种方式赋予了力量。在 PlayCanvas 中设置空间音频是多么容易给我留下了深刻的印象,特别是考虑到这是这款游戏的重要组成部分(能够在黑暗中听到怪物的脚步声)。你只是检查一个“位置?”框上您的声音组件,你就完成了!我们遇到了一个 Firefox 错误,从图像中读取像素返回的颜色与原始颜色略有不同。这是一个大问题,因为我们通过为单个像素着色来创建关卡,其中绿色表示玩家生成位置,黑色表示墙壁等。解决方案是直接在 JavaScript 中解码 PNG 图像,而不是依赖浏览器对其进行解码.
我们遇到了在 Newgrounds 上托管游戏时天空盒消失的问题。这是因为它缺少天空盒 (DDS) 的 mime 类型。解决方案是强制 PlayCanvas 将其加载为 PNG 图像。请参阅此处的解决方案。我使用 TexturePacker 为动画创建精灵表。要将其与 PlayCanvas 一起使用,您需要此自定义插件。我希望你觉得这很有用!我很想听听你的想法。在 Twitter 上找到我 @Omar4ur 或在 omarshehata.me 上查看更多联系我的方式。