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

[JSR-184][3D编程指南]Part III: Particle systems and immediate mode rendering (1)

    博客分类:
  • J2me
阅读更多

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

 

Introduction

Welcome to the third installment of the M3G tutorial! Today I'll go through how to gain total control over the rendering process (immediate rendering) and how to create a very nice particle system. Again, here are some links, in case you 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.

The goal of this tutorial is to show you how to render the same object several times, with different transformations. This is called immediate mode. You'll learn how powerful such a mode is for very many things. Also, this tutorial will be the base of the more advanced tutorials to come, since we'll be almost exclusively using immediate mode for rendering from now on.

Since the code is meant for educational purposes it isn't optimal nor does it cover all the errors that might occur. These are more advanced topics that will be addressed later on.

What you should know

Before you start reading this, you should have read the first two tutorials to have a somewhat firm grasp of basic M3G functionality.

Retained Mode contra Immediate Mode Rendering
The Retained mode is the mode used when you render an entire world with all the information that a world holds, including cameras and lights. This is a pretty restricted mode, since we almost always want to draw a single model multiple times, with different transformations, without invoking an entire scene graph. So, when we render a single group, node or a submesh in M3G it is called immediate mode rendering. These are the immediate mode methods:

 

render ( Node node, Transform transform)
render ( VertexBuffer vertices, IndexBuffer triangles, Appearance appearance, Transform transform)

render ( VertexBuffer vertices, IndexBuffer triangles, Appearance appearance, Transform transform, int scope)

 

As you can see, all three of the methods require some kind of vertex data: Node and VertexBuffer/IndexBuffer. A node can basically be any part of a scene graph, even a world is considered a Node. Usually you'll pass a Mesh or a Group to the first render method. The VertexBuffer, which we talked about in the second tutorial, is a gathering of mesh data that describes a model in 3D space. The last two methods also require Appearance classes to understand how to display the mesh data. In this tutorial we'll only use the first method, to show you how immediate mode works.

Another thing that all methods have in common is that they all need a Transform class that describes the model's transformation from local to world space. Remember, we talked about this in the last tutorial . Also, a thing to remember is that most objects you want to render are Transformables. Meaning they have their own internal transformation matrix. However, the immediate mode rendering ignores all such transformation information and uses only the Transform matrix supplied to the method. This is actually very handy, as you can hold one Mesh of a spaceship in memory, but render it many times with different Transform matrices to display many different spaceships. You'll see this in action as we start to design the particle engine.

Is that all?
The "problem" with immediate mode rendering is that you need to take care of more things before rendering, because you don't have the comfort of a handy World class that stores all camera, background and lighting information. So what you need to control manually now is the clearing of the viewport buffer with a Background object, the lighting of your scene and the camera.

Background
To render in immediate mode, we have to clear the viewport manually, thus preparing for the next drawing cycle. This can be done either before, or after a rendering loop, but it needs to be done after you've bound the Graphics3D object and before you've released it. M3G uses a Background class to help you with this. The Background class holds a lot of nifty information such as what background color to clear the screen with and what image to draw as a background. It is also very useful, for you can use a large image as a background, but only show bits and parts of it as you move around. For instance, you could have a large PNG of your horizon, and then as the player moves in the game world, you can move your Background's crop area to display other parts of the horizon. Beware however, using large PNGs is not only very slow, but also very memory inefficient. The most important methods of the Background class are the following:

setColor (int ARGB)
setCrop (int cropX, int cropY, int width, int height)
setImageMode (int modeX, int modeY)
setImage ( Image2D image)

Let's look at them one by one. The first method is the easiest and most used. It sets the color of the background (the color that the screen will be cleared with) in 0xAARRGGBB format. So for instance a bright red background would be 0xFFFF0000. Most people set this to either black, or to the color of the sky. Default color, however, is white.
The setCrop method is very useful if you are using a background image to display as a background. With this method, you can decide what part of the entire background image will be rendered. There are some special cases to think about here, like when the crop rectangle is outside the bounds of the background image. This is where the third method comes in. It determines what happens to the pixels that are "outside" of the source image. There are two valid modes; REPEAT and BORDER. REPEAT means that the picture repeats itself indefinitely, like a tiled texture, while BORDER means that pixels outside of the source image are painted with the background color supplied in the setColor method. One nifty thing is that you can determine the behavior of the image mode for the x and y-axes separately. This means that you could have a background image wrap around the x-axis (horizontally) but be static on the y-axis (vertically).
The last method, setImage is the one that determines which image will be used as a background. You can supply null to this method to turn off background image rendering and just have the screen fill up with the background color. This is the default mode. Note though, that the image is required to be an Image2D, and not the standard Java ME Platform Image. It's not hard to create an Image2D from an Image though, you can either use the very useful Loader class to point it directly onto a .PNG image, or just use the Image2D constructor that looks like this:
Image2D (int format, java.lang.Object image)
It's a very simple constructor that first takes the format of the image (which is in 99% of the cases an Image2D.RGBA or Image2D. RGB depending on if you use transparency or not) and then the Image itself. The second parameter should be your Image class. So this is how you would convert a normal Java ME Platform Image to an Image2D:

Image img = Image.createImage("/myimage.png");
Image2D img2d = new Image2D(Image2D.RGBA, img);
Easy as pie! So the Image2D isn't anything intimidating, just an Image wrapper that is used by the M3G system.
Now you might be wondering how you would clear the background by using the Background class before you start rendering anything. It's very easy and here's first a code snippet that shows you how:
// The background
Background back = null;


// Initializes our background
public void initBackground()
{
    back = new Background();
    back.setColor(0);

}
public void draw(Grahics g)
{
    // Here you bind your Graphics3D object
    //...

    // Now simply clear the screen
    g3d.clear(back);
}
See how easy that was? That is the first thing you need to control by yourself when using the immediate mode rendering and now we're one step closer to our goal: the particle engine.
Lighting
Another thing you need to control manually is the lighting. You need to create lights and position them in 3D space with Transform matrices. This is all done inside the Graphics3D class with the following methods:
addLight ( Light light, Transform transform)
setLight (int index, Light light, Transform transform)
resetLights ()
They are pretty self-explanatory but I'll go over them quickly. You should already know how to create a Light in M3G as we've done it in the past two tutorials. The first method simply adds a light to the internal array of lights that are to be rendered. You add a light by supplying an actual Light class and its Transform. This Transform matrix determines where the Light will be rendered. The addLight method also returns the index of the current light, which is neccessary to know in order to change the Light later on, via the setLight method that actually requires a specific index. It needs the index of the light to change and the new Light and Transform. By calling setLight with null as Light, you will actually remove that light from the array. The last method is a simple purge method that removes all lights associated with this Graphics3D-object.
Camera
You also need to create your own camera, instead of just using the one supplied in the World class, as we've done before. In this tutorial, we'll only create a Camera by calling its default constructor. In later parts of the tutorial we'll go through the more advanced things you can do with a Camera such as changing the projection matrix. I won't talk more about this topic right now, instead I'll just show you a code snippet on how this can be done:
Camera cam = new Camera();
Graphics3D g3d = Graphics3D.getInstance();
g3d.setCamera(cam, getCameraTransform());
Setting a camera is very similar to setting a light, since you add your Camera, and the Transform matrix that transforms the camera to a point in 3D space. Again! Be careful, since when you add the camera in this fashion, the internal node transformations of the Camera class will be ignored. Only the transformations in the Transform matrix supplied in the setCamera method are used.

Setting the stage
Now you know of the three things that we need to control manually and you're ready to render something in immediate mode. Before I show you any code, let's recap the steps needed:
  1. We need to add lights to our Graphics3D object, which is usually done when the scene is being initialized.
  2. We need to add the camera to the Graphics3D object. You can choose to do this once, or every game loop, depending on how you handle the Camera's Transform matrix.
  3. We need to clear the background so we can render onto a freshly painted canvas.
  4. We just render our meshes and release.
Let's see what this can look like in code:
// 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, identity);
// Render some mesh
g3d.render(someMesh, someMeshTransform);
It's a bit more complex than rendering a World, which was done with a single method call (g3d.render(world);) but in immediate mode you gain so much more control over the rendering process. Now, let's see how we can use immediate mode to actually do something useful! A particle system!

Particle Systems
A 3D-particle system usually consists of a data structure that represents a particle and its physical qualities (velocity, life and position) and of a system that handles the emittance of particles. This is a very simple model as you can make a particle system as complex as you wish. So, let's first create our Particle class. To represent a Particle in 3D space we'll probably need its position in 3D space, consisting of an x, y and z coordinate. We also need its velocity, since we want the Particle to move around in the 3D world. We could also need the color of a particle, so that we can make different particles different colors. Finally, we'll also need the life of a particle. The life of a particle is how long it stays in the 3D universe before it is either discarded, or re-animated at a new position with new velocities and colors. Here is a Particle class that'll cover our basic needs:
/**
* Holds all the information of a particle.
* A particle's alpha is controlled directly by its life. Its alpha is always
* life * 255.
*/
public class Particle
{
    // The life of the particle. Goes from 1.0f to 0.0f
    private float life = 1.0f;
   
    // The degradation of the particle
    private float degradation = 0.1f;
   
    // The velocities of the particle
    private float[] vel = {0.0f, 0.0f, 0.0f};
   
    // The position of the particle
    private float[] pos = {0.0f, 0.0f, 0.0f};
   
    // The color of the particle (RGB format 0xRRGGBB)
    private int color = 0xffffff;
   
    /** Empty initialization */
    public Particle()
    {
       
    }
   
    /**
     * Initializes the particle
     * @param velocity Sets the velocity
     * @param position Sets the position
     * @param color Sets the color (no alpha)
     */
    public Particle(float[] velocity, float[] position, int color)
    {
        setVel(velocity);
        setPos(position);
        this.setColor(color);
    }
/**
   * @param life The life to set.
   */
   void setLife(float life) {
   this.life = life;
   }
/**
   * @return Returns the life.
   */
   float getLife() {
   return life;
   }
/**
   * @param vel The vel to set.
   */
   void setVel(float[] tvel) {
   System.arraycopy(tvel, 0, vel, 0, vel.length);
   }
/**
   * @return Returns the vel.
   */
   float[] getVel() {
   return vel;
   }
/**
   * @param pos The pos to set.
   */
   void setPos(float[] tpos) {
   System.arraycopy(tpos, 0, pos, 0, pos.length);
   }
/**
   * @return Returns the pos.
   */
   float[] getPos() {
   return pos;
   }
/**
   * @param color The color to set.
   */
   void setColor(int color) {
   this.color = color;
   }
/**
   * @return Returns the color.
   */
   int getColor() {
   return color;
   }
/**
   * @param degradation The degradation to set.
   */
   public void setDegradation(float degradation) {
   this.degradation = degradation;
   }
/**
   * @return Returns the degradation.
   */
   public float getDegradation() {
   return degradation;
   }
   }
Since we want our particle system to be somewhat advanced, we'll also fade the particles to nothing as their life diminishes. This is also a very nice effect and I'll explain how we do it in M3G later. For now, let's look at the next part of the Particle system, the ParticleEffect. The ParticleEffect interface defines a common interface for all ParticleEffects. The init method is called on a Particle when it is brought to life, and the update method is then called periodically each game loop to update the particle's parameters. Finally the render method is used for rendering a Particle and is done after the update method.

import javax.microedition.m3g.Graphics3D;
/**
   * The interface that determines which effect the particle engine will display.
   * The ParticleEffect class also holds information about the bitmap used
   * for displaying particles (if any)
   */
   public interface ParticleEffect
   {
   // Initializes a particle
   public void init(Particle p);
  
   // Updates a particle
   public void update(Particle p);
  
   // Renders a particle
   public void render(Particle p, Graphics3D g3d);
   }
Now, finally we'll need a ParticleSystem, that actually creates Particles, emits them and applies a ParticleEffect onto them. Here is how I've chosen to write the class:
import javax.microedition.m3g.Graphics3D;      
/**
   * Manages emission of particles in our 3D world
   */
   public class ParticleSystem
   {
   // The effect
   private ParticleEffect effect = null;
  
   // The particles
   Particle[] parts = null;
  
   /**
   * Creates a particle system that emits particles according to a defined effect.
   * @param effect The effect that controls the behaviour of the particles
   * @param numParticles The number of particles to emit
   */
   public ParticleSystem(ParticleEffect effect, int numParticles)
   {
   // Copy the effect
   setEffect(effect);
  
   // Init the particles
   parts = new Particle[numParticles];
   for(int i = 0; i < numParticles; i++)
   {
   parts[i] = new Particle();
   effect.init(parts[i]);
   }
   }
  
   /** The method that does it all. Needs to be called every tick of a game loop */
   public void emit(Graphics3D g3d)
   {
   for(int i = 0; i < parts.length; i++)
   {
   getEffect().update(parts[i]);
   getEffect().render(parts[i], g3d);
   }
   }
/**
   * @param effect The effect to set.
   */
   public void setEffect(ParticleEffect effect) {
   this.effect = effect;
   }
/**
   * @return Returns the effect.
   */
   public ParticleEffect getEffect() {
   return effect;
   }
   }
As you can see, it's a pretty simple class that just creates a defined number of particles, runs them through the effect's init method and then keeps running them through the update method when its own emit method is invoked. It's a pretty simple but powerful particle system, since it allows us to do something like the following:
// We create a ParticleEffect class that we wrote
ParticleEffect pFx = createParticleEffect();


// Now we create a ParticleSystem with 20 particles
ParticleSystem pSys = new ParticleSystem(pFx, 20);

// Somewhere inside our game loop...
pSys.emit(g3d);
See how clean and simple it is? Those three lines initialize and use our new Particle System. Now, before I show you the actual rendering and updating of the particle system, let's look at another topic.
Creating Meshes in Code
To represent a particle in 3D space the best thing would be to use a Mesh that consists of a simple textured Quad. A Quad, as you might remember, is actually two triangles arranged so that they represent a square. Now, instead of creating a Mesh in 3D studio and exporting it as M3G and loading it into our program, it's much easier and faster to create the Mesh in code. If you remember the last tutorial, a model consists of faces, that themselves are composed of 3D-points, or vertrices. So to create a Quad, we'll need four points, one for each corner. In M3G a model is described by the Mesh class, which holds all kinds of information such as vertrices, texture coordinates, faces, polygon rendering modes, etc. We'll be creating a Mesh class from code. To be created, the Mesh class needs three things to display a model: a VertexBuffer, an IndexBuffer and an Appearance. Let's see how we'll create all of them in order.

The VertexBuffer
This class is a very handy one. It holds a lot of information about a model, including vertrices, texture coordinates, normals and colors. For our very simple model, we'll need vertrices and texture coordinates. We won't be using normals or colors this time since we don't need them. Now, the VertexBuffer stores vertex and texture information in a class called the VertexArray. The VertexArray is a pretty simple class that internally stores values in an array. When you create it you define how many elements each of your points has and how many bytes each element will occupy. Now you might be wondering, why do I choose the number of elements? Aren't 3D-coordinates always a triple of coordinates; x, y and z? Well, you are right of course, 3D-coordinates are always placed along three axes and do have three elements. However, there are other coordinates that are interesting as well, such as texture coordinates. In this example we will use a simple texture coordinate model that only uses pairs of coordinates. Now, before we actually start creating our VertexArrays, let's look at the coordinates. Here are the vertrices of our model (a simple limited plane with four corners).
// The vertrices of the plane
short vertrices[] = new short[] {-1, -1, 0,
1, -1, 0,
1, 1, 0,
-1, 1, 0};
As you can see, our plane is set with corners on the x and y-planes. Nothing really hard here, basic 3D coordinates. Before I show you the texture coordinates though, I'll have to tell you how the texture coordinates work in M3G. A texture is mapped onto a polygon by giving two coordinates. This is a bit of a complicated topic that we'll address in later parts of the tutorial but I'd like to tell you a bit about it already. Imagine that you have a cookie cutter and want to make cookies from an image. The texture coordinates actually tell you the size of your cookie cutter, meaning you use the texture coordinates to tell the M3G system which part of the texture you want mapped. You can even transform texture coordinates, to rotate your cookie cutter if you want. We won't be doing that today though. So the corners of a texture are then (0, 0), (0, 255), (255, 0) and (255, 255) and these coordinates tell the 3D engine that we want to use the entire texture on our polygon. Knowing this and knowing that we will use the entire texture, we can create the very simple texture coordinates.
// Texture coords of the plane
short texCoords[] = new short[] {0, 255,
255, 255,
255, 0,
0, 0};
All right, that wasn't so hard, was it? Now all we need to do is stuff our vertrices and texture coordinates into VertexArray classes and stuff them into a VertexBuffer. Then we're halfway there! Let's see how we do that in code:
// Create the model's vertrices
vertexArray = new VertexArray(vertrices.length/3, 3, 2);
vertexArray.set(0, vertrices.length/3, vertrices);
Let me now explain what we are doing here. First of all, we're creating a VertexArray. The constructor of a VertexArray takes three things; the number of vertrices (or points if you wish) that the Array will hold, the number of elements each vertex has (2, 3 or 4 is allowed) and the component size, which tells the array how many bytes it will use for storing each component of a vertex. The difference here is that if you supply it a value of 2 (2 bytes per component) it will use short integers to store components and if you supply a value of 1 (1 byte per component) it will use bytes to store them. So, let's see what we are doing: first we supply the number of the vertrices, which is of course our raw vertex array's length divided by three (remember, vertrices have three components). Next, we supply the number of components, which is 3 for vertex coordinates and lastly, we supply the number of bytes. We use two bytes here to store each component. Remember that using more bytes also consumes twice the amount of memory, so try using single-byte vertrices where ever you can. Now when our VertexArray is created, we can set the values into it. The set method is really simple and needs three arguments. The first one is the starting index to place vertrices on. This is of course 0, since we haven't placed any vertrices into the array yet. The second argument is the number of vertrices to copy in, which is again the length of our array divided by three. The third argument is the actual array to copy values from. See, simple! The same goes for creating a VertexArray that holds texture coordinates. Here's the code for it, so why don't you think about what's different here from the above example and think about why. Remember what I said about texture coordinates.
// Create the model's texture coords
texArray = new VertexArray(texCoords.length / 2, 2, 2);
texArray.set(0, texCoords.length / 2, texCoords);
All right, we're done with creating space coordinates and texture coordinates for our model and now we have to define what faces the model consists out of. Remember faces from the second tutorial? Well, we have to create the triangles that our plane will consist of.

--> 未完
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics