Jump to content

Soul Calibur 1 - Arcade and Dreamcast models


Recommended Posts

Posted

Early last year I worked with mariokart64n with the goal of converting Soul Calibur 1's Arcade models into a usable format in other software. I spent many months reverse engineering the arcade game and documenting my observations, which MK used to write his script in only a few days. We've been largely successful in getting geometry, UVs, and bone transforms to import into Max for both characters and weapons:


image.thumb.png.9c345e2a39fc85f713c3e03537eea12a.png


For a long time I thought that the Dreamcast model format, seeing how much higher quality the port's modeling work is, is completely different, and ditto for the custom LZSS compression scheme. Turns out I was completely wrong. The compression scheme is completely unchanged from Arcade, and much of the overall *structure* of the model format has been carried over from Arcade. Of course, some obvious changes like the switch from unsigned shorts to floats for vertices and normals were made, but also the face/triangle format.

The latter requires some further explanation. See, Namco elected to embed PSX GPU GP0 commands directly into the command list, not too dissimilar to Sony's standard TMD format, with the major difference being that references to vertices in the transformation buffer are done through bitpacked indices: Three 10-bit indices for triangles (largely used by characters), four 8-bit indices for triangle pairs (largely used by weapons), both of which fit into a single 32-bit word, with the caveat of each being limited to 1024 and 256 vertices respectively. These limits are breached in the DC port, and naturally GP0 commands are not compatible with the PVR, so this part of the format has received the most drastic changes. VincentNL has also confirmed that the game uses the low-level Kamui API for interfacing with the PVR.

Unfortunately, I am not familiar with the intricacies of Dreamcast rendering whatsoever, so I'd appreciate any help in refactoring the script to be DC compatible. Right now bone transforms will import, although it seems they're a bit screwed in the data itself:image.thumb.png.94531346875f551923fe7bbb5fda0fa3.png

Attached below are the script, and samples of Xianghua's 1P model from Arcade and Dreamcast respectively.

sc1_mxs.zip

  • Like 1
Posted

Some initial observations I made while examining the command packets during a MAME debugging session:

image.thumb.png.74e845218b10503263bb3733201a70dd.png

image.thumb.png.a79357b80dc86a3f5e1e5f40264c17a6.png

 

 

Red - Vertex counter (how many double words follow)
Orange - Vtx1 Color
Blue - Vtx1 Index
Unmarked word - Unknown, potentially UVs??
Violet - Vtx2 Color
Scarlet - Vtx2 Index
and the cycle repeats until the next Vertex Counter is hit

There doesn't appear to be any more bitpacking of vertex indices.

Posted

Well, even after referencing the original Arcade documentation and combining it with the new DC discoveries, I'm still at a dead end:
image.thumb.png.9e5af92bc2d03cbdf0b60bc5e82b0d11.png

If I'm doing something wrong, I can't tell what that is. Here's the docs:

Main header:

char[23]            filename; // 24 character filename that doesn't really contain anything useful
uint32_t            triangle_strip_table_pointer; // points to the first triangle_strip_t;
uint8_t             unk1;
uint8_t             unk2;
uint8_t             unk3;
uint8_t             submesh_count; // How many model_submesh_t instances exist;
model_submesh_t[n]  model_submesh_table; // One or more submesh headers. Usually no more than 17

typedef struct{
    uint8_t     bone_id0; // NULL on weapons?
    uint8_t     bone_id1;
    uint32_t    vertex_list_pointer; // points to a vertex_list_t
    uint32_t    unknown; // Always 0x00000000?
    uint16_t    vertex_count;
    uint16_t    normal_count;
    int16_t     bone_rotation_x;
    int16_t     bone_rotation_y;
    int16_t     bone_rotation_z;
    int16_t     unknown;
    int16_t     bone_position_x;
    int16_t     bone_position_y;
    int16_t     bone_position_z;
    int16_t     parent_bone_id;
} model_submesh_t;

typedef struct{
    float       vertex_x;
    float       vertex_y;
    float       vertex_z;
    uint16_t    vertex_index;
    uint16_t    flags: // It's not clear what these do
} vertex_list_t;

typedef struct{
    float       normal_x;
    float       normal_y;
    float       normal_z;
    uint16_t    normal_index;
    uint16_t    flags: // It's not clear what these do
} normal_list_t;

typedef struct{
    uint16_t    vertex_index;   // Reference to a vertex index fetched from a vertex_list_t;
    uint16_t    vertex_color;   // Influences shading
    uint32_t    uv_coordinates; // UVs  
    
} triangle_t;

typedef struct{
    uint16_t                    strip_length;
    uint8_t                     uv_coordinate;  // Why are there three UV coordinates?
    uint8_t                     unknown;
    triangle_t[strip_length-1]  triangles;  // Array of triangle_t elements;
} triangle_strip_t;

The current import script is attached below.

soul_calibur_1_importer.py

  • Like 1
  • 1 month later...
Posted

Consider this a bump. Every time I've been trying to get back to this, I couldn't make any progress. I can get a handful of bodyparts to form some kind of shape, like the shoes, but the actual strips are still totally screwed, as you can see in the attached screenies:

image.png.27ec3ef3b8d63b8549f247f992186e99.png
image.png.d30909d2a619a28c72382a65315d80fb.png

Additionally I'll attach the most up-to-date "workbench" of data and scripts:
sc1_mxs.zip

It has 010 and ImHex templates for highlighting, three DC character models, and the current import script, as well as reference data and scripts used for Arcade for comparison's sake. Treat the highlight templates as the documentation, the one I put previously is not quite correct.

Additionally, there isn't really a strip counter, the strips are continuously iterated until a pair of 32-bit 00000000 words is hit. I had to put one in to tell ImHex to at least stop somewhere.

 

  • Engineer
Posted (edited)

Are you sure that this point cloud is correct?

If so it's not surprising that you got what you get building faces all over these points.

xinghua_dc_1p-pointCloud.png

Right screen portion of points in meshlab:

meshlab_x.png

Edited by shak-otay
Posted (edited)
7 hours ago, shak-otay said:

Are you sure that this point cloud is correct?

The vertices themselves are good, that I can tell you with complete certainty, but the positioning of each submesh is not. Do note that there is no A/T-pose mesh at play here (at least not in the mesh itself), every body part is positioned at world origin, and each bone that it is bound to also sets that body part's default position. Take a look at an example from the Arcade version:
image.png.d267121b174a9d8166951214330793fc.png

Without the bone transforms, we get this.

image.png.a8ad212e6750bd308be93f2a08749f1e.png|

But with the skeleton applied, we get this.

A similar situation is happening on Dreamcast as well, with the major difference being that vertices and normals now use floats instead of integers. If you look at the header of the Arcade and Dreamcast model binaries, you'll see that they have a very similar structure. Even the bones are still stored as shorts, but rescaled by the game into floats at runtime. I think this is the part where I'm getting it wrong, hence why the positioning of each bodypart is bad in my current script.

Edited by spaztron64
  • Like 1
  • Engineer
Posted

I see.

In your script

for _ in range(submesh["vertex_count"]):

assembles small sub meshes with vertex counts from 3 to 14, 18 or 19.

But I don't see how to combine them to a shoe or a head sub mesh.

btw, "for _ in" looks weird to me - you shouldn't use _ as an index identifier.:classic_huh:

 

Posted

Yeah I was LLM-ing the crap out of this because I barely know anything about Blender's Py API (or 3D graphics programming in general), most of my work was analysis of the data at runtime with a debugger.

UVs are also a bit weird, I've been messing around with them a few months back:

 

  • Like 1
Posted

Been deleting some of the larger geometry to see if I could find something recognizable underneath. Lo and behold, I got something, specifically fingers for the left and right hand criss-crossing one another.

image.thumb.png.92258a738aeb3442e62a5a01229a3112.png

That alone confirms what I already knew before: that my positioning of objects based on the bone transforms is completely wrong..

But, now I can clearly see that my strip formation is somewhat functional. The geometry of each hand's fingers are being connected into a mesh just fine, but some of their vertices are connecting to other parts of the model they should have no contact with. The only thing I can conclude from this is that my termination of strip formations is fundamentally flawed. Problem is.... I can't really tell how.

Posted

Yeah, I see what you mean. I might be having a similar issue, but I won't know till I find a way to iterate over the strip table without all of the vertices being loaded. If I try to limit the number of submesh vertex lists being processed in my script to, I dunno, 5 or less, the strip formation loop will complain about out of range indices being referenced, obviously because they're not loaded, and not return any kind of mesh.

Posted

Slight progress improvement!

image.thumb.png.a1480253972b491ff22424b9b83dfdcc.png

It didn't make any sense to me that the tips of the foot would form triangles with other parts of the body, let alone sides of the fingers forming faces with one another, so when I actually looked at what my loop was doing, I spotted a major mistake: I was accumulating strips until a terminator was hit, and forming faces in batches.

What I should've done from the getgo was draw each strip the moment it was finished, 0x0 terminated afterwards or not. Which means...

image.png.b3ea470a892f4ac47bcde6e5e6817920.png

I can finally get some geometry out cleanly!

Submesh positioning still remains an issue, but honestly this is the biggest breakthrough yet.

 

  • Like 1
Posted

This is what I currently have:

soul_calibur_1_importer_v6.py
 

For the life of me I just can't get a good skeleton to form. I tried to implement some of the hardcoded corrections that MK did for Arcade, as well as the float scales as per the DC version's (well actually the Android port of DC SC1) code, but to no avail.

  • Like 1
Posted

Mariokart64n hopped in to help out with the armature a bit. By every metric, we finally have it sorted out.

Unfortunately, the weighting still seems super off. I theorize that what's happening is that every submesh is paired with one bone too high in the hierarchy, but that's just a wild guess right now. soul_calibur_1_importer_v8.py

image.thumb.png.f22249c29f8fdd592c9e96c4c40ea063.png

  • Like 1
Posted (edited)

EDIT: Yeah nevermind, that wasn't it.

image.thumb.png.95ea8bd1eb8c2847c0f009bb7cf6a82e.png

Sure, I have all the faces, but now I have waaay too many verts and transforms that are still broken. Back to the drawing board...


Seems strip processing isn't quite so done yet.

image.png.7eefad4e2327279257f7beba14ef15c1.png

Somebody was kind enough to do a scene rip, which is largely useless except for the number of faces: 3694, although I might've missed removing a few sword faces.

Now, my importer only reports back 3198. Pretty big gap. Blender will throw an exception if it tries to create duplicate faces. I edited the handler to count the number of such duplicated faces. Add them together with properly processed faces aaaand:

Drawn faces: 3198
Skipped faces: 494
Total faces: 3692

Bingo, there's our problem. Now the tricky part is forcing blender to accept duplicate faces.

Blender gurus, I'm all ears.

Edited by spaztron64

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...