注:本系列教程全部翻译完之后可能会以PDF的形式发布。
如果有什么错误可以留言或EMAIL:[email protected]给我。
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、 生成基于高度的纹理
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值减少了一半。这将得到我们想要的凹凸感,但让它们处于一个合理的值(不会太突然)。
现在,我们已经设置好了数据,我们能真正创建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去让它有一点层次感。
创建一个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。所以它每次都将不同。
尽管使用了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增加了一些层次感,而你能更好辨认出地形特征。
我们现在拥有了一个可以在上面奔跑的平面。然而,那还是存在令人讨厌的黑色背景。下一节课我们将适当关注个问题。
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;
}
}
}