No description
  • Roff 58.7%
  • TeX 37.2%
  • GDScript 4.1%
Find a file
Paragrimm e2dea6049b Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs
npcs_anim aus godothic-remake übernommen. tools/ (fremde Repos) und Godot-Caches ausgeschlossen.
2026-07-21 11:29:53 +02:00
extracted Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
godot Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
npcs_anim Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
.gitattributes Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
.gitignore Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
ANIM_OPEN_POINTS.md Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
README.md Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00
SYSTEMIC_RULESET_ARCHITECTURE.md Initial commit: Gothic assets (meshes, textures, worlds, npc anims) + docs 2026-07-21 11:29:53 +02:00

Gothic Assets → Godot

Converted geometry and textures from Gothic (Piranha Bytes / ZenGin), extracted with ZenKit and converted with phoenix-studio.

Source game: /media/ssd/SteamLibrary/steamapps/common/Gothic/

Directory layout

godot/
├── objects/    779 .obj+.mtl   static props & items   (from *.MRM)
├── meshes/       7 .obj+.mtl   raw meshes             (from *.MSH)
├── worlds/      13 worlds      game worlds (from *.ZEN) — Godot-optimised, see below
├── npcs/       292 .obj+.mtl   NPC/creature/door meshes (from *.MDL/.MDM/.MMB)
└── textures/  1796 .TGA        all textures           (from *.TEX)

extracted/      raw VDF contents (TEX/MRM/MDL/...  intermediate, can be deleted)
tools/          ZenKit + phoenix-studio sources/builds + conversion scripts

Every .obj references a sibling .mtl (relative path), and each .mtl points its map_Kd at ../textures/<NAME>.TGA. So objects/, meshes/, worlds/, npcs/ all resolve textures against the shared textures/ folder.

Importing into Godot

  1. Copy the godot/ subfolders you need into your Godot project (keep textures/ as a sibling of the model folders so the relative ../textures/ paths resolve).
  2. Godot imports .obj (with .mtl) and .TGA natively. Drag a folder into the FileSystem dock; Godot generates .mesh/StandardMaterial3D resources.
  3. The worlds in worlds/ are already prepared for Godot (see below). Use WORLD.tscn for the main colony world; the other worlds are single .obj.

Gothic uses a Y-up, centimetre coordinate system; the exporter already swaps the axis order (x↔z) so models face correctly. Scale to taste in Godot (1 Gothic unit ≈ 1 cm → import scale 0.01 for metres).

⚠️ The worlds are huge (the colony spans ~127 000 × 181 000 units). A default Camera3D has far = 4000, so you'll see almost nothing until you raise the camera's far plane (e.g. 600 000) and position it far out — or set an import scale of 0.01.

Worlds: the Godot 256-surface fix

A Godot mesh can hold at most 256 surfaces (RenderingServer::MAX_MESH_SURFACES), and Godot's OBJ importer makes one surface per usemtl run. zmodel emits a new usemtl every time the polygon material changes (the world mesh is ordered by its BSP tree, so the main world has 34 748 such runs across 1 397 materials). Godot therefore stopped after 256 surfaces and silently dropped ~99 % of the geometry — which looked like "the world is almost empty, only water + a few rocks".

tools/godot_fix_worlds.py post-processes every world OBJ:

  • merges materials that share the same texture (WORLD: 1986 → 332),
  • groups all faces by material (one surface each),
  • if a world still exceeds the cap it is split into *.partNN.obj meshes and a *.tscn is written that instances the parts under one Node3D.

Result, verified by re-importing into Godot 4.6 (--headless --import):

world before (imported) after
WORLD 256 surf / 1 133 tris 2 parts / 106 722 tris (100 %) → WORLD.tscn
OLDMINE (cave) 256 / 961 46 surf / 9 914 tris
ORCTEMPEL 256 / ~1k 117 surf / 37 661 tris

The FIRETREE_* and ITLSTORCHBURNING "worlds" contain no world mesh (they are effect/particle definitions) and are left as empty OBJs.

Re-run after a fresh conversion with:

python3 tools/godot_fix_worlds.py godot/worlds assets/worlds
#                                  ^ obj dir    ^ res:// path used inside *.tscn

Populated world (objects + lights placed)

The static world mesh (WORLD) is only the terrain + baked architecture. Every prop, door, chest, fire, ladder, etc. is a VOB (virtual object) in the ZEN's VOB tree, each with a transform + the name of its source mesh. tools/zvobs (built against phoenix) walks that tree and dumps it as JSON:

tools/zvobs extracted/worlds/DATA/WORLDS/WORLD.ZEN > WORLD.vobs.json

WORLD.ZEN contains 14 049 VOBs: 9 741 with a visual (8 808 are meshes, the rest are particle FX .PFX / decals .TGA) and 1 077 light sources.

tools/godot_build_populated.gd (run inside the Godot project) reads that JSON and writes WORLD_populated.tscn:

  • base world mesh +
  • one MultiMeshInstance3D per unique source mesh (349 meshes, 8 800 instances) — efficient, since most props repeat hundreds of times;
  • the 123 initially-on light sources as OmniLight3D/SpotLight3D;
  • a sun DirectionalLight3D + ambient WorldEnvironment (the world mesh carries no baked lighting).

The key detail is the coordinate fix: the OBJ exporter mirrors Gothic→Godot by swapping X↔Z. The VOB transforms must get the same change of basis, so each instance transform is origin = S·p, basis = S·R·S with S = swap(x,z). Verified by rendering: buildings stand upright on the terrain, roofs/walls and textures correct.

Not placed: 6 visuals (SURFACE, OLDCAMP, NEWCAMP, PSICAMP, FREEMINECAMP, DEMONTOWER) — these are level compounds already baked into WORLD.obj. Loose items (swords, potions…) are mostly not in the ZEN; Gothic spawns them at runtime via Daedalus scripts (Wld_InsertItem), so they don't appear here. .PFX particle effects and .TGA decals are skipped (would need GPUParticles3D / Decal). The 954 initially-off lights are in the JSON if wanted.

The same two tools work on any other world ZEN (caves etc.).

Skeletal animation pipeline (feasibility verified)

NPCs are currently static (OBJ can't hold rigs/animation). The next step is a glTF (.glb) skinned-animation export, which Godot imports natively. tools/zanim_probe confirms ZenKit exposes every needed piece (run on the human skeleton/body/walk):

  • Skeleton ModelHierarchy (HUMANS.MDH): 34 bones, full parent hierarchy (BIP01 → PELVIS → SPINE → … → NECK → HEAD), root_translation.
  • Skin weights SoftSkinMesh (HUM_BODY_NAKED0.MDM): 296 weighted vertices, up to 4 bones each (→ glTF JOINTS_0/WEIGHTS_0).
  • Animation ModelAnimation (HUMANS-S_WALKL.MAN): 24 frames @ 25 fps, 34 nodes, 24×34 = 816 position+quaternion samples (→ glTF animation channels).

So a glTF exporter is viable: emit the skeleton as joints, the soft-skin mesh with joint indices/weights, and each .MAN as an animation. Per skeleton (HUMANS, ORCS, …) one rigged .glb with all its .MAN clips, then the head as an attached mesh on the BIP01 HEAD bone. This is the gating prerequisite for living NPCs and a sizeable build of its own.

NPC placement via the Daedalus VM

NPC (and item) placement is not in the ZEN — Gothic spawns them by running its compiled Daedalus script (GOTHIC.DAT) at world load. tools/zspawn (built against ZenKit's DaedalusVm) reproduces this faithfully:

  • loads GOTHIC.DAT, registers all script classes, installs a no-op default external + the lenient exception handler, and runs every STARTUP_SUB_*;
  • captures each Wld_InsertNpc(instance, waypoint) and, by initializing each NPC instance, the body/head mesh it sets via Mdl_SetVisualBody;
  • resolves waypoint names against the world way-net → JSON.
tools/zspawn GOTHIC.DAT WORLD.ZEN > WORLD.spawn.json   # NPCs + loose items

It also captures Wld_InsertItem (loose ground items) and reads each item's IItem.visual mesh. Waypoints resolve against the way-net plus named free points (FP_*) from the VOB tree.

godot_build_populated.gd then adds to WORLD_populated.tscn:

  • NPCs as MultiMeshInstance3D grouped by body and head mesh. 1027 placed. Armour is honoured: the dressed body mesh comes from the armour C_ITEM's visual_change (passed as Mdl_SetVisualBody's last param) — 341 NPCs wear their guild armour, the rest (Old-Camp diggers) are correctly bare-chested. 62 body meshes total.
  • Loose items (11: potions, a scroll, a ring) as MultiMeshInstance3D.
  • Containers (240, from oCMobContainer.contents): a Marker3D per chest at its position with the parsed loot table + locked flag as metadata (22 350 loot items total) — ready for a future interaction/loot system. Contents stay invisible (they're inside the chest) so nothing is rendered for them.

Heads use a different local axis convention than bodies (their up/crown is +Z, not +Y — they attach to the head bone in-game), so a fixed Rot_X(-90°) correction is applied per head; without it the head lies sideways on the neck. The head pivot is placed at body_top 4%·height (≈ the BIP01 HEAD bone position, verified against HUMANS.MDH) so the neck sinks into the shoulders instead of floating above them.

Notes / limitations:

  • Wildlife is spread at its spawn points; camp humans share a few hub waypoints (OC1=143, NC_DAM=99, PSI_START=98) and would be dispersed by their daily routine (TA_), which is not run — so they're fanned out with a golden-angle spiral.
  • Per-NPC skin/face texture variants are applied: human bodies get HUM_BODY_NAKED_V{bodyTex}_C{skin} and human heads HUM_HEAD_V{headTex}_C{skin} (≈100 distinct faces). Done by duplicating the mesh and swapping only surface 0's albedo, then grouping into MultiMeshes per variant (66 body / 243 head groups). Remaining: weapons in hand (bone attachment) and non-human face variants (orcs etc. ship only one face texture anyway).
  • Loose items are genuinely few in Gothic (~11). Most loot is inside containers (CreateInvItems, ~1925 calls) or on NPCs, not lying in the world.
  • Behaviour/routines/dialog require running the VM at runtime (a GDExtension around ZenKit, OpenGothic-style).

⚠️ Build the scene with a display, NOT --headless. Godot's headless dummy renderer makes MultiMesh.set_instance_transform a no-op, so every instance ends up at the origin (the world looks unpopulated because only the baked world mesh shows). Run godot --path . --script res://build_populated.gd (no --headless).

Known limitations

  • Animations are not converted. .MAN skeletal animations and the rig/bone hierarchy in .MDL/.MDH/.MDS are not representable in Wavefront OBJ. NPC and creature meshes are exported as static geometry (all soft-skin meshes + attachments merged). For rigged + animated characters you would need a glTF exporter, which neither zmodel nor the OBJ format supports.
  • 31 of 527 referenced textures (~6%) are missing. They are genuinely absent from every VDF archive in this installation (mostly water, foliage/bushes/reed, bark, a few armor/carpet textures — several are engine-procedural). The corresponding materials will fall back to flat colour in Godot.
  • 1 texture (SKYRAINDROP) failed to convert due to a format edge case in phoenix.

Reproducing / re-running

The two repos are checked out and built under tools/:

  • tools/ZenKit/build/libzenkit.a
  • tools/phoenix-studio/build/bin/{zvdfs,zmodel,ztex,zscript,zdump}

Conversion scripts (idempotent):

bash tools/convert_textures.sh   # *.TEX  -> godot/textures/*.TGA
bash tools/convert_models.sh     # *.MRM/.MSH/.ZEN/.MDL/.MDM/.MMB -> godot/**/*.obj

Local patches applied to phoenix-studio

These were needed to build and to convert NPC models correctly (see git-less working tree under tools/phoenix-studio/):

  1. <algorithm> includes added to several vendor/phoenix sources — required by GCC 16 / libstdc++ 16 (std::sort/find_if/transform/binary_search/lower_bound).
  2. ztex -o/-m fix (src/cli/ztex/main.cc): the options were mistakenly bound to the vdf variable, so writing to a file never worked.
  3. zmodel MDL/MDM rewrite (src/cli/zmodel/main.cc): the original only dumped meshes[0], which segfaulted on the ~105 MDL / ~43 MDM files whose geometry lives in attachments or in multiple soft-skin meshes. It now emits every soft-skin mesh and every attachment into one OBJ with correct vertex/wedge offsets. This took the MDL success rate from 5/110 to 110/110 and MDM from 87/130 to 130/130.
  4. mesh.cc debug counters (vendor/phoenix/source/mesh.cc): opt-in per-polygon parse stats (non-leaf / portal / outdoor / kept) printed to stderr when PHOENIX_MESH_DEBUG=1. Used to confirm the world mesh parse keeps the right polygons (the "empty world" turned out to be a Godot importer limit, not a parse bug — see the worlds section above). No effect unless the env var is set.

Build prerequisite on a modern CMake: configure with -DCMAKE_POLICY_VERSION_MINIMUM=3.5 (the vendored submodules declare a pre-3.5 minimum that current CMake rejects).