在现代浏览器中重现3D飞行模拟器是一项令人惊叹的成就。当考虑到所有的3D效果都是手工完成的,是通过精确的计算和低级别的像素命令实现的,这就更加令人印象深刻了。当Bruce Atwick开始着手早期版本的飞行模拟器时,不仅没有3D框架,甚至没有任何框架!那些版本的游戏主要是用汇编语言编写的,仅仅比CPU中流动的0和1高一个级别。
当开始为Web重新构想飞行模拟器(或者称之为飞行街机)并展示在新的Microsoft Edge浏览器和EdgeHTML渲染引擎中可能实现的功能时,不禁想到了过去和现在创建3D的对比——旧的飞行模拟器,新的飞行模拟器,旧的Internet Explorer,新的Microsoft Edge。现代编码几乎是一种奢侈,因为使用像Babylon.js这样的伟大框架在WebGL中雕刻3D世界,让专注于非常高级别的问题。在本文中,将分享解决这些有趣挑战的方法之一:一种简单的创建逼真外观的大规模地形的方法。
大多数3D对象是使用建模工具创建的,这是有充分理由的。在代码中创建复杂的对象(比如飞机甚至建筑物)是非常困难的。建模工具几乎总是有意义的,但也有例外!其中一个可能就是像飞行街机岛的起伏丘陵这样的情况。最终使用了一种发现更简单甚至可能更直观的技术:高度图。
高度图是一种使用常规二维图像描述像岛屿或其他地形这样的表面的高程起伏的方法。这是一种在游戏以及制图师和地质学家使用的地理信息系统(GIS)中处理高程数据的常见方法。
要了解这是如何工作的,请查看下面的交互式高度图。尝试在图像编辑器中绘制,然后查看生成的地形。
高度图的概念相当简单。在上面的图像中,纯黑色是“地板”,纯白色是最高的山峰。中间的灰度颜色代表相应的高程。这为游戏提供了256个高程级别,细节已经足够了。实际应用可能会使用全色谱来存储更多的细节级别(如果包括alpha通道,256^4=4,294,967,296个细节级别)。
高度图比传统的多边形网格有几个优点:首先,高度图更加紧凑。只有最重要的数据(高程)被存储。它需要被程序化地转换成3D对象,但这是经典的权衡:现在节省空间,以后用计算来支付。通过将数据存储为图像,得到了另一个空间优势:可以利用标准图像压缩技术,使数据变得非常小(相比之下)!
其次,高度图是生成、可视化和编辑地形的便捷方式。当看到它时,它非常直观。它感觉有点像看地图。这在飞行街机中特别有用。在Photoshop中设计和编辑岛屿!这使得进行小的调整变得非常简单。例如,当想要确保跑道完全平坦时,只需要确保用单一颜色覆盖那个区域。
可以看到下面的飞行街机的高度图。看看是否能发现为跑道和村庄创建的“平坦”区域。
飞行街机岛的高度图。它是在Photoshop中创建的,基于著名的太平洋岛屿链中的“大岛”。有猜到的吗?
解码高度图
使用Babylon.js构建了飞行街机,Babylon为提供了从高度图到3D的相当直接的路径。Babylon提供了一个API,可以从高度图图像生成网格几何体:
var mesh = BABYLON.MeshBuilder.CreatePlane("heightMapPlane", {height: 200, width: 200}, scene);
var terrain = BABYLON.TerrainBuilder.CreateTerrain("terrain", heightMap, {width: 200, height: 200, subdivisions: 200}, scene);
细节的数量由细分属性决定。需要注意的是,该参数指的是高度图图像每边的细分数量,而不是总的单元格数量。所以稍微增加这个数字可能会对网格中的顶点总数产生很大的影响。
20细分=400单元格 50细分=2,500单元格 100细分=10,000单元格 500细分=250,000单元格 1000细分=1,000,000单元格
在下一节中,将学习如何对地面进行纹理处理,但在尝试创建高度图时,看到线框图是很有用的。以下是应用简单线框纹理的代码,这样可以很容易地看到高度图数据是如何转换成网格的顶点的:
var wireframeMaterial = new BABYLON.WireFrameMaterial("wireframe", scene);
terrain.material = wireframeMaterial;
创建纹理细节
一旦有了模型,映射纹理就相对简单了。对于飞行街机,只是创建了一个与高度图中的岛屿相匹配的非常大的图像。该图像被拉伸到地形的轮廓上,所以纹理和高度图保持相关。这非常容易可视化,而且所有的生产工作都是在Photoshop中完成的。
原始纹理图像的创建尺寸为4096x4096。那相当大!(最终将尺寸降低了一个级别到2048x2048,以保持下载合理,但所有的开发都是用全尺寸图像完成的)。下面是原始纹理的全像素样本。
原始岛屿纹理的全像素样本。整个城镇大约只有300像素见方。
那些矩形代表岛上城镇的建筑物。很快注意到了地形和其他3D模型之间可以实现的纹理细节水平的差异。即使有巨大的岛屿纹理,差异也非常明显!
为了解决这个问题,“混合”了额外的细节到地形纹理中,以随机噪声的形式。可以看到前后的变化。注意额外的噪声如何增强地形的细节外观。
创建了一个自定义着色器来添加噪声。着色器让对WebGL3D场景的渲染有惊人的控制力,这是一个着色器如何有用的很好的例子。
WebGL着色器由两个主要部分组成:顶点和片段着色器。顶点着色器的主要目标是将顶点映射到渲染帧的位置。片段(或像素)着色器控制像素的结果颜色。
着色器是用一种名为GLSL(图形库着色器语言)的高级语言编写的,它类似于C。这段代码在GPU上执行。要深入了解着色器的工作原理,请参阅这个教程,了解如何为Babylon.js创建自己的自定义着色器。
顶点着色器
没有改变纹理是如何映射到地面网格上的,所以顶点着色器相当简单。它只是计算标准映射并分配目标位置。
片段着色器
片段着色器有点复杂。它结合了两张不同的图像:基础和混合图像。基础图像映射在整个地面网格上。在飞行街机中,这是岛屿的颜色图像。混合图像是用于在近距离给地面一些纹理和细节的小噪声图像。着色器结合了每张图像的值,在整个岛屿上创建了一个组合纹理。
飞行街机的最后一课发生在雾天,所以像素着色器的另一个任务是根据顶点离相机的距离调整颜色以模拟雾。调整是基于顶点离相机的距离,远处的像素被雾“遮挡”得更厉害。将在上面的calcFogFactor函数中看到这种距离计算。
自定义混合着色器的最后部分是Babylon使用的JavaScript代码。这段代码的主要目的是准备传递给顶点和像素着色器的参数。
Babylon.js使得创建基于着色器的材料变得容易。混合材料相对简单,但当飞机低空飞行时,它确实在岛屿的外观上产生了巨大的变化。着色器将GPU的强大功能带入浏览器,扩展了可以应用到3D场景的创意效果类型。在例子中,这是最后的润色!
更多关于JavaScript的实践
微软有很多关于许多开源JavaScript主题的免费学习,正在努力用Microsoft Edge创建更多。以下是一些值得一看的:
Microsoft Edge Web峰会2015(新浏览器、新Web平台功能和社区嘉宾演讲的完整系列)
构建//BUILD/和Windows 10(包括新JavaScript引擎用于网站和应用程序)
在不破坏Web的情况下推进JavaScript(Christian Heilmann最近的主旨演讲)
托管Web应用程序和Web平台创新(关于manifold.JS等主题的深入探讨)
实用的性能技巧,让HTML/JavaScript更快(从响应式设计到休闲游戏到性能优化的7部分系列)
现代Web平台JumpStart(HTML、CSS和JS的基础知识)