Source code
/* Copyright (C) 2001-2006 by Peter Eastman
This program is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details. */
package artofillusion.animation;
import artofillusion.*;
import artofillusion.animation.Joint.DOF;
import artofillusion.math.*;
import artofillusion.object.*;
import java.awt.*;
import java.io.*;
/** This class represents the skeleton of an animated object. */
public class Skeleton
{
private Joint joint[];
private int nextID;
private static final int MARKER_WIDTH = 10;
private static final double BONE_WIDTH = 0.15;
private static final double WIDEST_POINT = 0.8;
public Skeleton()
{
joint = new Joint [0];
nextID = 1;
}
/** Create an exact duplicate of this skeleton. */
public Skeleton duplicate()
{
Skeleton s = new Skeleton();
s.nextID = nextID;
s.joint = new Joint [joint.length];
for (int i = 0; i < joint.length; i++)
s.joint[i] = joint[i].duplicate();
for (int i = 0; i < joint.length; i++)
{
if (joint[i].parent != null)
s.joint[i].parent = s.joint[s.findJointIndex(joint[i].parent.id)];
s.joint[i].children = new Joint [joint[i].children.length];
for (int j = 0; j < joint[i].children.length; j++)
s.joint[i].children[j] = s.joint[s.findJointIndex(joint[i].children[j].id)];
}
return s;
}
/** Make this skeleton idenical to another one. */
public void copy(Skeleton s)
{
nextID = s.nextID;
joint = new Joint [s.joint.length];
for (int i = 0; i < joint.length; i++)
joint[i] = s.joint[i].duplicate();
for (int i = 0; i < joint.length; i++)
{
if (s.joint[i].parent != null)
joint[i].parent = joint[findJointIndex(s.joint[i].parent.id)];
joint[i].children = new Joint [s.joint[i].children.length];
for (int j = 0; j < joint[i].children.length; j++)
joint[i].children[j] = joint[findJointIndex(s.joint[i].children[j].id)];
}
}
/** Determine if this skeleton is identical to another one. */
public boolean equals(Object o)
{
if (!(o instanceof Skeleton))
return false;
Skeleton s = (Skeleton) o;
if (joint.length != s.joint.length)
return false;
for (int i = 0; i < joint.length; i++)
if (!joint[i].equals(s.joint[i]))
return false;
return true;
}
/** Add a joint to the skeleton. */
public void addJoint(Joint j, int parentID)
{
Joint newjoint[] = new Joint [joint.length+1];
System.arraycopy(joint, 0, newjoint, 0, joint.length);
newjoint[joint.length] = j;
joint = newjoint;
if (j.id == -1)
j.id = nextID++;
int parentIndex = findJointIndex(parentID);
if (parentIndex == -1)
{
j.parent = null;
return;
}
Joint parent = joint[parentIndex];
j.parent = parent;
Joint newchildren[] = new Joint [parent.children.length+1];
System.arraycopy(parent.children, 0, newchildren, 0, parent.children.length);
newchildren[parent.children.length] = j;
parent.children = newchildren;
}
/** Delete a joint from the skeleton. If it has children, all of them will be deleted
as well. */
public void deleteJoint(int id)
{
int which = findJointIndex(id);
Joint j = joint[which], parent = j.parent;
while (j.children.length > 0)
deleteJoint(j.children[0].id);
which = findJointIndex(id);
Joint newjoint[] = new Joint [joint.length-1];
for (int i = 0, k = 0; i < joint.length; i++)
if (i != which)
newjoint[k++] = joint[i];
joint = newjoint;
if (parent == null)
return;
Joint newchildren[] = new Joint [parent.children.length-1];
for (int i = 0, k = 0; i < parent.children.length; i++)
if (parent.children[i] != j)
newchildren[k++] = parent.children[i];
parent.children = newchildren;
}
/** Add every joint from another skeleton to this one. */
public void addAllJoints(Skeleton s)
{
Joint newjoint[] = new Joint [joint.length+s.joint.length];
System.arraycopy(joint, 0, newjoint, 0, joint.length);
for (int i = 0; i < s.joint.length; i++)
newjoint[joint.length+i] = s.joint[i].duplicate();
for (int i = 0; i < s.joint.length; i++)
{
Joint newj = newjoint[joint.length+i];
Joint oldj = s.joint[i];
newj.id = nextID++;
if (oldj.parent != null)
newj.parent = newjoint[joint.length+s.findJointIndex(oldj.parent.id)];
newj.children = new Joint [oldj.children.length];
for (int k = 0; k < newj.children.length; k++)
newj.children[k] = newjoint[joint.length+s.findJointIndex(oldj.children[k].id)];
}
joint = newjoint;
}
/** Set the parent of a joint. */
public void setJointParent(Joint j, Joint parent)
{
if (j.parent != null)
{
Joint newchildren[] = new Joint [j.parent.children.length-1];
for (int i = 0, k = 0; i < j.parent.children.length; i++)
if (j.parent.children[i] != j)
newchildren[k++] = j.parent.children[i];
j.parent.children = newchildren;
}
if (parent != null)
{
Joint newchildren[] = new Joint [parent.children.length+1];
System.arraycopy(parent.children, 0, newchildren, 0, parent.children.length);
newchildren[parent.children.length] = j;
parent.children = newchildren;
}
j.parent = parent;
}
/** Find the array index for a given joint ID. */
public int findJointIndex(int id)
{
int min = 0, max = joint.length-1, current = (min+max)>>1;
if (joint.length == 0)
return -1;
while (true)
{
if (joint[current].id == id)
return current;
if (joint[current].id > id)
max = current-1;
else
min = current+1;
if (min >= max)
{
if (min < joint.length && joint[min].id == id)
return min;
return -1;
}
current = (min+max)>>1;
}
}
/** Get the joint with the specified ID, or null if there is none. */
public Joint getJoint(int id)
{
int which = findJointIndex(id);
if (which < 0 || which >= joint.length)
return null;
return joint[which];
}
/** Get an array of all the joints. */
public Joint [] getJoints()
{
Joint j[] = new Joint [joint.length];
for (int i = 0; i < j.length; i++)
j[i] = joint[i];
return j;
}
/** Get the number of joints in the skeleton. */
public int getNumJoints()
{
return joint.length;
}
/** Get the ID for the next joint to be added. */
public int getNextJointID()
{
return nextID;
}
/** Scale the skeleton by the specified amount along each axis. */
public void scale(double x, double y, double z)
{
for (int i = 0; i < joint.length; i++)
{
Vec3 pos = joint[i].coords.getOrigin();
pos.x *= x;
pos.y *= y;
pos.z *= z;
joint[i].coords.setOrigin(pos);
Vec3 zdir = joint[i].coords.getZDirection();
Vec3 newzdir = new Vec3(zdir.x*x, zdir.y*y, zdir.z*z);
double len = newzdir.length();
if (len > 0.0)
zdir = newzdir.times(1.0/len);
Vec3 updir = joint[i].coords.getUpDirection();
Vec3 newupdir = new Vec3(updir.x*x, updir.y*y, updir.z*z);
len = newupdir.length();
if (len > 0.0)
updir = newupdir.times(1.0/len);
joint[i].coords.setOrientation(zdir, updir);
}
for (int i = 0; i < joint.length; i++)
{
if (joint[i].parent == null)
joint[i].calcAnglesFromCoords(true);
else
joint[i].length.pos = joint[i].coords.getOrigin().distance(joint[i].parent.coords.getOrigin());
}
}
/** Modify a Skeleton to be a weighted average of an arbitrary list of Skeletons,
averaged about this one.
@param average the Skeleton to modify to be an average of other Skeletons
@param s the list of Skeletons to average
@param weight the weights for the different Skeletons
*/
public void blend(Skeleton average, Skeleton s[], double weight[])
{
for (int i = 0; i < joint.length; i++)
{
Joint javg = average.getJoint(joint[i].id);
Vec3 pos = null, newpos = null;
double rootangles[] = null;
if (joint[i].parent == null)
{
pos = joint[i].coords.getOrigin();
newpos = new Vec3(pos);
rootangles = joint[i].coords.getRotationAngles();
javg.coords = joint[i].coords.duplicate();
}
// Find the average DOF values.
javg.angle1.pos = joint[i].angle1.pos;
javg.angle2.pos = joint[i].angle2.pos;
javg.twist.pos = joint[i].twist.pos;
javg.length.pos = joint[i].length.pos;
for (int j = 0; j < s.length; j++)
{
Joint jother = s[j].getJoint(joint[i].id);
if (jother == null)
continue;
javg.length.pos += weight[j]*findOffset(jother.length, joint[i].length);
if (joint[i].parent == null)
{
newpos.add(jother.coords.getOrigin().minus(pos).times(weight[j]));
double angles[] = jother.coords.getRotationAngles();
RotationKeyframe rot = new RotationKeyframe(angles[0]-rootangles[0], angles[1]-rootangles[1], angles[2]-rootangles[2]);
rot.applyToCoordinates(javg.coords, weight[j], null, null, true, true, true, true);
}
else
{
javg.angle1.pos += weight[j]*findOffset(jother.angle1, joint[i].angle1);
javg.angle2.pos += weight[j]*findOffset(jother.angle2, joint[i].angle2);
javg.twist.pos += weight[j]*findOffset(jother.twist, joint[i].twist);
}
}
if (joint[i].parent == null)
{
javg.coords.setOrigin(newpos);
double angles[] = javg.coords.getRotationAngles();
javg.angle1.pos = angles[0];
javg.angle2.pos = angles[1];
javg.twist.pos = angles[2];
}
// Give each DOF a chance to clip the value.
javg.angle1.set(javg.angle1.pos);
javg.angle2.set(javg.angle2.pos);
javg.twist.set(javg.twist.pos);
javg.length.set(javg.length.pos);
}
// Update coordinate systems.
for (int i = 0; i < average.joint.length; i++)
if (average.joint[i].parent == null)
average.joint[i].recalcCoords(true);
}
/** Utility routine for finding the angle offsets. */
private double findOffset(DOF gesture, DOF defaultPose)
{
double diff = gesture.pos-defaultPose.pos;
if (gesture.loop)
{
double range = gesture.max-gesture.min;
while (diff > gesture.max)
diff -= range;
while (diff < gesture.min)
diff += range;
}
return diff;
}
/** Draw the skeleton onto a canvas. */
public void draw(MeshViewer view, boolean enabled)
{
Camera cam = view.getCamera();
int mode = view.getRenderMode();
boolean render = (mode != ViewerCanvas.RENDER_WIREFRAME && mode != ViewerCanvas.RENDER_TRANSPARENT && !(view.getCurrentTool() instanceof SkeletonTool));
Vec2 p[] = new Vec2 [joint.length], v1 = new Vec2(), v2 = new Vec2();
Point screenVert[] = new Point [joint.length], p1 = new Point(), p2 = new Point();
double screenZ[] = new double [joint.length];
Color color = (enabled ? ViewerCanvas.lineColor : ViewerCanvas.disabledColor), col;
int colInt;
// First calculate the positions of all the joints.
for (int i = 0; i < joint.length; i++)
{
Joint j = joint[i];
Vec3 pos = j.coords.getOrigin();
p[i] = cam.getObjectToScreen().timesXY(pos);
screenVert[i] = new Point((int) p[i].x, (int) p[i].y);
screenZ[i] = cam.getObjectToView().timesZ(pos);
}
// Now draw the bones.
col = color;
Mat4 objToScreen = cam.getObjectToScreen();
for (int i = 0; i < joint.length; i++)
{
Joint j = joint[i], parent = j.parent;
if (parent == null)
continue;
int parentIndex = findJointIndex(parent.id);
Vec3 zdir = j.coords.getOrigin().minus(parent.coords.getOrigin());
double length = zdir.length();
Vec3 xdir = j.coords.getUpDirection().times(BONE_WIDTH*length);
Vec3 ydir = zdir.cross(xdir);
ydir.normalize();
ydir.scale(0.5*BONE_WIDTH*length);
zdir.scale(WIDEST_POINT);
Vec3 center = parent.coords.getOrigin().plus(zdir);
Vec3 cx1 = center.plus(xdir), cx2 = center.minus(xdir);
Vec3 cy1 = center.plus(ydir), cy2 = center.minus(ydir);
if (render)
{
Vec3 pos[] = new Vec3 [] {
cx1, cx2, cy1, cy2, joint[i].coords.getOrigin(), joint[parentIndex].coords.getOrigin()
};
for (int k = 1; k < pos.length; k++)
for (int m = 0; m < k; m++)
view.renderLine(pos[k], pos[m], cam, col);
}
else
{
Vec2 pos[] = new Vec2 [] {
objToScreen.timesXY(cx1), objToScreen.timesXY(cx2),
objToScreen.timesXY(cy1), objToScreen.timesXY(cy2),
p[i], p[parentIndex]
};
for (int k = 1; k < pos.length; k++)
{
p1.x = (int) pos[k].x;
p1.y = (int) pos[k].y;
for (int m = 0; m < k; m++)
{
p2.x = (int) pos[m].x;
p2.y = (int) pos[m].y;
view.drawLine(p1, p2, col);
}
}
}
}
// Finally draw the markers for all the joints.
int selectedID = view.getSelectedJoint();
boolean locked[] = view.getLockedJoints();
for (int i = 0; i < joint.length; i++)
{
Joint j = joint[i];
if (locked[i])
col = ViewerCanvas.specialHighlightColor;
else if (j.id == selectedID)
col = ViewerCanvas.highlightColor;
else
col = color;
if (render)
{
v1.x = v2.x = p[i].x;
v1.y = p[i].y-MARKER_WIDTH;
v2.y = p[i].y+MARKER_WIDTH;
view.renderLine(v1, screenZ[i], v2, screenZ[i], cam, col);
v1.y = v2.y = p[i].y;
v1.x = p[i].x-MARKER_WIDTH;
v2.x = p[i].x+MARKER_WIDTH;
view.renderLine(v1, screenZ[i], v2, screenZ[i], cam, col);
}
else
{
p1.x = p2.x = screenVert[i].x;
p1.y = screenVert[i].y-MARKER_WIDTH;
p2.y = screenVert[i].y+MARKER_WIDTH;
view.drawLine(p1, p2, col);
p1.y = p2.y = screenVert[i].y;
p1.x = screenVert[i].x-MARKER_WIDTH;
p2.x = screenVert[i].x+MARKER_WIDTH;
view.drawLine(p1, p2, col);
}
}
}
/** Update a mesh after its skeleton has moved. oldMesh is the mesh before movement.
newMesh is a duplicate of it with its skeleton in a different position. This
method repositions the vertices of newMesh based on the skeleton. */
public static void adjustMesh(Mesh oldMesh, Mesh newMesh)
{
Skeleton s1 = oldMesh.getSkeleton(), s2 = newMesh.getSkeleton();
MeshVertex v1[] = oldMesh.getVertices(), v2[] = newMesh.getVertices();
Vec3 v[] = new Vec3 [v2.length];
Vec3 temp = new Vec3();
for (int i = 0; i < v2.length; i++)
{
v[i] = v2[i].r;
if (v2[i].ikJoint == -1)
continue;
Joint j1 = s1.getJoint(v1[i].ikJoint), j2 = s2.getJoint(v2[i].ikJoint);
if (j1 == null || j2 == null)
continue;
double weight = (j2.parent == null ? 1.0 : v2[i].ikWeight);
v[i].set(v1[i].r);
j1.coords.toLocal().transform(v[i]);
j2.coords.fromLocal().transform(v[i]);
if (weight < 1.0)
{
v[i].scale(weight);
temp.set(v1[i].r);
j1.parent.coords.toLocal().transform(temp);
j2.parent.coords.fromLocal().transform(temp);
temp.scale(1.0-weight);
v[i].add(temp);
}
// Adjust the vertex positions to reduce the "squashing" effect around
// bent joints.
double olddist = v1[i].r.distance2(j1.coords.getOrigin());
double newdist = v[i].distance2(j2.coords.getOrigin());
if (olddist > 0.0 && newdist > 0.0)
{
v[i].subtract(j2.coords.getOrigin());
v[i].scale(Math.pow(olddist/newdist, 0.5*v1[i].ikWeight));
v[i].add(j2.coords.getOrigin());
}
}
newMesh.setVertexPositions(v);
}
/** Write a serialized representation of this skeleton to a stream. */
public void writeToStream(DataOutputStream out) throws IOException
{
out.writeShort(0); // Version number
out.writeInt(joint.length);
for (int i = 0; i < joint.length; i++)
{
// Write the information about this joint.
Joint j = joint[i];
out.writeInt(j.id);
out.writeUTF(j.name);
j.coords.writeToFile(out);
j.angle1.writeToStream(out);
j.angle2.writeToStream(out);
j.twist.writeToStream(out);
j.length.writeToStream(out);
out.writeInt(j.parent == null ? -1 : j.parent.id);
out.writeInt(j.children.length);
for (int k = 0; k < j.children.length; k++)
out.writeInt(j.children[k].id);
}
}
/** Reconstruct a skeleton from its serialized representation. */
public Skeleton(DataInputStream in) throws IOException, InvalidObjectException
{
short version = in.readShort();
if (version != 0)
throw new InvalidObjectException("");
nextID = 1;
joint = new Joint [in.readInt()];
int parentID[] = new int [joint.length];
int childID[][] = new int [joint.length][];
for (int i = 0; i < joint.length; i++)
{
// Read in the information about joints.
int id = in.readInt();
String name = in.readUTF();
Joint j = new Joint(new CoordinateSystem(in), null, name);
joint[i] = j;
j.id = id;
j.angle1 = j.new DOF(in);
j.angle2 = j.new DOF(in);
j.twist = j.new DOF(in);
j.length = j.new DOF(in);
j.length.loop = false;
parentID[i] = in.readInt();
childID[i] = new int [in.readInt()];
for (int k = 0; k < childID[i].length; k++)
childID[i][k] = in.readInt();
if (j.id >= nextID)
nextID = j.id+1;
}
// Assign the parents and children for each joint.
for (int i = 0; i < joint.length; i++)
{
joint[i].parent = getJoint(parentID[i]);
joint[i].children = new Joint [childID[i].length];
for (int k = 0; k < childID[i].length; k++)
joint[i].children[k] = getJoint(childID[i][k]);
}
}
}
Run 1
The code is moderately readable but has several issues. The structure is logical with clear method separation, but many methods are overly long and complex (e.g., `blend`, `draw`, `adjustMesh`). Nesting depth reaches 4-5 levels in places, making logic harder to follow. Intent is generally clear from method names and comments, but the sheer density of operations within methods obscures the overall flow. Code length is excessive for what each method accomplishes, with repeated patterns that could be extracted. Action granularity is mixed - some areas pack multiple operations per line, while others are appropriately granular. Reading flow is disrupted by dense blocks, inconsistent brace style, and lack of blank lines between logical sections, though comments help somewhat. The duplicated logic between `duplicate()` and `copy()` adds unnecessary length and maintenance burden.
Run 2
The code is generally well-organized with clear method separation and descriptive names, but several factors reduce readability. Methods like `duplicate()` and `copy()` contain repetitive and deeply nested logic for rebuilding parent-child relationships, making them harder to follow. `findJointIndex()` uses a manual binary search with non-obvious edge-case handling and loops, which could be simplified with `Arrays.binarySearch` or a map. The `draw()` method is long and mixes rendering modes, leading to complex conditional blocks and array initializations. Some operations combine multiple actions per line (e.g., variable declarations and array copies), slightly hurting granularity. Comments are helpful but sparse in complex sections like `blend()` and `adjustMesh()`. While the structure is logical and intent is mostly clear, nesting depth, verbosity, and mixed concerns in rendering lower the score.
Run 3
The code is generally well-structured with clear method separation and consistent naming conventions. However, readability suffers from several issues: deep nesting in methods like duplicate(), copy(), and draw(); multiple actions per line in constructor initializations and array copies; and some overly long methods (draw() and blend() exceed 50 lines). The findJointIndex() method's binary search implementation is convoluted with awkward loop conditions. Mixed brace styles and inconsistent comment placement (sometimes above loops, sometimes inside) disrupt flow. While the intent is mostly clear through method names and Javadoc comments, complex logic in blend() and adjustMesh() lacks sufficient explanatory comments. The code could benefit from extracting helper methods, reducing nesting depth, and simplifying conditional logic. Overall, the code is maintainable but requires significant mental effort to follow in places.
Run 1
Code Structure: Methods are logically separated and documented, but the class combines joint management, animation, rendering, mesh deformation, and serialization. Nesting: Most methods are shallow, though rendering and blending contain several nested loops and conditionals. Clarity of intent: Descriptive method names, constants, and comments explain major operations; abbreviated variables, dense mathematical expressions, and undocumented assumptions such as sorted joint IDs require extra interpretation. Code length: Much of the length is justified by functionality, but repeated array resizing, relationship copying, and rendering logic could be extracted into helpers. Action granularity: Most statements perform one action, although multiple declarations, chained assignments, and increments inside indexing reduce clarity. Reading flow: Method-level flow is generally natural, with useful phase comments and early exits, but inconsistent brace placement, omitted braces, and long methods interrupt scanning.
Run 2
Code Structure: Well-separated methods and documented responsibilities provide logical organization, but the class combines hierarchy management, animation, rendering, mesh deformation, and serialization. Nesting: Most methods are shallow; draw() and blend() contain nested loops and conditionals that increase reading effort. Clarity of intent: Method names, constants, and comments explain purpose well, while abbreviated variables and dense mathematical expressions obscure implementation details. Code length: Much of the length is justified, but duplicated copying, array resizing, and rendering logic could be extracted into helpers. Action granularity: Most statements perform one action per line, though multiple declarations, chained assignments, and incrementing array indices introduce occasional compression. Reading flow: Operations generally proceed in clear stages, aided by comments and early returns. Inconsistent brace placement, frequent omitted braces, long expressions, and implicit assumptions such as sorted joint IDs interrupt otherwise coherent flow.
Run 3
Code Structure: Well-separated, documented methods organize skeleton operations, but the class combines management, rendering, blending, mesh adjustment, and serialization. Nesting: Most methods are shallow; draw() has deeply nested loops and branches that increase reading effort. Clarity of intent: Method names and explanatory comments convey purpose, while abbreviated variables, dense geometric expressions, and undocumented assumptions in joint lookup reduce clarity. Code length: Generally purposeful, but duplicate()/copy() and repeated array-management and rendering logic introduce avoidable repetition; draw() and blend() would benefit from helper methods. Action granularity: Most statements perform one action, though compound declarations, chained assignments, and embedded increments occasionally obscure individual steps. Reading flow: Operations usually follow natural sequences, reinforced by phase comments and early returns. Inconsistent brace placement, omitted braces, long lines, and older array declaration conventions make scanning less smooth.