Jump to content

Delta Force 2 (PC, 1999) .3DI


Go to solution Solved by mrmaller1905,

Recommended Posts

  • mrmaller1905 changed the title to Delta Force 2 (PC, 1999) .3DI
  • Engineers
Posted (edited)

Why not use the FwO viewer?

(It would be really nice if you linked to all related infos to not waste people's time more than necessary with searching for infos that were already present in this forum.)

FwO viewer - Copy.png

Edited by shak-otay
  • Members
Posted
2 hours ago, shak-otay said:

Why not use the FwO viewer?

(It would be really nice if you linked to all related infos to not waste people's time more than necessary with searching for infos that were already present in this forum.)

FwO viewer - Copy.png

Tried FwO Raven's 3DI Viewer to export as Wavefront .obj but I get bad or nasty or broken character meshes because they are skin weighted by bones.

bandicam 2025-07-19 17-38-49-464.jpg

bandicam 2025-07-19 17-31-30-535.jpg

  • 4 weeks later...
  • Members
  • Solution
Posted

Instead of using FwO 3di View I found a Python script that converts into Wavefront OBJs or Valve SMDs with support for character models like DF2PAR01.3DI

Here is the file format specification if you need to create, export and compile .3DI files from Wavefront OBJs and Valve SMDs instead of using nova-3di-lab which only supports Metasequoia MQOs:

.3di (Novalogic) — File Format Specification (V8)
Summary / quick facts

Magic / Signature (uint32 LE): 0x08494433 ('3' 'D' 'I' 0x08) — enum FileVersion.V8.

Endianness: little-endian.

Native alignment: packed / no padding (structures are read with Pack = 1).

Top-level header size: 128 bytes.

All integer sizes: int32 = 4 bytes, uint32 = 4 bytes, int16 = 2 bytes, byte = 1 byte, ushort = 2 bytes.

Many pointer fields in structures are stored as 32-bit integers (historic in-memory pointers or offsets) and are not dereferenced by the example reader — treat them as opaque unless you need to implement pointer-based seeking.

File layout (high level)
Signature (4 bytes): FileVersion magic 0x08494433.

Header (128 bytes).

Texture blocks — repeated TextureCount times:

ModelTexHeader (52 bytes)

bitmap scanlines (_bmSize bytes)

palette (256 × 4 = 1024 bytes)

LOD blocks — repeated LodInfo.Count times:

ModelLodHeader (192 bytes)

Vertices (nVertices × 8 bytes each — 4 × int16)

Normals (nNormals × 8 bytes each — 4 × int16)

Faces (nFaces × 72 bytes each)

SubObjects (nSubObjects × 112 bytes each)

PartAnims (nPartAnims × 12 bytes each)

ColPlanes (nColPlanes × 8 bytes each) — parser in given code skips these by advancing file position.

ColVolumes (nColVolumes × 0x50 (80) bytes each) — skipped by parser.

Materials (nMaterials × 0x78 (120) bytes each)

Detailed structures (offsets are relative to structure start)
Notation: offset (size) type — field name : description

1) FileVersion / Signature
0 (4) uint32 (little-endian) — Signature / FileVersion. For supported version V8 this must equal 0x08494433.

2) Header (total 128 bytes)
0 (4) uint32 — Signature again (the code reads signature as the first uint then later reads the 128-byte header).

4 (12) char[12] — Name (NUL-padded ASCII/ANSI bytes; the code uses Utility.TextEncode.GetString(name) then CleanString to trim leading/trailing NULs).

16 (4) uint32 — GAP_0 (unused/reserved).

20 (36) HeaderLodInfo — see below.

56 (68) byte[68] — GAP_1 (reserved/padding).

124 (4) int32 — TextureCount (number of ModelTexture entries following the header).

HeaderLodInfo (36 bytes; located at offset 20 within Header)
(9 × uint32)

0 (4) uint32 — Count — number of LODs.

4 (4) uint32 — DistHigh — distance cutoff values (engine LOD thresholds).

8 (4) uint32 — DistMedium

12 (4) uint32 — DistLow

16 (4) uint32 — DistTiny

20 (4) uint32 — RendHigh (LodRenderType_V8)

24 (4) uint32 — RendMedium

28 (4) uint32 — RendLow

32 (4) uint32 — RendTiny

Rend... values are uint32 codes (example: GENERIC = 0x676E7263).

3) Texture header — ModelTexHeader (52 bytes)
0 (28) char[28] — Name (NUL padded).

28 (4) int32 — _bmSize — size in bytes of the image scanline buffer that follows this header.

32 (2) ushort — Index — texture index.

34 (2) ushort — _flags — texture flags (engine-specific).

36 (2) ushort — _bmWidth — width in pixels.

38 (2) ushort — _bmHeight — height in pixels.

40 (4) uint32 — PTR_BMLines — pointer/offset to bitmap lines (legacy/in-memory pointer).

44 (4) uint32 — PTR_Palette — pointer to palette.

48 (4) uint32 — PTR_PaletteEnd

Texture image data follows immediately after the ModelTexHeader:

scanLines = reader.ReadBytes(_bmSize) — raw indices/scanline data.

palette = reader.ReadBytes(256 * 4) — 256 palette entries, 4 bytes each.

Texture decoding algorithm (as implemented):

numPixels = _bmWidth * _bmHeight

stride = _bmSize / numPixels (bytes per pixel in scanLines).

If stride == 1 — each scanLines entry is a palette index (0..255). Alpha is implicitly 255.

If stride == 2 — the second byte in each pixel (scanLines[i*2 + 1]) is used as alpha.

For pixel i:

palette_index = scanLines[i*stride + 0] * 4

A = (stride == 2) ? scanLines[i*stride + 1] : 255

R = palette[palette_index + 2]

G = palette[palette_index + 1]

B = palette[palette_index + 0]

The palette entry order in file is [B, G, R, A] (as bytes), and the final output buffer is written as 4 bytes per pixel in memory in BGRA order to a 32bpp bitmap.

Important: The code warns it only works for power-of-two textures.

4) LOD header — ModelLodHeader (192 bytes)
Fields in sequence (all 4-byte ints unless noted):

0 (16) int32[4] — null0 (reserved).

16 (4) int32 — Flags — if (Flags & 1) then vertices are offset by bone-related values.

20 (4) int32 — length

24 (4) int32 — PTR_ModelInfo (pointer/offset)

28 (4) int32 — SphereRadius

32 (4) int32 — CircleRadius

36 (4) int32 — zTotal

40 (4) int32 — xMin

44 (4) int32 — xMax

48 (4) int32 — yMin

52 (4) int32 — yMax

56 (4) int32 — zMin

60 (4) int32 — zMax

64 (64) int32[16] — null2 (reserved)

128 (4) int32 — nVertices

132 (4) int32 — null3 (unused)

136 (4) int32 — nNormals

140 (4) int32 — null4

144 (4) int32 — nFaces

148 (4) int32 — null5

152 (4) int32 — nSubObjects

156 (4) int32 — null6

160 (4) int32 — nPartAnims

164 (4) int32 — null7

168 (4) int32 — nMaterials

172 (4) int32 — null8

176 (4) int32 — nColPlanes

180 (4) int32 — null9

184 (4) int32 — nColVolumes

188 (4) int32 — null10

After reading ModelLodHeader, the parser reads in this order:

Vertices — nVertices entries, each entry is 4 × int16 (X, Y, Z, W) → 8 bytes per vertex.

Normals — nNormals × 4 × int16 → 8 bytes per normal.

Faces — nFaces × ModelFace (72 bytes each).

SubObjects — nSubObjects × ModelSubObject (112 bytes each).

PartAnims — nPartAnims × 12 bytes.

ColPlanes — skipped by advancing 0x08 * nColPlanes.

ColVolumes — skipped by advancing 0x50 * nColVolumes.

Materials — nMaterials × ModelMaterial (0x78 = 120 bytes each).

5) Vertex / Normal format
Each vertex or normal stored as:

X (int16)

Y (int16)

Z (int16)

W (int16)

The W value is present but its semantic is engine-specific (often unused in simple readers). Coordinates are raw integer values; the engine likely treats them as fixed-point or scaled units. (Source code uses them directly as Vector4 of short values — scaling/units are engine-dependent.)

6) ModelFace (72 bytes)
0 (2) int16 — null0 (unused)

2 (2) int16 — SurfaceIndex

4 (4) int32 — tu1 — texture U for vertex 1 (32-bit integer, fixed-point UV)

8 (4) int32 — tu2

12 (4) int32 — tu3

16 (4) int32 — tv1

20 (4) int32 — tv2

24 (4) int32 — tv3

28 (2) int16 — Vertex1 — index into vertex array

30 (2) int16 — Vertex2

32 (2) int16 — Vertex3

34 (2) int16 — Normal1 — index into normals array

36 (2) int16 — Normal2

38 (2) int16 — Normal3

40 (4) int32 — Distance (unused/engine)

44 (4) int32 — xMin

48 (4) int32 — xMax

52 (4) int32 — yMin

56 (4) int32 — yMax

60 (4) int32 — zMin

64 (4) int32 — zMax

68 (4) int32 — MaterialIndex — index into the Materials array

Notes about UVs (tu*, tv*):

Stored as 32-bit signed integers. The original engine likely used a fixed-point representation (common choices are 16.16 fixed-point). The reader code does not convert them; it leaves them as ints. If you want to create floating UVs, try dividing by 65536.0 (16.16) or experiment with 1<<n scaling to find the correct mapping for your target renderer.

7) ModelSubObject (112 bytes)
Sequence of fields (all int32 unless noted):

0 (4) int32GAP_0 (unused)

4 (4) int32nVerts

8 (4) int32PTR_VertGroup

12 (4) int32nFaces

16 (4) int32PTR_FaceGroup

20 (4) int32nNormals

24 (4) int32PTR_NormGroup

28 (4) int32nColVolumes

32 (4) int32PTR_ColVolumes

36 (4) int32parentBone

40 (4) int32diffXoff

44 (4) int32diffYoff

48 (4) int32diffZoff

52 (4) int32VecXoff

56 (4) int32VecYoff

60 (4) int32VecZoff

64 (48) int32[12] — GAP_1 (padding/reserved)

Helpers used by the C# code:

BoneOffset computed from VecXoff, VecYoff, VecZoff by (byte)VecXoff >> 8 etc. (engine-specific bit packing).

BoneDiffOffset from diffXoff, diffYoff, diffZoff.

8) ModelBoneAnim (12 bytes)
reserved 12 bytes; parser reads and discards.

9) Collision structures (sizes observed in code)
ColPlane each: 8 bytes (unknown layout — skipped by reader).

ColVolume each: 0x50 (80) bytes (unknown layout — skipped by reader).

10) ModelMaterial (0x78 = 120 bytes)
0 (16) byte[16] — name (NUL padded)

16 (1) byte — BitFlags (engine flags)

17 (3) byte[3] — pad0

20 (4) uint32 — gap14 (unused/reserved)

24 (28) uint32[7] — null0 (reserved)

52 (1) byte — IndexG — index of texture to use (primary)

53 (1) byte — IndexB

54 (1) byte — IndexW

55 (1) byte — IndexA

56 (64) uint32[16] — null1 (reserved)

=> Total 120 bytes

TexIndex (public accessor in code) returns IndexG.

Parse pseudocode (high-level)
text
Copy
Edit
open file as binary reader (LE)
version = read_uint32()
if version != 0x08494433: error

header_bytes = read_bytes(128)
parse header as described -> Header struct

// Textures
for i in 0 .. header.TextureCount-1:
    texHeader = read ModelTexHeader (52 bytes)
    scanLines = read_bytes(texHeader._bmSize)
    palette = read_bytes(256*4)
    decode texture using algorithm noted above

// LODs
for i in 0 .. header.LodInfo.Count-1:
    lodHeader = read ModelLodHeader (192 bytes)
    vertices = []
    for v in 0 .. lodHeader.nVertices-1:
        x = read_int16(); y=read_int16(); z=read_int16(); w=read_int16()
        vertices.append((x,y,z,w))
    normals = similar (nNormals * 4*int16)
    faces = [ read ModelFace (72 bytes) for _ in range(nFaces) ]
    subObjects = [ read ModelSubObject (112 bytes) for _ in range(nSubObjects) ]
    for _ in range(nPartAnims): skip 12 bytes
    // skip colplane/colvolumes if you don't want them:
    advance stream by 8 * nColPlanes
    advance stream by 0x50 * nColVolumes
    materials = [ read ModelMaterial (120 bytes) for _ in range(nMaterials) ]
Important implementation notes & caveats
Header.LodInfo size in source: the C# code defines HeaderLodInfo.STRUCT_SIZE = 20, but the layout in the Header forces the LodInfo block to be 36 bytes (9×4). Use 36 bytes (Count + 4 distances + 4 render type values) — this matches the real byte layout derived from how the header totals 128 bytes.

Pointers: Many fields named PTR_XXX are legacy pointers stored as 32-bit values. The example reader does not use them; it simply reads structures sequentially. If you need random access, these may be offsets relative to the file image or pointers valid only in memory of the original engine — inspect values before relying on them.

UV coordinates: tu / tv are 32-bit signed integers. The C# reader leaves them untouched. If converting to floats, a good starting guess is 16.16 fixed-point (divide by 65536.0). If textures look wrong, try other fixed-point scalings.

Vertex scaling / units: Vertices are int16. The engine may expect a scaling factor (units per meter etc.). The C# example uses the raw short values as coordinates; for export to OBJ you might want to scale (e.g., divide by 256 or by a known scale from game config).

Texture alpha: When stride == 2, the second byte in scanLines is used as the alpha value (0–255). Otherwise alpha is 255.

Image size constraint: the example GenerateTexture warns it only works for power-of-two texture sizes — but there is nothing in the file format requiring power-of-two; it's a limitation in that decoding routine. A robust decoder should not assume power-of-two dimensions.

Sanity checks: validate nVertices, nFaces, _bmSize, _bmWidth, _bmHeight, and ensure they don't cause reads beyond file size to avoid OOM or crashes. Many fields are untrusted numeric values.

C/C-like struct definitions (reference)
c
Copy
Edit
// All fields little-endian, packed (no padding)
typedef uint32_t u32;
typedef int32_t i32;
typedef int16_t i16;
typedef uint16_t u16;
typedef uint8_t u8;

struct HeaderLodInfo {
    u32 Count;
    u32 DistHigh;
    u32 DistMedium;
    u32 DistLow;
    u32 DistTiny;
    u32 RendHigh;
    u32 RendMedium;
    u32 RendLow;
    u32 RendTiny;
}; // 36 bytes

struct Header { // total 128 bytes
    u32 Signature;         // 0
    char Name[12];         // 4
    u32 GAP_0;             // 16
    HeaderLodInfo LodInfo; // 20 (36 bytes)
    u8 GAP_1[68];          // 56
    i32 TextureCount;      // 124
};

struct ModelTexHeader { // 52 bytes
    char Name[28];
    i32 _bmSize;
    u16 Index;
    u16 _flags;
    u16 _bmWidth;
    u16 _bmHeight;
    u32 PTR_BMLines;
    u32 PTR_Palette;
    u32 PTR_PaletteEnd;
};

struct ModelLodHeader { // 192 bytes
    i32 null0[4];
    i32 Flags;
    i32 length;
    i32 PTR_ModelInfo;
    i32 SphereRadius;
    i32 CircleRadius;
    i32 zTotal;
    i32 xMin, xMax, yMin, yMax, zMin, zMax;
    i32 null2[16];
    i32 nVertices;
    i32 null3;
    i32 nNormals;
    i32 null4;
    i32 nFaces;
    i32 null5;
    i32 nSubObjects;
    i32 null6;
    i32 nPartAnims;
    i32 null7;
    i32 nMaterials;
    i32 null8;
    i32 nColPlanes;
    i32 null9;
    i32 nColVolumes;
    i32 null10;
};

struct Vertex { i16 x, y, z, w; }; // 8 bytes
struct ModelFace { // 72 bytes
  i16 null0, SurfaceIndex;
  i32 tu1, tu2, tu3;
  i32 tv1, tv2, tv3;
  i16 Vertex1, Vertex2, Vertex3;
  i16 Normal1, Normal2, Normal3;
  i32 Distance;
  i32 xMin,xMax,yMin,yMax,zMin,zMax;
  i32 MaterialIndex;
};

 

3di_to_obj_or_smd.py

  • Engineers
Posted (edited)

Great! Who's the author of the py script?

Not sure whether the weighting is correct, though. edit: just a wild guess, maybe the rotations are not applied correctly

edit2: seems it's because "# Simple inverse-distance influences" are used

blender-app_df2par01.png

Edited by shak-otay
  • Members
Posted (edited)
1 hour ago, shak-otay said:

Great! Who's the author of the py script?

Not sure whether the weighting is correct, though. edit: just a wild guess, maybe the rotations are not applied correctly

blender-app_df2par01.png

It's me, I'm the author who wrote the script.

Edited by mrmaller1905
  • Engineers
Posted
8 minutes ago, mrmaller1905 said:

ChatGPT, the chatbot who wrote the script.

WHAT? I can't believe that.

Can you give the question that ChatGPT was given to create the script for .3di character models?

(The rotation of a cube (ChatGPT) is something the bot can handle but the 3di script is much more complex.)

Posted
19 minutes ago, mrmaller1905 said:

ChatGPT, the chatbot who wrote the script.

Did *you* create this script with AI?  If so, then see rule #17, as you clearly have made no effort to understand it.

 

  • Engineers
Posted
4 hours ago, mrmaller1905 said:

It's me, I'm the author who wrote the script.

That's cool! Why did you use "# Simple inverse-distance influences"?

(That's a method to automatically generate weights, isn't it?)

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...