Welcome, Guest!

Here are some links you may find helpful

Dreamcast More Agartha madness

Sifting

Well-known member
Original poster
Registered
Aug 23, 2019
63
168
33
SOLVED

I'll leave the post here for posterity. I'll put the solution in spoilers for anyone who still wants to try to solve it on their own. Most models are now decipherable, though it appears some have extra data that needs to be figure out before a general script may be written to convert them all to GLTF.


In hindsight it was kind of silly that I did not think of this sooner, as I've written tools in the past to process arbitrary geometry into triangle strips as well. The first block of vertices is a 'catch all' pool of primitives - single faces that cannot be included into a triangle strip. In the mesh header, entries [8] and [9] denote the number of triangles and quads in the pool. The length is thus 4*quads + 3*triangles. Subsequent blocks are all triangle strips, and have their lengths prefixed.


ORIGINAL TEXT:

I've been getting more time since settling into my new job, so I decided to return to finishing up my work on the Agartha project from last year. In the original thread I figured out No Cliche's weird custom compression algorithm to crack open the archive files, letting us access the textures and other assets. This was pretty cool in itself, but I wanted to convert the models and animations into GLTF like I had done with the Castlevania prototype.

To that end, I've made quite a bit of progress on saturday! I've mostly got the model format, .o6d, figured out, except for a huge sticking point. Here's an image - disregard the typenames, they're misnomers at the present:


NDAArst.png


I cannot for the life of me figure out how to compute the size of the first block of vertices. Technically speaking, it seems like the format stores geometry in batches of vertices arranged into triangle strips - no explicit indices are used - so this would be the first triangle trip out of 4. mesh2[10] in the image appears to indicate the number of strips that are in addition to the 'main' strip, the red block in the image whose size is mysteriously absent from the file. You can see the orange, yellow and green blocks are all prefixed with their lengths in the image. In the mesh header there is no such value, in fact, no where in the file is 18 even expressed outside of the vertex entries! I've asked everyone I know if they had any thoughts, but no one has managed to figure this one out.

As indicated in the file, we do have access to the length of the entire mesh block, which include header data and such. Subtracting the header size, we could in theory work backward, subtracting the lengths of the subsequent strips to get the size of the first block but... who does that? it makes no sense from a programming perspective to ever do it like that - there's no way to even figure out where to read the subsequent strips in the first place! So this solution is unacceptable. I cannot use it to create a GLTF translator.

For anyone interested in the files: https://we.tl/t-zjZXaUELBs (the link will expire in a week from writing this, unfortunately)

So does anyone have any ideas? I thought that maybe the size might be included in a bit field or something on the mesh header, but all the values there are fairly static between different files, so that appears not to be the case. If the value is explicitly encoded in the file, then it has to be somewhere at or before the mesh blocks. Worst case scenario I can try diving into the code with Ghidra...

EDIT:

Some basic layout info for anyone wanting to examine the file:

The file is mapped out in chunks. Each chunk has a 4 byte id, followed by a 32 bit length. Chunks ids in the o6d format: O6D!, MDLS, MATS, TEXS.

MDL data:

There appears to be only one of these per file. They are followed by one or more meshes.

Code:
struct Xform
{
    float m4x3[4][3];
    float unk[4]; //quaternion?
};
uint32 nbones;
uint32 unk0[3];
uint64 hierarchy[nbones];
Xform xforms[nbones];
uint8 unk1[100];
uint32 npoints;
uint32 nnormals;
uint8 unk2[10];
uint32 nmeshes;
uint32 unk3;

Mesh data:

Each mesh has a header, followed by one or more blocks of vertex data. NOTE: how do we known the length of the first block?

Code:
struct Vertex
{
    uint16 point;
    uint16 normal;
    float u, v;
};
struct Mesh
{
    uint32 size;
    uint16 unk[18];
    uint32 nblocks;
};

Following the mesh data, there is a block of 3D vectors giving the points, and another block of 3D vertices giving the normals.
Texture names follow from there.

Also note, most of the 'unknown' data is the same between files, regardless of complexity. I suspect most of it is actually serialised KAMUI2 structures.
 
Last edited:
  • Like
Reactions: Sega Dreamcast Info

Sifting

Well-known member
Original poster
Registered
Aug 23, 2019
63
168
33
And we have progress - the O6D model format is now readable. The template to read the file is attached below. I'll update my repository on github later as well. When I get the time I'll start on the GLTF converter so people can pull these assets into Blender, or Unity and such. An interesting quirk of this format is that it supports variable blend weights - I've only seen up to 3 used, but judging by the structure it may well support up to 4. For comparison, Half Life, a game noted for its use of skeletal animation, and also available on the Dreamcast, only permitted a maximum of 1. Using multiple weights improves the quality of of the model as it animates, but increases computation costs. Super impressive for the time!

The animation format, A6D remains to be reversed, as well as the files containing the level geometry.


Code:
//Agartha O6D template
//For use in 010editor
//Sifting

LittleEndian ();

struct Chunk
{
    char magick[4];
    uint size;
};
const uint O6D_VERSION = 0x279;
struct O6D
{
    SetBackColor (cYellow);
    Chunk chk;
    uint version;
    char txt[chk.size - sizeof (version)];
};
struct TEXS
{//Texture names, delimited by \0
    Chunk chk;
    char data[chk.size];
};
struct MATS
{
    Chunk chk;
    char data[chk.size];
};

struct Bone
{
    uint unk0;
    uint parent;
};
struct Vector
{
    float x, y, z;
};
struct M4x3
{
    Vector x, y, z, w;
};
struct Bone_xform
{
    M4x3 matrix1;
    float unk1[4];
    M4x3 matrix2;
    float unk2[4];
};

struct Bind
{
    uint bone;
    uint normals;
    uint points;
};
struct VBind
{
    uint16 count;
    uint16 weights;
    byte bones[4];
    float bias[weights];
};

struct Group
{
    uint unk;
    uint unk;
    uint npoints;
    uint unk;
    uint nnormals;
    uint unk;
    uint nsingle;
    uint nvariable;
    uint unk;
    uint nmeshes;
    uint unk;
};

struct Attractor
{
    uint bone;
    M4x3 xform;
};

struct Vertex
{
    SetBackColor (cGreen);
    uint16 normal;
    uint16 point;
    float u, v;
};

struct Strip
{
    uint length;
    Vertex verts[length];
};
struct Triangle
{
    Vertex a, b, c;
};
struct Quad
{
    Vertex a, b, c, d;
};
struct Mesh
{
    uint size;
    uint unk[7];
    uint ntris;
    uint nquads;
    uint nstrips;

    SetBackColor (cLtBlue);
    Triangle tris[ntris];
    
    SetBackColor (cBlue);
    Quad quads[nquads];
    
    SetBackColor (cDkBlue);
    local uint num = nstrips;
    while (num != 0)
    {
        Strip strip;
        num--;
    }
};
struct MDLS
{
    SetBackColor (cGreen);
    Chunk chk;
    uint nbones;
    uint unk;
    uint unk;
    uint unk;
    Bone bones[nbones];
    Bone_xform xforms[nbones];
    uint unk, unk;
    Vector frame[5];
    uint16 unk;
    uint16 nattractors;
    Attractor attractors[nattractors];
    uint ngroups;
    uint unk;
    uint unk;
    uint unk;
    uint unk;
    uint unk;
    uint unk;
    uint npoints;
    uint nnormals;
    uint unk;
    Group groups[ngroups];

    //Meshes
    local int i = 0;
    local int j = 0;
    while (i < ngroups)
    {
        j = groups[i].nmeshes;
        while (j--) Mesh meshes;
        i++;
    }

    //Points
    SetBackColor (cLtGreen);
    Vector points[npoints];

    //Normals
    SetBackColor (cLtBlue);
    Vector normals[nnormals];

    //Vertex weights
    SetBackColor (cLtPurple);
    i = 0;
    while (i < ngroups)
    {
        //Single weight vertices
        Bind sbind[groups[i].nsingle]<optimize=false>;
        //Variable length vertices
        VBind vbind[groups[i].nvariable]<optimize=false>;
        i++;
    }
};

//
//Parse out the data
//
O6D header;
if (header.version != O6D_VERSION)
{
    Error ("Wrong O6D version!");
}
MDLS mdls;
TEXS texs;

//Rarely present. Contains a file name to a material
//MATS mats;
 
  • Like
Reactions: Sega Dreamcast Info

Sifting

Well-known member
Original poster
Registered
Aug 23, 2019
63
168
33
Progress! I have most of the animation format figured out now. The WIP template is attached at the end of the post.

For the most part it's a fairly vanilla format. It seems to store both position and rotation keys. Positions are stored as full single precision 3D vectors, and rotations are stored as versors. Only the bones which have local transformation are stored to save space. The big twist here are the time keys, which are composed of TWO values. I'm unsure what the first one is for, but if I had to guess it's some kind of hold time or something to for easing between keys? Very strange. There's an interesting pattern that emerges when you look at the time data that's quite hard to describe. I have to figure out how exactly to read the key data, but that should be mostly a matter of observation. Once this is mapped out I can write script to translate the models and animations into GLTF for use in your favourite 3D programs and engines!

Code:
//Agartha A6D template
//For use in 010editor
//Sifting

LittleEndian ();
struct Chunk
{
    char magick[4];
    uint size;
};

const uint A6D_VERSION = 0x259;
struct A6D
{
    SetBackColor (cPurple);
    Chunk chk;
    uint version;
    char txt[chk.size - 4];
};

struct QKey
{
    uint16 time0;
    uint16 time1;
    float w, x, y, z;
};
struct PKey
{
    uint16 time0, time1;
    float x, y, z;
};
struct PQKey
{
    uint16 time0, time1;
    float q[4];
    float p[4];
};
struct PQKey2
{
    uint16 time0, time1;
    float q[4];
    float p[3];
};
struct Bone
{
    uint format;
    uint size;
    uint loop; //which key to loop back to? all keys occur at the same point in time, even though indices differ
    uint count;
    uint id;

    if (1049088 == format || 1049090 == format) PKey data[count];
    else if (1310976 == format) QKey data[count];
    else if (2097920 == format) PQKey2 data[count];
    else if (2363136 == format) PQKey data[count];
    else float data[(size - 20)/4];
};
struct DATA
{
    SetBackColor (cBlue);
    Chunk chk;
    uint length;
    uint length2;
    uint unk;
    uint nbones; //Only animated ones - not full skeleton!
    
    local uint i = nbones;
    while (i)
    {
        Bone bone;
        i--;
    }
};

A6D header;
if (header.version != A6D_VERSION)
{
    Error ("Wrong A6D version!");
}
DATA data;

I apologise again for using these threads as progress journals more or less, but I mean well haha.
 
Last edited:
  • Like
Reactions: Americandad

Sifting

Well-known member
Original poster
Registered
Aug 23, 2019
63
168
33
Small update - identified a few more of the unknown fields in the a6d format. One interesting find is loop field in the Bone header. Though I've not confirmed it, it seems very likely it is used to indicate where an animation should loop back to once it ends. This effectively means animations have two parts - a lead in and a loop body. The loop value specifies a key frame index, and may vary between bone, but the time keys are always the same from what I've observed, and always less than the number of keys, so it's unlikely to be anything else. Door opening animations always have it set at the end of their animations too - i.e. to make them stay open.

The other discovery is the encoding scheme. Each bone has a format bitfield which seems to specify the exact data encoded in each key. I'm unsure how to interpret it at the moment, but rotations seem to be versors, and positions seem to be 3D vectors, but there are odd values too. Perhaps scale? It's possible only some components of positions are encoded too. It might prove a bit annoying to figure out. Maybe collecting all values that occur over all animations then write a script to break them down into their constituent bits will prove useful...

I updated the template in the above post for the curious.
 

FamilyGuy

2049 Donator
Donator
Registered
May 31, 2019
344
337
63
AGName
-=FamilyGuy=-
AG Join Date
March 3, 2007
I'm unsure how to interpret it at the moment, but rotations seem to be versors, and positions seem to be 3D vectors, but there are odd values too. Perhaps scale?
Maybe it's a rotation around an axis defined by a vector, and the angle of rotation? That's all that's required to define a rotation matrix in 3D, i.e. any rotation.
 
  • Like
Reactions: Sifting

Sifting

Well-known member
Original poster
Registered
Aug 23, 2019
63
168
33
Maybe it's a rotation around an axis defined by a vector, and the angle of rotation? That's all that's required to define a rotation matrix in 3D, i.e. any rotation.
It's possible! In my experience though no one uses angle/axis form for this though. They will either use euler angles or versors (unit quaternions). When you examine the values too, you find they're all in the -1 ~ 1 range, i.e. sines. So rotations are most likely versors. I made a script to print out the constituent parts of the bitfields and it really only raised some more questions than answers. Bit 8 is always set when position is encoded, and bit 9 is always set when rotation is encoded, but there are several other bits that can't be accounted for. There seems to be no 1:1 to the number of fields encoded in a single key either. Keys will be either encoded in 3, 7 or 8 floats. It might be best to just start on a GLTF script and work out these out by trial and error :confused:
 

FamilyGuy

2049 Donator
Donator
Registered
May 31, 2019
344
337
63
AGName
-=FamilyGuy=-
AG Join Date
March 3, 2007
It's possible! In my experience though no one uses angle/axis form for this though. They will either use euler angles or versors (unit quaternions). When you examine the values too, you find they're all in the -1 ~ 1 range, i.e. sines. So rotations are most likely versors. I made a script to print out the constituent parts of the bitfields and it really only raised some more questions than answers. Bit 8 is always set when position is encoded, and bit 9 is always set when rotation is encoded, but there are several other bits that can't be accounted for. There seems to be no 1:1 to the number of fields encoded in a single key either. Keys will be either encoded in 3, 7 or 8 floats. It might be best to just start on a GLTF script and work out these out by trial and error :confused:
Yeah quaternions are super useful for rotations, although I've never heard of versors. But a rotation matrix around a given axis for a given angle would be equivalent (i.e. no gimbal lock).

I'd love to have the time to help you with that project, but I guess my contribution will be to wish you Good Luck !
 
  • Like
Reactions: Sifting

Make a donation