Imported from willy92wins/DayZ-Modding-Knowledge-Pack (
skills/dayz-characters/SKILL.md). Install upstream withnpx skills add willy92wins/DayZ-Modding-Knowledge-Pack --skill dayz-characters. Copyright stays with the author.
DayZ Humanoid Character Modding
Custom humanoid characters in DayZ — infected (zombies), survivors, human NPCs — are built by
reusing the vanilla character chain, not from EntityAI. The hierarchy
(DZ_LightAI → DayZInfected → ZombieBase → ZombieMaleBase / ZombieFemaleBase → variants) gives you the
skeleton OFP2_ManSkeleton, every infected animation, and the AI driver for free through inherited
config — so the only thing you author is a new mesh. The real cost of the whole project is one step:
rigging that custom mesh to OFP2_ManSkeleton. Everything else is generic pipeline work delegated to
the skills below.
This skill is the character-specific layer on top of dayz-model-pipeline (generic geometry / LODs),
dayz-texture-pipeline (textures), dayz-animation-pipeline (the skeleton/anim deep-dive), and
ai-3d-to-dayz / hunyuan3d-local / rodin3d-skill (mesh generation). It is the humanoid counterpart
to dayz-vehicles (wheeled) and dayz-aviation (flight).
Scope: anything that walks on the player skeleton — infected, survivors, NPCs — generated by AI, modeled from scratch, or imported from another game.
PREFLIGHT
Gate real build work on /dayz-preflight (P:\ mounted, AddonBuilder, P:\Mods junction), per
_shared/dayz-conventions.md. Authoring config / rigging / baking offline does not need it; packing does.
Before rigging a zombie/infected: its canonical bind is the 95-bone hermit_newbindpose.xob, a SUBSET of
the player skeleton — NOT OFP2's full player bone list. Read THE CANONICAL-BIND INVARIANT (ZED bind) first.
THE PIPELINE (who owns each stage)
| Stage | Owner | Notes |
|---|---|---|
| Generate / model the mesh | ai-3d-to-dayz, hunyuan3d-local, rodin3d-skill |
multi-view + A/T-pose helps the rig; see CASE LOG |
| Retopo to low-poly manifold | ai-3d-to-dayz / dayz-model-pipeline |
AI retopo leaves non-manifold — clean it |
| RIG to OFP2_ManSkeleton | THIS skill | the expensive, character-specific step → references/character-rigging.md |
UV-unwrap + normal/AO bake (high→low→_nohq) |
THIS skill + dayz-texture-pipeline |
character-specific gotchas (conform misalignment, AI-high normals) → references/character-uv-bake.md |
Texture _co / _nohq / _smdi |
dayz-texture-pipeline |
SubstanceToArma preset |
| Character LODs (Visual / Geometry / Fire / Memory / View) | THIS skill + dayz-model-pipeline |
see CHARACTER LODs |
| config.cpp inheritance + model.cfg | THIS skill | inherit ZombieMaleBase → skeleton+anims+AI free |
| PBO / binarize | dayz-pbo-build |
|
| In-game test | dayz-test-ingame (+ dayz-mcp-verify) |
anims / foot-slide / collision / hit / spawn |
RIGGING TO OFP2_ManSkeleton (the core — read references/character-rigging.md)
The single hardest step. Summary of the verified workflow ([LFInfectedBig ✓] 2026-06-24):
- Skeleton =
OFP2_ManSkeleton(player + infected share it). The mesh MUST use the exact bone / selection names. In DayZ a "bone" inmodel.cfgis a named vertex selection weighted per-vertex; there is no shipped armature —model.cfgdefines the skeleton, the.p3dcarries selections+weights. - Armature source = the official rig FBX
animation_rig_character.fbxfrom BohemiaInteractive/DayZ-Misc → "Rig and Animations". Blender-native FBX: armature with 114 bones (exact OFP2_ManSkeleton names) + a weighted reference bodyMale_bodyin the canonical bind A-pose. Units are centimetres. Helpers (*_Dummy,Weapon_*,EntityPosition) import as EMPTIES, not bones → ignore them for body deform. - GOTCHA that silently ruins the bind: before Blender bone-heat auto-weights, you MUST bake the scale
into the armature (
transform_apply(scale=True)). Scaling only the object/parent runs bone-heat at the rig's native scale against your differently-scaled mesh → every vertex collapses ontoPelvisand the rest of the bones get zero verts. Symptom: posing any limb does nothing. - Bind pose: the mesh MUST ship in the canonical A-pose (anims are relative to that rest). If your mesh's limbs sit at a different angle, conform them to the rig (see reference) — a per-region rotation masked by the auto-weight value gives a crease-free shoulder.
- Cleanup: limit total influences to ≤4 per vertex + normalize; target 0 zero-weight verts (zero-weight → pinched/spiky verts in-game). Disable deform on face/finger/eye bones for a body rig.
- Blender deform-check = gross-motion sanity ONLY (does a limb move at all; cross-midline weight
bleed). It is a FALSE GATE for real deform — the engine skins against the real
OFP2_ManSkeleton, not your rig-FBX armature (see the FALSE GATE section below). The deform gate is in-game/Buldozer. Bone-name mismatch → RPT logsBone X doesn't exist in skeleton OFP2_ManSkeletonand that bone silently does not animate; a stray unnamed selection not inskeletonBones[]makes the mesh explode.
Bone catalog by zone lives in dayz-animation-pipeline → references/player-skeleton.md.
THE CANONICAL-BIND INVARIANT — why a rig that looks fine at rest deforms under anims
[✓ in-game, LFInfectedBig S7 2026-06-24] The engine skins the mesh against the canonical
OFP2_ManSkeleton bind (the shared rig's bone positions), NOT a bind derived from your mesh. So the
inherited vanilla anims deform correctly only if your mesh matches that canonical bind in three ways:
- Scale — at ~vanilla size (the rig's
Male_body≈ 1.8 m). A mesh baked oversize (LFInfectedBig was 2.277 m ≈ 1.27×) is bound against the 1.8 m canonical positions → limbs swing from the wrong pivots → gross deform under anims (not just foot-slide). Rescaling to ~1.8 m removed the catastrophic deform. - Orientation — facing the same way as the rig. A 180°-about-vertical error makes the character
face / walk backward AND deform (see
references/character-rigging.md §6: the(x,z,−y)transform shipped backward;(−x,z,y)fixed it). - Proportions / joint positions — the mesh's anatomical elbow/knee/shoulder/wrist must sit at the
canonical bone positions. An AI mesh has its OWN proportions; a UNIFORM-scale fit (
character-rigging.md §1) preserves canonical proportions but cannot match a differently-proportioned mesh → bones land off the anatomy → limbs overextend / stretch under anims. Needs a proportion conform (§3), not just the arm-angle conform.
The tell: fine at rest, breaks only when animating. At the bind pose the skinned output equals the rest
mesh regardless of weights, so "looks right standing" proves nothing about the bind — only an anim (or a
deform-test posing real bone transforms, character-rigging.md §5) exposes a scale/orientation/proportion mismatch.
Diagnostic (offline, no in-game): per bone, distance from the bone head to the weighted centroid of its
verts; large distances (LFInfectedBig: LeftUpLeg 0.33 m vs 0.22 bone-len, hands 0.20 vs 0.06) = the bone
doesn't sit on the anatomy it drives → that limb will deform. (_export/diag_bind_mismatch.py.)
py3d transmits the per-vertex weights faithfully (the cross-side fix and the S7 fixes all propagated in-game) → Object Builder is not required for the export encoding itself.
⚠️ A ZED'S canonical bind is hermit_newbindpose.xob (95-bone SUBSET), NOT the player skeleton [✓ verified vanilla 2026-06-28]
Infected do NOT skin against the player rig. ZombieBase (and thus ZombieMaleBase, which your custom
infected inherits) carries an enfanimsys block — verified DZ\characters\zombies\config.cpp:71-77:
graphname=…\infected\infected_main\infected.agr, defaultinstance=…infected_main.asi,
skeletonName="hermit_newbindpose.xob", meshObject="…\z_hermit_m.xob". hermit_newbindpose.xob is the
zed's bind pose, and it is a reduced subset. Counting <bone> in DZ\anims\cfg\skeletons.anim.xml
(method: grep -c): hermit_newbindpose.xob = 95 bones; player_testing.xob = 172 bones. The 77-bone delta
the zed OMITS = the entire face rig (~40 face_*, eyeleft/right, lids, lips, tongue), the finger
bones (pinky/ring), IK helpers (*handik, *forearmdirection*, righthandik_helper), and all
weapon bones (weapon_bone_01..06, magazine, trigger, bolt, holsters, cameras).
- Implication for a custom infected: if your mesh carries weights on bones the zed bind lacks (face,
fingers, IK), the engine resolves them wrong at runtime → deform, including at idle (the infected graph's
default pose ≠ your A-pose). Conform/weight the mesh to the 95-bone
hermit_newbindposeset, or overrideenfanimsysskeletonName/meshObjecttoplayer_testing.xobto restore the missing bones (the community fix for headgear/face stretch on zeds — Hoxdolum, DayZ Modders Discord 2024, mechanism confirmed vanilla). - Three bone catalogs — do NOT conflate: the Enfusion
enfanimsys.xobbind pose (zed 95 / player 172) is a DIFFERENT layer from the RVmodel.cfgOFP2_ManSkeleton(vanilla zombie ~159 bones, below). "Match the canonical bind" means the.xobthe engine actually binds (hermit_newbindpose), not just the model.cfg skeleton. A mesh weighted to OFP2's full player bone list can still deform on a zed for this reason alone.
⚠️⚠️ The Blender deform-test is a FALSE GATE — only in-game (or Buldozer) validates deform [LFInfectedBig S8 2026-06-24]
The single most expensive lesson of this project. Posing your imported rig-FBX armature in Blender and measuring edge-stretch does NOT reproduce the engine's deform. S8 drove the Blender edge-stretch from 34× → 6× across THREE rebuilds (re-weight, TPS conform, depth-preserving conform) while the in-game deform stayed unchanged and broken (user: "sigue igual" — weird at idle AND animating) — a 3× false green. Do NOT trust the Blender armature pose-test as the deform gate; it cost ~12 offline iterations that produced zero in-game improvement.
- Why it lies: the engine skins against the REAL
OFP2_ManSkeletonfromDZ_Characters(its bind / inverse-bind matrices + the skeleton yourmodel.cfgdeclares), NOT the rig-FBX armature you posed in Blender. If yourmodel.cfgskeleton hierarchy or the mesh's authored rest pose diverges from vanilla even slightly, the engine deforms wrong no matter how clean the Blender pose-test renders. - "Weird at REST / idle too" (not only walking) ⇒ bind / skeleton mismatch, NOT weights. At the true bind pose LBS returns the rest mesh regardless of weights; if it looks wrong while barely moving, the engine's bind ≠ your mesh (model.cfg skeleton wrong, or rest pose ≠ engine bind). Re-weighting will NOT fix it.
- Ground truth = a debinarized VANILLA character (
DZ\characters\zombies\*.p3d→ external ODOL→MLOD converter). Its mesh IS authored in the exact engine bind pose, with the exact selection structure and a model.cfg that the engine accepts. Copy its model.cfg skeleton verbatim and conform/weight your mesh against ITS bind — do NOT hand-author a 114-bone hierarchy and assume it matches OFP2 (it didn't, here). (Scope: the debinarized vanilla is reliable for skeleton, named selections, model.cfg wiring and weights — NOT for visual-LOD vertex POSITIONS, which ODOL→MLOD conversion corrupts for character bodies; for positions use theMale_bodyof the rig FBX — see the addendum below / SP-034.) - Valid deform gates, in order: (1) in-game spawn — the real one; (2) Buldozer (BI model viewer, real engine skinning). The Blender armature pose-test is good only for gross "does a limb move at all" sanity.
- What DID hold up offline (so the offline work isn't all waste): the winding gate
check_dayz_winding.py(encodes the in-game-confirmed rule), scale (f from armature span), orientation (T2), and always LOOK at the REST SIDE/depth render — a TPS conform to near-coplanar bone-midpoint targets flattened the mesh to paper (2D in-game); the flat side-view was in the render folder a whole build cycle before it was noticed.
⚠️ The mesh can be left/right MIRRORED vs the canonical skeleton — anims fling a limb back/up [LFInfectedBig S9 2026-06-25]
The real root cause behind ~8 sessions of "arm bind" pain. After making model.cfg's OFP2_ManSkeleton
byte-match a vanilla zombie's (159 bones, isDiscrete=0, exact parents/order — which restored the leg
leftupleg→propthighleft→leftleg chain), the legs walked but the arms still flung back/up, equal at idle and
animating. The rest arms were a sane A-pose, so it was not pose/proportion — the whole mesh was reflected in
X relative to the skeleton's left/right convention: a left* selection's geometry sat where the canonical
right* bone lives, so the inherited anim drove it about a pivot ~2× a limb-width away → big lever → the limb
swings off even under tiny idle motion. AI/GLB meshes that went through a reflection (then had winding reversed
to render right-side-out) are prone to this, and a 180°-about-Y orientation fix can leave a residual reflection.
The test (offline, decisive): debinarize a vanilla male (external ODOL→MLOD converter), read the embedded
Skeleton bones + the visual LOD's per-vertex vertex_bone_ref, and compute each bone's vert-X relative to
the spine center. Vanilla puts left* on +X, right* on −X, consistent across shoulder/hip/arm (use
large-n bones; pelvis/hand centroids are noisy). Opposite side ⇒ mirrored. The conversion adds only
boundingCenter and reverses winding — it does NOT negate an axis — so ODOL-X == model-X and the comparison is
valid. (Gotcha: vanilla ODOL named_selections carry bone NAMES but EMPTY selected_vertices; real membership
is vertex_bone_ref pairs → sub_skeletons_to_skeleton → skeleton bone index.)
The fix (two options):
- Swap L↔R selection names in every LOD (
leftarm↔rightarm, all sub-bones + fingers + thel*/r*memory points). Geometry/winding/normals stay byte-identical (confirmed-good rendering is untouched); only which bone each side binds to changes. Lowest risk, reversible. Leaves the mesh visually mirrored — fine for a symmetric humanoid; an off-center detail (a chest hole) lands on the mirror side (cosmetic). - Un-reflect the geometry: negate X on all verts + reverse winding (and drop the swap). Makes the mesh properly handed. Bigger change (re-verify the winding gate + facing) but it can also clear a reflection side effect: single-sided faces culled from grazing angles the upright anim never reaches — e.g. a ragdoll corpse going invisible from one angle. Swap alone leaves the reflection, so that culling can persist; un-reflect fixes both at once.
UV + NORMAL/AO BAKE (read references/character-uv-bake.md)
Unwrapping the low and baking the high-poly detail into a tangent _nohq + AO is generic texture work,
but a rigged character adds gotchas that each cost an iteration ([LFInfectedBig ✓] 2026-06-25):
- Bake from a PRE-conform proxy, not the conformed low. The rig conforms the limbs (
character-rigging.md §3); the high was never conformed → after rigginglow.dims ≠ high.dimsin those regions and a direct high→low bake is garbage there. Bake onto a proxy = shipped low's topology+UV with the pre-conform retopo positions (which align with the high); a tangent normal is pose-invariant so it applies to the conformed shipped low. Verifyproxy.dims == high.dimsfirst. - Do NOT
normals_make_consistentthe AI high-poly (non-watertight → flips shells inward → black blotches). Use its original normals +shade_smooth. - Pre-fill the bake image with neutral
(0.5,0.5,1.0)anduse_clear=Falseso misses stay flat, not black (a(0,0,0)texel decodes to an inward normal → renders black). - AO: hide every mesh except the target (the coincident high self-occludes → dark AO) AND clear custom
split normals +
normals_make_consistenton the target. _nohqis DirectX Y- → green-invert the OpenGL bake (dayz-texture-pipeline). Triangulate before baking and ship triangulated so bake tangents == in-game tangents.- Internal decorative geometry (ribcage): bone-heat fails on thin self-intersecting meshes → weight procedurally by height to the spine chain (or DATA_TRANSFER from the body, but strip arm/shoulder leakage).
The gate is a lit preview of the low+normal vs the high, plus numeric checks (UV stretch spread, %
black pixels = 0, AO surface mean ~200). Render and look (blender-visual-review).
SCALING — runtime is broken, bake it into the mesh
"Make it bigger/smaller" has no config or script path: SetScale/GetScale are broken on entities
(T140705) and the collision box does not follow; the Object-Spawner scale is static-objects-only.
The only route is to bake the size into the mesh on the same OFP2_ManSkeleton. Geometry + collision
LODs scale together. BUT the engine binds against the canonical ~1.8 m bind (see THE CANONICAL-BIND
INVARIANT), so baked oversize scale does more than foot-slide — it DEFORMS the mesh under anims:
LFInfectedBig at ~1.27× deformed grossly; rescaling to ~1.8 m removed the catastrophic deform [✓ in-game S7].
Small departures (≤~1.1×) may pass with only foot-slide / IK drift; a genuinely BIGGER animated character
that still deforms cleanly is unsolved here — it would need a custom skeleton with a scaled bind, not
baked mesh scale. Verify foot-contact / melee reach / door clipping / AI pathing in-game; if limbs distort
under anims, the size is fighting the bind.
CONFIG INHERITANCE BY CHARACTER TYPE
Inherit the right base and the skeleton + anims + AI come for free via the inherited enfanimsys block.
Minimal infected pattern (verified vanilla, full detail in dayz-custom-infected.md §1):
class CfgVehicles {
class ZombieMaleBase;
class MyZ_Base: ZombieMaleBase { scope=0; model="\MyMod\infected\myz.p3d"; hiddenSelectionsMaterials[]={...}; };
class MyZ: MyZ_Base { scope=2; hiddenSelectionsTextures[]={"MyMod\data\myz_co.paa"}; };
};
| Character type | Inherit | Gives | Notes |
|---|---|---|---|
| Infected (male) | ZombieMaleBase |
OFP2_ManSkeleton + infected anim graph + AI | [recipe ✓] community rule (PvZmoD): inherit ZombieBase, never AnimalBase |
| Infected (female) | ZombieFemaleBase |
same, female proportions | |
| Survivor / human NPC | SurvivorBase / player chain |
OFP2_ManSkeleton + player anims | [TBD-verify] — not yet built here; verify the base + clothing/proxy slots vs vanilla |
THE WALL: only one mod that modifies player/creature animations can be loaded at a time — loading
two crashes the client/server (Enfusion limit). A character that ships new animations conflicts with
every other anim mod on the server. A pure reskin that inherits the vanilla enfanimsys does NOT hit
this wall. Warn the user whenever a plan ships custom character anims. (dayz-animation-pipeline anchor 3.)
CHARACTER LODs
- Visual: the rendered mesh (with the hole/detail).
- Geometry (collision): must be closed and convex (BI Validating Geometries). A holed/concave
torso is not convex → either convex-decompose into numbered
ComponentXX, or (simple) keep collision solid and ignore the visual hole. Collision need not match Visual. Mass ≥ 10. - FireGeometry: solid unless you want shots-through. With solid collision, bullets crossing a visual hole still hit — a design choice.
- Memory / View / LandContact: per
dayz-model-pipeline. Use DayZ LOD resolutions, not Arma 3 (_shared/dayz-conventions.mdLOD table — ViewGeo6e15, FireGeo7e15).
QUICK TRIAGE
| Symptom | Likely cause | Where |
|---|---|---|
| Posing the rig moves nothing / mesh won't deform | armature scale not applied before bone-heat → weights collapsed to Pelvis |
references/character-rigging.md GOTCHA |
| Whole mesh explodes in-game | a named selection not in skeletonBones[], or wrong skeleton name |
RIGGING; check model.cfg skeleton |
| Spiky / pinched vertices | zero-weight verts or a selection with no weight | cleanup: normalize, fill weights |
Bone X doesn't exist in skeleton OFP2_ManSkeleton (RPT) |
bone-name typo (casing/underscore) | bone catalog in dayz-animation-pipeline |
| Limbs lag / float during anims | mesh not in canonical bind A-pose, or baked-scale drift | RIGGING bind-pose; SCALING |
| Anims don't play at all | not inheriting ZombieMaleBase/enfanimsys, or new-anim wall |
CONFIG INHERITANCE |
| Bigger character, collision wrong | scaled at runtime instead of baked | SCALING |
| Character faces / walks BACKWARD in-game | export transform 180° about vertical ((x,z,−y) was backward for an AI mesh) |
references/character-rigging.md §6 ORIENTATION; THE CANONICAL-BIND INVARIANT |
| Limbs overextend / stretch under anims, fine at rest | mesh scale or proportions ≠ canonical bind | THE CANONICAL-BIND INVARIANT; proportion conform character-rigging.md §3 |
| Deforms wrong in-game but the Blender deform-test looked clean | the Blender armature ≠ engine skeleton — the pose-test is a FALSE GATE | THE CANONICAL-BIND INVARIANT (FALSE GATE); debinarize vanilla + gate in-game/Buldozer |
| Looks wrong at REST / idle, not only walking | bind / skeleton mismatch (model.cfg ≠ vanilla, or rest pose ≠ engine bind) — NOT weights | debinarize a vanilla zombie, copy its model.cfg skeleton + match its bind |
| Skeleton now matches vanilla but a limb flings back/up (idle AND animating), rest pose is a sane A-pose | mesh is left/right mirrored vs the canonical skeleton — left* geometry sits where the right* bone is |
debinarize a vanilla male, compare bone vert-X to spine center (vanilla left*=+X); fix = swap L↔R selection names (geometry untouched) or un-reflect (negate X + reverse winding). See THE CANONICAL-BIND INVARIANT (MIRROR) |
| Custom infected/zombie deforms even at idle; mesh was rigged to the full player skeleton | a zed binds to the 95-bone hermit_newbindpose.xob SUBSET (no face/finger/IK bones), not the player rig |
conform/weight to the 95-bone set OR override enfanimsys skeletonName. See THE CANONICAL-BIND INVARIANT (ZED bind) |
| Corpse/ragdoll goes invisible from a specific angle | single-sided faces of a reflected mesh culled at grazing angles the upright anim never reaches | un-reflect the mesh (negate X + reverse winding); a pure L/R selection swap leaves the reflection so the culling persists |
| Whole mesh renders flat / paper / 2D in-game | a conform/warp collapsed depth (Y) — e.g. TPS to near-coplanar bone-midpoint targets | conform in X/Z only, preserve Y; ALWAYS check the REST SIDE render, not just front |
In-game diffuse = "camo" / black-grey facet noise, persists without _nohq |
corrupt high→low bake (mis-aligned high/low) — the noise is in the _co DIFFUSE, not the normal map |
open and LOOK at the baked _co PNG; references/character-uv-bake.md pre-conform proxy |
Green/grey/brown "camo" patches that SURVIVE replacing the _co entirely (uniform _co too) AND disabling _nohq |
an ENVIRONMENT/terrain texture in an rvmat stage with uvSource="tex" projects that texture onto the body UV. [LFInfectedBig S10] dz\data\data\env_land_co.paa (a landscape photo = DayZ env map) was in Stage7 with uvSource="tex" → painted the landscape over the mesh; survived 6 _co iterations because the fix is the rvmat, not the diffuse |
Audit EVERY rvmat stage — an env/macro/detail map must be uvSource="none" (sampled by reflection), never "tex". ImageToPAA in.paa out.png to LOOK at each stage's texture. The _co+normal Blender preview HIDES rvmat stages = FALSE GATE. First prove the mesh is clean with the CLAY-TEST (render .p3d geometry, NO textures) |
| Textures on the INTERIOR / model see-through from outside (inside-out) | visual winding is glTF-CCW (AI/GLB/retopo source); DayZ renders it back-facing. Blender/Three.js previews are double-sided and HIDE this | reverse every visual face (face.vertices.reverse()); see references/character-rigging.md §6 WINDING |
OFFLINE GATE — run before EVERY PBO (catches the inside-out bug without an in-game cycle)
python references/check_dayz_winding.py <source_mlod.p3d> # exit 1 = will render inside-out
A double-sided preview never shows DayZ's single-sided culling, so an inside-out model (textures on the
interior) only surfaces in-game — losing a test cycle. This detector encodes the in-game-confirmed rule
(LFInfectedBig S6): a correct SOURCE-MLOD visual LOD has cross(v1-v0,v2-v0)·stored_normal < 0. Run it
after building the .p3d and before AddonBuilder; if it FAILs, reverse the visual winding and rebuild.
Detail + why (incl. why NOT to compare against a debinarized vanilla) in references/character-rigging.md §6.
SP-290 (measured 2026-08-17): exit 0 is not "safe to ship." The gate fails only on relative
winding (cross.normal_positive / cn<=0.30). It measures global inversion — it prints
normals_outward[info]=0.00 and NOTE normals look INWARD too (separate lighting issue): negate stored normals
— but does not fail for it. Three fixtures from one source (Crate\models\Crate_Wooden.p3d,
9 LODs), gate run as shipped: intact (A) → exit 0, cross.normal_positive=0.00; winding inverted,
784 faces (B) → exit 1, cross.normal_positive=1.00 (the named case — the gate DOES catch it);
winding AND stored normals inverted, 784 faces + 488 normals (C) → exit 0 PASS. That C state is
internally coherent, globally inside-out — the silent Blender Z-up → Y-up ship (DZ-R Rule 13;
WallLamp, Crate_Wooden). Do not ship on PASS; read the [info] line. Required of
references/check_dayz_winding.py: (1) normals_outward == 0 (or below a threshold) MUST fail,
not annotate — combined with inverted winding this is the state the gate exists to block, not "a
separate lighting issue"; (2) three exit codes, not two — today exit 1 is both "defect found"
and "invalid input". An ODOL .p3d (a6\LMGs\M249\m249_new.p3d) raises a bare AssertionError
and exits 1, indistinguishable from inside-out. Need: 0 PASS, 1 defect, 2 invalid/non-MLOD
with an explicit "this file is ODOL, debinarize first"; (3) keep the three fixtures next to the
check (f0_fixtures/crate_{A_intacto,B_solo_winding,C_winding_y_normales}.p3d, regenerable with
f0_winding_v2.py). Fixture C inverts stored normals to reach the internally-coherent
globally-inside-out state; it does not byte-reproduce an exporter that also mirrors coordinates.
CITE-THEN-VERIFY
Character config and bone names are easy to half-remember. Before writing a class name, base, property or
named selection, grep vanilla (P:\dz\characters\, DZ\characters\zombies\) or the cited reference, and
keep provenance labels ([LFInfectedBig ✓], [recipe ✓], [TBD-verify]). The bone names MUST match
OFP2_ManSkeleton exactly — verify against the rig FBX or player-skeleton.md, never from memory.
REFERENCES
references/character-rigging.md— the full verified rig workflow (armature source, scale gotcha, auto-weights, conform-to-bind, cleanup, deform-test) with the Blender-headless script patterns.references/character-uv-bake.md— UV-unwrap + high→low normal/AO bake: the pre-conform-proxy alignment fix, AI-high normal handling, neutral-prefill misses, AO occluder isolation, internal-mesh weighting, with Blender-headless script patterns.dayz-custom-infected.md(vault20_Knowledge) — the infected recipe: config inheritance, scaling, the see-through hole, LODs, with primary-source citations. Deep config detail lives here.dayz-animation-pipeline→references/player-skeleton.md— the OFP2_ManSkeleton bone catalog by zone.
CASE LOG (append one entry per real import)
LFInfectedBig — giant infected with a see-through torso hole (2026-06-23 → ongoing)
First character imported. ~1.2× infected, pass-through chest hole (ribs+spine), reskin of ZombieMaleBase.
- Mesh: AI-generated (Rodin v2.5 multi-view +
TAPoserecovers an A-pose from arms-down refs) → scripted ribcage cavity → retopo 14k manifold (hole open) → high-poly 1M kept for the bake. - Rig [✓ 2026-06-24]: auto-weights from the BI DayZ-Misc FBX; the armature-scale-apply GOTCHA cost an intermediate failure (all weights → Pelvis) before it was caught; arms were 28°/34° narrower than canonical → conformed via weight-masked rotation; cleanup to max-4 influences, 0 zero-weight; deform-test passed (6961/7000 verts move, smooth, cavity follows the torso).
- UV + bake [✓ 2026-06-25]: Smart UV + rotating pack (uniform density, bbox 96.8%, triangulated 14k).
Normal high 1M→low baked via a pre-conform proxy (the S4 conform had opened the arms ~8 cm →
low≠high); the rainbow/black-blotch/dark-AO failures all traced to alignment, AI-high
make_consistentflips, black-cleared misses, and coincident-high AO self-occlusion — all fixed (references/character-uv-bake.md). Maps in the NEW UV. Ribcage decimated 95k→15.8k + spine-height weights (bone-heat failed on the thin geometry); deform-test dot 0.823.pose_positionships REST=bind; set'POSE'for the deform-test. - Export rig→
.p3d+ texture + LODs + config + PBO [✓ 2026-06-25]: gate R15 closed. Export = py3d direct (now verified in §6, no longer[TBD-verify]): fractional per-vertex bone-weight selections (lowercase), transform(x,z,−y)det+1 DERIVED from the armature matrix, validated against a debinarized vanilla zombie (ground truth for selection names + Y-up frame + character LOD anatomy)._cobaked DIFFUSE high→pre-conform-proxy onto the new UV; Super rvmat (Stage4 ASuvSource="tex", not "none"). 6 LODs: Geometry hullclass='man'(not skinned), FireGeo skinned-to-bones +dmgzone_spine+meatbones.rvmat, ViewGeo, LandContact, Memory (points from bone positions). model.cfg = full OFP2_ManSkeleton hierarchy, self-contained. binarize + CfgConvert + AddonBuilder → 6.4 MB PBO, all offline-verified. - Inside-out winding fix [2026-06-25, deployed + offline-verified; in-game re-confirm pending]: first
in-game build rendered inside-out (textures on interior) — the AI/GLB+retopo source ships glTF-CCW winding
that DayZ back-faces despite the det+1 transform. Fix = reverse every visual face. Built
references/check_dayz_winding.py(FAILs the inside-out build, PASSes the fixed one) and wired it as a pre-PBO gate so this never costs an in-game cycle again. Corrected §6 (it wrongly said "no flip"). - Scale + orientation fixed, residual = proportions [✓ in-game S7 2026-06-24]: the deep investigation
diagnosed the S6 breakage offline. Texture "camo" = a corrupt high→low bake (noise in the
_coDIFFUSE, not_nohq; body UV was clean) → replaced with a clean procedural flesh. Deform had two systemic causes, both confirmed in-game: (a) scale — the 2.277 m (1.27×) mesh fought the canonical ~1.8 m bind → uniform rescale f=0.7901; (b) orientation — the(x,z,−y)transform was 180° about vertical → faced/walked backward → fix(−x,z,y). Both landed (faces forward, vanilla-sized, camo gone). py3d weights transmit fine (Object Builder not needed). See THE CANONICAL-BIND INVARIANT. (_export/{rescale_1x.py,build_full_p3d_1x_t2.py,diag_bind_mismatch.py}.) - S8 — re-rig attempt + the FALSE-GATE wall [2026-06-24, deform still broken in-game]: tried to fix the
proportion deform offline. Diagnosed the shipped deform: asymmetric auto-weights (LeftUpLeg 37 vs RightUpLeg
166 verts) + 19% cross-midline, and
clean_cross_sideAMPLIFIED it to 21–34× (it keeps the dominant side per vert; on bled asymmetric weights it reassigns whole regions to the OPPOSITE limb). Replaced the bind with envelope weighting (distance-to-bone-segment + sidedness fade + a Pelvis→crotch column) — robust for the see-through cavity (data-transfer from a solidMale_bodymis-weighted the tunnel interior → 100× spikes). A TPS proportion conform to bone-midpoint targets flattened the mesh to 2D (targets near- coplanar in Y); fixed with an X/Z-only conform preserving Y. Blender edge-stretch fell 34×→6× over 3 rebuilds. BUT all 3 in-game builds deformed the same (user: weird at idle AND animating) → the Blender deform-test is a FALSE GATE (see THE CANONICAL-BIND INVARIANT). Root cause now scoped to a bind/skeleton mismatch (model.cfg skeleton or authored rest ≠ vanilla OFP2). Confirmed in-game still-good: scale, orientation, winding, 3D volume (X/Z conform). Reusable: envelope weighting, the_conform/diagnostics. - Next (decided with user): STOP iterating against the rig FBX. Debinarize a vanilla zombie, copy its
model.cfg skeleton + match its bind/selection structure, re-author the custom mesh against THAT, gate on
in-game/Buldozer only. Brief
LFInfectedBig_dev\reviews\2026-06-24-prompt-vanilla-zombie-reproduce.md. - Detail:
30_Sessions\2026-06-24-LFInfectedBig-s8-offline-gate-false-green.md,2026-06-24-LFInfectedBig-scale-orient-fixed.md,LFInfectedBig_dev\HANDOFF.md(live state).
(added 2026-06-28) Custom character deforms ONLY under animation = proportion, and the vanilla-body escape
Case LFInfectedBig (AI zombie ~1.2x): correct shape at rest, limbs over-extend when animated ("Vietnam flashback" at shoulders/arms). Verified diagnosis chain:
-
A ZED's bind is
hermit_newbindpose.xob(95 bones), not the player's —enfanimsysblock ofZombieBase(DZ\characters\zombies\config.cpp:71-78). Quick triage: diff the mesh's WEIGHTED bones against those 95 (parseDZ\anims\cfg\skeletons.anim.xml). BUT 0% weight outside the 95 does NOT mean the deform is solved: LFInfectedBig had all 42 weighted bones inside → the deform was NOT missing bones nor Hoxdolum's facial fix, it was mesh-vs-skeleton proportion (bone heads 0.10-0.22 m off the anatomy). diff script pattern: see _judge\diff_zed_skeleton.py. -
When the mesh doesn't match the skeleton, conform/re-weight has a ceiling — it attenuates the deform (~30-45%) but never removes it; the geometry stays far from the pivots. Robust exit = Camino C: rebuild on a VANILLA well-rigged body (zero deform by construction) and graft only the distinctive bits (torso hole, look/texture, baked scale). Use this when conform/re-weight stalls.
-
To get an EDITABLE vanilla body with positions AND weights: use the
Male_bodyof the rig FBXanimation_rig_character.fbx(Blender), NOT the ODOL->MLOD converter. The conversion (ODOL->MLOD) CORRUPTS visual-LOD positions of character bodies (non-human bbox, span ~2.5x height); the ODOLvertex_bone_refcarries the weights but the positions are garbage. The FBX gives correct human positions + clean vertex groups (verified: Male_body 7499v, bbox 1.4x0.28x1.8, 0 unweighted, <=4 infl). Extract: import_scene.fbx -> mesh whose name contains 'body' -> matrix_world@v.co + v.groups; bone heads from the armature (bone.head_local). See _judge\extract_fbx_rig.py. -
py3d: to READ selection weights from an MLOD use the constructor
py3d.P3D(open(p,'rb')), NOTm=P3D(); m.read(f)— only the constructor populatesselection.points = {Point: weight}. An old debinarized MLOD may keep selection NAMES but with.pointsempty (weights lost). -
Offline deform judge (canonical LBS) — useful but with a hard limit. A hierarchical FK with bind = bone-head positions reproduces a mis-proportioned mesh's deform (validated against in-game) and, with a negative control of capsules centered on the bones, discriminates good/bad. BUT it assumes identity-rotation bind; it is NOT faithful to a rig with real bone orientations in the bind (a vanilla body) -> the FK chain explodes (edge-stretch 400x+). To validate a well-rigged body offline: FK with FULL bind matrices, or deform in Blender with the native armature (reconcile the bind-matrix after FBX import, which otherwise explodes). The faithful deform gate is in-game. Cross-ref: SP-030 (judge method + silhouette 2nd axis), SP-034 (converter gap), LL-169.
(added 2026-07-14) Offline deform judge = hierarchical LBS with CANONICAL bind, not the Blender armature (SP-030)
The deform-under-animation of a mesh rigged to OFP2_ManSkeleton CAN be judged OFFLINE - no in-game build, no real RTM - with a CANONICAL hierarchical LBS: v' = sum(wi * Gi * Bi^-1 * v), where the bind Bi = T(head_i) uses the REAL bone heads (the ones the engine uses: Memory-LOD pivots / a bones_*.json derived from the rig) and Gi is FK over the REAL model.cfg hierarchy (skeletonBones[]), rotating each bone about ITS head and propagating to children. This reproduces the real over-extension (bones far from the anatomy -> long arcs when rotated) that an in-game build takes ~hours to reveal.
This does NOT contradict the FALSE GATE section above: the Blender armature / Blender deform-test / single-pivot rotation ARE false gates (they re-bind to non-canonical positions). Two reliable gates and one not:
- FALSE: Blender armature / Blender deform-test / single-pivot rotation (e.g.
inv_hunch_test.py). - OK: hierarchical LBS with bind = canonical heads + model.cfg hierarchy.
- OK: mandatory synthetic negative control - a capsule dummy centered EXACTLY on each bone segment, weighted 100% to that bone. Under the same pose it must bend clean; if the judge also breaks it, the judge over-reports and is useless. Turns "reproduces the deform" into "discriminates good vs bad".
Second axis (from the head-to-head round): a conform judge needs TWO axes reported together - (1) reduced over-extension under anim (edge-stretch), (2) rest-pose silhouette preservation (mean radius of a region's verts to its dominant bone's head->tail axis, original vs conform). Optimizing only (1) lets silhouette distortion pass: in a blind A/B the user's eye caught an arm-thickness diff (orig 0.140 m -> A 0.124 vs B 0.113) that all three offline metrics missed.
CAVEAT (why this is doc-applied, not "verified"): this is an offline PREFILTER validated to DISCRIMINATE (negative control + the S8 false-green case), NOT quantitatively validated against the engine; the final fidelity gate is still the user's eye in-game, and the LFInfectedBig case that produced it is not yet in-game-confirmed. Reference impl: LFInfectedBig\_judge\judge_lbs.py + judge_control.py + judge_compare.py / judge_metric.py. Origin: 2026-06-28 deform gate.
(added 2026-08-03, ArmorHneck, IN-GAME VERIFIED) WORN CLOTHING binds via skeleton name "DayzTemporarySkeleton" — NOT OFP2_ManSkeleton
The single fact that makes custom worn clothing (ClothingTypes m/f) work. A worn p3d whose
model.cfg declares skeletonName="OFP2_ManSkeleton" renders as a RIGID block anchored above the
player (floating ~1 m, rotated, not animating) — the engine treats it as a model with its own
skeleton and never re-binds it to the wearer. ALL vanilla worn clothing (verified: armbend_dynamic_m,
chainmail_m via ODOL model_info.skeleton.name) compiles with the skeleton named
DayzTemporarySkeleton — same 159 bones, same hierarchy/order as OFP2, only the NAME differs
(a placeholder the engine substitutes with the wearer's skeleton). Fix = rename the CfgSkeletons
class + every skeletonName= in the clothing model.cfg to DayzTemporarySkeleton and rebinarize.
The community 114-bone OFP2 template fails TWICE for clothing: wrong bone count AND wrong name.
Worn-clothing canonical frame (measured on chainmail_m/hoodie_m, anatomical components — a global Z histogram is TOO COARSE and false-negatives a 180° flip): -Z = chest/front (hoodie front drawstrings at Z -0.14..-0.07; chainmail pelvis/spine centroids Z -0.04/-0.08), +X = anatomical left, +Y up, origin at feet. A mesh built facing +Z disperses into "exploded" rigid pieces when worn (each plate transformed by its bone from a flipped frame). Fix = R_y(180°) (det=+1, do NOT touch winding) + swap left*/right* selection PAIRS in every LOD.
Diagnosis ladder that isolated this (reusable, each step in-game-cheap):
- Cross-matrix bisection: {armband class + vanilla worn p3d} vs {vanilla top class + custom p3d} — separates config-side from p3d-side in ONE config-only rebuild.
- Compare
ODOL.model_info.skeleton.namecustom vs vanilla worn (the tell: vanilla has NO "ofp2_manskeleton" string anywhere; a compliant clothing ODOL says DayzTemporarySkeleton). - Structure parity checklist custom-vs-vanilla worn: skeleton 159 bones, per-vertex weight sums
(vanilla = raw bytes summing 255), sub_skeletons palette, sections (tops use
camomalealone; ONLY armbands carry camoMale_big_a/b + small_a/b complexion variants), empty Geometry LOD with autocenter=0 is the vanilla worn pattern. - The registration gate is a live spawn (
unknown_type= config not loaded — also caught here: config.bin from CfgConvert did not register at all; pack clothing PBOs with config.cpp as TEXT).
Origin: ArmorHneck 2026-08-03 (third-party RAR, ~6 in-game cycles to isolate; dual research
Claude+Codex on debinarized vanilla). Full case: ArmorHneck_dev\CLAUDE.md + research workspace
REPORT.md (armorhneck_research_ws).