A standard OBJ-to-DFF converter ignores hierarchy. An exclusive conversion rebuilds that hierarchy.
If you are diving into the world of modding—specifically for classic titles like Grand Theft Auto: San Andreas, GTA III, or Vice City—you have likely hit a specific wall. You have a beautiful 3D model in a standard generic format (OBJ), but the game engine requires a proprietary, strictly formatted file: the DFF.
Converting an OBJ to a DFF isn't like converting a JPEG to a PNG. It is an "exclusive" process because DFF files are picky, rigid, and require specific collision data and hierarchy structures to function in-game.
If you have tried generic converters and ended up with a game crash, this guide is for you. Here is how to bridge the gap between modern modeling software and retro game engines.
This method is 100% free and retains normals, materials, and hierarchies. convert obj to dff exclusive
If you have dozens of models (e.g., a map conversion project), you need automation.
Recommended Script (for Blender + DragonFF via Python):
import bpy import osinput_dir = "C:/objs/" output_dir = "C:/dffs/"
for file in os.listdir(input_dir): if file.endswith(".obj"): bpy.ops.import_scene.obj(filepath=os.path.join(input_dir, file)) bpy.ops.object.select_all(action='SELECT') bpy.ops.export_scene.dff(filepath=os.path.join(output_dir, file.replace(".obj", ".dff")), export_normals=True, export_materials=True, export_vertex_colors=True) bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False)A standard OBJ-to-DFF converter ignores hierarchy
Caution: Batch conversion only works reliably if all OBJs share identical material structures and triangulation.
Even with the steps above, things can go wrong. Here is the checklist for a successful conversion:
Why this is exclusive: ZModeler writes the RenderWare Frame List correctly, including unused dummy frames that GTA’s exe expects. If you are diving into the world of
Your DFF may export but fail in-game. Run these checks:
import struct import numpy as npclass DFFExclusiveBuilder: def init(self, name="object"): self.name = name self.geometries = [] # list of (verts, tris, uvs, normals, material_index) self.materials = [] # list of material names
def add_geometry(self, vertices, triangles, uvs, normals, material_name): self.geometries.append( 'verts': vertices, 'tris': triangles, 'uvs': uvs, 'normals': normals, 'material': material_name ) if material_name not in self.materials: self.materials.append(material_name) def build(self): # Minimal valid DFF structure for GTA SA (exclusive mode) data = bytearray() # RW version chunk data.extend(struct.pack('<III', 0x10F, 0x04, 0x1803FFFF)) # Section, size, version # Clump start data.extend(struct.pack('<III', 0x10F, 0x04, 0x1803FFFF)) # Frame list frame_count = 1 data.extend(struct.pack('<III', 0x253F2FE, 12 + frame_count*28, 0x1803FFFF)) data.extend(struct.pack('<I', frame_count)) # Identity matrix + position for _ in range(frame_count): data.extend(struct.pack('<ffffffffffff', 1,0,0,0, 0,1,0,0, 0,0,1,0)) # 3x4 matrix data.extend(struct.pack('<fff', 0,0,0)) # position # Geometry list for geo in self.geometries: # Atomic section data.extend(struct.pack('<III', 0x253F2F2, 12, 0x1803FFFF)) data.extend(struct.pack('<I', 0)) # frame index # Geometry struct verts = np.array(geo['verts'], dtype=np.float32) tris = np.array(geo['tris'], dtype=np.uint16) uvs = np.array(geo['uvs'], dtype=np.float32) normals = np.array(geo['normals'], dtype=np.float32) flags = 0x01 # has vertices if len(uvs) > 0: flags |= 0x08 # has UVs if len(normals) > 0: flags |= 0x10 # has normals geom_size = 36 + len(verts)*12 + len(tris)*6 + len(uvs)*8 + len(normals)*12 data.extend(struct.pack('<III', 0x253F2F1, geom_size, 0x1803FFFF)) data.extend(struct.pack('<II', len(verts), len(tris))) data.extend(struct.pack('<I', flags)) # Vertices for v in verts: data.extend(struct.pack('<fff', v[0], v[1], v[2])) # Triangles for t in tris: data.extend(struct.pack('<HHH', t[0], t[1], t[2])) # UVs for uv in uvs: data.extend(struct.pack('<ff', uv[0], uv[1])) # Normals for n in normals: data.extend(struct.pack('<fff', n[0], n[1], n[2])) return bytes(data)