j2me 3D开发-基本地形渲染技术

本章将讲述3D中常用的一些技术:包括HeightMap高度地图、粒子系统和碰撞检测技术。

HeightMap是地形的输入数据,可以理解为位图,一个2D矩阵,和位图不同的是,把元素的颜色值映射为高度值,建立HeightMap的方法有很多,这里使用灰度图来创建高度地图。

粒子系统在模仿自然现象、物理现象及空间扭曲上具备得天独厚的优势。每一种粒子系统都有一些相似的参数,但也都存在差异,如何优化它们的性能并运用于3D创作领域就看开发者的想象力了。

碰撞检测没有很标准的理论,但都建立在2D的基础上,这里沿用了AABB或者OBB的检测方法,或者先用球体做粗略的检测,然后用AABB和OBB作精细的检测。BSP技术并不广泛适用,但是能提高效率,本章没有太多的涉及。

本章主要包括以下几个方面的内容。

n     理解怎样通过灰度图产生顶点和三角面组成立体地图。

n     在高低不平的地形中使用摄影机来模拟行走或跑。

n     使用粒子系统构造火焰、爆炸等常用特效。

n     使用球体、AABB和OBB技术进行碰撞检测。

10.1  基本地形渲染技术
什么是HeightMap呢?所谓高度图实际上就是一个2D数组。创建地形为什么需要高度图呢?可以这样考虑,地形实际上就是一系列高度不同的网格而已,这样数组中每个元素的索引值刚好可以用来定位不同的网格(x,y),而所储存的值就是网格的高度(z)。

10.1.1  HeightMap简介
现实中的地形是真实的,不是由三角平面模拟的,但是3D图形图像处理中常常使用三角形来代替地形的表面,每个三角形的顶点高度在山脉到山谷之间转换,模拟自然地形,如图10-1所示。在这个过程中,还将应用纹理展示沙滩、丘陵和雪山。

(图)j2me 3D开发-基本地形渲染技术

HeightMap技术的灵感来源于等高地图的绘制,如图10-2所示,是一幅等高地图,它通常用来描绘高低起伏比较大的地形。

(图)j2me 3D开发-基本地形渲染技术

例如,飞行员必须了解哪里有高海拔的障碍物、山川、湍流的方向等,以便安全地飞行。从空中往下看,陆地上可能很平坦,但事实上等高线是展开的,没有相当的理解力和想象力,许多人并不能很好地领会地图实际提供的信息。

高度从平面图上无法立体地显现,所以用有规律的间隔来表示海拔,通常根据测量方法的不同一般间隔为50英尺或10m,同等位置的点被连成线,即等高线。

许多情况下这些等高线聚在图上形成封闭的环线,有些部分为不规则的心形,不时会凸出来一点。如果它们突然中断与其他线相冲突,则表示有高度的突然变化,事实上为悬崖或很深的落瀑。

自然界中能看到的惟一等高线只有沿着海岸的水平线(由于海潮的变化,事实上那也不是完全意义上的等高线),但可以把等高线想象成如同水平桌面的边线,如果把衣物或其他东西堆在桌面上,就如同小山峰或其他形状。

在这些等高线之间具体地形如何,都没有表示出来。等高线之间也不一定就是斜坡,可能是洞*、凸起的岩石,以及其他各种高度变化在10m之内的地势。从等高线的相应位置,可以简单地猜测出地表大概会是如何变化的。

等高线地图可以形象地反映山体的情况,如图10-3所示是实际山体与等高线地图的对应表示。等高线地图在军事、旅游、探矿中有着广泛的应用。

(图)j2me 3D开发-基本地形渲染技术

等高线之间的间隔只是表明同一理论高度下地平线上点之间的距离,并非是地面山坡上点间的实际距离,它们只是用来表明相应的位置,并非根据地平面的比例。

常见的一种错误想法是,一群等高线是按地图绘制比例缩小的地面高度——要知道典型的旅行地图比例是1∶50000,10m在图上只有0.02mm。图上间距5mm的等高线在地表面的距离为250m,而不是实际代表的10m落差,差距是1∶25。

(图)j2me 3D开发-基本地形渲染技术

理解等高线,就可以说掌握了一半的HeightMap技术。在3D地形渲染中,采用数组的形式来保存高度信息(数组内容可以通过高度图读取,图片上的一个像素或者一个区域代表同一高度),根据高度信息在不同的位置绘制多边形,从而通过2D图像展示出3D地形场景。

高度图可以使用画图板或者图像编辑器Adobe Photoshop产生。使用图像编辑器可能更容易,它能够帮助创建想要的交互地形,另外也可以通过图形编辑特性,例如过滤,创建有趣的高度图。

如图10-4所示是根据高度图“恢复”的3D山体地貌立体地图。

10.1.2  海岛地图的原理
从上面对等高图的阐述,可以知道,地形是真实世界的一个模型,有平原、山脉、河流、悬崖和丘陵等。以抽象角度来看,可以简单地认为地形仅仅是高度上的变化。

例如,一个草原就是一个高度基本为常数的地形(除了可能有一些起伏和山丘外);一个山区或者鸿沟是高低落差比较大的地形;一条河流就是由一个高地势平原和穿过它的曲线组成,这个曲线比它周围的地形高度稍低。

如图10-5所示,是一系列的高度地图,简单来看,它是一个像素的集合,每个像素都是在灰度上0~255之间变化,0是黑色,255是全白,读者也可以自己绘制高度地图。可以判断,明暗反差越大,地势高低起伏也越大。

(图)j2me 3D开发-基本地形渲染技术

说明:在灰度图像中,像素灰度级用8b表示,所以每个像素都是介于黑色和白色之间的256(28=256)种灰度中的一种。灰度图像只有灰度颜色而没有彩色。通常所说的黑白照片,其实包含了黑白之间的所有灰度色调。从技术上来说,就是具有从黑到白的256种灰度色域(Gamut)的单色图像。

10.1.3  地图元素图元
要将高度地图转换为场景,只需要读取图片的像素,然后根据像素的值设置平面的高度,最常用的平面是四边形。因为四边形是规则的,可以采用数组方便地统一创建和管理(并且能实现地图的无缝)。四边形由两个三角形组成,多个四边形组成了场景地图,如图10-6所示。

(图)j2me 3D开发-基本地形渲染技术

10.1.4  读取元素数据
为了将高度地图转换为场景,创建一个HeightMap类。要创建等高地图,首先应当读取图片数据,getRGB方法可以将颜色存储到数组中。

整型变量imgw和imgh用来保存图片文件的大小,resolution表示1个像素所表示的四边形个数,例如这里设置为0.25,则表示一个四边形涵盖了4个像素的颜色信息。

随着resolution的增大,使用了更多的四边形来代表这个地形,地形的平滑度也在不断地提高,然而,也在增加内存的使用空间和CPU需要运算的多边形数量。因此,每一个移动设备都需要依据可用内存、CPU运算能力等进行平衡。

Java代码
  1. private float resolution = 0.25f;   
  2.   
  3. private int imgw, imgh;    
  4.   
  5. private short[] heightMap;   
  6.   
  7. private int[] data;   
  8.   
  9. private int mapWidth;   
  10.   
  11. private int mapHeight;   
  12.   
  13. private static final String  TerrainImgFn ="/heightmap.png" ;   
  14.   
  15. private void loadMapImage() throws IOException   
  16.   
  17. {   
  18.   
  19.    Image img = Image.createImage(TerrainImgFn);         //加载文件   
  20.   
  21.    imgw = img.getWidth();                               //图片宽度   
  22.   
  23.    imgh = img.getHeight();                              //图片高度   
  24.   
  25.    data = new int[imgw * imgh];                         //根据文件大小创建数据   
  26.   
  27.    img.getRGB(data, 0, imgw, 00, img.getWidth(),imgh); //将颜色信息保存到数组中   
  28.   
  29.    mapWidth = (int)(resolution * imgw);                 //场景数组的宽度(列数)   
  30.   
  31.    mapHeight = (int)(resolution * imgh);                //地图数组的高度(行数)   
  32.   
  33.    heightMap = new short[mapWidth * mapHeight];         //创建地图数组   
  34.   
  35.    int xoff = imgw / mapWidth;                   //x方向上图片和场景地图的映射量   
  36.   
  37.    int yoff = imgh / mapHeight;                  //x方向上图片和场景地图的映射量   
  38.   
  39.    for(int y = 0; y < mapHeight; y++)            //根据图片数据设置网面高度   
  40.   
  41.    {   
  42.   
  43.        for(int x = 0; x < mapWidth; x++)   
  44.   
  45.        {                                                 //设置网面各个顶点高度   
  46.   
  47.          heightMap[x + y * mapWidth] = (short)((data[x * xoff + y * yoff * imgw] & 0x000000ff) * 10);   
  48.   
  49.        }   
  50.   
  51.    }   
  52.   
  53. }   
  54.   

10.1.5  构造四边形图元
HeightMap类使用createQuad方法创建四边形,四边形由4个顶点组成,每一个顶点都有一个变化的y坐标,使用heights数组设置,但是x和z不变(之后根据需要进行水平平移)。

四边形面的4个顶点的颜色根据位置线性变化,从而显现出不同高度的风貌,并且顶点的颜色在整个平面上以内插值替换,创建一个平滑的外观。创建四边形的代码如下:

Java代码
  1. public static Mesh createQuad(short[] heights)   
  2.   
  3. {   
  4.   
  5.    short[] POINTS = {-255, heights[0], -255,            //顶点0   
  6.   
  7.                          255, heights[1], -255,            //顶点1   
  8.   
  9.                          255, heights[2], 255,             //顶点2   
  10.   
  11.                          -255, heights[3], 255};           //顶点3   
  12.   
  13.    VertexArray POSITION_ARRAY = new VertexArray(POINTS.length/332);   
  14.   
  15.                                                         //创建顶点位置数组   
  16.   
  17.    POSITION_ARRAY.set(0, POINTS.length/3, POINTS);      //设置顶点位置数组   
  18.   
  19.    byte[] COLORS = new byte[12];                        //颜色数组   
  20.   
  21.    for(int i = 0; i < heights.length; i++)              //根据高度设置颜色数组   
  22.   
  23.    {   
  24.   
  25.       int j = i * 3;   
  26.   
  27.       if(heights[i] >= 1000)                             //高地   
  28.   
  29.       {   
  30.   
  31.          byte col = (byte)(57 + (heights[i] / 1550.0f) * 70);   
  32.   
  33.          COLORS[j] = col;   
  34.   
  35.          COLORS[j + 1] = col;   
  36.   
  37.          COLORS[j + 2] = col;   
  38.   
  39.       }else{   
  40.   
  41.          byte gCol = 110;   
  42.   
  43.          byte bCol = 25;   
  44.   
  45.          COLORS[j] = 0;   
  46.   
  47.          COLORS[j + 1] = (byte)(gCol - (heights[i] / 1000.0f) * 85);   
  48.   
  49.          COLORS[j + 2] = (byte)(bCol - (heights[i] / 1000.0f) * 20);   
  50.   
  51.       }   
  52.   
  53.    }   
  54.   
  55.    VertexArray COLOR_ARRAY = new VertexArray(COLORS.length/331);   
  56.   
  57.                                                         //创建颜色数组   
  58.   
  59.    COLOR_ARRAY.set(0, COLORS.length / 3, COLORS);       //设置颜色数组   
  60.   
  61.    VertexBuffer vertexBuffer = new VertexBuffer();      //创建顶点缓冲   
  62.   
  63.    vertexBuffer.setPositions(POSITION_ARRAY, 1.0f, null);   //设置顶点缓冲的位置数组   
  64.   
  65.    vertexBuffer.setColors(COLOR_ARRAY);                 //设置顶点缓冲的颜色数组   
  66.   
  67.    int INDICES[] = new int[] {0132};              //顶点索引   
  68.   
  69.    int[] LENGTHS = new int[] {4};                       //四边形   
  70.   
  71.    IndexBuffer indexBuffer = new TriangleStripArray(INDICES, LENGTHS);   
  72.   
  73.                                                         //创建索引缓冲   
  74.   
  75.    Appearance appearance = new Appearance();            //创建外观属性   
  76.   
  77.    PolygonMode polygonmode = new PolygonMode();         //创建多边形模式   
  78.   
  79.    polygonmode.setCulling(PolygonMode.CULL_NONE);       //双面显示   
  80.   
  81.    polygonmode.setPerspectiveCorrectionEnable(true);    //透视校正   
  82.   
  83.    polygonmode.setShading(PolygonMode.SHADE_SMOOTH);    //平滑显示   
  84.   
  85.    appearance.setPolygonMode(polygonmode);   
  86.   
  87.    Mesh mesh = new Mesh(vertexBuffer, indexBuffer, appearance);   
  88.   
  89.                                                         //创建四边形网格对象   
  90.   
  91.    return mesh;   
  92.   
  93. }   
  94.   

10.1.6  根据地图构造四边形组
在读取图片颜色数据和能够创建单个四边形网面之后,就可以根据等高图片设置的不同高度批量创建四边形来构成场景。

Java代码
  1. private void createQuads()   
  2.   
  3. {   
  4.   
  5.     map = new Mesh[mapWidth][mapHeight];   
  6.   
  7.     short[] heights = new short[4];   
  8.   
  9.     for(int x = 0; x < (mapWidth - 1); x++)   
  10.   
  11.     {   
  12.   
  13.        for(int y = 0; y < (mapHeight - 1); y++)   
  14.   
  15.        {   
  16.   
  17.            heights[0] = heightMap[x + y * mapWidth];          //设置顶点0的高度   
  18.   
  19.            heights[1] = heightMap[x + y * mapWidth + 1];      //设置顶点1的高度   
  20.   
  21.            heights[3] = heightMap[x + (y + 1) * mapWidth];    //设置顶点2的高度   
  22.   
  23.            heights[2] = heightMap[x + (y + 1) * mapWidth + 1];   //设置顶点3的高度   
  24.   
  25.            map[x][y] = createQuad(heights);                   //创建四边形网格   
  26.   
  27.        }   
  28.   
  29.     }   
  30.   
  31. }   
  32.   

所有创建的四边形是重叠的,因此有必要将四边形在xz平面上进行平移。平移的大小按照网格的实际大小来设置,例如createQuad创建的四边形边长为510,调用postScale方法将四边形缩小100倍,其边长为5.1,那么平移每个四边形的距离应该为5.1的整数倍。

Java代码
  1. private Transform localTransform = new Transform();   
  2.   
  3. for(int x = 0; x < map.length - 1; x++)   
  4.   
  5. {   
  6.   
  7.     for(int y = 0; y < map[x].length - 1; y++)   
  8.   
  9.     {   
  10.   
  11.         localTransform.setIdentity();                  //归一化为单位矩阵   
  12.   
  13.         localTransform.postTranslate(x * 5.1f, 0.0f, (mapHeight - y) * -5.1f);   
  14.   
  15.                                                       //平移到指定位置   
  16.   
  17.         localTransform.postScale(0.01f, 0.01f, 0.01f); //缩小网格   
  18.   
  19.         map[x][y].setTransform(localTransform);        //将变换矩阵应用到网格上   
  20.   
  21.     }   
  22.   
  23. }   
  24.   
  25. HeightMap类的构造方法负责调用上面的方法来根据高度图片构造3D网面场景,代码如下:   
  26.   
  27. public HeightMap() throws IOException   
  28.   
  29. {   
  30.   
  31.    if(resolution <= 0.0001f || resolution > 1.0f)  //检查resolution是否合理   
  32.   
  33.                            throw new IllegalArgumentException("Resolution too small or too large");   
  34.   
  35.    loadMapImage();                           //加载等高图片并将颜色数据保存到数组中   
  36.   
  37.    createQuads();                            //根据高度创建多个四边形   
  38.   
  39.    setTransform();                           //将四边形平移到场景相应位置   
  40.   
  41. }   
  42.   

HeightMap类根据等高图创建场景的流程如图10-7所示。

(图)j2me 3D开发-基本地形渲染技术

HeightMap类提供了一些公共方法给游戏画布类访问,在创建整个场景时以便将网面添加成为场景的一部分。

public Mesh[][]getQuads(){   return map;  }

public int getMapWidth(){    return mapWidth;}

public int getMapHeight(){    return mapHeight; }

10.1.7  构造游戏场景
游戏画布的构造方法负责创建场景和启动游戏线程。游戏画布继承了GameCanvas类,并引用了Runnable接口。场景包括摄影机、背景、等高地图和海平面,海平面由多个覆盖纹理的正方形组成。构造场景的代码如下:

Java代码
  1. World world = new World();                               //创建场景对象   
  2.   
  3. try{   
  4.   
  5.    heightmap = new HeightMap();                          //创建等高地图   
  6.   
  7. catch(Exception e){                                    //捕捉异常   
  8.   
  9.    System.out.println("Heightmap error: " + e.getMessage());   
  10.   
  11.    e.printStackTrace();   
  12.   
  13. }   
  14.   
  15. Mesh[][] map = heightmap.getQuads();                     //获取地图数组   
  16.   
  17. for(int x = 0; x < heightmap.getMapWidth()-1; x++)   
  18.   
  19. {   
  20.   
  21.    for(int y = 0; y < heightmap.getMapHeight()-1; y++)   
  22.   
  23.    {   
  24.   
  25.       world.addChild(map[x][y]);                        //将四边形组添加到场景   
  26.   
  27.    }   
  28.   
  29. }   
  30.   
  31. Camera camera = new Camera();                          //创建摄影机   
  32.   
  33. float aspect = (float) getWidth() / (float) getHeight();    //计算屏幕宽高比   
  34.   
  35. camera.setPerspective(60.0f, aspect, 0.1f, 150.0f);    //设置投影参数   
  36.   
  37. Transform camTrans = new Transform();                  //摄影机变换矩阵   
  38.   
  39. camTrans.postTranslate(0.0f, 5.0f, 0.0f);              //摄影机平移到相应位置   
  40.   
  41. camera.setTransform(camTrans);                         //移动摄影机   
  42.   
  43. world.addChild(camera);                                //将摄影机添加到场景   
  44.   
  45. world.setActiveCamera(camera);                   //将摄影机设置为场景的活动摄影机   
  46.   
  47. addWater();                                            //添加海平面   
  48.   
  49. Background back = new Background();                    //背景   
  50.   
  51. back.setColor(0xFF0000FF);                             //设置背景的颜色   
  52.   
  53. world.setBackground(back);                             //将背景添加到场景中   
  54.   
  55. Thread t = new Thread(this);                           //创建游戏线程   
  56.   
  57. t.start();       

10.1.8  绘制场景

Java代码
  1. draw方法负责将场景绘制屏幕上,代码如下:   
  2.   
  3. private void draw(Graphics g)   
  4.   
  5. {   
  6.   
  7.    try{   
  8.   
  9.        g3d = Graphics3D.getInstance();   
  10.   
  11.        g3d.bindTarget(g, true, Graphics3D.TRUE_COLOR | Graphics3D.DITHER);   
  12.   
  13.        g3d.render(world);                            //绘制场景   
  14.   
  15.    }catch(Exception e){                             //捕捉异常   
  16.   
  17.       System.out.println(e.getMessage());   
  18.   
  19.       System.out.println(e);   
  20.   
  21.       e.printStackTrace();   
  22.   
  23.    } finally{   
  24.   
  25.        g3d.releaseTarget();   
  26.   
  27.    }   
  28.   
  29. }   
  30.   

10.1.9  键盘输入
input方法根据键盘状态对摄影机进行平移和旋转变换,Fire键将摄影机前推,上下键将摄影机上下移动,左右键将摄影机左右旋转,代码如下:

Java代码
  1. protected void input()   
  2.   
  3. {   
  4.   
  5.    int keys = getKeyStates();   
  6.   
  7.    if((keys & GameCanvas.FIRE_PRESSED) != 0)            //摄影机前推   
  8.   
  9.    camTrans.postTranslate(0.0f, 0.0f, -1.0f);   
  10.   
  11.    if((keys & GameCanvas.UP_PRESSED) != 0)              //摄影机上移   
  12.   
  13.         camTrans.postTranslate(0.0f, 1.0f, 0.0f);   
  14.   
  15.    if((keys & GameCanvas.DOWN_PRESSED) != 0)            //摄影机下移   
  16.   
  17.         camTrans.postTranslate(0.0f, -1.0f, 0.0f);   
  18.   
  19.    if((keys & GameCanvas.LEFT_PRESSED) != 0)            //摄影机左转   
  20.   
  21.         camTrans.postRotate(50.0f, 1.0f, 0.0f);   
  22.   
  23.    if((keys & GameCanvas.RIGHT_PRESSED) != 0)           //摄影机右转   
  24.   
  25.         camTrans.postRotate(-50.0f, 1.0f, 0.0f);   
  26.   
  27.    camera.setTransform(camTrans) ;   
  28.   
  29. }   
  30.   
  31. 游戏线程中调用场景绘制和键盘输入方法,代码如下:   
  32.   
  33. public void run() {   
  34.   
  35.    while(true) {   
  36.   
  37.      try {   
  38.   
  39.          input();                                         //键盘输入   
  40.   
  41.          draw(getGraphics());                             //绘制场景   
  42.   
  43.          flushGraphics();                                 //刷新屏幕   
  44.   
  45.          try{ Thread.sleep(30); } catch(Exception e) {}   //休眠30ms   
  46.   
  47.      }catch(Exception e) {   
  48.   
  49.         e.printStackTrace();   
  50.   
  51.      }   
  52.   
  53.    }   
  54.   
  55. }   
  56.   

编译、运行程序,其结果如图10-8所示。

(图)j2me 3D开发-基本地形渲染技术

不同的地图能构造不同场景,读者可以自行创建多种有趣的地图,如图10-9所示是各种不同的场景。

(图)j2me 3D开发-基本地形渲染技术

10.1.10  地图纹理的应用
等高地图创建的场景如果铺上纹理将显得更真实。在HeightMap类中添加createTexture方法来创建纹理,代码如下:

Java代码
  1. private static void createTexture(){   
  2.   
  3.    Image2D waterIm = null;   
  4.   
  5.    try {   
  6.   
  7.        waterIm = (Image2D)Loader.load("/texture1.PNG")[0];   //加载纹理图片   
  8.   
  9.    }catch (Exception e){ System.out.println("Cannot make image " ); }   
  10.   
  11.                                                          //捕捉异常   
  12.   
  13.    texture = new Texture2D(waterIm);                      //根据纹理图片创建纹理   
  14.   
  15.    texture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST);   
  16.   
  17.    texture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);   
  18.   
  19. }   
  20.   
  21. 在创建四边形时在顶点缓冲中添加纹理坐标,并且在外观属性中添加纹理,代码如下:   
  22.   
  23. public static Mesh createQuad(short[] heights)   
  24.   
  25. {   
  26.   
  27.    …   
  28.   
  29.    short[] TEXCOORDS = {   
  30.   
  31.              0,0,   1,0,   
  32.   
  33.              1,1,   0,1     
  34.   
  35.    };   
  36.   
  37.    TEXCOORD_ARRAY = new VertexArray(TEXCOORDS.length/222);  //创建纹理数组   
  38.   
  39.    TEXCOORD_ARRAY.set(0, TEXCOORDS.length/2, TEXCOORDS);        //设置纹理数组   
  40.   
  41.    vertexBuffer.setTexCoords(0, TEXCOORD_ARRAY, 1.0f, null);   
  42.   
  43.                                                      //顶点缓冲中添加纹理数组   
  44.   
  45.    createTexture();                                  //创建纹理   
  46.   
  47.    appearance.setTexture(0, texture);                //在外观属性中添加纹理   
  48.   
  49.    …   
  50.   
  51. }   
  52.   

不同的纹理使地形呈现不同的地貌,例如沙漠、草地和雪山,编译、运行程序,其结果如图10-10所示。

(图)j2me 3D开发-基本地形渲染技术

 本章将讲述3D中常用的一些技术:包括HeightMap高度地图、粒子系统和碰撞检测技术。

HeightMap是地形的输入数据,可以理解为位图,一个2D矩阵,和位图不同的是,把元素的颜色值映射为高度值,建立HeightMap的方法有很多,这里使用灰度图来创建高度地图。

粒子系统在模仿自然现象、物理现象及空间扭曲上具备得天独厚的优势。每一种粒子系统都有一些相似的参数,但也都存在差异,如何优化它们的性能并运用于3D创作领域就看开发者的想象力了。

碰撞检测没有很标准的理论,但都建立在2D的基础上,这里沿用了AABB或者OBB的检测方法,或者先用球体做粗略的检测,然后用AABB和OBB作精细的检测。BSP技术并不广泛适用,但是能提高效率,本章没有太多的涉及。

本章主要包括以下几个方面的内容。

n     理解怎样通过灰度图产生顶点和三角面组成立体地图。

n     在高低不平的地形中使用摄影机来模拟行走或跑。

n     使用粒子系统构造火焰、爆炸等常用特效。

n     使用球体、AABB和OBB技术进行碰撞检测。

10.1  基本地形渲染技术
什么是HeightMap呢?所谓高度图实际上就是一个2D数组。创建地形为什么需要高度图呢?可以这样考虑,地形实际上就是一系列高度不同的网格而已,这样数组中每个元素的索引值刚好可以用来定位不同的网格(x,y),而所储存的值就是网格的高度(z)。

10.1.1  HeightMap简介
现实中的地形是真实的,不是由三角平面模拟的,但是3D图形图像处理中常常使用三角形来代替地形的表面,每个三角形的顶点高度在山脉到山谷之间转换,模拟自然地形,如图10-1所示。在这个过程中,还将应用纹理展示沙滩、丘陵和雪山。

图10-1  模拟地图

HeightMap技术的灵感来源于等高地图的绘制,如图10-2所示,是一幅等高地图,它通常用来描绘高低起伏比较大的地形。

图10-2  等高地图

例如,飞行员必须了解哪里有高海拔的障碍物、山川、湍流的方向等,以便安全地飞行。从空中往下看,陆地上可能很平坦,但事实上等高线是展开的,没有相当的理解力和想象力,许多人并不能很好地领会地图实际提供的信息。

高度从平面图上无法立体地显现,所以用有规律的间隔来表示海拔,通常根据测量方法的不同一般间隔为50英尺或10m,同等位置的点被连成线,即等高线。

许多情况下这些等高线聚在图上形成封闭的环线,有些部分为不规则的心形,不时会凸出来一点。如果它们突然中断与其他线相冲突,则表示有高度的突然变化,事实上为悬崖或很深的落瀑。

自然界中能看到的惟一等高线只有沿着海岸的水平线(由于海潮的变化,事实上那也不是完全意义上的等高线),但可以把等高线想象成如同水平桌面的边线,如果把衣物或其他东西堆在桌面上,就如同小山峰或其他形状。

在这些等高线之间具体地形如何,都没有表示出来。等高线之间也不一定就是斜坡,可能是洞*、凸起的岩石,以及其他各种高度变化在10m之内的地势。从等高线的相应位置,可以简单地猜测出地表大概会是如何变化的。

等高线地图可以形象地反映山体的情况,如图10-3所示是实际山体与等高线地图的对应表示。等高线地图在军事、旅游、探矿中有着广泛的应用。

图10-3  几种高度地图

等高线之间的间隔只是表明同一理论高度下地平线上点之间的距离,并非是地面山坡上点间的实际距离,它们只是用来表明相应的位置,并非根据地平面的比例。

常见的一种错误想法是,一群等高线是按地图绘制比例缩小的地面高度——要知道典型的旅行地图比例是1∶50000,10m在图上只有0.02mm。图上间距5mm的等高线在地表面的距离为250m,而不是实际代表的10m落差,差距是1∶25。

理解等高线,就可以说掌握了一半的HeightMap技术。在3D地形渲染中,采用数组的形式来保存高度信息(数组内容可以通过高度图读取,图片上的一个像素或者一个区域代表同一高度),根据高度信息在不同的位置绘制多边形,从而通过2D图像展示出3D地形场景。

高度图可以使用画图板或者图像编辑器Adobe Photoshop产生。使用图像编辑器可能更容易,它能够帮助创建想要的交互地形,另外也可以通过图形编辑特性,例如过滤,创建有趣的高度图。

如图10-4所示是根据高度图“恢复”的3D山体地貌立体地图。

10.1.2  海岛地图的原理
从上面对等高图的阐述,可以知道,地形是真实世界的一个模型,有平原、山脉、河流、悬崖和丘陵等。以抽象角度来看,可以简单地认为地形仅仅是高度上的变化。

例如,一个草原就是一个高度基本为常数的地形(除了可能有一些起伏和山丘外);一个山区或者鸿沟是高低落差比较大的地形;一条河流就是由一个高地势平原和穿过它的曲线组成,这个曲线比它周围的地形高度稍低。

如图10-5所示,是一系列的高度地图,简单来看,它是一个像素的集合,每个像素都是在灰度上0~255之间变化,0是黑色,255是全白,读者也可以自己绘制高度地图。可以判断,明暗反差越大,地势高低起伏也越大。

图10-5  高度地图

说明
 在灰度图像中,像素灰度级用8b表示,所以每个像素都是介于黑色和白色之间的256(28=256)种灰度中的一种。灰度图像只有灰度颜色而没有彩色。通常所说的黑白照片,其实包含了黑白之间的所有灰度色调。从技术上来说,就是具有从黑到白的256种灰度色域(Gamut)的单色图像。

10.1.3  地图元素图元
要将高度地图转换为场景,只需要读取图片的像素,然后根据像素的值设置平面的高度,最常用的平面是四边形。因为四边形是规则的,可以采用数组方便地统一创建和管理(并且能实现地图的无缝)。四边形由两个三角形组成,多个四边形组成了场景地图,如图10-6所示。

图10-6  四边形网格构成场景

10.1.4  读取元素数据
为了将高度地图转换为场景,创建一个HeightMap类。要创建等高地图,首先应当读取图片数据,getRGB方法可以将颜色存储到数组中。

整型变量imgw和imgh用来保存图片文件的大小,resolution表示1个像素所表示的四边形个数,例如这里设置为0.25,则表示一个四边形涵盖了4个像素的颜色信息。

随着resolution的增大,使用了更多的四边形来代表这个地形,地形的平滑度也在不断地提高,然而,也在增加内存的使用空间和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];                         //根据文件大小创建数据

   img.getRGB(data, 0, imgw, 0, 0, img.getWidth(),imgh); //将颜色信息保存到数组中

   mapWidth = (int)(resolution * imgw);                 //场景数组的宽度(列数)

   mapHeight = (int)(resolution * imgh);                //地图数组的高度(行数)

   heightMap = new short[mapWidth * mapHeight];         //创建地图数组

   int xoff = imgw / mapWidth;                   //x方向上图片和场景地图的映射量

   int yoff = imgh / mapHeight;                  //x方向上图片和场景地图的映射量

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

       }

   }

}

10.1.5  构造四边形图元
HeightMap类使用createQuad方法创建四边形,四边形由4个顶点组成,每一个顶点都有一个变化的y坐标,使用heights数组设置,但是x和z不变(之后根据需要进行水平平移)。

四边形面的4个顶点的颜色根据位置线性变化,从而显现出不同高度的风貌,并且顶点的颜色在整个平面上以内插值替换,创建一个平滑的外观。创建四边形的代码如下:

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] / 1550.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;

}

10.1.6  根据地图构造四边形组
在读取图片颜色数据和能够创建单个四边形网面之后,就可以根据等高图片设置的不同高度批量创建四边形来构成场景。

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[3] = 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();

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); //缩小网格

        map[x][y].setTransform(localTransform);        //将变换矩阵应用到网格上

    }

}

HeightMap类的构造方法负责调用上面的方法来根据高度图片构造3D网面场景,代码如下:

public HeightMap() throws IOException

{

   if(resolution <= 0.0001f || resolution > 1.0f)  //检查resolution是否合理

                           throw new IllegalArgumentException("Resolution too small or too large");

   loadMapImage();                           //加载等高图片并将颜色数据保存到数组中

   createQuads();                            //根据高度创建多个四边形

   setTransform();                           //将四边形平移到场景相应位置

}

HeightMap类根据等高图创建场景的流程如图10-7所示。

图10-7  等高图创建场景

HeightMap类提供了一些公共方法给游戏画布类访问,在创建整个场景时以便将网面添加成为场景的一部分。

public Mesh[][]getQuads(){   return map;  }

public int getMapWidth(){    return mapWidth;}

public int getMapHeight(){    return mapHeight; }

10.1.7  构造游戏场景
游戏画布的构造方法负责创建场景和启动游戏线程。游戏画布继承了GameCanvas类,并引用了Runnable接口。场景包括摄影机、背景、等高地图和海平面,海平面由多个覆盖纹理的正方形组成。构造场景的代码如下:

World world = new World();                               //创建场景对象

try{

   heightmap = new HeightMap();                          //创建等高地图

} catch(Exception e){                                    //捕捉异常

   System.out.println("Heightmap error: " + e.getMessage());

   e.printStackTrace();

}

Mesh[][] 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 camera = new Camera();                          //创建摄影机

float aspect = (float) getWidth() / (float) getHeight();    //计算屏幕宽高比

camera.setPerspective(60.0f, aspect, 0.1f, 150.0f);    //设置投影参数

Transform camTrans = new Transform();                  //摄影机变换矩阵

camTrans.postTranslate(0.0f, 5.0f, 0.0f);              //摄影机平移到相应位置

camera.setTransform(camTrans);                         //移动摄影机

world.addChild(camera);                                //将摄影机添加到场景

world.setActiveCamera(camera);                   //将摄影机设置为场景的活动摄影机

addWater();                                            //添加海平面

Background back = new Background();                    //背景

back.setColor(0xFF0000FF);                             //设置背景的颜色

world.setBackground(back);                             //将背景添加到场景中

Thread t = new Thread(this);                           //创建游戏线程

t.start();                                             //启动线程

10.1.8  绘制场景
draw方法负责将场景绘制屏幕上,代码如下:

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

   }

}

10.1.9  键盘输入
input方法根据键盘状态对摄影机进行平移和旋转变换,Fire键将摄影机前推,上下键将摄影机上下移动,左右键将摄影机左右旋转,代码如下:

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

}

游戏线程中调用场景绘制和键盘输入方法,代码如下:

public void run() {

   while(true) {

     try {

         input();                                         //键盘输入

         draw(getGraphics());                             //绘制场景

         flushGraphics();                                 //刷新屏幕

         try{ Thread.sleep(30); } catch(Exception e) {}   //休眠30ms

     }catch(Exception e) {

        e.printStackTrace();

     }

   }

}

编译、运行程序,其结果如图10-8所示。

图10-8  等高图构造场景

不同的地图能构造不同场景,读者可以自行创建多种有趣的地图,如图10-9所示是各种不同的场景。

图10-9  各种不同的场景

10.1.10  地图纹理的应用
等高地图创建的场景如果铺上纹理将显得更真实。在HeightMap类中添加createTexture方法来创建纹理,代码如下:

private static void createTexture(){

   Image2D waterIm = null;

   try {

       waterIm = (Image2D)Loader.load("/texture1.PNG")[0];   //加载纹理图片

   }catch (Exception e){ System.out.println("Cannot make image " ); }

                                                         //捕捉异常

   texture = new Texture2D(waterIm);                      //根据纹理图片创建纹理

   texture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST);

   texture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);

}

在创建四边形时在顶点缓冲中添加纹理坐标,并且在外观属性中添加纹理,代码如下:

public static Mesh createQuad(short[] heights)

{

   …

   short[] TEXCOORDS = {

             0,0,   1,0,

             1,1,   0,1 

   };

   TEXCOORD_ARRAY = new VertexArray(TEXCOORDS.length/2, 2, 2);  //创建纹理数组

   TEXCOORD_ARRAY.set(0, TEXCOORDS.length/2, TEXCOORDS);        //设置纹理数组

   vertexBuffer.setTexCoords(0, TEXCOORD_ARRAY, 1.0f, null);

                                                     //顶点缓冲中添加纹理数组

   createTexture();                                  //创建纹理

   appearance.setTexture(0, texture);                //在外观属性中添加纹理

   …

}

不同的纹理使地形呈现不同的地貌,例如沙漠、草地和雪山,编译、运行程序,其结果如图10-10所示。

图10-10  不同纹理呈现的不同地图

10.1.11  摄影机游历场景图
在高低起伏的地面漫游,为了模拟真实情况,要求视点高度随地形变化而变化,否则在遇到地面高度大于视点高度时,就直接钻入地下了。在漫游时动态改变视点高度,可以形成翻山越岭的真实效果,同样,当往地面场景中加入其他物体时也需要在高度上进行定位。

在计算某点的高度时,首先计算该点所在下方的四边形的4个顶点的高度,高度可以从地图数组中读取,计算该点的高度需要用到双线性插值算法。

为了方便理解,先考虑1D情况下的线性插值。对于一个数列c,假设c[a]到c[a+1]之间是线性变化的,那么对于浮点数x(a<=x<a+1),c(x)=c[a+1]*(x-a)+c[a]*(1+a-x)。

然后把这种插值方式扩展到2D情况:对于一个2D数组c,假设对于任意一个浮点数i,c(a, I)到c(a+1, I)之间是线性变化的,c(I, b)到c(I, b+1)之间也是线性变化的(a, b都是整数),那么对于浮点数的坐标(x,y)满足(a<=x<a+1,b<=y<b+1),可以先分别求出c(x, b)和c(x,b+1)。

c(x,b)= c[a+1][b]*(x-a)+c[a][b]*(1+a-x)

c(x,b+1)= c[a+1][b+1]*(x-a)+c[a][b+1]*(1+a-x)

现在已经知道c(x, b)和c(x, b+1)了,而根据假设c(x, b)到c(x, b+1)也是线性变化的,所以:c(x, y)= c(x, b+1)*(y-b)+ c(x, b)*(1+b-y)。

根据以上算法得出根据某点在XZ平面上的位置计算高度的程序,代码如下:

 

Java代码
  1. float getHeight(float x,float z){   
  2.   
  3.     int Col0 = int(x/MAP_SCALE);               //所在x坐标映射到地图数组的列号   
  4.   
  5.     int Row0 = int(z/MAP_SCALE);               //所在y坐标映射到地图数组的行号   
  6.   
  7.     nt Col0 = Col0+1;                          //相邻列    
  8.   
  9.     int Row0 = Row0+1;                         //相邻行   
  10.   
  11.     if(Col1>MapWidth)Col1=0;   
  12.   
  13.     if(Row1>MapHeight)Row1=0;   
  14.   
  15.     float heights_00 = heightMap[Col0 + Row0 * mapWidth];   
  16.   
  17.     float heights_01 = heightMap[Col1 + Row0 * mapWidth + 1];   
  18.   
  19.     float heights_11 = heightMap[Col1 + Row1 * mapWidth];   
  20.   
  21.     float heights_10 = heightMap[Col0 + Row1 * mapWidth + 1];   
  22.   
  23.     float tx = x/MAP_SCALE - Col0;             //块内x偏移   
  24.   
  25.     float tz = z/MAP_SCALE - Row0;             //块内z偏移   
  26.   
  27.     float txty = tx * ty;                      //双线性插值(内插)计算   
  28.   
  29.     float height = heights_00*(1.0f+txty -tx -ty)   
  30.   
  31.                            + heights_01*(tx - txty)   
  32.   
  33.                            + heights_11*txty   
  34.   
  35.                            + heights_10*(tx-txty); //插值计算的结果,为所求的高度   
  36.   
  37.     return height;   
  38.   
  39. }   
  40.   

程序中的参量如图10-11所示。

(图)j2me 3D开发-基本地形渲染技术

在求得的平面高度上再增加一个分量,代表人眼距离地面的距离,随着山势的高低,视点的高度也发生变化,这样能够更加真实地模拟草地场景,如图10-12所示。

(图)j2me 3D开发-基本地形渲染技术

阅读更多
个人分类: JavaME 3D
上一篇JList右击选中,焦点设置问题
下一篇eclipseME调试的设置
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭