
Building on the previous four installments of his getting-started tutorial series "3D programming for mobile devices using M3G (JSR 184)", Mikael Baros, Senior Programmer at Redikod, now guides you through the basics of heightmaps and terrain rendering. Below you can download the source code and application package zip files for part five, as well as link to the first four installments of the tutorial. |
![]() |
|
Part five: Heightmap terrain rendering using M3G Source Code (Java classes and resources)>> Application Package (JAR/JAD)>>
So, over to Mikael for part five. Introduction | |
|
Welcome to the fifth part of this M3G tutorial series. Today I'll show you a very simple technique that is used in almost all 3D games (in one form or the other). The heightmap. By using heightmaps, designers/developers can easily create natural terrain (perhaps even by using a perlin noise generator, but not required) in almost no time at all. The beauty of heightmaps is that it takes a complex concept, such as a beautiful and realistic 3D-terrain, and simplifies this problem to an easy 2D-image. As always, check here if you ever get lost: First of all, and probably most importantly, the dedicated Mobile Java 3D web section on Sony Ericsson Developer World. Second, if you ever get stuck, go to the Sony Ericsson Mobile Java 3D forum. For everything else, use the Sony Ericsson World Developer web portal, there you will find the answers to your questions and more. |
Mikael Baros, Senior Programmer at Redikod. |
What you should know | |
|
3D terrain As you can see, the above terrain is described by three areas of greater height (the three gray hills) and the rest is a deep gorge, that is filled with water. Again, nothing but variations in height. |
![]() |
Heightmaps
This is where heightmaps come in. They are very elegant solutions to storing variations in height and making surfaces smooth. Now let's look at this image before I start revealing anything.
| This is a grayscale image. Nothing fancy. It looks like a donut with a white speck in the middle. So what is a grayscale image? Well simply put, it is a collection of pixels where each pixel goes from 0 to 255 on the grayscale where 0 is black and 255 completely white. Right? Does this sound familiar? What if you could use each pixel to determine height then? If a black pixel (value 0) could be the lowest height and a white pixel (value 255) the highest you'd have a map that depicts height, a heightmap! Another great thing about this is that since pixels go from 0 to 255 you get automatic interpolation of the terrain (thus creating smooth terrain) if you just blur your image. | ![]() |
|
So, all you need to do is to open your favorite graphics program, draw some white stuff with a brush and you have a heightmap. This all sounds easy, sure, but how do we actually convert this to a mesh that we can render? Quads |
![]() |
The image above represents a quad that consists of two triangles. As you can see, the quad has four endpoints that are disjointed since we are using two triangles to represent it. These four corners can be given different heights and thus we have the beginning of describing heights in a 3D world. One quad is far from enough to describe an entire terrain however, we'll need a LOT of them if we want our terrain to look the least bit realistic. We'll get to that later, let's first see how we create a quad in code. We'll make it a x-z plane with variable y-coordinates, thus having varying height. Let's introduce a new method to the MeshFactory class we created in the previous tutorials and call it createQuad. All this method needs to know is the different heights at the different corners of the quad, and the culling flags. Here's the first piece of the method:
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};
// 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);
Here is some standard Appearance stuff, however I wanted you to see that now we're using smooth shading, which means that colors of the vertrices will be interpolated over the whole surface, creating a smooth appearance. Why we need this will become clearer later. All that's left now is to create the Mesh, which is fairly straightforward:
// Finally create the Mesh
Mesh mesh = new Mesh(vertexBuffer, triangles, appearance);
|
I have drawn a white grid over the previous heightmap. If you look at each piece of the grid, you can see that the rectangular grid sector is another heightmap but smaller. If we create a very high-resolution grid, you'll probably realize that the grid sectors become very small and thus very easy to approximate with a single quad. To put it simply; in order to approximate a heightmap, we split it into many very small parts which each represent a quad. How do we create a quad? Easy, here are the steps necessary:
|
![]() |
So, it's really simple to create the quads from a heightmap. After creation, you just render these quads, one after another and you have your heightmap. Now, there are some things you should know. As the resolution of the heightmap grid increases, so does the smoothness of the terrain, as you use more quads to represent the terrain. However you are also drastically increasing the memory footprint and increasing the number of polygons that the GPU has to push. This is a trade-off that needs to be done on every mobile phone depending on available memory, GPU power, etc.
Implementation
Let's see how to implement a heightmap in M3G. We already have a method that creates Quads with varying heights so all we need is to:
1. Load a heightmap
2. Create a new array that is scaled proportionally to the grid size
3. Read pixels from heightmap and store in the new array
4. Use said array to generate Quads with varying heights
It's a simple four-step procedure. Let's begin by inspecting the private members of the HeightMap class:
// 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();
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);
}
Next we load the image that we supplied as a constructor parameter. However, there are some other interesting things done in the loadImage method I'd like you to see. Here is the code:
// 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();
Next method in the constructor body is the createQuads. This is a very straightforward method that takes the generated heightMap array and creates quads from it. Let's look at its guts:
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);
}
}
}
I'll leave it up to you to check the createWater method. It should be something you know by heart at this point. We just use the MeshFactory.createPlane method to create a large plane textured with a watery texture.
Rendering
How do we render the Quads we've generated? You should know the answer to this question, but let's go through the render method of the HeightMap class anyway. Here it is:
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);
}
Putting it all together
Now to use the very nifty HeightMap class we need to do the following:
1. Load a HeightMap from an existant greyscale image
2. Render it
Sounds easy? That's because it is. Let's take a look at the code that loads a 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();
}
}
Another very important thing to keep in mind is that the HeightMap in this tutorial is rendered without any culling at all. This is needed, especially on large terrains. However to keep clarity in the code I have chosen to remove any kind of space partitioning or software culling. You can see it as an exercise to only send meshes to the renderer that are visible (that is, not meshes that are too far away, or behind the camera).
Finally, what's the code for rendering the HeightMap? Here is the main draw method:
// 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);
Conclusion
So, to continue this lesson, why don't you try loading the other heightmaps supplied in the source code zip files? See what kind of terrains come out. Or even better; create your own heightmap image!
Let your imagination go wild, put it into the MIDlet and cruise through your landscape.
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();
}
}
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;
}
}
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);
}
}
About Redikod
Redikod from Malmo, Sweden, has been developing web and mobile games since 1997, and this small company is now one of the leaders in the Nordic games industry. Its disproportionate influence stems from strategic initiatives such as Nordic Game Potential, the yearly conference, and the Nordic booth at E3 2006. Redikod is commissioned to design the Nordic public support system and funding for games development, including mobile, that, pending final political decision this fall, will come into effect in 2006. But 3D and multiplayer mobile games development are their day job. To find out more, visit Redikod's website>>
Copyright © 2001 - 2009 Sony Ericsson Mobile Communications AB. All Rights Reserved.