高度图实际上就是一个2D数组。地形实际上就是一系列高度不同的网格而已,这样数组中每个元素的索引值刚好可以用来定位不同的网格((x,y),而锁存储的值就是网格的高度(z)。
要将高度地图转换为场景,只需要读取图片的像素,然后根据像素的值设置平面的高度,最常用的平面是四边形,因为四边形是规则的,可以采用数组方便统一创建和管理(并能实现地图的无缝),四边形由两个三角形组成,多个四边形组成了场景地图。
为将高度地图转化为场景,创建一个HeightMap类。要创建等高地图,首先应当读取图片数据,getRGB方法可以将颜色存储到数组中。
resolution表示1个像素所表示的四边形个数,这里设置为0.25,一个四边形要四个像素颜色表示。该值增大,使用更多的四边形来代表地形,地形的平滑度也在不断的提高,然而,也在增加内存的使用空间和CPU需要运算的多边形数量,因此,每一个移动的设备都需要依据可用内存,CPU运算能力等进行平衡。
加载图片代码如下
private float resolution = 0.25f;
private int imgw,imgh;
private short[] heightMap;
private int[] data;
private int mapWidth;
private int mapHeight;
private static final String TerrainImgFn="/heightmap.png";
private void loadMapImage() throws IOException
{
Image img = Image.createImage(TerrainImgFn); //加载文件
imgw = img.getWidth(); //图片宽度
imgh = img.getHeight(); //图片高度
data = new int[imgw * imgh]; //根据文件大小创建数据
.getRGB(data, 0, imgw, 0, 0, img.getWidth(), imgh); //将颜色信息保存到数组中
mapWidth = (int)(resolution * imgw); //场景数组的宽度(列)
mapHeight = (int)(resolution * imgh); //地图数组的高度
heightMap = new short[mapWidth * mapHeight]; //创建地图数组
// Clear image
img = null;
System.gc();
int xoff = imgw / mapWidth; //x方向上图片和场景地图的映射量
int yoff = imgh / mapHeight; //y方向上图片和场景地图的映射量
for(int y = 0; y < mapHeight; y++)
{
for(int x = 0; x < mapWidth; x++) //设置网面各个顶点高度
{
heightMap[x + y * mapWidth] = (short)((data[x * xoff + y * yoff * imgw]& 0x000000ff) * 10);
}
}
//Clear data
data = null;
img = null;
System.gc();
}
HeightMap 类使用createQuad方法创建四边形,四边形由四个顶点组成,每一个顶点有一个变化的y坐标,使用heights数组设置,但x和z不变。
方法代码如下:
//创建四边形方法createQuad
public static Mesh createQuad (short[] heights)
{
short[] POINTS = {-255,heights[0],-255,//顶点0
255,heights[1],-255, //顶点1
255,heights[2],255, //顶点2
-255,heights[3],255 //顶点3
};
VertexArray POSITION_ARRAY = new VertexArray(POINTS.length/3,3,2); //创建顶点位置数组
POSITION_ARRAY.set(0, POINTS.length/3, POINTS); //设置顶点数组
byte[] COLORS = new byte[12];
for(int i = 0; i < heights.length; i++) //根据高度设置颜色数组
{
int j = i*3;
if(heights[i] >= 1000) //高地
{
byte col = (byte)(57 + (heights[i] / 1555.0f) * 70);
COLORS[j] = col;
COLORS[j + 1] = col;
COLORS[j + 2] = col;
}
else{
byte gCol = 110;
byte bCol = 25;
COLORS[j] = 0;
COLORS[j + 1] = (byte)(gCol - (heights[i] / 1000.0f) * 85);
COLORS[j + 2] = (byte)(bCol - (heights[i] / 1000.0f) * 20);
}
}
VertexArray COLOR_ARRAY = new VertexArray(COLORS.length/3, 3, 1); //创建颜色数组
COLOR_ARRAY.set(0, COLORS.length / 3, COLORS); //设置颜色数组
VertexBuffer vertexBuffer = new VertexBuffer(); //创建顶点缓冲
vertexBuffer.setPositions(POSITION_ARRAY, 1.0f, null); //设置顶点缓冲的位置数组
vertexBuffer.setColors(COLOR_ARRAY); //设置顶点缓冲的颜色数组
int INDICES[] = new int[] {0, 1, 3, 2}; //顶点索引
int[] LENGTHS = new int[] {4}; //四边形
IndexBuffer indexBuffer = new TriangleStripArray(INDICES , LENGTHS); //创建索引缓冲
Appearance appearance = new Appearance(); //创建外观属性
PolygonMode polygonmode = new PolygonMode() ; //创建多边形模式
polygonmode.setCulling(PolygonMode.CULL_NONE); //双面显示
polygonmode.setPerspectiveCorrectionEnable(true); //设置透视校正
polygonmode.setShading(PolygonMode.SHADE_SMOOTH); //设置平滑显示
appearance.setPolygonMode(polygonmode);
Mesh mesh = new Mesh(vertexBuffer , indexBuffer , appearance);
return mesh;
}
在读取图片颜色数据和能够构建单个四边形网面之后,就可以根据等高图片设置不同高度批量创建四边形来构成场景
方法如下:
//method
private void createQuads()
{
map = new Mesh[mapWidth][mapHeight];
short[] heights = new short[4];
for(int x = 0; x < (mapWidth -1) ; x++)
{
for(int y = 0; y < (mapHeight - 1); y++)
{
heights[0] = heightMap[x + y * mapWidth]; //设置顶点0的高度
heights[1] = heightMap[x + y * mapWidth + 1]; //设置顶点1的高度
heights[2] = heightMap[x + (y + 1) * mapWidth]; //设置顶点2的高度
heights[2] = heightMap[x + (y + 1) * mapWidth + 1]; //设置顶点3的高度
map[x][y] = createQuad(heights); //创建四边形网络
}
}
}
所有创建的四边形是重叠的,所以有必要将四边形在xz平面上进行平移, 平移的大小按照网格的实际大小来设置,例如createQuad创建的四边形边长为510,调用postScale方法将四边形缩小100倍,其边长为5.1,那么平移每个四边形的距离应该为5。1的整数倍
private Transform localTransform = new Transform();
public void setTransform(Transform t)
{
for(int x = 0; x < map.length - 1; x++)
{
for(int y = 0; y < map[x].length - 1; y++)
{
localTransform.setIdentity();
localTransform.postTranslate(x * 5.1f, 0.0f, (mapHeight - y) * -5.1f);
localTransform.postScale(0.01f, 0.01f, 0.01f);
localTransform.postMultiply(t);
map[x][y].setTransform(localTransform);
}
}
}
HeightMap类的构造方法负责调用上面的方法来根据高度图片构造3D网面场景,代码如下:
//构造函数
public HeightMap() throws IOException
{
if(resolution <= 0.0001f || resolution > 0.1f) //检查resolution 是否合理
throw new IllegalArgumentException("Resolution too small or too large");
loadMapImage(); //加载等高图片并将颜色数据保存到数组中
createQuads(); //根据高度创建多个四边形
}
HeightMap类根据等高图创建场景流程为: 加载图片--》data数组读取颜色-->heightMap地图数组--》createQuads()根据地图数组创建四边行数组--》createQuad(heights)根据高度创建四边形--》setTransform()将四边形在XZ平面上平移
HeightMap类提供了一些公用方法给游戏画布类访问在创建整个场景时以便将网面添加成场景的一部分
public Mesh[][] getQuads(){ return map;}
public int getMapWidth() { return mapWidth;}
public int getMapHeight() { return mapHeight;}
下面开始构造游戏场景
游戏场景类代码如下:
public class H3DCanvas extends GameCanvas implements Runnable{
private World world; //创建场景对象
boolean[] key = new boolean[9];
private Graphics3D g3d = null;
private Background back = null;
private Camera camera = null;
private Transform camTrans = new Transform();
private HeightMap heightmap;
private Mesh[][] map;
/** Constructs the canvas
*/
public H3DCanvas()
{
super(true);
setFullScreenMode(true);
g3d = Graphics3D.getInstance();
world = new World();
createMap();
map = heightmap.getQuads();
for(int x = 0; x < heightmap.getMapWidth()-1; x++)
{
for(int y = 0; y < heightmap.getMapHeight()-1; y++)
{
world.addChild(map[x][y]);
}
}
camera = new Camera();
float aspect = (float) getWidth() / (float) getHeight();
camera.setPerspective(60.0f, aspect, 0.1f, 150.0f);
camTrans.postTranslate(0.0f, 5.0f, 0.0f);
camera.setTransform(camTrans);
world.addChild(camera);
world.setActiveCamera(camera);
addWater();
back = new Background();
back.setColor(0xFF0000FF);
world.setBackground(back);
Thread t = new Thread(this);
t.start();
}
private void createMap()
{
try
{
heightmap = new HeightMap();
Transform t = new Transform();
t.postTranslate(0.0f, -5.0f, -5.0f);
heightmap.setTransform(t);
//camTrans.postTranslate(0.0f, 5.0f, 2.0f);
}
catch(Exception e)
{
System.out.println("Heightmap error: " + e.getMessage());
e.printStackTrace();
}
}
private void draw(Graphics g)
{
try
{
g3d = Graphics3D.getInstance();
g3d.bindTarget(g, true, Graphics3D.TRUE_COLOR | Graphics3D.DITHER);
g3d.render(world);
}
catch(Exception e)
{
System.out.println(e.getMessage());
System.out.println(e);
e.printStackTrace();
}
finally
{
g3d.releaseTarget();
}
}
private void addWater()
{
Image2D waterIm = null;
try {
waterIm = (Image2D)Loader.load("/water.png")[0];
}
catch (Exception e)
{ System.out.println("Cannot load image " ); }
TiledWater w = new TiledWater(waterIm,8);
Mesh water = w.getWaterMesh();
water.scale(2,2,2);
world.addChild( w.getWaterMesh() );
}
public void run() {
while(true) {
try {
input();
draw(getGraphics());
flushGraphics();
try{ Thread.sleep(30); } catch(Exception e) {}
}
catch(Exception e) {
System.out.println(e.getMessage());
System.out.println(e);
e.printStackTrace();
}
}
}
protected void input()
{
int keys = getKeyStates();
if((keys & GameCanvas.FIRE_PRESSED) != 0)
camTrans.postTranslate(0.0f, 0.0f, -1.0f);
if((keys & GameCanvas.UP_PRESSED) != 0)
camTrans.postTranslate(0.0f, 1.0f, 0.0f);
if((keys & GameCanvas.DOWN_PRESSED) != 0)
camTrans.postTranslate(0.0f, -1.0f, 0.0f);
if((keys & GameCanvas.LEFT_PRESSED) != 0)
camTrans.postRotate(5, 0.0f, 1.0f, 0.0f);
if((keys & GameCanvas.RIGHT_PRESSED) != 0)
camTrans.postRotate(-5, 0.0f, 1.0f, 0.0f);
camera.setTransform(camTrans) ;
}
}
TiledWater类代码如下:
public class TiledWater
{
private Mesh waterMesh;
private VertexBuffer vertexBuffer;
private IndexBuffer indexBuffer;
private Appearance appearance;
public TiledWater(Image2D waterIm, int sz)
{
int size = (sz/2)*2;
if (size != sz)
System.out.println("Size set to multiple of 2: " + size);
int numTiles = size*size;
vertexBuffer = new VertexBuffer();
makeGeometry(size, numTiles);
makeAppearance(waterIm);
waterMesh = new Mesh(vertexBuffer, indexBuffer, appearance);
waterMesh.setScale(25, 25, 25) ;
}
public Mesh getWaterMesh()
{ return waterMesh; }
private void makeGeometry(int size, int numTiles)
{
short[] POINTS = new short[12*numTiles]; // 3 * 4 points for each tile
int i=0;
for(int z = (-size/2)+1; z <= size/2; z++)
for(int x = -size/2; x <= (size/2)-1; x++) {
POINTS[i] = (short) x; POINTS[i+1]=0; POINTS[i+2] = (short) z;
POINTS[i+3] = (short)(x+1); POINTS[i+4]=0; POINTS[i+5] = (short) z;
POINTS[i+6] = (short)(x+1); POINTS[i+7]=0; POINTS[i+8] = (short)(z-1);
POINTS[i+9] = (short) x; POINTS[i+10]=0; POINTS[i+11] = (short)(z-1);
i += 12;
}
VertexArray POSITION_ARRAY = new VertexArray(POINTS.length/3, 3, 2);
POSITION_ARRAY.set(0, POINTS.length/3, POINTS);
short[] TEXCOORDS = new short[8*numTiles];
for(i = 0; i < 8*numTiles; i += 8) {
TEXCOORDS[i] = 0; TEXCOORDS[i+1] = 1;
TEXCOORDS[i+2] = 1; TEXCOORDS[i+3] = 1;
TEXCOORDS[i+4] = 1; TEXCOORDS[i+5] = 0;
TEXCOORDS[i+6] = 0; TEXCOORDS[i+7] = 0;
}
VertexArray TEXCOORD_ARRAY = new VertexArray(TEXCOORDS.length/2, 2, 2);
TEXCOORD_ARRAY.set(0, TEXCOORDS.length/2, TEXCOORDS);
vertexBuffer.setPositions(POSITION_ARRAY, 1.0f, null);
vertexBuffer.setTexCoords(0, TEXCOORD_ARRAY, 1.0f, null);
int pos1 = 1; int pos2 = 2;
int pos3 = 0; int pos4 = 3;
int[] INDICES = new int[4*numTiles];
for(i = 0; i < 4*numTiles; i += 4) {
INDICES[i] = pos1; pos1 += 4;
INDICES[i+1] = pos2; pos2 += 4;
INDICES[i+2] = pos3; pos3 +=4;
INDICES[i+3] = pos4; pos4 += 4;
}
int[] LENGTHS = new int[numTiles];
for(i = 0; i < numTiles; i++)
LENGTHS[i] = 4;
indexBuffer = new TriangleStripArray(INDICES,LENGTHS);
}
private void makeAppearance(Image2D waterIm)
{
appearance = new Appearance();
if (waterIm != null) {
Texture2D texture = new Texture2D(waterIm);
texture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST);
texture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);
appearance.setTexture(0, texture);
}
PolygonMode polygonmode = new PolygonMode();
polygonmode.setPerspectiveCorrectionEnable(true);
polygonmode.setCulling(PolygonMode.CULL_NONE);
appearance.setPolygonMode(polygonmode);
}
}