Skip to main content

XBG Format — GeomParser Decompile Reference

Source: Hex-Rays decompile of GeomParser_r64.dll (Ubisoft leak) — the actual XBG compiler ("DuniaGeometryDeserializer" = XBG WRITER). Reference Importer: import_wdl_xbg.py (READ ONLY, works on real files). Cross-reference: XeNTaX forum knowledge (WD1 vertex strides, UV scaling, WD2 version bump) → XeNTaX Watch Dogs knowledge §3


1. Header Layout​

OffsetSizeTypeMeaningSource
0x004char[4]Magic "MOEG"Importer line 112
0x042u16Version major (0x95 for WDL)Importer line 113, 394
0x062u16Version minor (0x46 for WDL)Importer line 113, 395
0x0816—Unknown/reservedImporter line 403
0x184u32unk_countImporter line 404
0x1C4u32odd_flagImporter line 405
0x20var—Odd tables (if odd_flag > 0)Importer lines 118-122
—76—Unknown/reservedImporter line 410
—4u32lod_countImporter line 412
—8×lodu32[2]LOD data pairsImporter lines 414-416

Note: The compiler's ParseFileBuffer deserializes the asset via GXAsset::Asset::Deserialize but does NOT write the MOEG header — that is likely written by EditorExporter_r64.dll which consumes GeomParser output.


2. Hierarchy Structure (ParseGeometryAssetHierarchy)​

Function: DuniaGeometryDeserializer::ParseGeometryAssetHierarchy

The hierarchy walk:

  1. Reads node type, export_tag, export_name
  2. Filters node types: BREAKABLE, FACADE, BASIC_ANIMATED, DYNAMIC_ANIMATED, LOFTSHAPE
  3. Checks for LOD/OCM suffixes
  4. For geometry nodes: calls DeserializePolyMesh
  5. For skeleton nodes: calls SkeletonNodeToMeshSkeleton
  6. For breakable: calls BreakableNodeHierarchyToBoneHierarchy

Mesh list written per-LOD via GXData::WriteBuffer (vtable offset 16 = GetData, 24 = GetSize).


3. PolyMesh Write Order (DeserializePolyMesh)​

Function: DuniaGeometryDeserializer::DeserializePolyMesh

Field-by-field emission order:​

OrderFieldChannel NameType/PrecisionDecompile LinesNotes
1Vertex Count—u3264187, 64191Position channel element count
2Positions"Positions"3×i16 (quantized)64196-64242Transformed by node hierarchy matrix; quantized: i16 / 32768 * bbox_ext + bbox_min
3Face Indices"PolyFacesIndices"u32 (tri-list)64580-647773 indices per face; written per-face with vertex links
4Normals"Triangulation"/"TriangulationIndex"3×f32 (computed)64595-64770Computed from face vertices via cross product; normalized
5Tangents"Triangulation" (channel 0)4×f3264595-64770Computed per-face, stored in vertex link struct
6UV Channels (0-6)"TexCoords"2×f32 per channel65033-65371Up to 7 UV channels; loop j=0..6; channel index = v252 - 1
7Vertex Colors"VertexColors"4×u8 (normalized)65448-65589RGBA u8 → f32 / 255.0
8Vertex Illumination"VertexIllumination"3×f3265372-65466Only if valid alongside VertexColors
9Bone Weights"BoneWeights"4×f32 (normalized)64252-64568Max 8 bones/vertex; sorted by weight descending; weights sum to 1.0
10Bone Indices"BoneIDs"4×u1664252-64568Indices into skeleton bone array
11Smoothing Groups"SmoothingGroups"u3264024From off_180118080[0]
12Material IDs"MaterialIDs"u3264024From off_180118080[1]
13Custom Faces"CustomFaces"—64024From off_180118080[2]
14Vertex Links—struct (312 bytes)64806-64899Per-vertex face adjacency (max 64 faces/vert)
15Sub-parts——114242Called after main mesh
16Transform—4×4 matrix64077-64185Node world transform + parent hierarchy

Key struct offsets in GamExMesh (size 0xC0=192):​

OffsetFieldSize
+0x08Position buffer ptr8
+0x10Position count4
+0x14Position stride4
+0x18Normal/Tangent buffer8
+0x24Face index buffer8
+0x2CFace count4
+0x30UV buffers (7×)8×7
+0x38UV counts4×7
+0x56Vertex color buffer8
+0x5ESkin data (weights/indices)inline in vertex links
+0x72Vertex link buffer8
+0x90Sub-mesh parts8
+0x98Material refs8
+0xB0Name string64
+0xF0LOD index4

4. Discrepancy Analysis: Compiler Write vs Importer Read​

#AspectCompiler (GeomParser)Importer (import_wdl_xbg.py)Status
1Header magicNOT written by GeomParserExpects "MOEG" at offset 0Gap — EditorExporter writes header
2Vertex positionsQuantized i16 in local spaceReads quantized i16, applies bboxMatch
3Face indicesTri-list, 3×u32 per faceReads via quad-split index buffer decodePartial
4NormalsComputed per-face, stored in vertex linksNOT directly read — reconstructed from facesGap
5TangentsComputed per-face (4×f32)NOT readGap
6UV channelsUp to 7, 2×f32 eachReads UVFlag (params[3]) but decode unclearPartial
7Bone weights/indices4×f32 + 4×u16, sorted by weightNOT read — .skel file used insteadGap
8Vertex colors4×u8 RGBANOT readGap
9Vertex illumination3×f32NOT readGap
10Smoothing groupsu32 per faceNOT readGap
11Material IDsu32 per faceRead via mesh descriptor matID (params[2])Partial match
12Sub-parts / LODSplitMeshSubParts calledLOD handled via lod_count + mesh listsDifferent approach
13SkeletonSkeletonNodeToMeshSkeletonReads separate .skel fileDifferent file
14Transform hierarchyApplied to positions during compileNOT applied — uses bbox for position decodeGap

5. Bone/Skeleton Mapping​

Function: DuniaGeometryDeserializer::SkeletonNodeToMeshSkeleton

  • Maps skeleton hierarchy nodes to mesh bone indices
  • Called from ParseGeometryAssetHierarchy for animated nodes (BASIC_ANIMATED, DYNAMIC_ANIMATED)
  • Bone IDs from "BoneIDs" channel are indices into this skeleton
  • Companion .skel file holds actual bone transforms/names (not in .xbg)

6. Collision (.col) Generation​

Search results in GeomParser_r64.dll.c:

  • No CPhys, hkpBvCompressedMeshShape, .col.hkx emission found
  • Only Collada parser references: CColladaParser::FindCollisionMeshIDs, COLLISION_PRIMITIVE_LIST
  • CGamExUtils::gamExSerializeOcc serializes occlusion mesh (GXOccMesh), NOT physics collision

Conclusion: GeomParser does NOT emit retail-compatible .col.hkx.


7. Channel Name Reference (from off_1801180xx arrays)​

ArrayIndex 0Full List
off_180118038"Positions"Positions, VertexColors, VertexIllumination, TexCoords, Normals, BoneIDs, BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118020"PolyFacesIndices"PolyFacesIndices, Triangulation, TriangulationIndex, Positions, VertexColors, VertexIllumination, TexCoords, Normals, BoneIDs, BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118028"Triangulation"Triangulation, TriangulationIndex, Positions, VertexColors, VertexIllumination, TexCoords, Normals, BoneIDs, BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118030"TriangulationIndex"TriangulationIndex, Positions, VertexColors, VertexIllumination, TexCoords, Normals, BoneIDs, BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118060"BoneIDs"BoneIDs, BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118068"BoneWeights"BoneWeights, BlendTargets, BlendTarget, SmoothingGroups, MaterialIDs, CustomFaces
off_180118080"SmoothingGroups"SmoothingGroups, MaterialIDs, CustomFaces

Summary​

The GeomParser decompile reveals the full compiler-side mesh data preparation (quantization, transform application, normal/tangent computation, skin sorting). The actual XBG binary serialization (MOEG header, mesh descriptor lists, index buffers, position buffers) is handled downstream — likely in EditorExporter_r64.dll which consumes the GeomParser output via WriteBuffer.

Critical gaps for importer enhancement:

  1. Explicit normal/tangent reading (currently reconstructed)
  2. UV channel mapping (UVFlag in mesh params needs decoding)
  3. Skin data in .xbg vs .skel split
  4. Vertex color/illumination support
  5. Smoothing groups for hard edges