我最近尝试在PlayCanvas中按程序生成的地形上使用平淡的阴影处理游戏卡纸。 这并不像我预期的那么简单,我最终花了很多时间试图弄清楚它,而不是制作游戏。 我想如果您遇到同样的情况,我会写这份快速指南作为参考。
目标是创建类似于PlayCanvas主页中的内容。
![](http://mdm.miximages.com/Programming/1m28ez3ky39stOSUOGBZq1A.png)
这种“低聚”外观营造出我想要的复古感觉。 通常,您可以通过导出带有平面阴影的模型来执行此操作。 在Blender中,您只需选择模型并在工具面板中切换平坦/平滑阴影即可。
![](http://mdm.miximages.com/Programming/1vLRrWOTe4oBessUm6XTFxA.png)
问题是当您尝试对过程生成的模型(例如terrain)执行此操作时。 在下图中,左侧是我遵循PlayCanvas教程有关地形生成的内容,右侧是我想要的。
![](http://mdm.miximages.com/Programming/1PS1dhoH3czhWXrKzTMvDpQ.png)
生成的几何图形本身不平滑,看起来像上面图像的右半部分。 左半部分看起来很光滑的原因是由于现代图形技巧在顶点之间的法线被插值 。
如果我们采取一个平面并像这样弯曲它:
![](http://mdm.miximages.com/Programming/1uK5GqRCO4UEX_c3iQ_8KZQ.gif)
沿表面的真实法线如下所示:
![](http://mdm.miximages.com/Programming/1ADslETBh2QMrTLUlfOfcQg.gif)
如果要使该表面看起来更平滑,则可以创建更多的细分,但是渲染成本很高。 取而代之的是,您可以只在每个像素处插入法线,以便在应用照明时看起来很平滑。
![](http://mdm.miximages.com/Programming/1IgMdIBoyC8YX84Q9-u956A.gif)
图6显示了所有法线都指向真实法线的情况,以及法线在顶点的法线之间逐渐插值的情况。 这种插值不是微妙的效果。 在Blender中,这将是此模型的两个版本之间的差异:
![](http://mdm.miximages.com/Programming/1BHtMFXtw7bgBmFaXIwA_lA.jpg)
插值法线是硬件为我们处理的方便技巧。 GLSL文档提到了一个flat
关键字,可以关闭此插值,但是WebGL不支持此关键字。
我们要做的就是简单地复制顶点。 实际上,它是如此“简单”,以至于我找不到关于复制顶点意味着什么或为什么固定任何东西的任何解释。 仅当您知道如何存储几何并将其发送到GPU时,这才是直截了当的,但是这些细节通常隐藏在高级框架中,因此我将在此处进行说明。
让我们看一下如何渲染这样的平面:
![](http://mdm.miximages.com/Programming/1CSFi0gIkaDu9d_Wt2vEXOw.png)
我们知道所有对象的构造块都是三角形,因此需要以某种方式将此平面分解为三角形。 为了向GPU传达有关绘制哪些三角形的信息,WebGL使用buffers 。 您可以在概念上将缓冲区视为简单的平面数组。
我们在这里关注两个缓冲区: 顶点缓冲区和索引缓冲区 。
顶点缓冲区是坐标列表。 对于上面的飞机,它可能看起来像这样:
vertexBuffer = [0,5,0,5,5,0,5,0,0,0,0,0]
每个三联体是一个点的坐标。 此数组中有12个数字,分别对应于飞机的4个点。
顶点缓冲区中的点的顺序无关紧要,因为索引缓冲区指定哪些三元组组成一个三角形。 它可能看起来像这样:
indexBuffer = [0,1,3,1,2,3]
这表示第一个三角形由第0,第1和第3点组成。 另一个三角形由第一,第二和第三点组成。
请注意,indexBuffer中的1
并不引用vertexBuffer[1]
。 它指的是第一个三元组,它将是三个数字5,5,0
。
请注意,这两个三角形之间如何有两个共享的顶点。 索引1
和3
出现两次。
请记住,法线是在顶点之间插值的。 在这种特殊的平面情况下,顶点处的所有法线均指向摄影机,因此即使进行插值,它们也都指向同一方向。 让我们看看当我们的表面不平坦时会发生什么。
![](http://mdm.miximages.com/Programming/1qx1RUWyfvhTHLj9XK6RxTg.png)
很明显,法线0
和2
应当指向何处(与它们所连接的三角形的法线方向相同),但是顶点1
和3
呢? 它们属于两个不同的三角形,其法线指向不同的方向。 处理共享顶点的方式是,它们的法线是它们所连接的三角形的法线的平均值。 因此,在这种情况下,位于1
和3
的法线直接指向相机(就像图6中部的法线一样)。
这就是问题所在。 考虑三角形0,1,3。 0处的法线指向左侧。 1和3处的法线直线向上。 三角形上的任何像素将在其间具有插值法线,这将为您提供平滑的阴影。
为了避免这种情况,我们必须以某种方式重组我们的数据,以便不共享任何顶点。 这样,每个顶点的法线将匹配其所属的三角形的法线。 插值仍然会发生,但是在等值法线之间插值与完全不插值相同。
如果我们不想要任何共享的顶点,则需要创建副本,以便每个三角形都可以拥有自己的顶点。 具体来说,我们希望有两个顶点1
和两个顶点3
以便每个三角形都能得到一个。 我们的新几何将如下所示:
![](http://mdm.miximages.com/Programming/1LBNhalegzP7sd4mxVoDHeQ.png)
现在我们的两个三角形分别是0,1,4和2,3,5。 这使我们总共有6个顶点,每个三角形获得3个唯一点。 我们的缓冲区将重新组织如下:
之前:
vertexBuffer = [0,5,0,5,5,0,5,0,0,0,0,0]
indexBuffer = [0,1,3,1,2,3]
后:
vertexBuffer = [0,5,0,5,5,0,5,5,0,5,0,0,0,0,0,0,0,0]
indexBuffer = [0,1,4,2,3,5]
因此,“复制顶点”意味着复制所有共享顶点,以使每个三角形具有3个唯一点。 必须始终为真的不变式是,顶点(索引缓冲区中的实际元素)的顶点数(顶点缓冲区中的三元组)应该一样多。 换一种说法:
indexBuffer.length ==(vertexBuffer.length / 3)
您可以在该项目中看到一个实际的代码示例。 特别是, terrain.js
第120行定义了一个用于复制所有必要顶点的函数。
如果您觉得本教程很有用,我很想听听您的工作/如何发现它! 您可以在Twitter上找到我(我也很高兴听到一两次鼓掌)!