Bones - Skeletal and Pose animations for jPCT

Started by raft, January 06, 2010, 11:45:01 PM

Previous topic - Next topic

raft

i have a 32 bit windows 7 and didnt experience a similar problem.

BonesIO is used to load/save Bones' native format. to import Ogre, you need to use BonesImporter. have a look at OgreSample for usage.

AGP

What are the dependencies for importing the Ogre files? I usually use a simple programmer's notepad I wrote myself so I can't read the Eclipse project. In other words, what do I pass to the classpath argument (where, for instance, is the OgreLoader class)?

raft

you need the jME jar (in the zip) to import from Ogre format. you can either programatically or use the spcripts provided to convert to bones' native format. that way you need no external dependencancy..

AGP

#93
I got it, thanks. I just made a jar with all the necessary JME class files which is about 11% of the three jme jars. If that's at all legal, you should include it in place of the three jars.

raft

not 100% sure but i guess it's legal. but i dont see it as a necessity. suggested way is to convert to bones' native format and use it. you can of course use your smaller combined jar and load directly from ogre format.

EgonOlsen

I've played around a little bit with Bones just for fun...it seems to me, that the normals are never updated with the animation, because when using the rotate-animation of the ninja in combination with a light source, the source seems to move with the model instead of lighting the parts that it's actually facing. Am i missing something?

raft

no, you are right, normals arent updated. i've never used a light source in combination with animation so never noticed it.

i guess calling Object3D.calcNormals() each frame is expensive, so any suggestions? maybe i should do some research for this

EgonOlsen

Yes, that would be too expensive...for keyframes, i'm interpolating normals in the same way as i do interpolate vertices, but i guess it's another story here...

Another question: I tried to render three models at a time, which works fine...unless i'm trying to do the animation processing in parallel in three threads. The models are all loaded and created individually (i.e. i've simply loaded the ninja three times), but if i call animateSkin(...) in parallel, all hell breaks loose and they start to look as if they had a severe car accident or something. If i synchronize the animation calls to some common object, all is fine again. Am i doing something wrong? Any ideas?

Here's my hacky test case (with multi threaded animations disabled...enable it with useMultipleThreads=true), if that helps:


package bones.samples;

import java.io.FileInputStream;

import org.lwjgl.opengl.Display;

import raft.jpct.bones.Animated3D;
import raft.jpct.bones.AnimatedGroup;
import raft.jpct.bones.BonesIO;

import com.threed.jpct.Camera;
import com.threed.jpct.Config;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.IPaintListener;
import com.threed.jpct.IRenderer;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;
import com.threed.jpct.threading.WorkLoad;
import com.threed.jpct.threading.Worker;
import com.threed.jpct.util.Light;

public class SimpleBones implements IPaintListener {

private static final long serialVersionUID = 1L;

private Ninja ninja = null;
private Ninja anotherNinja = null;
private Ninja theLastNinja = null;
private FrameBuffer buffer = null;
private World world = null;
private int fps = 0;
private long time = System.currentTimeMillis();
private Worker worker = new Worker(3);
private WorkLoad[] loads = new WorkLoad[3];

private boolean useMultipleThreads = false;
private boolean compile = true;

public static void main(String[] args) {
SimpleBones sb = new SimpleBones();
sb.runIt();
}

public void runIt() {
config();
init();
loop();
}

public void finishedPainting() {
fps++;
if (System.currentTimeMillis() - time >= 1000) {
System.out.println(fps + "fps");
fps = 0;
time = System.currentTimeMillis();
}
}

public void startPainting() {
}

private void config() {
Config.useMultipleThreads = true;
}

private void loop() {
try {
while (!buffer.isInitialized()) {
Thread.sleep(100);
}

while (!Display.isCloseRequested()) {

if (!useMultipleThreads) {
for (WorkLoad wl : loads) {
wl.doWork();
}
} else {
for (WorkLoad wl : loads) {
worker.add(wl);
}
}

buffer.clear();
if (useMultipleThreads) {
worker.waitForAll();
}
world.renderScene(buffer);
world.draw(buffer);
buffer.update();
buffer.displayGLOnly();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private void init() {
buffer = new FrameBuffer(1024, 768, FrameBuffer.SAMPLINGMODE_GL_AA_2X);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
buffer.enableRenderer(IRenderer.RENDERER_OPENGL);
buffer.setPaintListener(this);

world = new World();

Texture texture = new Texture("./samples/data/ninja/nskingr.jpg");
TextureManager.getInstance().addTexture("ninja", texture);

ninja = new Ninja();
ninja.addToWorld(world);

anotherNinja = new Ninja();
anotherNinja.addToWorld(world);

theLastNinja = new Ninja();
theLastNinja.addToWorld(world);

if (compile) {
ninja.compile();
anotherNinja.compile();
theLastNinja.compile();
}

WorkLoad w0 = new WorkLoad() {

public void doWork() {
ninja.animate(1);
}

public void done() {
}

public void error(Exception arg0) {
}
};

WorkLoad w1 = new WorkLoad() {

public void doWork() {
anotherNinja.animate(2);
}

public void done() {
}

public void error(Exception arg0) {
}
};

WorkLoad w2 = new WorkLoad() {

public void doWork() {
theLastNinja.animate(5);
}

public void done() {
}

public void error(Exception arg0) {
}
};

loads[0] = w0;
loads[1] = w1;
loads[2] = w2;

anotherNinja.translate(new SimpleVector(-200, 0, 0));
theLastNinja.translate(new SimpleVector(200, 0, 0));

world.getCamera().moveCamera(Camera.CAMERA_MOVEIN, 600);
world.getCamera().moveCamera(Camera.CAMERA_MOVEUP, 200);
world.getCamera().lookAt(new SimpleVector(0, -200, 0));

Light light = new Light(world);
light.setIntensity(255, 255, 255);
light.setPosition(new SimpleVector(800, -400, 0));
light.setAttenuation(-1);
}

private static class Ninja {

private AnimatedGroup animatedGroup = null;
private float index = 0;
private long time = 0;

public Ninja() {
init();
}

public void compile() {
for (Animated3D o : animatedGroup) {
o.compile(true);
}
}

public void translate(SimpleVector trns) {
for (Animated3D o : animatedGroup) {
o.translate(trns);
}
}

public void animate(int seq) {
if (time == 0) {
time = System.nanoTime() / 1000000L;
}

long dt = (System.nanoTime() / 1000000L) - time;
time = dt + time;
index += dt / 1000f;

while (index > 1) {
index -= 1;
}
animatedGroup.animateSkin(index, seq);
}

public void addToWorld(World world) {
animatedGroup.addToWorld(world);
}

private void init() {
try {
FileInputStream fis = new FileInputStream("./samples/data/ninja/ninja.group.bones");
try {
AnimatedGroup skinnedGroup = BonesIO.loadGroup(fis);

for (Animated3D o : skinnedGroup) {
o.setTexture("ninja");
o.build();
o.discardMeshData();
o.setVisibility(true);
}
animatedGroup = skinnedGroup;
} finally {
fis.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

}
}


EgonOlsen

BTW: With a calcNormals() every frame for each model, my frame rate drops from 3100 fps to 400 fps in this test case...still fast enough for this case, but a huge drop nonetheless.

raft

thanks for the test case. Bones is not thread safe. the case you have discovered happens because JointChannel uses static temporary filler objects. i had made it this way to reduce memory usage. now i re-thinked about it and found it too restrictive. i've made the filler objects instance fields. this will slightly use more memory but i guess the gain worths it. i've uploaded the new version.

instead of loading a new instance from file, you can also clone AnimatedGroup. that will save a lot of memory. below is a sample. here to go multi-threaded there are two ways: either synchronize animation code or make each AnimatedGroup use its own SkeletonPose. the static switch shareSkeletonPose in below example demonstrates this.

a final note, instead of translate/rotate individual objects in a group, you can translate/rotate AnimatedGroup.getRoot(). it's a dummy object that all animated objects are added as child. and its sole purpose is to translate/rotate the whole group easily.

import java.io.FileInputStream;

import org.lwjgl.opengl.Display;

import raft.jpct.bones.Animated3D;
import raft.jpct.bones.AnimatedGroup;
import raft.jpct.bones.BonesIO;

import com.threed.jpct.Camera;
import com.threed.jpct.Config;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.IPaintListener;
import com.threed.jpct.IRenderer;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;
import com.threed.jpct.threading.WorkLoad;
import com.threed.jpct.threading.Worker;
import com.threed.jpct.util.Light;

public class SimpleBones2 implements IPaintListener {

private static final long serialVersionUID = 1L;

private Ninja ninja = null;
private Ninja anotherNinja = null;
private Ninja theLastNinja = null;
private FrameBuffer buffer = null;
private World world = null;
private int fps = 0;
private long time = System.currentTimeMillis();
private Worker worker = new Worker(3);
private WorkLoad[] loads = new WorkLoad[3];

private static boolean shareSkeletonPose = true;
private boolean useMultipleThreads = true;
private boolean compile = true;

public static void main(String[] args) {
SimpleBones2 sb = new SimpleBones2();
sb.runIt();
}

public void runIt() {
config();
init();
loop();
}

public void finishedPainting() {
fps++;
if (System.currentTimeMillis() - time >= 1000) {
System.out.println(fps + "fps");
fps = 0;
time = System.currentTimeMillis();
}
}

public void startPainting() {
}

private void config() {
Config.useMultipleThreads = true;
}

private void loop() {
try {
while (!buffer.isInitialized()) {
Thread.sleep(100);
}

while (!Display.isCloseRequested()) {

if (!useMultipleThreads) {
for (WorkLoad wl : loads) {
wl.doWork();
}
} else {
for (WorkLoad wl : loads) {
worker.add(wl);
}
}

buffer.clear();
if (useMultipleThreads) {
worker.waitForAll();
}
world.renderScene(buffer);
world.draw(buffer);
buffer.update();
buffer.displayGLOnly();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private void init() {
buffer = new FrameBuffer(1024, 768, FrameBuffer.SAMPLINGMODE_GL_AA_2X);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
buffer.enableRenderer(IRenderer.RENDERER_OPENGL);
buffer.setPaintListener(this);

world = new World();

Texture texture = new Texture("./samples/data/ninja/nskingr.jpg");
TextureManager.getInstance().addTexture("ninja", texture);

ninja = new Ninja();
ninja.addToWorld(world);

anotherNinja = new Ninja();
anotherNinja.addToWorld(world);

theLastNinja = new Ninja();
theLastNinja.addToWorld(world);

if (compile) {
ninja.compile();
anotherNinja.compile();
theLastNinja.compile();
}

WorkLoad w0 = new WorkLoad() {

public void doWork() {
ninja.animate(1);
}

public void done() {
}

public void error(Exception arg0) {
}
};

WorkLoad w1 = new WorkLoad() {

public void doWork() {
anotherNinja.animate(2);
}

public void done() {
}

public void error(Exception arg0) {
}
};

WorkLoad w2 = new WorkLoad() {

public void doWork() {
theLastNinja.animate(5);
}

public void done() {
}

public void error(Exception arg0) {
}
};

loads[0] = w0;
loads[1] = w1;
loads[2] = w2;

anotherNinja.translate(new SimpleVector(-200, 0, 0));
theLastNinja.translate(new SimpleVector(200, 0, 0));

world.getCamera().moveCamera(Camera.CAMERA_MOVEIN, 600);
world.getCamera().moveCamera(Camera.CAMERA_MOVEUP, 200);
world.getCamera().lookAt(new SimpleVector(0, -200, 0));

Light light = new Light(world);
light.setIntensity(255, 255, 255);
light.setPosition(new SimpleVector(800, -400, 0));
light.setAttenuation(-1);
}

private static class Ninja {

private AnimatedGroup animatedGroup = null;
private float index = 0;
private long time = 0;

public Ninja() {
init();
}

public void compile() {
for (Animated3D o : animatedGroup) {
o.compile(true);
}
}

public void translate(SimpleVector trns) {
animatedGroup.getRoot().translate(trns);
}

public void animate(int seq) {
if (time == 0) {
time = System.nanoTime() / 1000000L;
}

long dt = (System.nanoTime() / 1000000L) - time;
time = dt + time;
index += dt / 1000f;

while (index > 1) {
index -= 1;
}

if (shareSkeletonPose) {
synchronized (ninjaMaster) {
animatedGroup.animateSkin(index, seq);
}
} else {
animatedGroup.animateSkin(index, seq);
}
}

public void addToWorld(World world) {
animatedGroup.addToWorld(world);
}

private void init() {
this.animatedGroup = getNinjaMaster().clone(AnimatedGroup.MESH_DONT_REUSE);
if (!shareSkeletonPose)
animatedGroup.setSkeletonPose(animatedGroup.get(0).getSkeletonPose().clone());
}
}

private static AnimatedGroup ninjaMaster = null;

private static synchronized AnimatedGroup getNinjaMaster() {
if (ninjaMaster == null) {
try {
FileInputStream fis = new FileInputStream("./samples/data/ninja/ninja.group.bones");
try {
AnimatedGroup skinnedGroup = BonesIO.loadGroup(fis);

for (Animated3D o : skinnedGroup) {
o.setTexture("ninja");
o.build();
o.discardMeshData();
o.setVisibility(true);
}
ninjaMaster = skinnedGroup;
} finally {
fis.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return ninjaMaster;
}
}




raft

#100
after some research, seems as normals are updated similar to vertices but only with rotations (w/o translations) which logically makes sense.

i've implemented it, it seems ok but i'm not sure. @Egon, can u please check the following jar for lighting, my eyes are not as educated as yours ;)

<link removed>
edit: removed obsolute link

raft

normals in Pose animations is another story. seems as even Ogre doesn't do it, since it's too expensive. see this post. pose animations are typically used for small deformations (like facial animations) so i guess this wont be a problem for most cases.

EgonOlsen

Normals are looking fine now and so does using multiple threads to animate multiple meshes in parallel. Performance has suffered due to the additional normal calculation, but it's really worth it.
The whole point of this test case was to find out how much some simple multi-threading can help with the animations...here are the results on my 3.2Ghz quad core:


  • All in one thread: 1500fps
  • Animations in 3 different threads: 2100fps
  • Rendering in its own thread, animations and other logic in another thread (jPCT's default multi-threading support): 2700fps
  • Rendering in its own thread, logic in another and animations in 3 other threads: 2900fps

So i think that it can be worth it you run animations in different threads. That's all that i wanted to find out for no particular reason.

raft

cool, thanks for testing ;D i've made the changes official and uploaded the new version.

for performance lost, i think likewise. if it becomes a bottleneck for someone, i can later make updating normals optional. afterall there is no point of updating normals if a light source is not used.

Disastorm

Hello, I havn't tried this yet so I don't know much about it as I've always used keyframe animations, but does this only support premade animations or can we manipulate the bones directly.  I ask this because I just got a kinect the other day, and I think it would be really awesome to allow the player to control a model on the screen with their body.