天空

本篇教程资料请点击这里下载

本篇教程转载于:https://www.cntofu.com/book/71/chapter-17-outdoor-scene.md

3D游戏中的天空有很多实现方法,总的来说,皆是障眼法。最简单的手法不过是改变画面的背景色,让玩家“感觉”到天色的变化。

如果在玩家头顶上放置一个平面,再把云朵、太阳、星星、月亮等图片“贴”上去,就可以混合成类似下面的效果。

image.png

这种技术称为**“天空面(SkyPlane)”**。

显然,这种方法是很容易露馅的。当玩家的视野足够远时,就会发现天空的“边缘”。

image.png

稍微改进一下这个障眼法,可以试图让“天空面”始终遮挡在摄像机的前方,或者在远处用高山挡住玩家的视线,这样玩家就没有机会看到边缘。

image.png

image.png

环境贴图

比天空面更精妙一些的障眼法,当属环境贴图:用一个立方体或球体把场景包裹起来,然后使用360°无死角的贴图,让场景中的玩家看不到边缘。

使用立方体环境贴图时,这种技术也被称为“天空盒(SkyBox)”。使用球体环境贴图时,则叫**“天空穹(SkyDome)”**。

这种障眼法的破绽依然很明显。第一,环境贴图是静止的,这于我们的常识不符;其二,一切看起来都是扁平的,而云应该有体积。

对于第一个破绽,可以使用多层环境贴图。保持背景图层静止,再设法让云层、太阳、月亮运动起来。在夜晚,可以让整个星空贴图缓慢转动,用来模拟时间流逝。

对于第二个破绽,有很多方法可以让云/雾看起来更加真实。可以为云层增加法线贴图,可以用粒子特效,可以用3D云模型,也可以利用特殊的着色器。

JME3制作天空的方式

上面介绍了天空的多种制作方法,并不局限于某种引擎,是3D游戏中通用的手段。这些方法在jME3中都可以实现。

调节画面背景色是最简单的办法,调用 viewPort.setBackgroundColor(new ColorRGBA(0.6f, 0.7f, 0.9f, 1)); 即可。天空面(SkyPlane)也很容易,用一个纸片加载贴图即可。

jME3提供了 com.jme3.util.SkyFactory 类,使开发者可以对环境贴图提供了完整的支持。你可以通过SkyFactory来加载环境贴图,生成天空盒或天空穹。

至于动态云层,无非是在场景中添加多个Spatial,或者使用特殊的着色器来控制云层和光照。诸如体积云、体积光、光晕、彩虹等画面效果,实则是通过着色器完成的,与jME3无关。如果你希望实现这方面的效果,需要另外学习着色器编程。

SkyFactory

在jME3中,SkyFactory是使用最频繁的天空工具类。它支持三种不同的环境贴图:

  • 球体贴图(Sphere Map)
  • 立方体贴图(Cube Map)
  • 等距矩形贴图(Equirectangle Map)

对于这三种不同类型的环境贴图,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 会完成下列工作

  • 根据纹理贴图的路径,使用 assetManager 去加载纹理;
  • 根据 EvnMapType ,加载对应类型的材质,使着色器可以正确渲染环境贴图。
  • 生成网格和几何体,返回一个 Spatial 对象。

这个Spatial就是我们所需的天空盒,把它添加到场景图中就可以看到天空。在SkyFactory的内部,它还调用了下列方法:

  • sky.setQueueBucket(Bucket.Sky); 设置天空盒的渲染顺序,确保它比场景中的所有物体都先绘制,这样它就会出现在画面的最底层。
  • sky.setCullHint(Spatial.CullHint.Never); 确保天空盒在渲染时不会被剔除。天空盒本身也是场景图中的一个模型。对于一般的3D物体来说,如果没有出现在摄像机的视锥中,就不会被绘制到画面上。但是对于天空而言,它应该始终都存在于画面中。
  • SkyFactory 内部使用的是 Common/MatDefs/Misc/Sky.j3md 材质。它是专门用于天空的着色器,可以对3种不同环境贴图进行渲染。

SkyFactory中提供了多个重载的 createSky() 方法,其中之一是使用加载好的 Texture 来代替图片路径。

createSky(AssetManager, Texture, EnvMapType);

有时我们在程序中加载未知来源的环境贴图,可能会出现上下颠倒、左右颠倒等情况。为此,SkyFactory中提供了createSky的重载方法,可以使用一个 Vector3f 变量来对 X/Y/Z轴进行翻转。

如何制作环境贴图呢?

  • 环境贴图应该使用专业工具来制作,比如Terragen、Bycle等3D景观生成器,或者Blender、3DS Max等3D建模工具。
  • 环境贴图的尺寸不是很重要,因为实际在游戏中,天空会被渲染到无穷远处。贴图的分辨率越高,天空看起来越清晰,游戏运行越慢。
  • 实际在开发中,可以使用Node来容纳多个不同的天空图层,用来制作更加复杂的效果。

SphereMap

球体映射(Sphere Mapping 是基于这样一个事实:将一个理想高反射的球体置于场景中央,从一个角度无穷远处拍摄此球体,将得到一张全景图。例如:

圣彼得大教堂

image.png

平原

image.png

通常在场景建模中,朝向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);
}

效果

image.png

image.png

SphereMap出现的时间比较早,它有一个很大的破绽,就在摄像机的背后。对于那些制作得不够精细的贴图,边缘汇聚在一起的痕迹会非常重。

CubeMap

立方体贴图的做法比较简单:把摄像机置于场景中央,朝着x,-x,y,-y,z,-z方向将场景渲染出6张纹理。然后用6张纹理组成一个立方体的6个面。这样一个真正的全景图组成了。

image.png

image.png

于那些制作得不够好的CubeMap,破绽在于面与面的接缝处。这6个面的排列顺序是一个很有趣的问题,在OpenGL和Dirext3D中,加载同样的天空盒会出现上下颠倒的情况。你可以从这篇文章了解更多内容:OpenGL和D3D中Cubemap的图象方向问题

image.png

实际加载CubeMap时,有两种截然不同的方式。不过这两种方式只是图片格式不同,效果并没有什么区别。

image.png

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);
}

EquirectMap

等距矩形投影(Equirectangle Projection),又称球面投影、方格投影、等距柱状投影等。假想球面与圆筒面相切于赤道,赤道为没有变形的线。经纬线网格,同一般正轴圆柱投影,经纬线投影成两组相互垂直的平行直线。

其特性是:保持经距和纬距相等,经纬线成正方形网格;沿经线方向无长度变形;角度和面积等变形线与纬线平行,变形值由赤道向高纬逐渐增大。该投影适合于低纬地区制图 。

image.png

实际应用中,最常见的就是地图。

image.png

还有全景实景展示:

image.png

image.png

public void simpleInitApp() {
    // 天空盒
    Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/SkyEquirectMap.jpg", SkyFactory.EnvMapType.EquirectMap);
}

效果:

image.png

image.png

这种贴图的破绽在于头顶和脚底,即纬度最高处。那些制作得不够精细的贴图,在这两个点会有明显的“汇聚感”。

image.png