NJ Model Format
Ninja (NJ) 3D Model Format
The Ninja format (NJ) is a proprietary 3D model format used by Sega, particularly on the Dreamcast console. The format was created by Sega AM2 for their “Ninja” game engine, which was used for many Dreamcast titles.
Format Overview
Ninja models typically have the following structure:
- A header section containing format information
- A bone/skeleton hierarchy for animated models
- Vertex data (positions, normals, UVs)
- Material and texture references
- Triangle/strip data for rendering
Files typically have the .nj
extension (model data) or may come with paired .njm
files (animation data).
Viewing 3D Models
Below is an example of a 3D model viewer rendering a Ninja format model:
Texture Debug Panel:
Format Details
File Structure
The NJ format uses a chunk-based structure with different sections indicated by 4-character codes like:
NJTL
- Texture listNJCM
- Model data (vertex, polygon, material)NMDM
- Animation data
NJTL (Texture List)
The NJTL chunk contains a list of texture names referenced by the model. It uses a pointer-based structure to efficiently store and access texture references.
Memory Layout
NJTL Chunk Structure:+-------------------------+| "NJTL" Magic (4 bytes) |+-------------------------+| Chunk Size (4 bytes) |+-------------------------+| Pointer to tex list | ---++-------------------------+ || Number of textures | |+-------------------------+ || ... | |+-------------------------+ || | || | |+-------------------------+ || | <--+| Texture List: || - Name Pointer 1 | ---> "texture1"| - Attributes (8 bytes)|| - Name Pointer 2 | ---> "texture2"| - Attributes (8 bytes)|| ... |+-------------------------+| Texture Names: || "texture1\0" || "texture2\0" || ... |+-------------------------+
The texture list is read in a multi-step process:
- Store the current position as a reference point
- Read the pointer to the texture list and number of textures
- Seek to the texture list (pointer + reference position)
- Read all texture name pointers (and skip additional data)
- For each pointer, seek to the actual texture name and read the string
Here’s the actual implementation code that parses the NJTL section:
const readNjtl = (reader: ByteReader): string[] => { // Store current position as reference point const ref = reader.tell();
// Read pointer to texture list and count const ptr = reader.readUInt32(); const count = reader.readUInt32();
// Seek to texture list reader.seek(ptr + ref);
// Read all texture name pointers const textureNamePointers: number[] = []; for (let i = 0; i < count; i++) { textureNamePointers.push(reader.readUInt32()); reader.seekRel(8); // Skip additional data (8 bytes) }
// Read actual texture names const textureNames: string[] = []; textureNamePointers.forEach((ptr) => { reader.seek(ref + ptr); const name = reader.readString(); textureNames.push(name); });
return textureNames;};
This approach allows the model to reference textures stored in separate files. The returned texture names are typically used to locate and load the corresponding textures needed for rendering the model.
Texture Names in Ikaruga
In Ikaruga, the texture names stored in NJTL chunks do not include file extensions (like .PVR). These names reference textures that are typically stored within PVM files (which are collections of PVR textures). For example, a texture name like “COCK” would reference a texture within the corresponding PVM file (e.g., COCK.PVM).
The NJ model renderer needs to:
- Extract the texture name from the NJTL chunk
- Locate the corresponding texture within the PVM collection
- Apply the texture to the appropriate mesh parts
This naming convention allows the same model to work with different texture formats while maintaining the same internal references.
NJCM (Ninja Chunk Model)
The NJCM chunk contains the actual model data, including bones, vertices, and rendering information. It uses a complex hierarchical structure that starts with bones and branches out to include mesh data.
Bone Structure
Each bone in the model is defined with:
Bone Structure:- Flags (32-bit) - Controls bone properties and interpretation- Chunk Offset (32-bit) - Pointer to mesh data for this bone (if any)- Position (3x float) - x, y, z coordinates- Rotation (3x int32) - Euler angles (mode determined by flags)- Scale (3x float) - x, y, z scaling factors- Child Offset (32-bit) - Pointer to first child bone (if any)- Sibling Offset (32-bit) - Pointer to next sibling bone (if any)
The bone flags determine:
- Whether to ignore position, rotation, or scale
- Rotation order (XYZ or ZXY)
- Other bone-specific properties
The parsing process is recursive, following child and sibling pointers to build the complete skeleton hierarchy. Here’s a simplified example of the bone reading code:
function readBone(parentBone?: Bone) { // Read bone structure const boneOffset = reader.tell(); const flags = reader.readUInt32(); const chunkOfs = reader.readUInt32(); const position = readVector3(); const rotation = readRotation(isBitFlagSet(flags, 5)); // Bit 5 determines rotation order const scale = readVector3(); const childOfs = reader.readUInt32(); const siblingOfs = reader.readUInt32();
// Create new bone const bone = new Bone();
// Apply transformations if not ignored in flags if (!isBitFlagSet(flags, 0)) bone.position.copy(position); if (!isBitFlagSet(flags, 1)) bone.rotation.copy(rotation); if (!isBitFlagSet(flags, 2)) bone.scale.copy(scale);
// Add to parent if it exists if (parentBone) parentBone.add(bone);
// Process mesh data if present if (chunkOfs) { reader.seek(chunkOfs); readMeshData(); }
// Process child and sibling bones recursively if (childOfs) { reader.seek(childOfs); readBone(bone); }
if (siblingOfs) { reader.seek(siblingOfs); readBone(parentBone); }}
Mesh Data
When a bone has associated mesh data (Chunk Offset is non-zero), the data includes:
Mesh Data Header:- Vertex List Offset (32-bit) - Pointer to vertex data- Strip List Offset (32-bit) - Pointer to strip/triangle data- Center Position (3x float) - Center of the mesh section- Radius (float) - Bounding sphere radius (for collision detection)
Vertex List
The vertex list contains all vertex data for a specific bone’s mesh:
Vertex List Header:- Header Byte - Determines vertex format (with/without normals, colors, etc.)- Flag Byte - Additional vertex properties- Length (16-bit) - Number of vertices- Index Offset (16-bit) - Starting index in the vertex buffer- Vertex Count (16-bit) - Number of vertices in this list- Vertex Data - Array of vertices with positions, normals, etc. based on header
Each vertex can include:
- Position (3x float) - x, y, z coordinates
- Normal (3x float) - x, y, z normal vector (if specified in header)
- Color (4x byte) - RGBA values (if specified in header)
- Skinning weights (for animated models with partial bone weights)
Chunk System for Materials and Triangles
After the vertex data, strip/triangle data is organized into a series of “chunks” that define how to render the mesh:
-
Material Chunks (IDs 0x10-0x17) - Define material properties:
- Diffuse color (RGBA)
- Specular color (RGBA)
- Ambient color (RGBA)
- Alpha blending settings
-
Tiny Chunks (IDs 0x08-0x0F) - Define texture properties:
- Texture ID (reference to the NJTL list)
- Texture addressing modes (clamp/repeat)
- Texture filtering modes
- Flip U/V flags
-
Strip Chunks (IDs 0x40-0x4B) - Define triangle strips:
- Strip count and properties
- For each strip:
- Strip length and orientation
- Vertex indices (referencing the vertex list)
- UV coordinates for each vertex
Chunks are processed sequentially until an end chunk (0xFF) is encountered. The system allows for changing textures and materials multiple times within a single mesh.
Here’s a simplified example of how the chunk processing system works:
function readChunk() { const NJD_NULLOFF = 0x00; const NJD_BITSOFF = 0x01; const NJD_TINYOFF = 0x08; const NJD_MATOFF = 0x10; const NJD_VERTOFF = 0x20; const NJD_STRIPOFF = 0x40; const NJD_ENDOFF = 0xff;
// Initialize default material state let currentMaterial = { texId: -1, blending: false, doubleSide: false }; let currentColor = { r: 1, g: 1, b: 1, a: 1 };
do { // Read chunk header const head = reader.readUInt8(); const flag = reader.readUInt8();
if (head === NJD_ENDOFF) { // End of chunk section break; }
// Process different chunk types if (head >= NJD_STRIPOFF) { // Strip chunk (triangle data) readStripChunk(head, flag); } else if (head >= NJD_VERTOFF) { // Vertex chunk (already processed separately) console.log("Vertex chunk in strip section?"); } else if (head >= NJD_MATOFF) { // Material chunk (colors) readMaterialChunk(head, flag); } else if (head >= NJD_TINYOFF) { // Tiny chunk (texture settings) readTinyChunk(head, flag); } else if (head >= NJD_BITSOFF) { // Bits chunk (blending modes) readBitsChunk(head, flag); } } while (true);}
This chunk-based approach allows for efficient storage and rendering of complex models with varying material properties throughout different parts of the mesh.
Parsing Flow
The NJCM parsing process follows this general flow:
- Read the root bone
- If the bone has mesh data:
- Read the vertex list and store vertices
- Process chunks for materials and triangles
- Recursively process child bones (if any)
- Process sibling bones (if any)
- Continue until all bones are processed
NJCM Structure Visualization
NJCM└── Root Bone ├── Mesh Data (if present) │ ├── Vertex List │ │ ├── Vertex 1 (position, normal, etc.) │ │ ├── Vertex 2 │ │ └── ... │ └── Chunk Data │ ├── Material Chunk (diffuse, specular, ambient) │ ├── Tiny Chunk (texture ID) │ ├── Strip Chunk (triangle strips with UVs) │ ├── Tiny Chunk (different texture ID) │ ├── Strip Chunk (another set of triangles) │ └── End Chunk ├── Child Bone 1 │ ├── Mesh Data │ └── ... ├── Child Bone 2 │ └── ... └── Sibling Bone └── ...
This hierarchical structure allows for complex models with articulated parts and different materials/textures for each section. In Ikaruga, this is used to organize enemies and player ships into logical components that can be animated independently.
Bone Hierarchy
Models can contain a hierarchical bone structure for animation:
- Bones are organized in a parent-child relationship
- Each bone has position, rotation, and scale information
- Bones can have mesh data attached to them
Vertex Format
Vertex data includes:
- 3D position (x, y, z)
- Normal vectors
- Texture coordinates (UVs)
- Color information
- Skinning weights (for animated models)
Materials
Material definitions include:
- Texture references
- Diffuse, specular, and ambient colors
- Alpha/transparency settings
- Various render flags
Technical Implementation
The model viewer uses Three.js with React Three Fiber to render the 3D models in the browser. The implementation:
- Loads and parses the NJ file format
- Extracts bone hierarchy, vertex data, and material information
- Loads associated textures from PVM/PVR files
- Creates a Three.js skinned mesh with the appropriate materials
- Renders the model with interactive controls for rotating and zooming