- Roff 58.7%
- TeX 37.2%
- GDScript 4.1%
npcs_anim aus godothic-remake übernommen. tools/ (fremde Repos) und Godot-Caches ausgeschlossen. |
||
|---|---|---|
| extracted | ||
| godot | ||
| npcs_anim | ||
| .gitattributes | ||
| .gitignore | ||
| ANIM_OPEN_POINTS.md | ||
| README.md | ||
| SYSTEMIC_RULESET_ARCHITECTURE.md | ||
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
- Copy the
godot/subfolders you need into your Godot project (keeptextures/as a sibling of the model folders so the relative../textures/paths resolve). - Godot imports
.obj(with.mtl) and.TGAnatively. Drag a folder into the FileSystem dock; Godot generates.mesh/StandardMaterial3Dresources. - The worlds in
worlds/are already prepared for Godot (see below). UseWORLD.tscnfor 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
Camera3Dhasfar = 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 of0.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.objmeshes and a*.tscnis written that instances the parts under oneNode3D.
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
MultiMeshInstance3Dper 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+ ambientWorldEnvironment(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 (→ glTFJOINTS_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 everySTARTUP_SUB_*; - captures each
Wld_InsertNpc(instance, waypoint)and, by initializing each NPC instance, the body/head mesh it sets viaMdl_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 asMdl_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): aMarker3Dper chest at its position with the parsed loot table +lockedflag 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 headsHUM_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 makesMultiMesh.set_instance_transforma no-op, so every instance ends up at the origin (the world looks unpopulated because only the baked world mesh shows). Rungodot --path . --script res://build_populated.gd(no--headless).
Known limitations
- Animations are not converted.
.MANskeletal animations and the rig/bone hierarchy in.MDL/.MDH/.MDSare 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 neitherzmodelnor 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.atools/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/):
<algorithm>includes added to severalvendor/phoenixsources — required by GCC 16 / libstdc++ 16 (std::sort/find_if/transform/binary_search/lower_bound).ztex-o/-mfix (src/cli/ztex/main.cc): the options were mistakenly bound to thevdfvariable, so writing to a file never worked.zmodelMDL/MDM rewrite (src/cli/zmodel/main.cc): the original only dumpedmeshes[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.mesh.ccdebug counters (vendor/phoenix/source/mesh.cc): opt-in per-polygon parse stats (non-leaf / portal / outdoor / kept) printed to stderr whenPHOENIX_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).