本篇教程转载于:https://www.cntofu.com/book/71/chapter-17-outdoor-scene.md
3D游戏中的天空有很多实现方法,总的来说,皆是障眼法。最简单的手法不过是改变画面的背景色,让玩家“感觉”到天色的变化。
如果在玩家头顶上放置一个平面,再把云朵、太阳、星星、月亮等图片“贴”上去,就可以混合成类似下面的效果。
这种技术称为**“天空面(SkyPlane)”**。
显然,这种方法是很容易露馅的。当玩家的视野足够远时,就会发现天空的“边缘”。
稍微改进一下这个障眼法,可以试图让“天空面”始终遮挡在摄像机的前方,或者在远处用高山挡住玩家的视线,这样玩家就没有机会看到边缘。
比天空面更精妙一些的障眼法,当属环境贴图:用一个立方体或球体把场景包裹起来,然后使用360°无死角的贴图,让场景中的玩家看不到边缘。
使用立方体环境贴图时,这种技术也被称为“天空盒(SkyBox)”。使用球体环境贴图时,则叫**“天空穹(SkyDome)”**。
这种障眼法的破绽依然很明显。第一,环境贴图是静止的,这于我们的常识不符;其二,一切看起来都是扁平的,而云应该有体积。
对于第一个破绽,可以使用多层环境贴图。保持背景图层静止,再设法让云层、太阳、月亮运动起来。在夜晚,可以让整个星空贴图缓慢转动,用来模拟时间流逝。
对于第二个破绽,有很多方法可以让云/雾看起来更加真实。可以为云层增加法线贴图,可以用粒子特效,可以用3D云模型,也可以利用特殊的着色器。
上面介绍了天空的多种制作方法,并不局限于某种引擎,是3D游戏中通用的手段。这些方法在jME3中都可以实现。
调节画面背景色是最简单的办法,调用 viewPort.setBackgroundColor(new ColorRGBA(0.6f, 0.7f, 0.9f, 1)); 即可。天空面(SkyPlane)也很容易,用一个纸片加载贴图即可。
jME3提供了 com.jme3.util.SkyFactory 类,使开发者可以对环境贴图提供了完整的支持。你可以通过SkyFactory来加载环境贴图,生成天空盒或天空穹。
至于动态云层,无非是在场景中添加多个Spatial,或者使用特殊的着色器来控制云层和光照。诸如体积云、体积光、光晕、彩虹等画面效果,实则是通过着色器完成的,与jME3无关。如果你希望实现这方面的效果,需要另外学习着色器编程。
在jME3中,SkyFactory是使用最频繁的天空工具类。它支持三种不同的环境贴图:
对于这三种不同类型的环境贴图,SkyFactory 使用枚举类型 SkyFactory.EnvMapType 来表示
public enum EnvMapType{
CubeMap,
SphereMap,
EquirectMap
}
举个例子,假设要使用立方体贴图来创建天空盒,用法是这样的:
@Override
public void simpleInitApp() {
Spatial sky = SkyFactory.createSky(assetManager,
"Textures/Sky/Bright/BrightSky.dds", // 贴图路径
SkyFactory.EnvMapType.CubeMap);// 贴图类型
rootNode.attachChild(sky);
}
调用 createSky() 方法时,SkyFactory 会完成下列工作
这个Spatial就是我们所需的天空盒,把它添加到场景图中就可以看到天空。在SkyFactory的内部,它还调用了下列方法:
SkyFactory中提供了多个重载的 createSky() 方法,其中之一是使用加载好的 Texture 来代替图片路径。
createSky(AssetManager, Texture, EnvMapType);
有时我们在程序中加载未知来源的环境贴图,可能会出现上下颠倒、左右颠倒等情况。为此,SkyFactory中提供了createSky的重载方法,可以使用一个 Vector3f 变量来对 X/Y/Z轴进行翻转。
如何制作环境贴图呢?
球体映射(Sphere Mapping 是基于这样一个事实:将一个理想高反射的球体置于场景中央,从一个角度无穷远处拍摄此球体,将得到一张全景图。例如:
圣彼得大教堂
平原
通常在场景建模中,朝向z轴正方向,利用正交投影模拟无穷远处进行渲染,就可以得到这个纹理图。
public void simpleInitApp() {
// 天球,圣彼得大教堂
Texture texture = assetManager.loadTexture("Textures/Sky/St Peters/StPeters.hdr");
Spatial sky = SkyFactory.createSky(assetManager, texture,
new Vector3f(1, -1, 1), // 图片上下颠倒,故改变Y方向的法线
SkyFactory.EnvMapType.SphereMap);
rootNode.attachChild(sky);
}
效果
SphereMap出现的时间比较早,它有一个很大的破绽,就在摄像机的背后。对于那些制作得不够精细的贴图,边缘汇聚在一起的痕迹会非常重。
立方体贴图的做法比较简单:把摄像机置于场景中央,朝着x,-x,y,-y,z,-z方向将场景渲染出6张纹理。然后用6张纹理组成一个立方体的6个面。这样一个真正的全景图组成了。
于那些制作得不够好的CubeMap,破绽在于面与面的接缝处。这6个面的排列顺序是一个很有趣的问题,在OpenGL和Dirext3D中,加载同样的天空盒会出现上下颠倒的情况。你可以从这篇文章了解更多内容:OpenGL和D3D中Cubemap的图象方向问题
实际加载CubeMap时,有两种截然不同的方式。不过这两种方式只是图片格式不同,效果并没有什么区别。
Texture * 6: 比较常见的情况是,CubeMap并不是一张贴图,而是由6张贴图组成。有时甚至只有5个面,因为可能不需要底部的贴图。
public void simpleInitApp() {
Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg");
Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg");
Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg");
Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg");
Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg");
Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg");
Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
rootNode.attachChild(sky);
}
TextureCubeMap: 另一种情况是使用特殊格式,在一个文件里同时保存了6个图层,用一个贴图文件来代表整个立方体贴图,常见于微软的dds格式。
public void simpleInitApp() {
TextureKey key = new TextureKey("Textures/Sky/Bright/BrightSky.dds", true);
key.setGenerateMips(false);
key.setTextureTypeHint(Texture.Type.CubeMap);
Texture tex = assetManager.loadTexture(key);
// 天空盒
Spatial sky = SkyFactory.createSky(assetManager, tex, SkyFactory.EnvMapType.CubeMap);
rootNode.attachChild(sky);
}
等距矩形投影(Equirectangle Projection),又称球面投影、方格投影、等距柱状投影等。假想球面与圆筒面相切于赤道,赤道为没有变形的线。经纬线网格,同一般正轴圆柱投影,经纬线投影成两组相互垂直的平行直线。
其特性是:保持经距和纬距相等,经纬线成正方形网格;沿经线方向无长度变形;角度和面积等变形线与纬线平行,变形值由赤道向高纬逐渐增大。该投影适合于低纬地区制图 。
实际应用中,最常见的就是地图。
还有全景实景展示:
public void simpleInitApp() {
// 天空盒
Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/SkyEquirectMap.jpg", SkyFactory.EnvMapType.EquirectMap);
}
效果:
这种贴图的破绽在于头顶和脚底,即纬度最高处。那些制作得不够精细的贴图,在这两个点会有明显的“汇聚感”。