« Previous: Starter Tutorial 10 - Hello Animation (1)
Next: Starter Tutorial 12 - Hello LevelOfDetail »
(Tip: Up-to-date source files for the tutorials are always in the repository)
This program introduces KeyframeController. You will learn how to make your own animations and how to manipulate vertex information after an object is created (morphing).
import java.nio.FloatBuffer; import java.util.ArrayList; import com.jme.app.SimpleGame; import com.jme.math.FastMath; import com.jme.math.Vector3f; import com.jme.renderer.ColorRGBA; import com.jme.scene.Controller; import com.jme.scene.TexCoords; import com.jme.scene.TriMesh; import com.jme.scene.shape.Sphere; import com.jme.scene.state.MaterialState; import com.jme.util.geom.BufferUtils; import com.jmex.model.animation.KeyframeController; /** * Started Date: Jul 23, 2004<br> * <br> * * Demonstrates making your own keyframe animations. * * @author Jack Lindamood */ public class HelloKeyframes extends SimpleGame { public static void main(String[] args) { HelloKeyframes app = new HelloKeyframes(); app.setConfigShowMode(ConfigShowMode.AlwaysShow); app.start(); } protected void simpleInitGame() { // The box we start off looking like TriMesh startBox = new Sphere("begining box", 15, 15, 3); // Null colors,normals,textures because they aren't being updated startBox.setColorBuffer(null); startBox.setNormalBuffer(null); startBox.setTextureCoords((ArrayList<TexCoords>) null); // The middle animation sphere TriMesh middleSphere = new Sphere("middleSphere sphere", 15, 15, 3); middleSphere.setColorBuffer(null); middleSphere.setNormalBuffer(null); middleSphere.setTextureCoords((ArrayList<TexCoords>) null); // The end animation pyramid TriMesh endPyramid = new Sphere("End sphere", 15, 15, 3); endPyramid.setColorBuffer(null); endPyramid.setNormalBuffer(null); endPyramid.setTextureCoords((ArrayList<TexCoords>) null); FloatBuffer boxVerts = startBox.getVertexBuffer(); FloatBuffer sphereVerts = middleSphere.getVertexBuffer(); FloatBuffer pyramidVerts = endPyramid.getVertexBuffer(); Vector3f boxPos = new Vector3f(), spherePos = new Vector3f(), pyramidPos = new Vector3f(); for (int i = 0, len = sphereVerts.capacity() / 3; i < len; i++) { BufferUtils.populateFromBuffer(boxPos, boxVerts, i); BufferUtils.populateFromBuffer(spherePos, sphereVerts, i); BufferUtils.populateFromBuffer(pyramidPos, pyramidVerts, i); // The box is the sign of the sphere coords * 4 boxPos.x = FastMath.sign(spherePos.x) * 4; boxPos.y = FastMath.sign(spherePos.y) * 4; boxPos.z = FastMath.sign(spherePos.z) * 4; if (boxPos.y < 0) { // The bottom of the pyramid pyramidPos.x = boxPos.x; pyramidPos.y = -4; pyramidPos.z = boxPos.z; } else // The top of the pyramid pyramidPos.set(0, 4, 0); BufferUtils.setInBuffer(boxPos, boxVerts, i); BufferUtils.setInBuffer(spherePos, sphereVerts, i); BufferUtils.setInBuffer(pyramidPos, pyramidVerts, i); } // The object that will actually be rendered TriMesh renderedObject = new Sphere("Rendered Object", 15, 15, 3); renderedObject.setLocalScale(2); // Create my KeyframeController KeyframeController kc = new KeyframeController(); // Assign the object I'll be changing kc.setMorphingMesh(renderedObject); // Assign for a time, what my renderedObject will look like kc.setKeyframe(0, startBox); kc.setKeyframe(.5f, startBox); kc.setKeyframe(2.75f, middleSphere); kc.setKeyframe(3.25f, middleSphere); kc.setKeyframe(5.5f, endPyramid); kc.setKeyframe(6, endPyramid); kc.setRepeatType(Controller.RT_CYCLE); // Give it a red material with a green tint MaterialState redgreen = display.getRenderer().createMaterialState(); redgreen.setDiffuse(ColorRGBA.red.clone()); redgreen.setSpecular(ColorRGBA.green.clone()); // Make it very affected by the Specular color. redgreen.setShininess(10f); renderedObject.setRenderState(redgreen); // Add the controller to my object renderedObject.addController(kc); rootNode.attachChild(renderedObject); } }
What I am going to do is make one TriMesh called renderedObject: It first looks like a cube, then it changes into a sphere, then it changes into a pyramid. For this morphing effect, I will use a KeyframeController.
The first new thing you see here is:
// Null colors,normals,textures because they aren't being updated startBox.setColorBuffer(null); startBox.setNormalBuffer(null); startBox.setTextureCoords((ArrayList<TexCoords>) null);
I set these Buffers to null right after I create startBox. To understand what I’m doing here, you must first understand how KeyframeController (jME API) works. It animates geometry information from one shape into another, and then into another shape, etc. This effect is called morphing. Each morphing step is one keyframe of the animation. The animation between two keyframes is interpolated (this means jME calculates them for you in a smooth way).
Setting any TriMesh values to null in my KeyframeController means that those values will NOT be interpolated (they won't be animated, they will just change abruptly). So because I don’t want to interpolate color/texture/normal, I set color/texture/normal null.
One important requirement of the KeyframeController is that all the Keyframes and the meshes that I’m transforming all have the same number of vertexes in them. All the KeyframeController does, is to interpolate from one to the next, which is why the array lengths must all be equal.
The simplest way to make them equal is to create the same object multiple times. The most complex shape is the Sphere. So I create three Spheres and change the vertex values to turn two Spheres into the appropriate other shapes, box and pyramid. This is the method I use here in this example.
After I make my three objects, I get their vertex positions and loop through each vertex value, in order to change there shape. Only the Sphere stays as it is.
FloatBuffer boxVerts = startBox.getVertexBuffer(); FloatBuffer sphereVerts = middleSphere.getVertexBuffer(); FloatBuffer pyramidVerts = endPyramid.getVertexBuffer(); Vector3f boxPos = new Vector3f(), spherePos = new Vector3f(), pyramidPos = new Vector3f(); for (int i = 0, len = sphereVerts.capacity() / 3; i < len; i++) { ... }
My box's corners are (-4,-4,-4) and (4,4,4). To morph a sphere into this box shape, I take my sphere's vertex values, and change them to either -4 or 4, depending upon their sign:
// The box is the sign of the sphere coords * 4 boxPos.x = FastMath.sign(spherePos.x) * 4;
Notice I use FastMath to calculate the sign.
Morphing the box shape into a pyramid shape is a little more complex. The explanation is more math-based than jME-based (sadly, math keeps coming up in 3D graphics), so just trust me that this works:
if (boxPos.y < 0) { // The bottom of the pyramid pyramidPos.x = boxPos.x; pyramidPos.y = -4; pyramidPos.z = boxPos.z; } else // The top of the pyramid pyramidPos.set(0, 4, 0);
Each of these three meshes is one keyframe of the animation.
After creating my keyframes and my objects that will actually be rendered, I create a KeyframeController:
// Create my KeyframeController KeyframeController kc = new KeyframeController(); // Assign the object I'll be changing kc.setMorphingMesh(renderedObject);
This assigns the mesh that my KeyframeController will animate, renderedObject. Next, I assign time values to keyframes:
// What my renderedObject will look like at various times kc.setKeyframe(0, startBox); kc.setKeyframe(.5f, startBox); kc.setKeyframe(2.75f, middleSphere); kc.setKeyframe(3.25f, middleSphere); kc.setKeyframe(5.5f, endPyramid); kc.setKeyframe(6, endPyramid); kc.setRepeatType(Controller.RT_CYCLE);
For example, from 0 to .5f seconds, it animates between startBox and startBox (which look the same, so for .5 seconds nothing happens). From time .5 to 2.75 seconds, animate between startBox and middleSphere. And so on. A repeat type of RT_CYCLE cycles my animation. Once it reaches 6 seconds, the timeline goes backwards towards 0 again. Run the code and you will see the morphing effect.
As last steps in the source code, I create a material state to colorize the meshes. I attach the keyframe controller to renderedObject, and attach the renderedObject to my rootNode.
The scene graph looks something like this:
| rootNode | ||
| renderedObject | ||
| controllers | renderstates | |
| kc | Redgreen | |
| [startBox, middleSphere, endPyramid] | ||