PSO Asset Exports

Holy shit this is phenomenal. Lot of late nights and coffee for me.

Thank you for sharing!!
 
Kind of debating which step to take next. Should I go with .rel files (for Dreamcast?) or look into .xj for PsoBB?
 
As much as I have genunine love for the Dreamcast i'd personally focus my efforts on the more active platform. Just my two penneth worth.
 
Okay, decided to go with xj.

xj_mesh.PNG

Managed to get the geometry working pretty quickly thanks to Wilhuf's Documentation. I guess next we'll have to figure out how to implement .xvm / .xvr textures.
 
kinda_working.PNG
That went faster than I could have expected. The basics of the plugin are generally working. There are still a few issues.
- Right now i'm being lazy and using double-sided for everything, so need to fix strips
- Alpha blending isn't set
- Some models have an error and break completely

Plugin Download can be found here: https://gitlab.com/dashgl/ikaruga/-/snippets/2049539

Thanks to Wilhuf on Discord for answering my stupid questions.
 
Okay, for the issues above.
- Strips are weird, debugging that now
- Alpha Blending is now set (in limited cases)
- The models that break were breaking on animation, so i'll debug that when i finish strips

For XJ strips, the approach to convert strips back into triangles is not straight forward. Normally it's ACB BCD and just reverse in that order. In the case of XJ, there are doubled values. And it's kind of a challenge to figure out how to convert those back into a triangle in the order intended. For the saber I went back to the .nj model and copy and pasted the triangle order into the XJ model and it worked. So i'll be comparing the input to the XJ and NJ models and then trying to compare what works and when.

The input to the NJ strips is:
Code:
7 [15, 16, 4, 14, 0, 9, 13]
16 [3, 12, 18, 17, 3, 13, 12, 9, 10, 14, 15, 2, 16, 1, 14, 2]
-6 [17, 7, 12, 19, 10, 15]
4 [8, 23, 5, 21]
3 [20, 22, 6]
-10 [5, 20, 21, 22, 23, 11, 8, 24, 5, 20]
-5 [20, 24, 6, 11, 22]
7 [15, 4, 19, 0, 7, 13, 17]

As a not for the number on the front. The size is the number of points in the strip. And plus or minus is start clockwise or counter clockwise.

The input to the XJ strips is:
Code:
[15, 16, 4, 14, 0, 9, 13, 13,
3, 3, 12, 18, 17, 3, 13, 12, 9, 10, 14, 15, 2, 16, 1, 14, 2, 2,
17, 17, 7, 12, 19, 10, 15, 15,
8, 8, 23, 5, 21, 21,
20, 20, 22, 6, 6,
5, 5, 20, 21, 22, 23, 11, 8, 24, 5, 20, 20,
20, 20, 24, 6, 11, 22, 22,
15, 15, 4, 19, 0, 7, 13, 17]

Which means that if we read the XJ strips like this:
Code:
all_strips = []
strip = []
self.bs.seek(mesh_info['index_list_offset'], NOESEEK_ABS)
for i in range(strip['index_count']):
   index = self.bs.readShort()
   
   if len(strip) == 1 and index == strip[len(strip) - 1]:
       continue
   elif len(strip) > 1 and index == strip[len(strip) - 1]:
       all_strips.append(strip)
       strip = []
   else:
       strip.append(index)
all_strips.append(strip)

Then we can convert back to the original nj strips. The problem we have no consistent way of knowing what the start direction of any given strip is.
 
Last edited:
mines.PNG

キャプチャ.PNG

I'm going to call this good enough for now. I went and added in the animated models as a static part of the mesh. If needed I can go back and export them as separate animated meshes. In that case I would need to add in some user data of where the animated model is in the scene.
 
I guess we'll take a look at the c.rel collision maps. Like pretty much all of the .rel files, we start at the end of file minus 0x10 bytes, which gives us a pointer to the header.

c_rel01.PNG
In this case we read the pointer 0x018B34, seek to it. And all we see is another pointer 0x0184A4. We can then follow this pointer to see where it goes.

c_rel02.PNG
And it looks like it points to a fairly generic list. We can make a few assumptions from here. It looks like we have a pointer to somewhere, followed by three floats for the x, y, z center, and a float for the radius. Capped off by 0xc0000000 on the end before the next entry.

Since we didn't see anything else going on, we can assume that the previous pointer that got us here is the terminating point of the list. So we can read through the list until we hit zero, or until we hit the original pointer.

We can look at the length of each section. The first pointer is 0x2A8 followed by 0x05E0. That means we potentially have a length of 0x338, which could be useful if we need to divide later. But right now we mysteriously have padding on the front of the file that's unaccounted for. We'll have to follow a few more pointers to see what's going on.
 
Okay, we can actually solve two mysteries right away. If we follow the first pointer we come to 0x2A8.

c_rel03.PNG

We get the value 0x0A followed by 0x0000. So 0x0A is probably the count of triangles, and 0x0000 is probably to the pointer for the start of the triangle block. We know that the size of the block is 0x02a8, so 0x02a8 / 0x0a gives us a struct size of 0x44.

c_rel04.PNG
And here we have a single struct. We probably have three x, y, z floats for each position point on a raw triangle. And then we probably have three x, y, z points for the normal face. That still leaves something else. So i don't know what else is packed in here. And then the 0x40000000 on the end looks like a flag. So it probably tells us what kind of collision (floor, wall, ect), this triangle is supposed to represent.
 
collision_color.PNG

Added color to the flags for each collision mesh. The colors aren't exactly nice looking, I threw in a random color for each flag, since it's pretty hard to know what each flag does until you assign a color to see what it is. If you want anyone wants to adjust the colors, the rgb values should be easy enough to tweak.

Code:
            mat = NoeMaterial(key, '')
            if key == '0x1':
                r = 157 / 255.0
                g = 0 / 255.0
                b = 255 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x4':
                r = 0 / 255.0
                g = 8 / 255.0
                b = 255 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x10':
                r = 21 / 255.0
                g = 161 / 255.0
                b = 171 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x20':
                r = 88 / 255.0
                g = 21 / 255.0
                b = 171 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x101':
                r = 207 / 255.0
                g = 252 / 255.0
                b = 223 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x104':
                r = 207 / 255.0
                g = 219 / 255.0
                b = 252 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x110':
                r = 25 / 255.0
                g = 117 / 255.0
                b = 39 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x111':
                r = 174 / 255.0
                g = 252 / 255.0
                b = 186 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x120':
                r = 255 / 255.0
                g = 251 / 255.0
                b = 0 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))           
            elif key == '0x140':
                r = 242 / 255.0
                g = 224 / 255.0
                b = 85 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))   
            elif key == '0x142':
                r = 255 / 255.0
                g = 0 / 255.0
                b = 123 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x800':
                r = 255 / 255.0
                g = 0 / 255.0
                b = 0 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x801':
                r = 199 / 255.0
                g = 167 / 255.0
                b = 78 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x920':
                r = 247 / 255.0
                g = 252 / 255.0
                b = 174 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x2000':
                r = 0 / 255.0
                g = 255 / 255.0
                b = 213 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 1.0]))
            elif key == '0x40000000':
                r = 250 / 255.0
                g = 207 / 255.0
                b = 249 / 255.0
                mat.setDiffuseColor(NoeVec4([r, g, b, 0.1]))
            else:
                print("No Material Set for Key: ", key)
                continue

https://gitlab.com/dashgl/ikaruga/-/snippets/2052429
 
Might as well move on to 'r.rel'. We start at EoF minus -0x10 which has a pointer to the header. In this case 0x27E4. That gives us a pointer to 0x253c and then a count of 0x11.

From there we have a similar table that starts at 0x253c and ends at 0x27E4, which gives us a length of 0x2A8. And since we have a count of 0x11, that gives us a struct size of 0x28.

r_rel.PNG
Striking part is the front of each struct. We have 1-1, 2-1, 3-1, 4-1, ect. So the first number is probably an index for showing the increment. The second number could be a number of meshes per section. Not really sure what the meaning of anything else in the struct is, but we have a pointer at the end that will probably indicate the location of the mesh list.
 
Okay, and r.rel is probably supported now. I haven't really tried a lot of variations, but in general forest works. So that's good enough until someone send a bug report or something.

r_relnoe.PNG
 
Might as well try out the PSOBB maps.

psobb_01.PNG

Starts the same. EOF - 0x10 points to 0x2e9c
 
psobb_2.PNG

Then we come to the table that starts with the characters 'fmt2'. Followed by 0x40 and 0x0E. After that are the letters 'HD', which was the same in the Dreamcast stage models (for some reason), ending off with two pointers 0x2BC4 and 0x32EC.

What this probably means is the 0x2BC4 section has 0x40 entries and the 0x32EC section has 0x0E entries. Looking over the format, it doesn't look like it has a texture list. And the .xvm doesn't have any texture names either. So everything is probably just texture id's.
 
psobb_3.PNG

We can follow the first pointer 0x2BC4 to the table. And one thing that really sticks out is the section number for every struct. Which means we can easily get a struct size of 0x34. In this case we probably have position, rotation and radius. And then we have two pointers 0x01d4 and 0x0204. Followed by 0x03 and 0x7c. With 0x100000 on the end for termination.

So we have another pointer split, this will probably be a list of xj nodes that make up the actual mesh. I guess we might as well follow the pointers to see what's there.
 
psobb_4.PNG

So this is kind of interesting. We follow the pointer to 0x1d4 where we find there structs with a length of 0x10. Each one with a pointer probably to an xj node. Following that we see the other pointer of 0x204 with the 0x7c entries. And each one of these structs seems to have a size of 0x20 and two points.

Time to push up my nerd glasses and talk to myself like a crazy person. What I think is going is is they basically shoved d.rel and n.rel into a single file. So the header gives two points, one is probably to the internal d.rel reference, and the other to the internal n.rel reference.

And then each internal d.rel and n.rel have a list of sections. And each section is has two types of mesh, animated and non-animated. The non-animated meshes have one pointer, and the animated meshes have two pointers. Or something like that. I'd like to start parsing a single section. So I'll try to get to the part where we read the mesh and then take another look at the structure after that.
 
Using this as notes for .gj as this format gets pretty crazy pretty fast.

gl_saber.PNG

The part that we're interested in is outlined in red. Up to this point, the format is pretty straight forward. We have the same bone/node definition as xj and nj with position, rotation and scale with pointer to child and sibling if they exist.

Then we have the mesh definition. The mesh has a pointer to a list of attributes. These attributes are positions, normals, vertex color and uv. Specifically in this case we have 25 pos, 25 normals and 58 uv's.

After that we move on to the mesh itself. The mesh has a pointer to the material, and a pointer to the strip itself. This is outlined in the top in red. So the material offset is 0x3ec. The material 'count' is 0x0a (10), the strip offset is 0x440 and the strip 'count' is 0xE0.

First let's take a quick look at the material. Right now the material isn't too important as we can focus on the geometry first and come back to this later. But the format itself doesn't look too complicated. It looks like we have a leading type number that defined what comes after it. In the section we can see the types for 0x00 follows by 12 bytes, 0x01 followed by 12 bytes. 0x04 followed by 4 bytes, 0x05 followed by 4 bytes, and then 0x08, 0x09, and 0x0a all followed by 4 bytes as well.

The only weird thing about the materials is that's not 10 attributes. So it could be that 0x0a is the last material to be defined. Or it could be something else entirely. In any case, the material should generally end when the strip starts, so we can always isolate this section, and simply read up until the end of the section.

Moving on the big mystery here is the strip format. And god this is ugly. It starts off with 0x98 which we have no idea what that means. A quick break down of the bytes is 0x08, 0x10 and 0x80, so these could stand for pos, norm, and uv. The next issue we have is count. The 0xE0 declared in the struct is probably the length. Which means the 0xA0 could either have something to do with the material, or otherwise it could be the number of strips.

From there we have several issues with parsing out the strips. The main issue being the padding on the end of the section. Basically since 0x00 is a viable strip format, the end of the strips could be theoretically almost anywhere, but we can generally assume it ends after trailing off to zero. With the main question being which zero.

The next question is going to be where is the strip length, and where is the number of strips. Because the main thing that is annoying about this is we have three lists of pos, norm and uv. And each of them is being referenced by a byte. And that leaves a lot of bytes which could mean any number of things.
 
Last edited:
Back
Top