egg yolk
Member
For a while now it has been known that with the Dreamcast Development Kit (DDK), it is possible to export PSOBB model files known as Ninja Chunk Models (NJ) and ASCII versions of those models (NJA) from 3DS MAX 2.5/3.0, with the use of an old Windows virtual machine (VM).
To describe NJA, it is a readable version of an NJ file, and the documentation in the DDK explains most of what is going on in NJA files. The reason NJA has recently become important, is because there is another tool in the DDK (njaconv.exe) that can convert NJA files to NJ without the need of a VM or 3DS MAX, since njaconv.exe still works in Windows 10 as a command line tool. njaconv.exe has a lot of similarities to the 3DS MAX exporter in terms of options and capabilities, the only issue here is that it's still required to use a VM to produce an NJA file, which is needed to make use of njaconv.exe.
With the mentioned resources I decided to start reverse engineering NJA to hopefully remove the need of using a VM to make custom models. The end goal is to have a tool that converts an OBJ (possibly a different format) to NJA, and then using njaconv.exe to convert NJA to NJ. I am using this forum to post my findings similar to a Kion or blog thread, since I think it is a good location for those interested to learn about it and possibly help out. For this first post, I have carried out several tests to help prove that it should be possible, but it is a large task, and this is only the beginning of my attempt to figure out NJA files, so I am by no means saying this will be finished anytime soon. I am also quite amateur when it comes to floating point conversions, so please let me know if I am doing something wrong.
This is the model I have been using for the first few tests, the reason for this model, is that it gives a varied amount of different XYZ coordinates based on it's vertices. I have exported this model as an OBJ file, and again as an NJA file through VM. Thanks to DOOMGUY for helping me out with a new VM!
Below are my findings:
With those tests done, the next step is to take a look at normal values, which at the moment I assume are also just x20 of the OBJ counterparts. I will link the full NJA file below as embedded code if anyone wants to see it, the hard parts will likely be triangle strips and texture mapping but for now I am only working on an NJA without texture mapping. Everything else seems pretty trivial.
To describe NJA, it is a readable version of an NJ file, and the documentation in the DDK explains most of what is going on in NJA files. The reason NJA has recently become important, is because there is another tool in the DDK (njaconv.exe) that can convert NJA files to NJ without the need of a VM or 3DS MAX, since njaconv.exe still works in Windows 10 as a command line tool. njaconv.exe has a lot of similarities to the 3DS MAX exporter in terms of options and capabilities, the only issue here is that it's still required to use a VM to produce an NJA file, which is needed to make use of njaconv.exe.
With the mentioned resources I decided to start reverse engineering NJA to hopefully remove the need of using a VM to make custom models. The end goal is to have a tool that converts an OBJ (possibly a different format) to NJA, and then using njaconv.exe to convert NJA to NJ. I am using this forum to post my findings similar to a Kion or blog thread, since I think it is a good location for those interested to learn about it and possibly help out. For this first post, I have carried out several tests to help prove that it should be possible, but it is a large task, and this is only the beginning of my attempt to figure out NJA files, so I am by no means saying this will be finished anytime soon. I am also quite amateur when it comes to floating point conversions, so please let me know if I am doing something wrong.
This is the model I have been using for the first few tests, the reason for this model, is that it gives a varied amount of different XYZ coordinates based on it's vertices. I have exported this model as an OBJ file, and again as an NJA file through VM. Thanks to DOOMGUY for helping me out with a new VM!
Below are my findings:
The first test is to see which header types can be created with njaconv.exe, to do this I created two versions of the above model with and without a texture as NJA files.
Next I converted those NJA files with njaconv.exe into NJ files, as expected, an NJ with the header NJTL was created from the NJA with a texture, and an NJ with the header NJCM was created from the NJA without a texture.
I also tested that these models ran in Model View, which they did, this basically means the models would be viable for replacements in PSOBB and that njaconv.exe is a possible solution to avoiding VM usage.
This is what njaconv.exe looks like for context, I used the -bin option:
Next I converted those NJA files with njaconv.exe into NJ files, as expected, an NJ with the header NJTL was created from the NJA with a texture, and an NJ with the header NJCM was created from the NJA without a texture.
I also tested that these models ran in Model View, which they did, this basically means the models would be viable for replacements in PSOBB and that njaconv.exe is a possible solution to avoiding VM usage.
This is what njaconv.exe looks like for context, I used the -bin option:
With the help of a friend (Rize), we were able to figure out that NJA lists it's vertex values as floating points, but they are displayed as hexadecimal values in the NJA file. This took some trial and error to figure out, but I have mapped out the original shape shown in the opening, as a cross comparison between it's NJA and OBJ file. The parts in the below screenshot surrounded with "[ ]" were added in by myself, to show which vertices from NJA match those in OBJ.
The hope was that there would be some kind of order in the OBJ file to make this part a bit easier, but it doesn't seem to be the case. Either way, we figured out the formula for preparing the OBJ values to floating point. NJA values are simply 20 times the amount of OBJ values, and are converted to hexadecimal, I'm personally unsure if we are using the wrong type of floating point converter here, but it works anyway. This is the converter we used: https://www.h-schmidt.net/FloatConverter/IEEE754.html
While doing this comparison I also made a key, to show how the NJA values match the OBJ values. There were some rounding errors in the process, mostly where values were negative zero, but also sometimes when they were just zero, so the next test is to replace all negative zero values with 0x00000000 and to see if the file still converts using njaconv.exe.
The hope was that there would be some kind of order in the OBJ file to make this part a bit easier, but it doesn't seem to be the case. Either way, we figured out the formula for preparing the OBJ values to floating point. NJA values are simply 20 times the amount of OBJ values, and are converted to hexadecimal, I'm personally unsure if we are using the wrong type of floating point converter here, but it works anyway. This is the converter we used: https://www.h-schmidt.net/FloatConverter/IEEE754.html
While doing this comparison I also made a key, to show how the NJA values match the OBJ values. There were some rounding errors in the process, mostly where values were negative zero, but also sometimes when they were just zero, so the next test is to replace all negative zero values with 0x00000000 and to see if the file still converts using njaconv.exe.
Using the key from before, I replaced all vert entries with rounding errors to just contain 0x00000000. Following this I ran this modified file through njaconv.exe, and sure enough the file converted correctly to NJ.
I also tested it in Model View to see if any of the verts were messed up, but it worked fine! This means there will have to be some detection for rounding errors in the tool, that just converts them to 0x00000000.
I also tested it in Model View to see if any of the verts were messed up, but it worked fine! This means there will have to be some detection for rounding errors in the tool, that just converts them to 0x00000000.
With those tests done, the next step is to take a look at normal values, which at the moment I assume are also just x20 of the OBJ counterparts. I will link the full NJA file below as embedded code if anyone wants to see it, the hard parts will likely be triangle strips and texture mapping but for now I am only working on an NJA without texture mapping. Everything else seems pretty trivial.
Code:
/* NJA 2.11.00 Ninja2AsciiDataMix CnkModel (MAX) */
/* ROOT OBJECT : object_cube_sub_1_WaveObj_WaveObj n(1) d(1) v(26) */
CNKOBJECT_START
PLIST strip_cube_sub_1_WaveObj_WaveObj[]
START
CnkM_DAS( FBS_SA|FBD_ISA ), 6,
MDiff( 255, 153, 228, 184 ),
MAmbi( 255, 255, 255, 255 ),
MSpec( 8, 255, 255, 255 ),
CnkS( 0x0 ), 67, _NB( UFO_0, 6 ),
StripR(18), 3, 20, 11, 8, 2, 24, 12, 15, 6, 22,
14, 16, 7, 25, 13, 10, 3, 20,
StripR(6), 11, 2, 21, 12, 14, 6,
StripR(6), 14, 7, 21, 13, 11, 3,
StripR(18), 22, 5, 16, 19, 25, 1, 10, 9, 20, 0,
8, 18, 24, 4, 15, 17, 22, 5,
StripR(6), 17, 4, 23, 18, 9, 0,
StripR(6), 9, 1, 23, 19, 17, 5,
CnkEnd()
END
VLIST vertex_cube_sub_1_WaveObj_WaveObj[]
START
CnkV_VN(0x0, 157),
OffnbIdx(0, 26),
VERT( 0xc1200000, 0x41200000, 0x41200000 ),
NORM( 0xbf4203ce, 0x3eec3220, 0x3eec321f ),
VERT( 0xc1200000, 0x41200000, 0xc1200000 ),
NORM( 0xbedde50b, 0x3f2322ce, 0xbf2322ce ),
VERT( 0xc1200000, 0xc1200000, 0x41200000 ),
NORM( 0xbedde50c, 0xbf2322ce, 0x3f2322ce ),
VERT( 0xc1200000, 0xc1200000, 0xc1200000 ),
NORM( 0xbf4203ce, 0xbeec3220, 0xbeec321f ),
VERT( 0x41200000, 0x41200000, 0x41200000 ),
NORM( 0x3edde50d, 0x3f2322d0, 0x3f2322ce ),
VERT( 0x41200000, 0x41200000, 0xc1200000 ),
NORM( 0x3f4203ce, 0x3eec321f, 0xbeec3220 ),
VERT( 0x41200000, 0xc1200000, 0x41200000 ),
NORM( 0x3f4203ce, 0xbeec321f, 0x3eec3222 ),
VERT( 0x41200000, 0xc1200000, 0xc1200000 ),
NORM( 0x3edde50c, 0xbf2322cf, 0xbf2322ce ),
VERT( 0xc1430fd0, 0x00000000, 0x41430fd0 ),
NORM( 0xbf3504f4, 0x33203be1, 0x3f3504f4 ),
VERT( 0xc1430fd0, 0x41430fd0, 0xb50f0cb3 ),
NORM( 0xbf3504f4, 0x3f3504f3, 0xb28957e5 ),
VERT( 0xc1430fd0, 0xb50f0cb3, 0xc1430fd0 ),
NORM( 0xbf3504f4, 0xb32baddf, 0xbf3504f4 ),
VERT( 0xc1430fd0, 0xc1430fd0, 0x00000000 ),
NORM( 0xbf3504f4, 0xbf3504f3, 0x328957e5 ),
VERT( 0x00000000, 0xc1430fcf, 0x41430fd1 ),
NORM( 0x3e2e354c, 0xbf32616a, 0x3f32616a ),
VERT( 0x00000000, 0xc1430fd1, 0xc1430fcf ),
NORM( 0xbe2e354c, 0xbf32616b, 0xbf32616a ),
VERT( 0x41430fd0, 0xc1430fd0, 0x00000000 ),
NORM( 0x3f3504f3, 0xbf3504f4, 0x32e4e7d3 ),
VERT( 0x41430fd0, 0x00000000, 0x41430fd0 ),
NORM( 0x3f3504f4, 0x32fbcbce, 0x3f3504f3 ),
VERT( 0x41430fd0, 0x00000000, 0xc1430fd0 ),
NORM( 0x3f3504f4, 0xb28957e5, 0xbf3504f3 ),
VERT( 0x41430fd0, 0x41430fd0, 0x00000000 ),
NORM( 0x3f3504f3, 0x3f3504f3, 0xb2e4e7d2 ),
VERT( 0x00000000, 0x41430fd1, 0x41430fcf ),
NORM( 0xbe2e354d, 0x3f32616a, 0x3f32616b ),
VERT( 0x00000000, 0x41430fcf, 0xc1430fd1 ),
NORM( 0x3e2e354d, 0x3f32616b, 0xbf32616a ),
VERT( 0xc186522a, 0x00000000, 0x00000000 ),
NORM( 0xbf800000, 0x00000000, 0x00000000 ),
VERT( 0x00000000, 0xc186522a, 0x00000000 ),
NORM( 0x3316e3e0, 0xbf800000, 0x335ace42 ),
VERT( 0x4186522a, 0x00000000, 0x80000000 ),
NORM( 0x3f800000, 0x31af0b68, 0x31af0b68 ),
VERT( 0x00000000, 0x4186522a, 0xb545028f ),
NORM( 0x3284d79f, 0x3f800000, 0xb35ace42 ),
VERT( 0x00000000, 0x00000000, 0x4186522a ),
NORM( 0xb233442a, 0x3383488e, 0x3f800000 ),
VERT( 0x00000000, 0x00000000, 0xc186522a ),
NORM( 0xb2b09a79, 0xb344ecd5, 0xbf800000 ),
CnkEnd()
END
CNKMODEL model_cube_sub_1_WaveObj_WaveObj[]
START
VList vertex_cube_sub_1_WaveObj_WaveObj,
PList strip_cube_sub_1_WaveObj_WaveObj,
Center 0.000000F, 0.000000F, 0.000000F,
Radius 17.320509F,
END
CNKOBJECT object_cube_sub_1_WaveObj_WaveObj[]
START
EvalFlags ( FEV_UT|FEV_UR|FEV_US|FEV_BR ),
CNKModel model_cube_sub_1_WaveObj_WaveObj,
OPosition ( 0.000000F, 0.000000F, 0.000000F ),
OAngle ( 0.000000F, 0.000000F, 0.000000F ),
OScale ( 1.000000F, 1.000000F, 1.000000F ),
Child NULL,
Sibling NULL,
OQuatRe ( 0.000000 ),
END
CNKOBJECT_END
DEFAULT_START
#ifndef DEFAULT_OBJECT_NAME
#define DEFAULT_OBJECT_NAME object_cube_sub_1_WaveObj_WaveObj
#endif
DEFAULT_END
Last edited: