注:本系列教程全部翻译完之后可能会以 PDF 的形式发布。
如果有什么错误可以留言或 EMAIL : kakashi9bi@gmail.com 给我。
jME 版本 : jME_2.0.1_Stable
开发工具: MyEclipse8.5
操作系统: Window7/Vista
这个向导中我们涉及到一些好玩的,我们将为我们的游戏加载地形(下文将使用 Terrain 代替)。这里对于我想要的类型的 terrain 有一些要求:
l 每次随机
l 不需太多三角形
l 为了跳跃“崎岖”
l 对于快速的交通工具足够大
我们将在第二课中的框架上构建。首先,由清除 Sphere 渲染代码开始。我们不再需要这个例子。你现在应该有相当干净的框架用于工作。现在,我们将创建的地形会相当大。所以我想改变 Camera 的位置保证地形在视野里面。因此,在 initSystem 中作出如下改变:
Vector3f loc = new Vector3f(0.0f,0.0f,25.0f);
改为:
Vector3f loc = new Vector3f(500f,150f,500f);
这向上、远、后移动,确保我们对地形有恰当的视野。
现在,在 initGame 方法里面我们将加入一个对新方法的调用,这为这个 scene 增加一个 TerrainBlock 。这个 TerrainBlock 叫做 tb 并应该在类顶部定义。这个新的方法叫做 buildTerrain 并应该在增加 tb 到 scene 之前调用。你应该像下面一样:
protected void initGame() {
scene = new Node( "Scene Graph Node" );
buildTerrain();
scene .attachChild( tb );
// 更新 scene 用于渲染
scene . updateGeometricState (0.0f, true );
scene .updateRenderState();
}
这引导我们到这个向导的核心, buildTerrain 。
这里有我们 terrain 创建的核心:
1、 创建一个 heightmap
2、 从 heightmap 生成网格(下文将以 Mesh 代替)
3、 生成基于高度的纹理
3.1 、创建一个 heightmap
AbstractHeightMap 定义了一个方法用于保存高度数据。在它的核心,主要是一个二维矩阵的数据,任何一个点( X,Z )的高度 Y 。然而这不允许创建复杂 terrain (窑洞、悬崖等等)。它提供了很基础的方形 terrain ,然而这正是我们 FlagRush 中所需要的。
我们将创建一个 MidPointHeightMap ,它使用中点取代不规则碎片。这将允许地形足够有趣和真实,为我们提供了一些颠簸和跳跃。
创建这个 heightmap 很直截了当,在我们 buildTerrain 方法中的第一行:
/**
* 创建 heightmap 和 terrainBlock
*/
private void buildTerrain() {
// 生成随机地形数据
MidPointHeightMap heightMap = new MidPointHeightMap(64,1f);
……
}
我们调用 MidPointHeightMap 的构造方法创建一个新的 heightMap 对象。它只需要 2 个参数:大小和粗糙程度。
MidPointHeightMap 的大小必须是 2 的幂。那就是 2 、 4 、 8 、 16 、 32 、 64 等等。在我们的例子中,我们选择 64 。这正好符合我们的需要(我们的行为将被局限在一个相当小的舞台)。粗糙程度才是有趣的东西。这个值越低,则 terrain 越粗糙,反之越平滑。我们先选择它为 1 ,让 terrain 看起来像地狱般凹凸还带着尖刺。然而,我们还没设置完,这些尖刺将被调下来。
我们将定义一个 terrain 缩放因数。这将简单拉伸或挤压 mesh 以满足我们的需求。所以,增加:
// 缩放数据
Vector3f terrainScale = new Vector3f(20, .5f, 20);
到 buildTerrain 方法。这意味着:我们将拉伸 terrain 的 X 和 Z 的值 20 。这将让 terrain 感觉更大(实际上大了 20 倍)。然而与此同时,我们让 Y 值减少了一半。这将得到我们想要的凹凸感,但让它们处于一个合理的值(不会太突然)。
3.2 、生成 Terrain Mesh
现在,我们已经设置好了数据,我们能真正创建 mesh 。我们将创建一个 TerrainBlock ,它是一个简单的 Geometry 。这个将增加到 scene 里,就像我们之前增加 Sphere 那样。
// 创建一个 terrain block
tb = new TerrainBlock(
"terrain" ,
heightMap.getSize(),
terrainScale,
heightMap.getHeightMap(),
new Vector3f(0, 0, 0)
);
tb . setModelBound ( new BoundingBox());
tb .updateModelBound();
TerrainBlock 接受一些参数,大多数都很直接。首先,是 terrain 的名字。 heightMap 的大小,接着是我们之前所设的 terrain 的缩放值。接着给出 heightMap 真正的数据。下一个参数定义了 terrain 的起点。我们这里没有理由设置一些奇怪的值,因此设置了基本的( 0 , 0 , 0 )。
我们接着设置了 terrain 的 BoundingVolume 。
你现在或许能继续并运行游戏,看到类似下面的一些东西:
这里并不能看到很多东西,因为 terrain 仅是一大块白色。我们需要应用 texture 去让它有一点层次感。
3.3 、生成 Texture
创建一个 Texture 将通过使用 ProceduralTextureGenerator 。这个类将生成一个基于 heightmap 的高度的纹理,并在多个 texture 间混合。一个 texture 被指定到一个高度区域,而它们之后混合进单一的 texture map 。这允许我们很容易创建一个看起来相当真实的 Terrain 。在我们的例子中,我们将使用 3 张 texture ,一个用于低区域的草地 texture ,中部的岩石和高处的雪。
// 通过三个纹理生成地形纹理
ProceduralTextureGenerator pt =
new ProceduralTextureGenerator(heightMap);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/grassb.png" )
),
-128, 0, 128
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/dirt.jpg" )
),
0, 128, 256
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/highest.jpg" )
),
128, 256, 374
);
pt.createTexture(32);
你将注意到每个 Texture 有 3 个值。这描述了这个 texture 将被应用到低的,最佳的和高的海拔。例如( dirt.jpg )将混合从海拔 0-256 。 heightmap 生成从 0-256 的值。所以这意味着 dirt 在 128 将更强烈(看得更多),然后向 0 和 256 混合其它的 texture 。同时其它的 2 个 texture 被填充在低和高的区域。
addTexture 接受 ImageIcon 对象去定义 texture 数据。在这个例子中,我们通过我们的类的 getResource 方法获取到的 URL 创建 ImageIcon 。这个在 classpath 里面搜索 images 。这当然不是一定要这么做, ImageIcon 能在其它某个地方被创建,它将适用于你应用程序。
createTexture 真正创建了我们需要使用的 texture 。在这个例子中,我让它生成一个 32X32 像素的 texture 。虽然这个看起来很小,但是我并不需要它的细节。这只是用于基础颜色,之后我们将创建更详细的 texture 和对象。
例如:在运行游戏期间,我保存了一个生成的 texture 。它看起来像这样:
你能看到三个 texture ( grassb , dirt , highest )是怎样被混合为一个单一的 texture 。白色的区域将会是 terrain 的高点,而 grass 将是 terrain 的低点。
现在我们已经生成了 Terrain ,我们把它放入一个 TextureState 并把它应用到 terrain 。
// 将纹理赋予地形
ts = display .getRenderer().createTextureState();
Texture t1 = TextureManager. loadTexture (
pt.getImageIcon().getImage(),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear ,
true
);
ts .setTexture(t1, 0);
tb .setRenderState( ts );
通过这样, terrain 就能正常工作了。你现在能运行游戏并看到类似下面的:
注意: 我一直说类似,因为我们使用的是随机方法去生成 terrain 。所以它每次都将不同。
3.4 、创建灯光( Light )
尽管使用了 texture ,我们依然很难辨别出 terrain 。那是因为没有灯光和阴影帮助我们辨别 terrain 的部分。所以,让我们继续并增加一个“太阳”。增加一个 buildLighting 到你的 initGame 。我们将增加一个 DirectionalLight 去照耀 terrain 。增加 light 有 2 部分。首先,创建 DirectionalLight ,然后把它增加到 LightState 。
private void buildLighting() {
/* 设置一个基础、默认灯光 */
DirectionalLight light = new DirectionalLight();
light.setDiffuse( new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
light.setAmbient( new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
light.setDirection( new Vector3f(1, -1, 0));
light.setEnabled( true );
LightState lightState =
display .getRenderer().createLightState();
lightState.attach(light);
scene .setRenderState(lightState);
}
这个 DirectionalLight 被设置于照耀( 1 , -1 , 0 )那个方向(向下和向右)。它接着被增加到 LightState 并应用到 scene 。
这为 terrain 增加了一些层次感,而你能更好辨认出地形特征。
3.5 、总结
我们现在拥有了一个可以在上面奔跑的平面。然而,那还是存在令人讨厌的黑色背景。下一节课我们将适当关注个问题。
3.6 、源码
import javax.swing.ImageIcon;
import com.jme.app.BaseGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.light.DirectionalLight;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.state.LightState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.system.JmeException;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.MidPointHeightMap;
import com.jmex.terrain.util.ProceduralTextureGenerator;
public class Lesson3 extends BaseGame{
private int width , height ;
private int freq , depth ;
private boolean fullscreen ;
// 我们的 camera 对象,用于观看 scene
private Camera cam ;
protected Timer timer ;
private Node scene ;
private TextureState ts ;
private TerrainBlock tb ;
public static void main(String[] args) {
Lesson3 app = new Lesson3();
java.net.URL url = app.getClass().getClassLoader()
.getResource( "res/logo.png" );
app.setConfigShowMode(ConfigShowMode. AlwaysShow ,url);
app.start();
}
/*
* 清除 texture
*/
protected void cleanup() {
ts .deleteAll();
}
protected void initGame() {
scene = new Node( "Scene Graph Node" );
buildTerrain();
buildLighting();
scene .attachChild( tb );
// 更新 scene 用于渲染
scene .updateGeometricState(0.0f, true );
scene .updateRenderState();
}
private void buildLighting() {
/* 设置一个基础、默认灯光 */
DirectionalLight light = new DirectionalLight();
light.setDiffuse( new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
light.setAmbient( new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
light.setDirection( new Vector3f(1, -1, 0));
light.setEnabled( true );
LightState lightState =
display .getRenderer().createLightState();
lightState.attach(light);
scene .setRenderState(lightState);
}
/**
* 创建 heightmap 和 terrainBlock
*/
private void buildTerrain() {
// 生成随机地形数据
MidPointHeightMap heightMap = new MidPointHeightMap(64,1f);
// 缩放数据
Vector3f terrainScale = new Vector3f(20, .5f, 20);
// 创建一个 terrain block
tb = new TerrainBlock(
"terrain" ,
heightMap.getSize(),
terrainScale,
heightMap.getHeightMap(),
new Vector3f(0, 0, 0)
);
tb .setModelBound( new BoundingBox());
tb .updateModelBound();
// 通过三个纹理生成地形纹理
ProceduralTextureGenerator pt =
new ProceduralTextureGenerator(heightMap);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/grassb.png" )
),
-128, 0, 128
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/dirt.jpg" )
),
0, 128, 256
);
pt.addTexture(
new ImageIcon(
getClass().getClassLoader()
.getResource( "res/highest.jpg" )
),
128, 256, 374
);
pt.createTexture(32);
// 将纹理赋予地形
ts = display .getRenderer().createTextureState();
Texture t1 = TextureManager. loadTexture (
pt.getImageIcon().getImage(),
Texture.MinificationFilter. Trilinear ,
Texture.MagnificationFilter. Bilinear ,
true
);
ts .setTexture(t1, 0);
tb .setRenderState( ts );
}
protected void initSystem() {
// 保存属性信息
width = settings .getWidth();
height = settings .getHeight();
depth = settings .getDepth();
freq = settings .getFrequency();
fullscreen = settings .isFullscreen();
try {
display = DisplaySystem. getDisplaySystem (
settings .getRenderer()
);
display .createWindow(
width , height , depth , freq , fullscreen
);
cam = display .getRenderer().createCamera( width , height );
} catch (JmeException e){
e.printStackTrace();
System. exit (-1);
}
// 设置背景为黑色
display .getRenderer().setBackgroundColor(ColorRGBA. black );
// 初始化摄像机
cam .setFrustumPerspective(
45.0f,
( float ) width /( float ) height ,
1f,
1000f
);
Vector3f loc = new Vector3f(500f,150f,500f);
Vector3f left = new Vector3f(-1.0f,0.0f,0.0f);
Vector3f up = new Vector3f(0.0f,1.0f,0.0f);
Vector3f dir = new Vector3f(0.0f,0.0f,-1.0f);
// 将摄像机移到正确位置和方向
cam .setFrame(loc, left, up, dir);
// 我们改变自己的摄像机位置和视锥的标志
cam .update();
// 获取一个高分辨率用于 FPS 更新
timer = Timer. getTimer ();
display .getRenderer().setCamera( cam );
KeyBindingManager. getKeyBindingManager ().set(
"exit" ,
KeyInput. KEY_ESCAPE
);
}
/*
* 如果分辨率改变将被调用
*/
protected void reinit() {
display .recreateWindow( width , height , depth , freq , fullscreen );
}
/*
* 绘制场景图
*/
protected void render( float interpolation) {
// 清除屏幕
display .getRenderer().clearBuffers();
display .getRenderer().draw( scene );
}
/*
* 在 update 期间,我们只需寻找 Escape 按钮
* 并更新 timer 去获取帧率
*/
protected void update( float interpolation) {
// 更新 timer 去获取帧率
timer .update();
interpolation = timer .getTimePerFrame();
// 当 Escape 被按下时,我们退出游戏
if (KeyBindingManager. getKeyBindingManager ()
.isValidCommand( "exit" )
){
finished = true ;
}
}
}