`
univasity
  • 浏览: 800747 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

[JSR-184][3D编程指南]Part V: Heightmap terrain rendering using M3G

    博客分类:
  • J2me
阅读更多

<!-- 整理收集自网络,收藏以便日后查阅 -->

 

Compliments of Redikod:
3D programming tutorial part five: Heightmap terrain rendering using M3G

 

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.

What you should know
Before working through this section of the tutorial you should have read the proceeding four parts in order to keep up with the already-written code I'll be using here.

3D terrain
Let's start by defining terrain, shall we? Terrain is a model of the real world, with smooth surfaces, mountains, rivers, cliffs, hills … you name it. The point of the terrain is to give the impression that the user is actually walking around in a "real" or "realistic" world. However, if we look at it from a more abstract point of view, we quickly realize that terrain is only variations in height. For instance, a grassy plain is a terrain with constant height (except for maybe some bumps and hills). A mountain region is a terrain that has very large height variations, creating large glyphs between areas and thus creating the illusion of mountains. A river is a plain combined with a curve through it that contains a much lower height than the surrounding plain. Check out this image of 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
Converting a heightmap into something we can render isn't hard at all. We need to read the pixels of the heightmap and then create surfaces that reflect the variations in height. A very easy-to-use surface in this problem is of course the Quad. Since all images are rectangular, quads fit really nice in them. So what are Quads? Simple, a Quad is just two triangles put together to form a rectangular surface.

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:

  • Divide image into many small sectors (a minimum size of 2x2 pixels)
  • Take each sector's corner pixels and read their values (0-255)
  • Assign these values as the heights of the Quad (see method declaration)

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.

 

Not much to say really. The HeightMap.render(g3d, t) method is pretty clean and straightforward. The controls might be a bit odd to you though. You move the camera with the joystick. Up, Down and rotate left and right. To actually move the camera forward, use the FIRE key.
Nothing strange here. We just load the heightmap and perform some transforms on it, as it will be the transform supplied to the HeightMap's render method. We just want it back into the screen a bit and a bit up. We also scale it a large amount as a terrain is normally huge, but I just want you to see a small overview of it.
All you need to do is go through the table of quads and render them at their given position in space. The user of the render method may supply a transform to be applied to each quad after the local transform, which is only putting each quad in its own place. Finally we place the water mesh at the height level defined during heightmap creation.
As you see, all we do is iterate over the heightMap table and extract 4 values, which we use as the height values in the MeshFactory.createQuad method.
I won't go into detail about the above code as I'd like you to look through it as an exercise. Anyhow, we start by loading the actual image into memory and then extract its pixel values. Then using the resolution parameter supplied from the constructor, we create a grid of according size and fill it with pixel values. Lastly we do a manual garbage collection to get rid of unnecessary data. This is mostly because the loadImage method is a memory intensive method and we want to make sure garbage data isn't taking up vital memory for the next few tasks.
Let's dissect the above code into steps. First we check for invalid resolution values. Invalid values are values beyond 1.0f (a quad has 4 corners, thus the smallest grid sector is a 2x2) and below 0.0001f (a VERY low resolution that more or less creates the entire terrain with one Quad). They are pretty self-explanatory but I'll go through some of them. First of all the heightMap array is the scaled array that holds the heights. It is not holding the pixels from the heightmap image. The Mesh table is holding all the generated Quads that we render. Finally, the water Mesh is simply a blue plane that will represent the water in our terrain (for creating rivers etc). Let's see how we create a HeightMap then:

Creating Quads from a Heightmap
With the above method we can create a Quad with varying height but as I said earlier, we need a lot of quads to make realistic terrain so now the problem is how to convert the heightmap into quads. This is really not a problem. Just look at this image:
Here we created the arrays necessary for describing a Mesh in the M3G system. The VertexBuffer that holds two vertex arrays, the color array and the position array. I have intentionally left out the allocation of the color array from the above code, since I'll talk more about that later. Right now let's focus on creating the quad.
Looks familiar doesn't it? A simple quad consisting of four vertrices that each have varying y-components but static x and z.

 

--------------------------------------------------------

源码见附件:

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics