joXSI - open source xsi model loader and renderer, with skinned animations

http://www.mojang.com/joXSI/logo.gif

http://www.mojang.com/joXSI/

[quote]joXSI is currently far from done. It doesn’t even have a version number yet.
So far, it does the following:

* Parses all templates for xsi files version 3.5
* Builds vertex lists for vertex positions, colors, normals and up to four texture units
* Applies all FCurves in a clip on command (this means it supports simple srt animation)
* If there is an envelope, they are automatically applied to the vertex lists (this means it supports skinned animation)
* Loads a texture by name. This is the ONLY thing it does with materials so far.
* Renders the scene via JOGL/JSR 231

[/quote]
Demo via webstart: http://www.mojang.com/joXSI/demo/joXSI_demo.jnlp

i can’t get the “templates” frame to grab focus :-\

now that i look, alt-tabbing to the “model displayer” window it doesn’t grab the focus back. i’ll have to kill the java procces to exit :-\

whoa, odd

Either there’s some deadlock, or it’s starving the awt dispatch thread, would be my guess.

Excuse my ignorance, but where does XSI fit in in the world of 3d content creators?

It’s an open and very powerful format, supporting things like inverse kinematics, NURBS, skeletal animation, hierarchical materials, and, of course, vertex data.
It’s used for Half-Life 2, and there are XSI exporters available for most popular 3d modelling tools.

Hi Markus,

As you may be aware, our design pipeline for Marathon is exclusively based on XSI. So, I’ve created a dotXSI parser about two years ago, which is built in our main pre-processing tool. The parser is relatively old (version 3.0), but currently good enough for our needs (haven’t had the chance to upgrade it). Anyway, if you’re interested in building a more general/useful tool out of this and not just an importer/viewer, I may be able to contribute some of the following:

  • Vertex indices optimization (for any vertex cache size)
  • FCurve keys optimization
  • Detection of bad bone weights + fix up
  • Tangent-space creation with a Java port of NVMeshMender (although there’s decent support for tangents in the latest XSI versions)
  • Normal map generation with the use of ATI NormalMapper (automatic export of low & high res models to .NMF and exec of NormalMapper, with full UI defined parameters)
  • Texture map generation (support for any kind of texture, mip creation, possibly hardware accelerated with shaders)
  • LWJGL renderer
  • Support for dotXSI 3.6
  • Some kind of general/customizable binary export, for immediate inclusion to third-party file formats.

Code for everything but the last two already exists in our tool, although not general enough for a simple copy/paste. Tell me if you’re interested.

I’m extremely interested in contributions. :slight_smile:
You’ll get full credits, of course.

I’m going to use joXSI in wurm for all model rendering, so it needs to be flexible enough replace fairly advanced things like intercepting/replacing materials (for unique skin textures on players), retrieving the transformed location of a mesh in the model (for particle emitters and pluggable weapons), and loading clips from one scene and applying to a model from another scene.

I’ve got big plans for joXSI. :wink:

The goal is to make this be able to handle everyting in an XSI file, including the materials, inverse kinematics and nurbs.
I call it an “importer” since the rendering code is jogl specific, and really only supplied as a refence so far, but I’ve got a feeling supporting the materials will force it to have slightly more integrated rendering code.

You don’t happen to know the specification for the binary xsi formats? So far, I only support text encoding.
I’ve looked, but all I can find is that it can either be binary (but in what format? seems to be hidden in the ftk binaries somewhere), or compressed (lzw or gzip).

[quote=“Markus_Persson,post:7,topic:26010”]
Cool, I was planning some of these too. One question though, what are your needs exactly wrt materials? I could imagine using the illumination shaders & Cg shaders, extracting per cluster materials, textures, etc. Do you plan something more elaborate?

[quote=“Markus_Persson,post:7,topic:26010”]
I’ve no idea. That’s why I recommended a more open binary file, easily exportable by joXSI. Cleaned up of course (no xsi stuff) and having anything joXSI adds to it.

BTW, I noticed the cubic() method in the Interpolator class. Do you know if that is the exact interpolation xsi does internally for cubic fcurves?

[quote=“Spasi,post:8,topic:26010”]
Awesome, let’s join forces. :slight_smile:
We probably should move it to sourceforge or java.net so we get some free cvs. Much more convenient than emailing code.

[quote=“Spasi,post:8,topic:26010”]
Yes; replacing materials on the fly. For example, the textures for player models would be baked with the textures of the clothes they wear, so they need to be replaced with something dynamic. Probably something as simple like this:

playerModel.setMaterial("player.torso", new PlayerMaterial(player.getClothes());
playerWalkAnimation.apply(playerModel, player.getWalkTime());
modelRenderer.render(playerModel);

[quote=“Spasi,post:8,topic:26010”]
Ack, i’ll keep looking. It’d be nice to be able to import those as well.
I’m thinking that instead of a new file format, we could just prove an XSI file optimizer that removes templates not needed/used by joXSI (I know, this goes against the idea of supporting everything…) and optimses away noop fcurves (i’ve seen a lot of fcurves with just zeroes in them), then saves that as a gzip or lzw compressed XSI file.

[quote=“Spasi,post:8,topic:26010”]
Oh how I struggled with that one. The documentation is… wrong, so that one is the result of a lot of guessing and manual .xsi file inspection… it seems to work with my test models, but feel free to do some more testing. =)
It makes absolutely no sense that it would use the input vectors from the next keyframe as the output from the current (instead of the output vectors from the current), so I think it’s wrong. :wink:

Awesome :slight_smile: I might want to contribute too, if there’s anything specific which needs to be done. But could you comment your code please? ;D

Haha, working on it. =)

New version up, with plenty of comments.

The javadocs are still a bit sparse, but it’s soooo boooring to write good javadocs.

[quote=“Markus_Persson link=topic=12261.msg97671#msg97671 date=1138397887][quote author=Spasi,post:9,topic:26010”]
Oh how I struggled with that one. The documentation is… wrong, so that one is the result of a lot of guessing and manual .xsi file inspection… it seems to work with my test models, but feel free to do some more testing. =)
It makes absolutely no sense that it would use the input vectors from the next keyframe as the output from the current (instead of the output vectors from the current), so I think it’s wrong. :wink:
[/quote]
OK, I spent the whole weekend and a couple of hours today trying to finally find a solution for this. I think I succeeded! :smiley:

So, basics first. I found this definition in an FTK file (xsi_fcurve.h):

[quote]Interpolation (how the value is evaluated between FCurveKeys) can be constant, linear, or cubic. Cubic means that a Bezier curve is calculated as the interpolation between the keys. XSI uses cubic Bezier curves defined as follows:

Definition from de Casteljau’s algorithm:

Four points A, B, C and D in the plane or in three-dimensional space define a cubic Bezier curve. The curve starts at A going toward B and arrives at D coming from the direction of C. In general, it will not pass through B or C; these points are only there to provide directional information. The distance between A and B determines “how long” the curve moves into direction B before turning towards D.
[/quote]
A is the “left” keyframe and B is its right tangent. D is the “right” keyframe and C is its left tangent. I believe that explains it well.

One thing I noticed was that de Casteljau’s algorithm is not the most efficient way to do cubic interpolation. Simply evaluating an optimized version of the curve’s parametric form is 4 instructions less. Compare the following:

de Casteljau’s algorithm

public static float interpolateCubic(float a, float b, float c, float d, float t) {
    // 18 instructions
    float ab = a + (b - a) * t;
    float bc = b + (c - b) * t;
    float cd = c + (d - c) * t;

    float abbc = ab + (bc - ab) * t;
    float bccd = bc + (cd - bc) * t;

    return abbc + (bccd - abbc) * t;
}

Parametric form

public static float interpolateCubic(float a, float b, float c, float d, float t) {
    // B(t) = a(1-t)^3 + 3bt(1-t)^2 + 3ct^2(1-t) + dt^3

    // Let tInv = (1-t)
    // B(t) = a * tInv^3 + 3 * t * tInv * (b * tInv + c * t) + d * t^3
    // B(t) = tInv * (a * tInv^2 + 3 * t * (b * tInv + c * t)) + d * t^3

    // 14 instructions
    float tInv = 1.0f - t;
    return tInv * (a * tInv * tInv + 3.0f * t * (b * tInv + c * t)) + d * t * t * t;
}

Anyway, the cubic method in your Interpolator class is wrong, simply because you’re not taking into account the x-axis values (time in keyframes). Imagine a key with “flat” left/right tangents (aligned to x-axis, height equal to the key). Grab one of the tangents and drag it left or right, without changing the height. Obviously, the curve profile is changing even without touching the y-axis values. We have to incorporate this effect to the interpolation algorithm. Some pics (click for hi-res, curve sampling is sparse on purpose to show the differences):

Original curve in XSI


http://www.zdimensions.gr/spasi/public/060130/curveXSI_thumb.jpg

Interpolating Y-Axis values only


http://www.zdimensions.gr/spasi/public/060130/curveYOnly_thumb.jpg

So, why don’t we just interpolate the x-axis values too? Yeah, that’s also what I thought would easily give some kind of solution. But, no:

Interpolating X-Axis values


http://www.zdimensions.gr/spasi/public/060130/curveXPulled_thumb.jpg

Well, we did get a nice curve. But there’s obviously something wrong with the above image. The (not so) vertical lines show where/when exactly we asked an interpolated value and what we ended up getting. As you can see, the sampled points tend to be pulled towards areas of the curve that change significantly. This is great if you only need to draw the curve (with a given amount of samples, you get the best quality), but not so if you just want to get an interpolated value at some time offset. You want the value at exactly that offset.

My solution was the simplest I could think of, but I believe it’s far from optimal performance-wise. Suppose I want to get the interpolated value at time t. Let Bx and By be equivalent to calling the above interpolateCubic with the x-axis and y-axis values respectively. To get the correct value, I have to interpolate the y-axis values at a time t’, where t’ is the value that satisfies the equation Bx(t’) = t. So, the correct result would be By(t’).

To solve the Bx(t’) = t for t’, we need to bring the curve’s parametric form to a better one:

B(t) = a(1-t)^3 + 3bt(1-t)^2 + 3ct^2(1-t) + dt^3 ==> (1)
B(t) = (-a + 3b - 3c + d)t^3 + 3(a - 2b + c)t^2 + 3(-a + b)t + a ==> (2)
// Let A = (-a + 3b - 3c + d)
// Let B = (a - 2b + c)
// Let C = (-a + b)
B(t) = At^3 + Bt^2 + Ct + a (3)

(1) is the original equation, (2) is what occurs after some math and (3) is a standard, solvable cubic equation. So, once you’ve solved the equation, with something like this:


public static float interpolateCubicInverse(final float a, final float b, final float c, final float d, final float t) {
    float A = (-a + 3.0f * b - 3.0f * c + d);
    float B = (3.0f * (a - 2.0f * b + c));
    float C = (3.0f * (b - a));
    float D = a - t;

    int rootCount = solveCubic(A, B, C, D, roots, 0); // TODO: How to handle multiple roots?

    return roots[0];
}

You can finally get the proper result:

float t = ...;
t = interpolateCubicInverse(x1, influenceX1, influenceX2, x2, t);
float value = interpolateCubic(y1, influenceY1, influenceY2, y2, t);

Correct Interpolation


http://www.zdimensions.gr/spasi/public/060130/curveCorrect_thumb.jpg

Some notes:

  • Solving the cubic equation is the most costly part of this algorithm. 20+ instructions, one sqrt and two cube roots for the common case.
  • I’m not sure how to handle multiple roots returned by solveCubic. From my tests, it occured very few times and just picking the first one (they’re ordered by value) produced no problems.
  • An obvious optimization is to take advantage of equation (2). By storing A, B, C per key interval, instead of key & tangent values, interpolateCubic falls to 9 instructions, whereas solveCubic is the only cost in interpolateCubicInverse.
  • I’m sure there are faster ways. One is obvious from the “Interpolating X-Axis values” image: Sample the curve at several points, optimize based on an error threshold, then store the resulting keyframes as a linear curve. There’s a performance-memory trade-off here of course.

Pheew! Sorry for the long post! :wink:

Very nice. :slight_smile:
Exhaustive, and good pictures so even I can understand it. :wink:

What’s the source code for solveCubic?
Obviously, having a bunch of srqts in there isn’t exactly optimal.

Hehe, I just found an implementation in the JDK. I had forgotten Java2D supports cubic curves. You can find a solveCubic in java.awt.geom.CubicCurve2D. It also has a couple of methods that handle multiple roots.

edit: There are some nice, generic math methods in java.awt.geom (solveCubic in CubicCurve2D, solveQuadratic in QuadCurve2D). Why don’t they put these in java.lang.Math?

Ah, cool. I did not know that!

I’ll work some more on joXSI later this week. Right now I’m rewriting the terrain renderer in wurm, for greater fps.

(Screenies:

http://www.mojang.com/notch/screenshots/newterrain.jpg
http://www.mojang.com/notch/screenshots/newterrain2.jpg

The second one is slower because of wireframe mode only. I have no idea why it’s that much slower…
There’s not a single shared texel in that scene. :smiley: All unique texturing)

If anyone with access to XSI wants to save a test dotXSI file in both binary and text format, so I can get started on reverse engineering the binary format, I’d appreciate it. =)

[quote=“Markus_Persson,post:16,topic:26010”]
Consumer-level GPUs do not hardware accelerate line rendering. The chips can do it, but it is disabled by the drivers. That’s how they sell professional cards (Quadro, FireGL, etc), which essentially use the same chips.

[quote=“Markus_Persson,post:16,topic:26010”]
No need to, I figured it out a few days ago. :wink:

private static final Pattern HEADER_RE = Pattern.compile("xsi ([0-9][0-9])([0-9][0-9])(txt|bin) 00(32|64)");

public static enum Format {
    TXT,
    BIN,
}

public XSIFile(File file) throws IOException, XSIParserException {
    ByteBuffer buffer = fileRead(file).order(ByteOrder.nativeOrder());

    if ( buffer.limit() < 16 )
        throw new IllegalArgumentException("Corrupt dotXSI file. Size: " + buffer.limit());

    String header = strReadBytes(buffer, 16);

    Matcher matcher = HEADER_RE.matcher(header);
    if ( !matcher.matches() )
        throw new XSIParserException("Invalid dotXSI header: " + header);

    this.majorVersion = parseInt(matcher.group(1));
    this.minorVersion = parseInt(matcher.group(2));

    this.format = Enum.valueOf(Format.class, matcher.group(3).toUpperCase());

    this.floatSize = parseInt(matcher.group(4));

    if ( format == Format.BIN ) {
        // Skip empty space
        while ( buffer.hasRemaining() && buffer.get(buffer.position()) == '\n' )
            buffer.position(buffer.position() + 1);

        if ( buffer.remaining() < 3 * 4 )
            throw new XSIParserException("Corrupt binary dotXSI file.");

        // Skip trailing zeros.
        buffer.limit(buffer.limit() - 4);

        int decompressedSize = buffer.getInt();
        int compressedSize = buffer.getInt();

        if ( decompressedSize < 0 )
            throw new XSIParserException("Corrupt binary dotXSI file. Decompressed size found: " + decompressedSize);
        if ( compressedSize < 0 || compressedSize != buffer.remaining() )
            throw new XSIParserException("Corrupt binary dotXSI file. Compressed size found: " + compressedSize);

        // Allocate a buffer to hold the decompressed data.
        ByteBuffer decompressed = ByteBuffer.allocate(decompressedSize);

        // Create the decompressor.
        InputStream input = new InflaterInputStream(new BufferInputStream(buffer));

        // Read the decompressed data.
        byte[] bytes = new byte[1024];
        int count;
        while ( (count = input.read(bytes)) > 0 ) {
            if ( decompressed.remaining() < count )
                throw new XSIParserException("Corrupt binary dotXSI file.");

            decompressed.put(bytes, 0, count);
        }

        decompressed.flip();

        new XSIParser(this, decompressed);
    } else
        new XSIParser(this, buffer);
}

Just a note, BufferInputStream is a simple InputStream implementation using a ByteBuffer as the stream source.

The “binary format” is just a deflated version of the text format? Wow. :-\

Awesome work, btw. :slight_smile:

[quote=“Markus_Persson,post:18,topic:26010”]
Yeah, I got the hint from the presence of zlib headers in the FTK.

[quote=“Markus_Persson,post:18,topic:26010”]
:wink:

Hm, are you sure it wasn’t one of the compressed formats?

There’s both gzip and… something else, I think