2 * Copyright (c) 2003-2009 jMonkeyEngine
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
9 * * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 package com.jmex.model.animation;
35 import java.io.IOException;
36 import java.nio.FloatBuffer;
37 import java.util.ArrayList;
39 import com.jme.math.Quaternion;
40 import com.jme.math.TransformMatrix;
41 import com.jme.math.Vector3f;
42 import com.jme.scene.Controller;
43 import com.jme.system.JmeException;
44 import com.jme.util.export.InputCapsule;
45 import com.jme.util.export.JMEExporter;
46 import com.jme.util.export.JMEImporter;
47 import com.jme.util.export.OutputCapsule;
48 import com.jme.util.export.Savable;
49 import com.jme.util.geom.BufferUtils;
50 import com.jmex.model.JointMesh;
53 * Started Date: Jun 9, 2004 <br>
55 * This controller animates a Node's JointMesh children acording to the joints
56 * stored inside <code>movementInfo</code>.
58 * @author Jack Lindamood
60 public class JointController extends Controller {
62 private static final long serialVersionUID = 1L;
65 * It is JointController's responsibility to keep changePoints sorted by
66 * <code>time</code> at all times.
71 * movementInfo[i] contains a float value time and an array of
72 * TransformMatrix. At time <code>time</code> the joint i is at movement
73 * <code>jointChange[i]</code>
75 public ArrayList<PointInTime> movementInfo;
78 * parentIndex contains a list of who's parent a joint is. -1 indicates a
79 * root joint with no parent
81 public int[] parentIndex;
84 * Local refrence matrix that can determine a joint's position in space
85 * relative to its parent.
87 public TransformMatrix[] localRefMatrix;
89 /** Currently unused. */
93 * Array of all the meshes this controller should consider animating.
95 public ArrayList<JointMesh> movingMeshes;
98 * This controller's internal current time.
100 private float curTime;
103 * This controller's internal current PointInTime index.
105 private int curTimePoint;
108 * Used internally, they are updated every update(float) call to tell points
111 private TransformMatrix[] jointMovements;
114 * The inverse chain matrix of every joint. Calculated once with
115 * prosessController()
117 private TransformMatrix[] inverseChainMatrix;
119 // Internal worker classes
120 private final static Quaternion unSyncbeginAngle = new Quaternion();
122 private final static Vector3f unSyncbeginPos = new Vector3f();
124 private final static TransformMatrix tempUnSyncd = new TransformMatrix();
127 * Tells update that it should be called every <code>skipRate</code>
130 public float skipRate;
133 * Used with skipRate internally.
135 private float currentSkip;
137 /** If true, the model's bounding volume will update every frame. */
138 private boolean updatePerFrame = true;
140 private boolean movingForward = true;
142 public JointController() {
148 * Constructs a new JointController that will hold the given number of
152 * The number of joints this jointController will have
154 public JointController(int numJoints) {
155 this.numJoints = numJoints;
156 parentIndex = new int[numJoints];
157 localRefMatrix = new TransformMatrix[numJoints];
158 movingMeshes = new ArrayList<JointMesh>();
159 jointMovements = new TransformMatrix[numJoints];
160 inverseChainMatrix = new TransformMatrix[numJoints];
161 for (int i = 0; i < numJoints; i++) {
162 localRefMatrix[i] = new TransformMatrix();
163 jointMovements[i] = new TransformMatrix();
164 inverseChainMatrix[i] = new TransformMatrix();
166 movementInfo = new ArrayList<PointInTime>();
173 public float getCurrentTime() {
178 * Tells JointController that at time <code>time</code> the joint
179 * <code>jointNumber</code> will translate to x,y,z relative to its parent
182 * Index of joint to affect
184 * Which time the joint will take these values
186 * Joint's x translation
188 * Joint's y translation
190 * Joint's z translation
192 public void setTranslation(int jointNumber, float time, float x, float y,
194 findUpToTime(time).setTranslation(jointNumber, x, y, z);
198 * Tells JointController that at time <code>time</code> the joint
199 * <code>jointNumber</code> will translate to x,y,z relative to its parent
202 * Index of joint to affect
204 * Which time the joint will take these values
206 * Joint's translation
209 public void setTranslation(int jointNumber, float time, Vector3f trans) {
210 findUpToTime(time).setTranslation(jointNumber, trans);
214 * Tells JointController that at time <code>time</code> the joint
215 * <code>jointNumber</code> will rotate acording to the euler angles x,y,z
216 * relative to its parent's rotation
219 * Index of joint to affect
221 * Which time the joint will take these values
229 public void setRotation(int jointNumber, float time, float x, float y,
231 findUpToTime(time).setRotation(jointNumber, x, y, z);
235 * Tells JointController that at time <code>time</code> the joint
236 * <code>jointNumber</code> will rotate acording to
237 * <code>Quaternion</code>.
240 * Index of joint to affect
242 * Which time the joint will take these values
244 * The joint's new rotation
246 public void setRotation(int jointNumber, float time, Quaternion quaternion) {
247 findUpToTime(time).setRotation(jointNumber, quaternion);
251 * Used with setRotation and setTranslation. This function finds a point in
252 * time for given time. If one doesn't exist then a new PointInTime is
253 * created and returned.
256 * @return The PointInTime at that given time, or a new one if none exist so
259 private PointInTime findUpToTime(float time) {
261 for (PointInTime point : movementInfo) {
262 float curTime = point.time;
263 if (curTime >= time) break;
266 PointInTime storedNext = null;
267 if (index == movementInfo.size()) {
268 storedNext = new PointInTime(numJoints);
269 movementInfo.add(storedNext);
270 storedNext.time = time;
272 if (movementInfo.get(index).time == time) {
273 storedNext = movementInfo.get(index);
275 storedNext = new PointInTime(numJoints);
276 movementInfo.add(index, storedNext);
277 storedNext.time = time;
284 * Updates the <code>movingMeshes</code> by updating their joints +=time
287 * Time from last update
289 public void update(float time) {
290 if (numJoints == 0) return;
292 curTime += time * this.getSpeed();
294 curTime -= time * this.getSpeed();
296 if (currentSkip >= skipRate) {
303 PointInTime now = movementInfo.get(curTimePoint);
304 PointInTime then = movementInfo.get(curTimePoint - 1);
306 float delta = (curTime - then.time) / (now.time - then.time);
307 createJointTransforms(delta);
308 combineWithInverse();
312 private void setCurTimePoint() {
314 int repeatType = getRepeatType();
316 if (curTime > getMaxTime()) {
317 if (repeatType == Controller.RT_WRAP)
318 curTime = getMinTime();
319 else if (repeatType == Controller.RT_CYCLE) {
320 curTime = getMaxTime();
321 movingForward = false;
322 } else if (repeatType == Controller.RT_CLAMP) {
324 curTime = getMaxTime();
328 if (curTime < getMinTime()) {
329 if (repeatType == Controller.RT_WRAP)
330 curTime = getMaxTime();
331 else if (repeatType == Controller.RT_CYCLE) {
332 curTime = getMinTime();
333 movingForward = true;
334 } else if (repeatType == Controller.RT_CLAMP) {
336 curTime = getMinTime();
340 // if curTimePoint works then return
341 PointInTime p1 = movementInfo.get(curTimePoint);
342 PointInTime p2 = movementInfo.get(curTimePoint - 1);
343 if (curTime <= p1.time && curTime >= p2.time) return;
344 for (curTimePoint = 1; curTimePoint < movementInfo.size(); curTimePoint++) {
345 p1 = movementInfo.get(curTimePoint);
346 if (p1.time >= curTime) return;
351 * Sets the frames the joint controller will animate from and to. The frames
352 * are dependant upon the FPS. Remember that the first frame starts at 1,
356 * The starting frame number.
358 * The ending frame number.
360 public void setTimes(int start, int end) {
363 || movementInfo.get(movementInfo.size() - 1).time < end
365 String message = "Malformed times: start="
369 + " start limit: 0 End limit: "
370 + movementInfo.get(movementInfo.size() - 1).time
372 throw new JmeException(message);
374 setMinTime(start / FPS);
375 setMaxTime(end / FPS);
376 curTime = getMinTime();
378 movingForward = true;
382 * Used with update(float). <code>updateData</code> moves every normal and
383 * vertex acording to its jointIndex
385 private void updateData() {
386 for (int currentGroup = 0; currentGroup < movingMeshes.size(); currentGroup++) {
387 JointMesh updatingGroup = movingMeshes
389 int currentBoneIndex;
390 FloatBuffer verts = updatingGroup.getVertexBuffer();
391 FloatBuffer normals = updatingGroup.getNormalBuffer();
393 for (j = 0; j < updatingGroup.jointIndex.length; j++) {
394 currentBoneIndex = updatingGroup.jointIndex[j];
395 if (currentBoneIndex == -1) continue;
396 unSyncbeginPos.set(updatingGroup.originalVertex[j]);
397 BufferUtils.setInBuffer(jointMovements[currentBoneIndex]
398 .multPoint(unSyncbeginPos), verts, j);
399 unSyncbeginPos.set(updatingGroup.originalNormal[j]);
400 BufferUtils.setInBuffer(jointMovements[currentBoneIndex]
401 .multNormal(unSyncbeginPos), normals, j);
404 if (updatePerFrame) updatingGroup.updateModelBound();
410 * Used with update(float) to combine joints with their inverse to properly
413 private void combineWithInverse() {
414 for (int i = 0; i < numJoints; i++)
415 jointMovements[i].multLocal(inverseChainMatrix[i], unSyncbeginPos);
419 * Processes a JointController by filling holes and creating inverse
420 * matrixes. Should only be called once per JointController object lifetime
422 public void processController() {
423 if (movementInfo.size() == 1) { // IE no times were added or only time 0
425 movementInfo.add(0, new PointInTime(0));
427 setMinTime(movementInfo.get(0).time);
428 setMaxTime(movementInfo.get(movementInfo.size() - 1).time);
429 curTime = getMinTime();
435 * If true, the model's bounding volume will be updated every frame. If
436 * false, it will not.
439 * The new update model volume per frame value.
441 public void setModelUpdate(boolean update) {
442 updatePerFrame = update;
446 * Returns true if the model's bounding volume is being updated every frame.
448 * @return True if bounding volume is updating.
450 public boolean getModelUpdate() {
451 return updatePerFrame;
455 * Inverts joints with their parents. Only called once per JointController
456 * lifetime during processing.
458 private void invertWithParents() {
459 for (int i = 0; i < numJoints; i++) {
460 inverseChainMatrix[i] = new TransformMatrix(localRefMatrix[i]);
461 inverseChainMatrix[i].inverse();
462 if (parentIndex[i] != -1)
463 inverseChainMatrix[i].multLocal(
464 inverseChainMatrix[parentIndex[i]], unSyncbeginPos);
469 * Called with update to create the needed joint transforms for that point
473 * The % diffrence (from 0-1) between two points in time
475 private void createJointTransforms(float changeAmnt) {
476 PointInTime now = movementInfo.get(curTimePoint);
477 PointInTime then = movementInfo.get(curTimePoint - 1);
478 for (int index = 0; index < numJoints; index++) {
479 int theParentIndex = parentIndex[index];
481 unSyncbeginAngle.set(then.jointRotation[index]);
482 unSyncbeginPos.set(then.jointTranslation[index]);
484 unSyncbeginAngle.slerp(now.jointRotation[index], changeAmnt);
485 unSyncbeginPos.interpolate(now.jointTranslation[index], changeAmnt);
487 tempUnSyncd.set(unSyncbeginAngle, unSyncbeginPos);
488 jointMovements[index].set(localRefMatrix[index]);
489 jointMovements[index].multLocal(tempUnSyncd, unSyncbeginPos);
490 if (theParentIndex != -1) {
491 tempUnSyncd.set(jointMovements[index]);
492 jointMovements[index].set(jointMovements[theParentIndex]);
493 jointMovements[index].multLocal(tempUnSyncd, unSyncbeginPos);
499 * Fills null rotations and translations for any joint at any point in time.
501 private void fillHoles() {
507 * Gives every PointInTime for every joint a valid rotation.
509 private void fillRots() {
510 for (int joint = 0; joint < numJoints; joint++) {
511 // 1) Find first non-null rotation of joint <code>joint</code>
513 for (start = 0; start < movementInfo.size(); start++) {
514 if (movementInfo.get(start).jointRotation[joint] != null)
517 if (start == movementInfo.size()) { // if they are all null then
518 // fill with identity
519 for (int i = 0; i < movementInfo.size(); i++)
520 movementInfo.get(i).jointRotation[joint] = new Quaternion();
521 continue; // we're done with this joint so lets continue
523 if (start != 0) { // if there -are- null elements at the begining,
524 // then fill with first non-null
526 .set(movementInfo.get(start).jointRotation[joint]);
527 for (int i = 0; i < start; i++)
528 movementInfo.get(i).jointRotation[joint] = new Quaternion(
531 int lastgood = start;
532 boolean allGood = true;
533 for (int i = start + 2; i < movementInfo.size(); i++) {
534 if (movementInfo.get(i).jointRotation[joint] != null) {
535 fillQuats(joint, lastgood, i); // fills gaps
540 if (!allGood && lastgood != movementInfo.size() - 1) { // fills tail
541 movementInfo.get(movementInfo.size() - 1).jointRotation[joint] = new Quaternion(
542 movementInfo.get(lastgood).jointRotation[joint]);
543 fillQuats(joint, lastgood, movementInfo.size() - 1); // fills
550 * Gives every PointInTime for every joint a valid translation.
552 private void fillTrans() {
553 for (int joint = 0; joint < numJoints; joint++) {
554 // 1) Find first non-null translation of joint <code>joint</code>
556 for (start = 0; start < movementInfo.size(); start++) {
557 if (movementInfo.get(start).jointTranslation[joint] != null)
560 if (start == movementInfo.size()) { // if they are all null then
561 // fill with identity
562 for (int i = 0; i < movementInfo.size(); i++)
563 movementInfo.get(i).jointTranslation[joint] = new Vector3f(
565 continue; // we're done with this joint so lets continue
567 if (start != 0) { // if there -are- null elements at the begining,
568 // then fill with first non-null
570 .set(movementInfo.get(start).jointTranslation[joint]);
571 for (int i = 0; i < start; i++)
572 movementInfo.get(i).jointTranslation[joint] = new Vector3f(
575 int lastgood = start;
576 boolean allGood = true;
577 for (int i = start + 2; i < movementInfo.size(); i++) {
578 if (movementInfo.get(i).jointTranslation[joint] != null) {
579 fillPos(joint, lastgood, i); // fills gaps
584 if (!allGood && lastgood != movementInfo.size() - 1) { // fills tail
585 movementInfo.get(movementInfo.size() - 1).jointTranslation[joint] = new Vector3f(
586 movementInfo.get(lastgood).jointTranslation[joint]);
587 fillPos(joint, lastgood, movementInfo.size() - 1); // fills tail
593 * Interpolates missing quats that weren't specified to the JointController.
596 * Index of the joint that has missing quats
597 * @param startRotIndex
598 * Begining index of a valid non-null quat
600 * Ending index of a valid non-null quat
602 private void fillQuats(int jointIndex, int startRotIndex, int endRotIndex) {
604 .set(movementInfo.get(startRotIndex).jointRotation[jointIndex]);
605 for (int i = startRotIndex + 1; i < endRotIndex; i++) {
606 movementInfo.get(i).jointRotation[jointIndex] = new Quaternion(
608 movementInfo.get(i).jointRotation[jointIndex]
610 movementInfo.get(endRotIndex).jointRotation[jointIndex],
611 ((float) i - startRotIndex)
612 / (endRotIndex - startRotIndex));
617 * Interpolates missing vector that weren't specified to the
621 * Index of the joint that has missing vector
622 * @param startPosIndex
623 * Begining index of a valid non-null vector
625 * Ending index of a valid non-null vector
627 private void fillPos(int jointIndex, int startPosIndex, int endPosIndex) {
629 .set(movementInfo.get(startPosIndex).jointTranslation[jointIndex]);
630 for (int i = startPosIndex + 1; i < endPosIndex; i++) {
631 movementInfo.get(i).jointTranslation[jointIndex] = new Vector3f(
633 movementInfo.get(i).jointTranslation[jointIndex]
635 movementInfo.get(endPosIndex).jointTranslation[jointIndex],
636 ((float) i - startPosIndex)
637 / (endPosIndex - startPosIndex));
642 * Adds a jointmesh for this JointController to consider animating.
645 * Child JointMesh to consider
647 public void addJointMesh(JointMesh child) {
648 movingMeshes.add(child);
651 public void write(JMEExporter e) throws IOException {
653 OutputCapsule capsule = e.getCapsule(this);
654 capsule.write(numJoints, "numJoints", 0);
655 capsule.writeSavableArrayList(movementInfo, "movementInfo", new ArrayList<PointInTime>());
656 capsule.write(parentIndex, "parentIndex", new int[numJoints]);
657 capsule.write(localRefMatrix, "localRefMatrix", new TransformMatrix[numJoints]);
658 capsule.write(FPS, "FPS", 0);
659 capsule.writeSavableArrayList(movingMeshes, "movingMeshes", new ArrayList<JointMesh>());
660 capsule.write(jointMovements, "jointMovements", new TransformMatrix[numJoints]);
661 capsule.write(skipRate, "skipRate", 0);
662 capsule.write(updatePerFrame, "updatePerFrame", false);
663 capsule.write(movingForward, "movingForward", false);
666 @SuppressWarnings("unchecked")
667 public void read(JMEImporter e) throws IOException {
669 InputCapsule capsule = e.getCapsule(this);
670 numJoints = capsule.readInt("numJoints", 0);
671 movementInfo = capsule.readSavableArrayList("movementInfo", new ArrayList<PointInTime>());
672 parentIndex = capsule.readIntArray("parentIndex", new int[numJoints]);
674 Savable[] savs = capsule.readSavableArray("localRefMatrix", new TransformMatrix[numJoints]);
676 localRefMatrix = null;
678 localRefMatrix = new TransformMatrix[savs.length];
679 for (int x = 0; x < savs.length; x++) {
680 localRefMatrix[x] = (TransformMatrix)savs[x];
684 savs = capsule.readSavableArray("jointMovements", new TransformMatrix[numJoints]);
686 jointMovements = null;
688 jointMovements = new TransformMatrix[savs.length];
689 for (int x = 0; x < savs.length; x++) {
690 jointMovements[x] = (TransformMatrix)savs[x];
694 FPS = capsule.readFloat("FPS", 0);
695 movingMeshes = capsule.readSavableArrayList("movingMeshes", new ArrayList<JointMesh>());
696 skipRate = capsule.readFloat("skipRate", 0);
697 updatePerFrame = capsule.readBoolean("updatePerFrame", false);
698 movingForward = capsule.readBoolean("movingForward", false);
701 inverseChainMatrix = new TransformMatrix[numJoints];
702 for (int i = 0; i < numJoints; i++) {
703 inverseChainMatrix[i] = new TransformMatrix();