Members mrmaller1905 Posted July 19 Members Posted July 19 (edited) Does anyone have a file format specification for Delta Force 2 .3DI files to import or export custom character meshes with skin weights by bones or decompile into Wavefront .obj files? https://github.com/Acruid/NovalogicTools/blob/master/Tools/lib-novalogic/3DI/File3di.cs DF2PAR01.rar Edited July 19 by mrmaller1905
Engineers shak-otay Posted July 19 Engineers Posted July 19 (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.) Edited July 19 by shak-otay
Members mrmaller1905 Posted July 19 Author Members Posted July 19 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.) 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.
Members Solution mrmaller1905 Posted August 13 Author Members Solution Posted August 13 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) int32 — GAP_0 (unused) 4 (4) int32 — nVerts 8 (4) int32 — PTR_VertGroup 12 (4) int32 — nFaces 16 (4) int32 — PTR_FaceGroup 20 (4) int32 — nNormals 24 (4) int32 — PTR_NormGroup 28 (4) int32 — nColVolumes 32 (4) int32 — PTR_ColVolumes 36 (4) int32 — parentBone 40 (4) int32 — diffXoff 44 (4) int32 — diffYoff 48 (4) int32 — diffZoff 52 (4) int32 — VecXoff 56 (4) int32 — VecYoff 60 (4) int32 — VecZoff 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 shak-otay Posted August 13 Engineers Posted August 13 (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 Edited August 13 by shak-otay
Members mrmaller1905 Posted August 13 Author Members Posted August 13 (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 It's me, I'm the author who wrote the script. Edited August 13 by mrmaller1905
Engineers shak-otay Posted August 13 Engineers Posted August 13 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.)
DKDave Posted August 13 Posted August 13 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 shak-otay Posted August 13 Engineers Posted August 13 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?)
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now