[JSR-184][3D编程指南]Part III: Particle systems and immediate mode rendering (1)!-- 整理收集自网络
[JSR-184][3D编程指南]Part III: Particle systems and immediate mode rendering (1)
<!-- 整理收集自网络,收藏以便日后查阅 -->
?
Introduction
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 backgroundBackground back = null;
// Initializes our backgroundpublic 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:
We need to add lights to our Graphics3D object, which is usually done when the scene is being initialized.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.We need to clear the background so we can render onto a freshly painted canvas.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 meshg3d.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 wroteParticleEffect 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 CodeTo 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 VertexBufferThis 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.
--> 未完