在“使用M3G(JSR184)进行移动3D编程”入门指南系列的前四部分的基础上,Mikael Baros,Redikod的高级程序员,现在将指导你学习heightmap和地形渲染的基础知识。
下面是指南前四部分的链接:
l 第一部分:快速进入移动java 3D编程世界
l 第二部分:光的3D理论与定位
l 第三部分:粒子系统和立即模式渲染
l 第四部分:M3G内建碰撞、光照物理学和照相机视点
介绍
欢迎来到M3G指南系列的第五部份。今天我将向你展示一种简单的技术,它几乎在所有的3D游戏中都用得到(以一种或者另外一种形式),heightmap
通过使用heightmap,设计者/开发者能够容易地创建自然地形(可能相当于使用一个花边噪音生成器,但不是必须的),几乎不花费时间。Heightmap的魅力在于它有一个复杂的概念,例如一个漂亮的现实的3D地形,则把这个问题简化为一个简单的2D图像。
和以前一样,无论什么时候你感到了困惑就参考这里。
首先,或许也是最重要的,就是在索尼爱立信开发者世界上专业的移动Java 3D网络区。其次,如果你碰到困难,就去索尼爱立信移动Java 3D论坛。对于其他的任何情况,使用索尼爱立信开发者世界网络门户,在那里你可以找到你的问题的答案,并且可以了解到更多。
预备知识
在通读指南的这一部分之前你应该已经阅读过前四部分,因为我将在这里使用前面已经写过的代码。
3D地形
我们先来对地形进行定义,好吗?地形是真实世界的一个模型,有平原、山脉、河流、悬崖和丘陵等。地形的要点就是给用户如在“真实”或者“现实”世界中漫步的印象。然而,如果从一个更加抽象的观点看,我们会很快的意识到地形仅仅是在高度上的变化。例如,一个草原就是一个高度为常数的地形(除了可能有一些起伏和山丘外)。一个山区就是一个具有很大高度变化的地形,在空地间创建鸿沟,如此创建山区的幻觉。一个河流就是由一个平原和穿过它的曲线组成,这个曲线比它周围的平原高度稍低。检查这张地形图:
如你所见,上面的地形由三块高一些的区域描述(三个灰色的山丘),剩下的是一个深深的峡谷,里面充满了水。此外,除了高度变化没有什么。
Heightmap
下面就是heightmap了。它们是存储高度变化的和使表面平滑的上好解决方案。在我开始讲解有启发的东西之前,现在我们观察这个图片。
这个一个灰度等比变化的图像。没有任何奇特的。它看起来像一个中间有个白色斑点的圆环。如此看来什么是一个灰色等比变化的图像呢?那么,简单来看,它是一个像素的集合,每个像素都是在灰度上从0到255上等比变化,0是黑色,255是全白。对吗?听起来很熟悉吧?那么如果你使用像素确定高度,如何?如果黑色的像素(值0)是最小的高度,白色的像素(值255)是最大的高度,如此你就有了一个描述高度的地图,heightmap!对此另外一个重要的事情就是,如果你只想模糊图像,因为像素是从0到255,所以你要对地形自动插补。
那么,你所有需要做的就是打开最中意的图像程序,使用画笔画一些白色素材,然后建立一个heightmap。这听起来很容易,当然,但是我们怎么样将它转化为可以渲染的网面呢?
Quad
将一个heightmap转化为可以渲染的网面一点都不难。我们需要读取heightmap的像素,然后创建由高度变化反应出的平面。在这个问题上一个非常易用的平面当然是Quad(四边形)。因为所有图像都是规则的矩形,四方形能够很好地适合他们。那么Quad是什么呢?很简单,一个Quad就是两个三角形放在一起形成一个矩形平面。
上面的图像代表了由两个三角形组成的一个四边形。如你所见,这个四边形有四个脱节的端点,因为我们使用了两个三角形描述它。四个角都被给予不同的高度,如此我们就开始了在3D世界中描述高度。然而一个四边形描述整个地形还是远远不够的,如果想要地形看起来有一点真实的感觉,需要很多四边形。我们稍后再来讲解这些,首先来看如何使用代码创建一个四边形。我们将在xz平面上创建一个有y变量的四边形,因而有变化的高度。我们为前篇指南中创建的MeshFactory类引入一个新的方法,叫做createQuad。这个方法需要知道的就是在这个四边形不同的角上有不同的高度,和选择标志。下面是这个方法的第一个片段:
public static Mesh createQuad(short[] heights, int cullFlags)
{
// The vertrices of the quad
short[] vertrices = {-255, heights[0], -255,
255, heights[1], -255,
255, heights[2], 255,
-255, heights[3], 255};
看起来熟悉吗?一个普通的四边形由四个顶点组成,每一个顶点都有一个变化的y坐标,但是x和z不变。
// Create the model's vertex colors
VertexArray colorArray = new VertexArray(color.length/3, 3, 1);
colorArray.set(0, color.length / 3, color);
// Compose a VertexBuffer out of the PRevious vertrices and texture coordinates
VertexBuffer vertexBuffer = new VertexBuffer();
vertexBuffer.setPositions(vertexArray, 1.0f, null);
vertexBuffer.setColors(colorArray);
// Create indices and face lengths
int indices[] = new int[] {0, 1, 3, 2};
int[] stripLengths = new int[] {4};
// Create the model's triangles
triangles = new TriangleStripArray(indices, stripLengths);
这里我们创建在M3G系统中描述一个网面所需要的数组。VertexBuffer保存两个顶点数组、颜色数组和位置数组。我在上面代码中有意省去对颜色数组的分配,因为我将在稍后讨论。现在我们把注意力放在创建四边形上。
// Create the appearance
Appearance appearance = new Appearance();
PolygonMode pm = new PolygonMode();
pm.setCulling(cullFlags);
pm.setPerspectiveCorrectionEnable(true);
pm.setShading(PolygonMode.SHADE_SMOOTH);
appearance.setPolygonMode(pm);
这是一些标准的Appearance材料,然而我想让你明白如何使用平滑阴影,这就意味着顶点的颜色在整个平面上以内插值替换,创建一个平滑的外观。我们需要在以后说明为什么。现在剩下的就是创建网面,相当直截了当:
// Finally create the Mesh
Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
从Heightmap创建四边形
使用上面的方法,我们可以创建一个高度变化的四边形,但是如我前面所说,我们需要很多四边形来使地形看起来更真实,那么现在的问题就是如何将heightmap转化为四边形。这实际上并不算问题。看这个图片:
我在前面的heightmap上面画了一个白色的栅格。如果观察栅格每一个小块,你会发现矩形的格子区域就是另外一个heightmap,只是小了些。如果我们创建一个分辨率很高的栅格,你可能会意识到格子的区域变得非常小,这样也非常容易接近一个四边形。把它简单化,为了接近一个heightmap,我们将它划分成很多十分小的部分,每一部分代表一个四边形。我们如何创建一个四边形呢?简单,下面就是必须的步骤:
l 划分图像为很多小部分(最小尺寸为2*2像素)
l 得到每一部分的角的像素,读取它们的值(0-255)
l 将这些值作为高度赋给四边形(参考方法声明)
那么,从heightmap创建四边形实在是简单。在创建后,你只需渲染这些四边形,一个接一个。现在有些事情你必须了解。随着heightmap栅格分辨率的不断增加,而地形的平滑度也在不断的提高,因为你使用了更多的四边形来代表这个地形。然而,你也在激剧增加内存的使用空间和GPU需要运算的多边形的数量。这是在每一个移动电话都需要依据可用内存、GPU运算能力等等进行平衡的。
实现
我们来看在M3G中如何实现一个heightmap。我们已经有了一个方法可以根据不同的高度来创建四边形,那么我们需要作的就是:
1、 装载一个heightmap
2、 建立一个数组,等比划分栅格的尺寸
3、 从heightmap中读取像素值,并且保存在一个新的数组里
4、 使用上述数组根据不同的高度产生四边形(Quads)
这是一个简单的四步程序。来,我们先来查看HeightMap类的私有成员:
// The actual heightmap containing the Y-coords of our triangles
private short[] heightMap;
private int[] data;
private int imgw, imgh;
// Map dimensions
private int mapWidth;
private int mapHeight;
// Actual quads
private Mesh[][] map;
// Water
private Mesh water;
// Local transform used for internal calculations
private Transform localTransform = new Transform();
它们是相当地自明,但是我将讲解其中的一部分。首先heightMap数组是保存高度的等比数组。它不保存来自heightmap图像中的像素。Mesh保存产生的所有要渲染的四边形。最后,water网面是一个普通的平面,代表地形中的水(用来创建河流等等)。然后,我们看如何创建一个HeightMap:
public HeightMap(String imageName, float resolution, int waterLevel) throws IOException
{
// Check for invalid resolution values
if(resolution <= 0.0001f resolution > 1.0f)
throw new IllegalArgumentException("Resolution too small or too large");
// Load image and allocate the internal array
loadImage(imageName, resolution);
// Create quads
createQuads();
// Create the water
createWater(waterLevel);
}
我们将上面的代码划分为几个步骤。首先检查非法的分辨率值。非法值大于1.0f(一个四边形有四个角,因此最小的栅格区域就是2*2)或者小于0.0001f(一个非常低的分辨率就是或多或少的使用一个四边形创建整个地形)。
接下来我们要装载图像,作为一个构造器参数提供。然而,我想你要明白在loadImage方法中还有一些其它有趣的事情。代码如下:
// Load actual image
Image img = Image.createImage(path);
// Allocate temporary memory to store pixels
data = new int[img.getWidth() * img.getHeight()];
// Get its rgb values
img.getRGB(data, 0, img.getWidth(), 0, 0, img.getWidth(), img.getHeight());
imgw = img.getWidth();
imgh = img.getHeight();
// Clear image
img = null;
System.gc();
// Calculate new width and height
mapWidth = (int)(res * imgw);
mapHeight = (int)(res * imgh);
// Allocate heightmap
heightMap = new short[mapWidth * mapHeight];
// Calculate height and width offset into image
int xoff = imgw / mapWidth;
int yoff = imgh / mapHeight;
// Set height values
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();
我将不再讲述上面代码的细节,因为我希望你能把它作为一个练习来审查。总之,我们先将实际的图像装载到内存中,然后抽取它的像素值。接着利用构造函数提供的分辨率参数,我们依据尺寸创建一个栅格,并且使用像素值进行填充。最后我们做一个手动的垃圾收集释放不必要的数据。这是非常重要的,因为loadImage方法是一个大量使用内存的方法,我们想要确保被接下来的少许认为所需要的至关重要的内存不被垃圾数据占有。
构造函数中的下一个方法就是createQuads。这是一个十分易懂的方法,得到生成的heightMap数组并且从其中创建四边形。我们来看它的具体内容:
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++)
{
// Set heights
setQuadHeights(heights, x, y, mapWidth);
// Create mesh
map[x][y] = MeshFactory.createQuad(heights, PolygonMode.CULL_NONE);
}
}
}
如你所见,我们所有需要做的就是反复遍历heightMap表和抽取四个值,在MeshFactory.createQuad方法中我们作为高度值使用。
我要留给你的是检查createWater方法。在这一点上它应该是你烂熟于胸的东西。我们仅仅使用MeshFactory.createPlane方法创建一个具有水面纹理的大平面。
渲染
如何渲染生成的四边形呢?你应该知道这个问题的答案,不过无论如何我们还是来看看HeightMap类的render方法。如下:
public void render(Graphics3D g3d, 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.0f, 0.0f, (mapHeight - y) * -5.0f);
localTransform.postMultiply(t);
g3d.render(map[x][y], localTransform);
}
}
localTransform.setIdentity();
localTransform.postScale(255, 255, 255);
localTransform.postRotate(-90, 1.0f, 0.0f, 0.0f);
g3d.render(water, localTransform);
}
所有你需要做的就是遍历四边形表,并对它们在空间中给定的位置上进行渲染。Render方法的使用者可能会在本地变换后提供一个应用于每个四边形的变换,本地变化仅仅是将每一个四边形放在自己的位置上。最后,我们将water网面放在heightmap创建期间定义好的高度上。
集合所有
现在使用称心的HeightMap类需要的事情如下:
1、 从一个现有的灰度等变化图像装载一个HeightMap
2、 渲染
听起来很简单吧?因为的确是这样。我们来看看装载HeightMap的代码:
private void createScene()
{
try
{
// We're using a pretty high resolution. If you want to test this on an actual
// handheld, try using a lower resolution, sUCh as 0.20 or 0.10
hm = new HeightMap("/res/heightmap4.png", 0.30f, 40);
t.postTranslate(0.0f, -2.0f, -5.0f);
t.postScale(0.01f, 0.01f, 0.01f);
camTrans.postTranslate(0.0f, 5.0f, 0.0f);
//camTrans.postTranslate(0.0f, 5.0f, 2.0f);
}
catch(Exception e)
{
System.out.println("Heightmap error: " + e.getMessage());
e.printStackTrace();
TutorialMidlet.die();
}
}
这里没有任何陌生的。我们只是装载heightmap,并且对它进行一些变换,因为它将是提供给HeightMap渲染方法的变换。我们只是想让它在屏幕后面向上一点点。我们还应该以很大数量测量它,因为一个地形通常是无限的,但是我只想你看到一个小的总揽图。
另外一件重要的事情就是这篇指南中的HeightMap完全没有使用任何剔出渲染。这是很必要,特别是很大的地形。然而为了使代码保持清晰,我选择移除任何种类的空间分割或者软件剔出。你可以把它作为一个练习,只发送网面给可见的渲染者(也就是说,没有网面离得太远,或者在照相机后面)。
最后,渲染HeightMap的代码是什么呢?下面就是主要的draw方法:
// Get the Graphics3D context
g3d = Graphics3D.getInstance();
// First bind the graphics object. We use our pre-defined rendering hints.
g3d.bindTarget(g, true, RENDERING_HINTS);
// Clear background
g3d.clear(back);
// Bind camera at fixed position in origo
g3d.setCamera(cam, camTrans);
// Render everything
hm.render(g3d, t);
// Check controls for camera movement
if(key[UP])
{
camTrans.postTranslate(0.0f, 1.0f, 0.0f);
}
if(key[DOWN])
{
camTrans.postTranslate(0.0f, -1.0f, 0.0f);
}
if(key[LEFT])
{
camTrans.postRotate(5, 0.0f, 1.0f, 0.0f);
}
if(key[RIGHT])
{
camTrans.postRotate(-5, 0.0f, 1.0f, 0.0f);
}
// Fly forward
if(key[FIRE])
camTrans.postTranslate(0.0f, 0.0f, -1.0f);
真是没有多说的了。HeightMap.render(g3d,t)方法相当干净和直观。尽管控制对你来说可能有些多余。你可以使用操纵杆移动照相机。上、下和旋转左、右。使用FIRE键向前移动照相机。
总结
那么,为了延伸这个课程,你为什么不试着装载源代码zip文件中提供的其它的heightmap呢?看看出现了什么样的地形。甚至更近一步,创建你自己的heightmap图像!
放飞你的思绪,把它放在MIDlet中,在你的风景中巡游!
TutorialMidlet
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
public class TutorialMidlet extends MIDlet implements CommandListener
{
// A variable that holds the unique display
private Display display = null;
// The canvas
private M3GCanvas canvas = null;
// The MIDlet itself
private static MIDlet self = null;
/** Called when the application starts, and when it is resumed.
* We ignore the resume here and allocate data for our canvas
* in the startApp method. This is generally very bad practice.
*/
protected void startApp() throws MIDletStateChangeException
{
// Allocate
display = Display.getDisplay(this);
canvas = new M3GCanvas(30);
// Add a quit command to the canvas
// This command won't be seen, as we
// are running in fullScreen mode
// but it's always nice to have a quit command
canvas.addCommand(new Command("Quit", Command.EXIT, 1));
// Set the listener to be the MIDlet
canvas.setCommandListener(this);
// Start canvas
canvas.start();
display.setCurrent(canvas);
// Set the self
self = this;
}
/** Called when the game should pause, such as during a call */
protected void pauseApp()
{
}
/** Called when the application should shut down */
protected void destroyApp(boolean unconditional) throws MIDletStateChangeException
{
// Method that shuts down the entire MIDlet
notifyDestroyed();
}
/** Listens to commands and processes */
public void commandAction(Command c, Displayable d) {
// If we get an EXIT command we destroy the application
if(c.getCommandType() == Command.EXIT)
notifyDestroyed();
}
/** Static method that quits our application
* by using the static field 'self' */
public static void die()
{
self.notifyDestroyed();
}
}
M3GCanvas
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.m3g.Background;
import javax.microedition.m3g.Camera;
import javax.microedition.m3g.Graphics3D;
import javax.microedition.m3g.Transform;
public class M3GCanvas
extends GameCanvas
implements Runnable {
// Thread-control
boolean running = false;
boolean done = true;
// If the game should end
public static boolean gameOver = false;
// Rendering hints
public static final int STRONG_RENDERING_HINTS = Graphics3D.TRUE_COLOR Graphics3D.DITHER;
public static final int WEAK_RENDERING_HINTS = 0;
public static int RENDERING_HINTS = STRONG_RENDERING_HINTS;
// Key array
boolean[] key = new boolean[9];
// Global identity matrix
Transform identity = new Transform();
// Global Graphics3D object
Graphics3D g3d = null;
// The background
Background back = null;
// The global camera object
Camera cam = null;
Transform camTrans = new Transform();
// Transforms
/** Constructs the canvas
*/
public M3GCanvas(int fps)
{
// We don't want to capture keys normally
super(true);
// We want a fullscreen canvas
setFullScreenMode(true);
// Create our scene
createScene();
// Load our camera
loadCamera();
// Load our background
loadBackground();
// Set up graphics 3d
setUp();
}
/** Prepares the Graphics3D engine */
private void setUp()
{
// Get the instance
g3d = Graphics3D.getInstance();
}
/** When fullscreen mode is set, some devices will call
* this method to notify us of the new width/height.
* However, we don't really care about the width/height
* in this tutorial so we just let it be
*/
public void sizeChanged(int newWidth, int newHeight)
{
}
/** Loads our camera */
private void loadCamera()
{
// Create a new camera
cam = new Camera();
// Set the perspective of our camera. On a handheld you might want to set the far plane
// (currently 150.0f) to a lower value such as 50.0f
cam.setPerspective(60.0f, (float)getWidth() / (float)getHeight(), 0.1f, 150.0f);
}
/** Loads the background */
private void loadBackground()
{
// Create a new background, set bg color to black
back = new Background();
back.setColor(0);
}
// The heightmap and its transform
HeightMap hm;
Transform t = new Transform();
/** Creates our scene */
private void createScene()
{
try
{
// We're using a pretty high resolution. If you want to test this on an actual
// handheld, try using a lower resolution, such as 0.20 or 0.10
hm = new HeightMap("/res/heightmap4.png", 0.30f, 40);
t.postTranslate(0.0f, -2.0f, -5.0f);
t.postScale(0.01f, 0.01f, 0.01f);
camTrans.postTranslate(0.0f, 5.0f, 0.0f);
//camTrans.postTranslate(0.0f, 5.0f, 2.0f);
}
catch(Exception e)
{
System.out.println("Heightmap error: " + e.getMessage());
e.printStackTrace();
TutorialMidlet.die();
}
}
/** Draws to screen
*/
private void draw(Graphics g)
{
// Envelop all in a try/catch block just in case
try
{
// Get the Graphics3D context
g3d = Graphics3D.getInstance();
// First bind the graphics object. We use our pre-defined rendering hints.
g3d.bindTarget(g, true, RENDERING_HINTS);
// Clear background
g3d.clear(back);
// Bind camera at fixed position in origo
g3d.setCamera(cam, camTrans);
// Render everything
hm.render(g3d, t);
// Check controls for camera movement
if(key[UP])
{
camTrans.postTranslate(0.0f, 1.0f, 0.0f);
}
if(key[DOWN])
{
camTrans.postTranslate(0.0f, -1.0f, 0.0f);
}
if(key[LEFT])
{
camTrans.postRotate(5, 0.0f, 1.0f, 0.0f);
}
if(key[RIGHT])
{
camTrans.postRotate(-5, 0.0f, 1.0f, 0.0f);
}
// Fly forward
if(key[FIRE])
camTrans.postTranslate(0.0f, 0.0f, -1.0f);
}
catch(Exception e)
{
reportException(e);
}
finally
{
// Always remember to release!
g3d.releaseTarget();
}
}
/** Starts the canvas by firing up a thread
*/
public void start() {
Thread myThread = new Thread(this);
// Make sure we know we are running
running = true;
done = false;
// Start
myThread.start();
}
/** Runs the whole thread. Also keeps track of FPS
*/
public void run() {
while(running) {
try {
// Call the process method (computes keys)
process();
// Draw everything
draw(getGraphics());
flushGraphics();
// Sleep to prevent starvation
try{ Thread.sleep(30); } catch(Exception e) {}
}
catch(Exception e) {
reportException(e);
}
}
// Notify completion
done = true;
}
/**
* @param e
*/
private void reportException(Exception e) {
System.out.println(e.getMessage());
System.out.println(e);
e.printStackTrace();
}
/** Pauses the game
*/
public void pause() {}
/** Stops the game
*/
public void stop() { running = false; }
/** Processes keys
*/
protected void process()
{
int keys = getKeyStates();
if((keys & GameCanvas.FIRE_PRESSED) != 0)
key[FIRE] = true;
else
key[FIRE] = false;
if((keys & GameCanvas.UP_PRESSED) != 0)
key[UP] = true;
else
key[UP] = false;
if((keys & GameCanvas.DOWN_PRESSED) != 0)
key[DOWN] = true;
else
key[DOWN] = false;
if((keys & GameCanvas.LEFT_PRESSED) != 0)
key[LEFT] = true;
else
key[LEFT] = false;
if((keys & GameCanvas.RIGHT_PRESSED) != 0)
key[RIGHT] = true;
else
key[RIGHT] = false;
}
/** Checks if thread is running
*/
public boolean isRunning() { return running; }
/** checks if thread has finished its execution completely
*/
public boolean isDone() { return done; }
}
MeshFactory
import java.io.IOException;
import javax.microedition.lcdui.Image;
import javax.microedition.m3g.Appearance;
import javax.microedition.m3g.Image2D;
import javax.microedition.m3g.IndexBuffer;
import javax.microedition.m3g.Light;
import javax.microedition.m3g.Material;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.PolygonMode;
import javax.microedition.m3g.Texture2D;
import javax.microedition.m3g.TriangleStripArray;
import javax.microedition.m3g.VertexArray;
import javax.microedition.m3g.VertexBuffer;
/**
* Static class that handles creation of code-generated Meshes
*/
public class MeshFactory
{
/** Creates a texture plane that is alpha-blended
*
* @param texFilename The name of the texture image file
* @param cullFlags The flags for culling. See PolygonMode.
* @param alpha The alpha value of blending. Is a full color in 0xAARRGGBB format
* @return The finished textured mesh
*/
public static Mesh createAlphaPlane(String texFilename, int cullFlags, int alpha)
{
// Create a normal mesh
Mesh mesh = createPlane(texFilename, cullFlags);
// Make it blended
MeshOperator.convertToBlended(mesh, alpha, Texture2D.FUNC_BLEND);
return mesh;
}
/**
* Creates a colored quad consisting of two triangles.
* The heights parameter is an array that holds the heights of the new quad.
* The array must have a length of 4 and goes counter-clockwise starting from the
* northwestern position of the quad (-1, -1)
* @param heights The height array, denoting the heights (y-coords) of the four corners.
* @param cullFlags
* @param texFilename
* @return
*/
public static Mesh createQuad(short[] heights, int cullFlags)
{
// The vertrices of the quad
short[] vertrices = {-255, heights[0], -255,
255, heights[1], -255,
255, heights[2], 255,
-255, heights[3], 255};
// The arrays
VertexArray vertexArray;
IndexBuffer triangles;
// Create the model's vertrices
vertexArray = new VertexArray(vertrices.length/3, 3, 2);
vertexArray.set(0, vertrices.length/3, vertrices);
// Allocate color array
byte[] color = new byte[12];
for(int i = 0; i < heights.length; i++)
{
int j = i * 3;
// Altitude check
if(heights[i] >= 1000)
{
byte col = (byte)(57 + (heights[i] / 1550.0f) * 70);
color[j] = col;
color[j + 1] = col;
color[j + 2] = col;
}
else
{
byte gCol = 110;
byte bCol = 25;
color[j] = 0;
color[j + 1] = (byte)(gCol - (heights[i] / 1000.0f) * 85);
color[j + 2] = (byte)(bCol - (heights[i] / 1000.0f) * 20);
}
}
// Create the model's vertex colors
VertexArray colorArray = new VertexArray(color.length/3, 3, 1);
colorArray.set(0, color.length / 3, color);
// Compose a VertexBuffer out of the previous vertrices and texture coordinates
VertexBuffer vertexBuffer = new VertexBuffer();
vertexBuffer.setPositions(vertexArray, 1.0f, null);
vertexBuffer.setColors(colorArray);
// Create indices and face lengths
int indices[] = new int[] {0, 1, 3, 2};
int[] stripLengths = new int[] {4};
// Create the model's triangles
triangles = new TriangleStripArray(indices, stripLengths);
// Create the appearance
Appearance appearance = new Appearance();
PolygonMode pm = new PolygonMode();
pm.setCulling(cullFlags);
pm.setPerspectiveCorrectionEnable(true);
pm.setShading(PolygonMode.SHADE_SMOOTH);
appearance.setPolygonMode(pm);
// Finally create the Mesh
Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
// All done
return mesh;
}
/**
* @param texFilename
* @return
* @throws IOException
*/
public static Texture2D createTexture2D(String texFilename) throws IOException {
// Open image
Image texImage = Image.createImage(texFilename);
Texture2D theTexture = new Texture2D(new Image2D(Image2D.RGBA, texImage));
// Modulated blending
theTexture.setBlending(Texture2D.FUNC_MODULATE);
// Set wrapping and filtering
theTexture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);
theTexture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST);
return theTexture;
}
/**
* Creates a textured plane.
* @param texFilename The name of the texture image file
* @param cullFlags The flags for culling. See PolygonMode.
* @return The finished textured mesh
*/
public static Mesh createPlane(String texFilename, int cullFlags)
{
// The vertrices of the plane
short vertrices[] = new short[] {-1, -1, 0,
1, -1, 0,
1, 1, 0,
-1, 1, 0};
// Texture coords of the plane
short texCoords[] = new short[] {0, 255,
255, 255,
255, 0,
0, 0};
// The classes
VertexArray vertexArray, texArray;
IndexBuffer triangles;
// Create the model's vertrices
vertexArray = new VertexArray(vertrices.length/3, 3, 2);
vertexArray.set(0, vertrices.length/3, vertrices);
// Create the model's texture coords
texArray = new VertexArray(texCoords.length / 2, 2, 2);
texArray.set(0, texCoords.length / 2, texCoords);
// Compose a VertexBuffer out of the previous vertrices and texture coordinates
VertexBuffer vertexBuffer = new VertexBuffer();
vertexBuffer.setPositions(vertexArray, 1.0f, null);
vertexBuffer.setTexCoords(0, texArray, 1.0f/255.0f, null);
// Create indices and face lengths
int indices[] = new int[] {0, 1, 3, 2};
int[] stripLengths = new int[] {4};
// Create the model's triangles
triangles = new TriangleStripArray(indices, stripLengths);
// Create the appearance
Appearance appearance = new Appearance();
PolygonMode pm = new PolygonMode();
pm.setCulling(cullFlags);
appearance.setPolygonMode(pm);
// Create and set the texture
try
{
// Open image
Texture2D theTexture = createTexture2D(texFilename);
// Add texture to the appearance
appearance.setTexture(0, theTexture);
}
catch(Exception e)
{
// Something went wrong
System.out.println("Failed to create texture");
System.out.println(e);
}
// Finally create the Mesh
Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
// All done
return mesh;
}
}
MeshOperator
import javax.microedition.m3g.CompositingMode;
import javax.microedition.m3g.Mesh;
/**
* Performs some basic operations on Mesh objects
*/
public class MeshOperator
{
/** Sets the alpha blending of a mesh. Only meaningful if the mesh already is alpha blended */
public static void setMeshAlpha(Mesh m, int alpha)
{
m.getVertexBuffer().setDefaultColor(alpha);
}
/**
*
* @param m The mesh to convert to a blended one
* @param alpha The alpha color to blend with
* @param textureBlending The texture blending parameter.
*/
public static void convertToBlended(Mesh m, int alpha, int textureBlending)
{
// Set the alpha
setMeshAlpha(m, alpha);
// Fix the compositing mode
CompositingMode cm = new CompositingMode();
cm.setBlending(CompositingMode.ALPHA);
m.getAppearance(0).setCompositingMode(cm);
m.getAppearance(0).getTexture(0).setBlending(textureBlending);
}
public static void setPerspectiveCorrection(Mesh m, boolean on)
{
m.getAppearance(0).getPolygonMode().setPerspectiveCorrectionEnable(on);
}
}
HeightMap
import java.io.IOException;
import javax.microedition.lcdui.Image;
import javax.microedition.m3g.Graphics3D;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.PolygonMode;
import javax.microedition.m3g.Transform;
/**
*
*/
public class HeightMap
{
// The actual heightmap containing the Y-coords of our triangles
private short[] heightMap;
private int[] data;
private int imgw, imgh;
// Map dimensions
private int mapWidth;
private int mapHeight;
// Actual quads
private Mesh[][] map;
// Water
private Mesh water;
// Local transform used for internal calculations
private Transform localTransform = new Transform();
/**
* Allocates the internal heightmap and prepares it for rendering.
* @param imageName The path to the actual heightmap BW image
* @param resolution The resolution of the heightmap. A value of 1.0 means that each pixel is one quad.
* @param waterLevel TODO
* @throws IOException if any error occurs while loading the heightmap image or the texture images.
*/
public HeightMap(String imageName, float resolution, int waterLevel) throws IOException
{
// Check for invalid resolution values
if(resolution <= 0.0001f resolution > 1.0f)
throw new IllegalArgumentException("Resolution too small or too large");
// Load image and allocate the internal array
loadImage(imageName, resolution);
// Create quads
createQuads();
// Create the water
createWater(waterLevel);
}
/** Create water in a very simple manner by representing it with
* a textured quad.
*/
private void createWater(int level) throws IOException
{
water = MeshFactory.createPlane("/res/water0.png", PolygonMode.CULL_NONE);
}
/**
* Create all quads (Triangle pairs) that our
*/
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++)
{
// Set heights
setQuadHeights(heights, x, y, mapWidth);
// Create mesh
map[x][y] = MeshFactory.createQuad(heights, PolygonMode.CULL_NONE);
}
}
}
/**
* @param heights
* @param x
* @param y
*/
private void setQuadHeights(short[] heights, int x, int y, int scanline)
{
heights[0] = heightMap[x + y * scanline];
heights[1] = heightMap[x + y * scanline + 1];
heights[3] = heightMap[x + (y + 1) * scanline];
heights[2] = heightMap[x + (y + 1) * scanline + 1];
}
private void loadImage(String path, float res) throws IOException
{
// Load actual image
Image img = Image.createImage(path);
// Allocate temporary memory to store pixels
data = new int[img.getWidth() * img.getHeight()];
// Get its rgb values
img.getRGB(data, 0, img.getWidth(), 0, 0, img.getWidth(), img.getHeight());
imgw = img.getWidth();
imgh = img.getHeight();
// Clear image
img = null;
System.gc();
// Calculate new width and height
mapWidth = (int)(res * imgw);
mapHeight = (int)(res * imgh);
// Allocate heightmap
heightMap = new short[mapWidth * mapHeight];
// Calculate height and width offset into image
int xoff = imgw / mapWidth;
int yoff = imgh / mapHeight;
// Set height values
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();
}
/**
* Renders this heightmap using a specified graphics context and a transform.
* @param g3d
* @param t
*/
public void render(Graphics3D g3d, 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.0f, 0.0f, (mapHeight - y) * -5.0f);
localTransform.postMultiply(t);
g3d.render(map[x][y], localTransform);
}
}
localTransform.setIdentity();
localTransform.postScale(255, 255, 255);
localTransform.postRotate(-90, 1.0f, 0.0f, 0.0f);
g3d.render(water, localTransform);
}
}
相关资源链接:
源代码、应用程序(JAR/JAD)
原文地址
(出处:http://www.knowsky.com)