Skip to content
Back to skills

Dayz Model Pipeline

ASecurity

Skill for generating complete DayZ mod objects autonomously — from 3D models to game-ready .p3d files with LODs, memory points, named selections, procedural textures, .rvmat materials, animations (model.cfg), and config.cpp. Primary path: Blender headless for pro geometry (bevel, subdiv, booleans, Smart UV, AO bake, auto LOD decimate — user-gated for visual LODs) + py3d for .p3d assembly. Textures use OpenSimplex FBM multi-layer compositing. Use when user mentions: Object Builder, p3d files, ...

  • 16 stars
  • 0 votes
  • 1 copy
  • 9 views
  • Added October 2, 2026
ai-agentspythonrustgoshellbashangularnodeexpressawsdebugging

Works with

  • cursor
  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 15 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add willy92wins/DayZ-Modding-Knowledge-Pack --skill dayz-model-pipeline --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dayz Model Pipeline?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dayz Model Pipeline
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/willy92wins-dayz-model-pipeline/badge)](https://www.skillsdirectory.com/skills/willy92wins-dayz-model-pipeline)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dayz-model-pipeline
description: >
  Skill for generating complete DayZ mod objects autonomously — from 3D models to game-ready
  .p3d files with LODs, memory points, named selections, procedural textures, .rvmat materials,
  animations (model.cfg), and config.cpp. Primary path: Blender headless for pro geometry
  (bevel, subdiv, booleans, Smart UV, AO bake, auto LOD decimate — user-gated for visual LODs) + py3d for .p3d assembly.
  Textures use OpenSimplex FBM multi-layer compositing. Use when user mentions: Object Builder,
  p3d files, LODs, memory points, named selections, model.cfg, PBO packing, 3D models for DayZ,
  animated objects, procedural textures, .rvmat materials, or LF_PowerGrid electrical mod objects,
  cacheP3D, liquid_source, disableSimulation, ViewGeometry inventory-attachment proxy, unpacked -mod.
  Also trigger for Blender-to-DayZ workflow or generating models from scratch.
---

# DayZ Model Pipeline — Complete Autonomous Object Generation

This skill enables generating complete DayZ mod objects from scratch — 3D model, textures,
materials, animations, and all config files — using Python and Blender headless.
Primary path uses Blender for pro-grade geometry (bevel, subdiv, booleans, Smart UV,
AO baking, auto LODs via decimate) + py3d for .p3d assembly. Fallback path uses
py3d only for quick prototyping without Blender.
(until 1.29: the LOD table, Rule 14 visual-LOD proxies, and AddonBuilder packing loop below remain the 1.29 contract.) (since 1.30 Exp: inventory-attachment proxies also need ViewGeometry on dedicated server; wells need ViewGeometry `liquid_source`; `-cacheP3D` for unbinarized meshes; `disableSimulation` on static houses. Details: [dayz-1-30-model-pipeline.md](references/dayz-1-30-model-pipeline.md) and ## DayZ 1.30 Exp.)


## Artist-return .p3d preflight (SP-246)

BEFORE deploying any `.p3d` that came back from an artist, run this offline py3d
preflight against the file that was sent. An artist exporter keeps what is VISIBLE
and drops the rest. An untouched LOD is suspicious, not safe: the envelope may have
moved while ViewGeometry, FireGeometry, `boundingbox_min/max` and `*_axis` came back
intact and now lie.

Origin: GunRacks T2, 2026-08-15 (`rack_t2_editado_final.p3d`). Visual improvements
were real (modelled hinges replacing a 1792-face wire mesh; cabinet depth
0.268 → 0.436 m). The return had no Geometry LOD, 7 proxy triangles deleted from
the visual LOD, `hideproxy` declared but empty (0 points / 0 faces), `Component01`
and proxy paths lowercased. ViewGeometry / FireGeometry / `boundingbox_min/max` /
`doors1_axis` were intact — the front protruded 0.168 m outside FireGeometry; the
door axis sat 198.8 mm behind its own leaf.

**Preflight (cheap, all offline with py3d, before deploying anything):**
1. Count LODs by `classify_lod_resolution` against the file that was sent. Geometry is the first to vanish.
2. Proxy selections present in visual **+ ViewGeo + FireGeo**, and all of them inside the bone. Compare their coordinates against the sent model: if they change, the in-game alignment is lost.
3. **Visual-LOD bbox vs collision boxes vs `boundingbox_min/max`.** If they diverge, rebuild the boxes, not the visual.
4. Animation axes (`*_axis`) against the piece they rotate: axis-to-piece distance. On T2 that number was 198.8 mm and jumped out on its own.
5. Casing of `Component01` and of the mod's own paths against the validated artifact.
6. Orphan points and leftovers of replaced geometry — here, 3 points (one 0.57 m outside the model, which poisons any bbox computed from `lod.points`) and 16 stray faces.

**Reintegration supports (any artist round-trip):**
- `#SharpEdges#` are POINT indices. Any filtering that reindexes points leaves them pointing at foreign points **without failing** — remap them by hand. Measured here: max 2466 of 2505 points, 781 pairs, 0 lost after remap.
- A py3d round-trip of a third-party exporter file is **not byte-identical** (here +892 B: regenerates `#UVSet#` and drops empty `#SharpEdges#`) but it does keep points, faces, selections, properties and hard edges. Checking the round-trip BEFORE editing is the gate that authorizes touching the file with py3d — a `.p3d` generated by py3d does come back identical.

**More than one artist round (same day, T1 `gunrackt1_nuevo.p3d` + T2 `rack_t2_puerta`):**
- The artist starts from an old copy, so every correction later than the one they have comes back undone. Both returns re-applied the 180° proxy rotation removed the same day — exactly `R_orig·Ry(180)` with the anchor fixed, on all 10 proxies. Proxy-triangle legs are 1–2 mm, so 180° moves vertices **~4 mm** and no view flags it. The post-artist correction list is a **re-runnable script**, not a finished job; re-run it whole every round-trip, and compare against the DEPLOYED file, not the previous delivery.
- Do not parameterize the patch as a delta on whatever the delivery brought. Memory point `doors1` computed as `delivered_value + dz` would have been silently wrong on the 2nd delivery (memory origin 8.11 mm lower in Y, different Z). Always anchor to **geometry in the file itself** (`Z = leaf_midplane + offset`), and verify the reformulation reproduces the already-deployed value before trusting it.
- An artist change that *reduces* a prior improvement is not a regression by default. Here it gave back 43 mm of depth because the door went inside the body (it had hung in front of the sides). What decides is **functional magnitude**, not delta direction: useful gap became 0.284–0.303 m vs 0.168–0.187 m on the model that clipped. Measure it; do not opine it.

## Capabilities

1. **Blender headless geometry** (bevel, subdivision, boolean cutouts, shade smooth)
2. **Automatic LOD generation** via Blender Decimate modifier (LOD0 → LOD3)
   - **Decimation gate**: Any decimation whose output is VISUAL (LOD0 or visible LODs) must be flagged to the user and approved BEFORE running — default is the user decimates manually. Automated decimation without asking is only acceptable for non-visual collision/view LODs.
3. **Smart UV projection** and AO baking from real 3D geometry
4. **Procedural texture generation** via Pillow + OpenSimplex (coherent noise FBM, multi-layer compositing)
5. **Direct .p3d assembly** via py3d library (LOD packaging, named selections, memory points)
6. **Full LOD pipeline** (Resolution, Geometry, Fire Geometry, View Geometry, Shadow, Memory)
7. **Animation setup** (model.cfg with skeletons, bones, rotation/translation animations)
8. **Config generation** (config.cpp with AnimationSources, DamageSystem, hiddenSelections)
9. **Material generation** (.rvmat files with emissive support for LEDs/indicators)
10. **Complete addon packaging** ($PBOPREFIX$, folder structure ready for AddonBuilder)
11. **Interactive 3D preview** via Three.js HTML artifacts for visual validation before generating

## Quick Reference

Before doing anything, read the relevant reference file:

- **Wheeled vehicle? READ FIRST, before baking** → `references/vehicle-structural-parity.md`. A from-scratch vehicle reveals the pieces a real DayZ vehicle has of stock (Geometry wheel hubs, crew proxies in ViewGeometry, FireGeometry damage zones, lights, AnimationSources) one error at a time in-game unless you extract the full anatomy up front. Debinarize the civiliansedan once, diff against it, build every missing piece in one pass — that is the anatomy a project's readiness gate expects in hand before the spec is written (LL-030).
- **1.30 Exp vanilla P3Ds are ODOL v56 (1.29: v54).** Before debinarizing a 1.30 vanilla reference model, read the P3D header (bytes 4-7); a v54 reader fails with `ValueError: subsection not found`. Preflight detail: `dayz-vehicles/references/dayz-1-30-vehicles.md`.
- **Wheeled vehicle config.cpp + model.cfg (complete, worked example)** → `references/vehicle-config-and-modelcfg.md`. The full car class: SimulationModule (engine/gearbox/drive/differential/suspension), Crew, lights, DamageZones, cargo, wheel/door items + slots + proxies, sound, AnimationSources, plus the matching model.cfg (CfgSkeletons bone hierarchy + wheel/steering/damper animations). Reproduces a verified working car (Tyson89/Landrover). Read AFTER vehicle-structural-parity.md.
- **Wheeled vehicle per-LOD content, memory points & proxies** → `references/vehicle-structural-parity.md` Addendum 2026-05-30b (what each LOD carries for a car, the full memory-point catalog, and proxy placement per LOD).
- **Vehicle-specific work beyond assembly** (CarScript config tuning, vanilla parity debugging, wheel simulation, dedicated-server packaging failures) → delegate to the USER skill `dayz-vehicles`.
- **Blender headless pipeline (PRIMARY)** → `references/blender-headless.md`
- **Procedural texture generation (OpenSimplex)** → `references/procedural-textures.md`
- **Direct P3D assembly (for LOD packaging)** → `references/py3d-direct-generation.md`
- **LODs & geometry rules** → `references/lods-and-geometry.md`. A large building or rock with an interior (hundreds of collision pieces, an MLOD of hundreds of MB, `binarize` `OTHER_FAIL` with no capacity line): its section "Collision of a large building with an interior" (SP-453, SP-457, SP-458).
- **Memory points & named selections** → `references/memory-and-selections.md`
- **Animations (model.cfg)** → `references/animations.md`
- **Config.cpp & addon structure** → `references/config-and-packing.md`
- **DayZ 1.30 Exp (proxies / cacheP3D / disableSimulation / liquid_source)** → `references/dayz-1-30-model-pipeline.md`
- **Blender manual workflow (ALTERNATIVE)** → `references/blender-workflow.md`
- **LF_PowerGrid object recipes** → `references/lfpg-recipes.md`
- **AnswerOverflow community findings (mined 2026-05-17)** → `references/answeroverflow-2026-05-17.md` (binarize misalignment on personality meshes, nested proxy disappearance on binarized models, Maya Object Builder alternative)
- **Multi-material rvmat: per-section texturing (Multi shader, RGB+Black mask, dual UV set, uvTransform)** → `references/multi-rvmat-section-texturing.md` (mined from DayZ Modders Discord 2026-06). Read when one mesh region needs several tiling detail textures with per-section specular (metal vs paint vs rubber), or when scaling/offsetting textures via uvTransform.

## Pipeline Overview

### Phase 0: external-binary provenance (LL-230)

Before editing any `.p3d`, `.paa`, `.rvmat`, or other binary received outside the
repository, inventory the live asset and the latest known external delivery. Compare SHA-256,
byte size, modification date, and the relevant structural count (for a mesh, face count).
A newer `_artist_drop/`, `_from_provider/`, or `scratch/` tree is a provenance gate,
not background context.

Resolve every divergence in Phase 0: integrate the external delivery now with an explicit
budget; retain the live asset as a conscious decision recorded in the bug ledger with owner
and review date; or update the provider package when the live asset is newer. “Separate
surgery” without an owner and date does not close the gate.

### Path A: Blender Headless + py3d Assembly (PRIMARY)

This is the PRIMARY path. Blender generates pro-grade geometry; py3d assembles the .p3d.
Install: `apt-get install -y blender && pip install opensimplex --break-system-packages` + py3d fork DayZ (bloque CRITICAL abajo).

> **CRITICAL: py3d installation** — Do NOT use `pip install py3d` (different
> point-cloud library) and do NOT install upstream from GitHub anymore. Use the
> **py3d DayZ fork >= 1.6.0** (KoffeinFlummi codec + anti-corruption guards,
> DayZ LOD constants, `validate()`, proxies, recipe, and CLI `python -m py3d`),
> pack distribution `py3d-dayz` via `pip install -e tools/py3d`:
>
> ```bash
> SKILL_DIR=<this skill dir>
> # py3d DayZ fork >= 1.6.0 (`pip install -e tools/py3d`).
> # NEVER `pip install py3d` (PyPI = point-cloud lib) NOR git+upstream (no guards).
> pip install -e tools/py3d
> python3 -c "import py3d; assert getattr(py3d,'IS_DAYZ_FORK',False) and tuple(map(int,py3d.__version__.split('.')))>=(1,6,0), (py3d.__version__, py3d.__file__)"
> ```

**Step 1: Define the Object**
- User describes what the object does, how it looks, what moves
- Identify: static parts, animated parts, interaction points, electrical connections
- Select recipe from `lfpg-recipes.md` or define a new one

**Step 2: Create Interactive Preview (Optional but Recommended)**
- Generate a Three.js HTML artifact showing the object in 3D
- Include state toggles (on/off/disconnected, open/closed, etc.)
- Let the user validate the design before generating the real model

**Step 3: Generate Geometry in Blender Headless**

> For imported vehicles, follow the domain profile (`rip-vehicle-import` / `dayz-vehicles`), NOT the generic Smart UV / decimation defaults. Domain profile: measured det=+1 transform; face partition, not decimation; preserve winding with minority repair; UV by sections. Winding preserve-vs-flip is per profile/transform (Rule 12), not a universal constant — generic DCC geometry keeps its face order under a det=-1 map (Rule 12; section "Generic DCC visual-winding profile (SP-071)"). A synthetic-cube A/B measures the collision-raycast mechanism, not a universal flip doctrine; measure a real rip before generalizing, and collision A/B is not the same axis as SP-071 visual culling. *(Aligned 2026-10-03 with Rule 12: this note read "NOT the generic Smart UV / decimation / flip defaults" and "SP-071 remains the generic-DCC visual-cull guide.")*
- See `references/blender-headless.md` for complete API reference
- Create geometry using primitives + modifiers (bevel, subdiv, boolean)
- Apply shade smooth for curved surfaces
- Run Smart UV Project for automatic UV mapping
- Export LOD0 as OBJ (full detail, ~1500-2000 verts for small objects)
- Auto-generate LOD1/2/3 via Decimate modifier (ratios: 0.50, 0.25, 0.12)
  - **Decimation gate**: LOD1/2/3 output is VISUAL — flag the planned decimation to the user and get approval BEFORE running; default is the user decimates manually. Automated decimation without asking is only acceptable for non-visual collision/view LODs.
- Optionally bake AO map from real geometry (Cycles, 64+ samples)
- Save .blend reference file

**Step 4: Generate Procedural Textures (Coherent Noise Pipeline)**
- See `references/procedural-textures.md` for complete API reference
- Use OpenSimplex coherent noise + FBM for organic detail
- Multi-layer compositing: base + flow + grain + micro + wear + sparse detail
- If AO was baked in Step 3, composite it into diffuse (multiply blend)
- Generate _co, _nohq, _smdi maps per surface at 512x512
- Output as .png (user converts to .paa with TexView/Pal2PacE)
- NEVER use random()-per-pixel for surface patterns

**Step 5: Assemble .p3d (py3d library)**
- See `references/py3d-direct-generation.md` for py3d API
- Read Blender OBJ exports → build visual LODs (0.0, 1.0, 4.0, 8.0)
- Generate Geometry/Fire/View LODs as convex boxes (py3d primitives — these don't need Blender quality)
- Generate Memory LOD with animation axes, port points, actionPos, CE center
- Add named selections across all LODs
- Assign texture/material paths (.paa, .rvmat)
- Write .p3d binary, verify by reading back

**Step 6: Generate Materials (.rvmat)**
- Standard materials per surface type
- Emissive materials for LEDs (green, red, off)
- Penetration materials for Fire Geometry

**Step 7: Generate model.cfg + config.cpp**
- CfgSkeletons, CfgModels, animations
- CfgVehicles with AnimationSources, hiddenSelections, DamageSystem
- Class name MUST match .p3d filename

**Step 8: Package**
- $PBOPREFIX$, folder structure, output .zip

**User's only remaining steps:**
1. Convert .png → .paa (TexView or Pal2PacE)
2. Pack with AddonBuilder → .pbo
3. Test in-game

### Path B: py3d Only (Fallback — Quick Prototyping)

Use when Blender is unavailable or for very simple geometry (cubes, basic cylinders).
Generates everything in pure Python. Lower quality but zero dependencies beyond pip.
Follow Steps 1-8 above but skip Step 3 (Blender) and generate geometry directly
in Step 5 using py3d primitives. See `references/py3d-direct-generation.md`.

### Path C: Blender Manual Workflow (Alternative)

Use when the user already has a complex mesh from an external source (.fbx, .obj, .glb)
that cannot be generated procedurally. See `references/blender-workflow.md`.

### Path D: 3D Model Viewer/Tagger (For External Models)

For cases where the user imports a model and needs to tag parts interactively:
- Build a Three.js artifact that loads .glb files
- User clicks parts to label them (animated, static, axis location)
- Claude generates the configuration from the labels
- Requires the model in .glb format (export from Blender if needed)
- Delegate to the sibling skills `dayz-p3d-inspector` (interactive .p3d inspect/edit/round-trip) or `dayz-3d-viewer` (textured 3D viewer) instead of building an ad-hoc viewer

## Critical Rules (Common Mistakes)

1. **Geometry LOD components MUST be convex** — non-convex geometry causes collision failures
2. **All Geometry/Fire/View LOD components MUST have mass** — minimum 10 for character collision
3. **Fire Geometry thickness matters** — too thick glass becomes bulletproof
4. **Shadow LOD must be slightly smaller than visual LOD** — otherwise object appears fully shaded
5. **Memory points are single vertices** — not edges, not faces
6. **Animation axes need exactly 2 points** — defining a line in 3D space
7. **Named selections must be consistent across LODs** — same name in Resolution, Geometry, Fire, View
8. **Texture paths in p3d must use .paa** — not .png or .tga (but generate as .png, user converts)
    - [EXACT] New visible faces need a real `.paa` FILE as the face texture: a procedural `#(argb,...)` face texture binarizes to section special `0x0000C000` (no hidden-selection bits) and the engine never draws the section, while drawn sections carry `0x0002C000` (opaque) or `0x0002C100` (alpha). Verify by reading the packed ODOL section table, not the MLOD — full method and gate in `dayz-vehicles` SKILL.md, "Verify new geometry by its ODOL SECTION" (SP-396, measured 2026-09-14).
9. **model.cfg class names must match p3d filenames** — e.g., class MyObject for MyObject.p3d
10. **LOD resolution values matter** — wrong values = engine doesn't recognize the LOD type
11. **Always verify the .p3d** — read it back with py3d after writing to confirm integrity
12. **Blender Z-up → DayZ Y-up conversion is MANDATORY, and it is a REFLECTION (det=-1)** — Blender is right-handed (Z up); DayZ is left-handed (X east, Y up, Z north). A map that preserves the SHAPE between them must flip one axis in the numbers. Two source cases:
    - **FBX / Blender-authored geometry (outward faces and normals):** axis `(x,y,z)→(x,z,y)` (det=-1) applied to ALL vertices AND normals in ALL LODs. The vertex order then lands in the MLOD convention by itself (cross product INWARD): do **not** reverse faces; **negate** the shading normals (MLOD stores them inward). [EXACT] Measured in game 2026-10-01, DayZDiag 1.29.163709: one chiral model exported three ways — this map renders solid and reads correctly; the old det=+1 recipe renders solid but MIRRORED; `P3D.transform(py3d.BLENDER_TO_DAYZ)` alone renders inside-out AND mirrored (see the LL-504 section below). With py3d: `P3D.transform(((1, 0, 0), (0, 0, 1), (0, 1, 0)))` maps points and normals and, because det<0, reverses every face itself — reverse them back and negate every normal (net: original order, negated normals); from py3d 1.8.0 `py3d.blender_to_dayz(model)` is exactly that call, writing the same bytes as the measured model, and `py3d.BLENDER_TO_DAYZ` is deprecated (same det=+1 value, now `py3d.ROT_X_NEG90`). Geometry authored directly in DayZ space (collision boxes built in code) does not go through the map. The old recipe — `(x,y,z)→(x,z,-y)` (det=+1) + reverse faces + negate normals, validated on five LFPG models with offline gates only — passes every winding gate because it is a MIRROR: the mirror is what turned the faces inside out, so reversing them "fixed" it. Invisible on symmetric props; text, logos and one-sided parts ship reversed — re-export those. [EXACT] Measured the same day with the same map (DayZDiag 1.29.163709, ODOL read with the LL-505 reader): Geometry/ViewGeometry/FireGeometry components register `scene_raycast` hits in `geom`, `view` and `fire` from both sides, and after the det=+1 map alone they register none; static proxies drawn in Blender as py3d canonical raw triangles and converted with the model get the same ODOL engine frames as `add_proxy(space="engine")` with the matching rotation and render in the pose drawn in Blender; the two binarized parents are byte-identical, and that engine-space path is the one LFDucati was driven with (frame fix LL-505, drives LL-506), so a crew or wheel proxy drawn this way with the identity frame gets a frame that works (claim: CLAIM-B2D-PROXY-FRAME-EQUIV). That is an equivalence of the proxy frame, not a drive through this conversion: selections, bones, model.cfg bindings and LODs must still match a working model, and only the identity crew frame is known to work in game (a tilted one came out of binarize as asked and the rider turned to the left instead of leaning forward, LFDucati T93). Proxies drawn another way: compare them in the ODOL against a working model (LL-505).
    - **GLB/glTF-sourced geometry: the same recipe.** glTF front faces are CCW by spec, as Blender's are, and Blender's glTF importer turns Y-up into Z-up with a rotation (det=+1) that keeps them so: a GLB brought through Blender converts like the FBX/Blender case above — det=-1 map, face order kept, normals negated, **no** reversal. [EXACT] Measured in game on GLB/glTF-origin models: the kept order rendered correctly on A6_MK47 v12c (2026-06-11) and on MercedesAMGLF (2026-06-24), whose earlier build with its faces reversed after a det=-1 map rendered see-through. A glTF read without Blender first bakes its node transforms and takes its own det=-1 map. Detail, sources and the det=+1 history: section "GLB/glTF imports — LL-020 refined" below. *(Aligned 2026-10-03: this bullet read "**GLB/glTF-sourced geometry (separate case):** glTF front faces are CCW by spec — do **not** treat this as the same rule as the FBX/Blender measured recipe above. See section "GLB/glTF imports — LL-020 refined" below for GLB-only guidance (optional pure-swap det=-1 + reverse, lateral-mirror notes).")*
    - Never assume either case. Winding gates cannot see a mirror (the old recipe passes all of them): check chirality on an asymmetric feature — text, a logo, a one-sided part — in game, or compare the binarized ODOL against a vanilla model through the same reader.
13. *(Merged into Rule 12 — the Blender/FBX map is the det=-1 reflection with negated normals, and a GLB/glTF brought through Blender takes the same one. Aligned 2026-10-03: this note ended "GLB remains a separate case.")*
14. **Attachments require proxy system (3 parts)** — for items to render visually when attached: (a) proxy face + selection in visual LODs of the parent p3d, (b) a proxy p3d referenced by the selection, (c) `CfgNonAIVehicles` entry mapping `inventorySlot` to the proxy model. Missing any part = attachment is logically present but invisible. See `memory-and-selections.md` and `config-and-packing.md`.
(until 1.29: visual LODs of the parent were sufficient for the server to read proxy position.) (since 1.30 Exp [CHANGELOG]: duplicate the proxy triangle in **ViewGeometry** so a dedicated server does not fall back to the parent origin — `work\changelog-1.30-exp-modding.md:39`. See `references/dayz-1-30-model-pipeline.md`.)
15. **Proxy selections use special naming** — `proxy:addon_path\proxy_model.p3d.NNN` where NNN is a 3-digit index starting at 001. The face assigned to this selection defines position and orientation of the rendered attachment.
16. **`autocenter=0` named property required on EVERY collision LOD** — not just Geometry. Without it on FireGeometry / ViewGeometry / Roadway / Hitpoints / LandContact, the engine recenters that LOD's mesh based on its bbox center, displacing the collision mesh from the visual mesh by half-height (typically 20-25cm Y). Empirical symptoms: bullets pass through, hologram raycast falls through to ground when stacking, action cursor doesn't register the object. Detection: `lod.properties.get("autocenter") != "0"` on any collision LOD. Fix: `lod.properties["autocenter"] = "0"` on all collision LODs, then write back. Vanilla reference: LFPG `gate_and.p3d` has `autocenter=0` on Geometry, ViewGeometry, FireGeometry uniformly.
17. **Every face in collision LODs needs a penetration `.rvmat`** — assign `face.material = "dz\\data\\data\\penetration\\<surface>.rvmat"` (e.g. `wood_desk` for wood, `metalplate` for metal, `plastic` for plastic). The .rvmat references a `.bisurf` that defines ballistic properties (penetration thickness, deflection, damage). Without this assignment, ObjIntersectFire raycasts hit the geometry but the engine cannot resolve a surface — bullets pass through, no footstep sound. Vanilla: `wooden_case` has every collision-LOD face assigned to `wood_desk.rvmat`. Detection: `(face.material or "").strip() == ""` on any collision LOD face is a fail.
18. **Post-export winding verification (per-component outward check) — Check A is INSUFFICIENT** — Faces left in OUTWARD order (MLOD wants the cross product INWARD, Rule 12) pass Check A unnoticed, because Check A (`cross(e1,e2)` vs declared normal) only compares the two and returns UNIFORM_NON_FLIPPED whenever they agree, broken or not. The reliable check is the **per-component outward check** (per-component-centroid outward direction; named to avoid collision with the different "Check C" in `dayz-p3d-audit`). Prerequisites (`dayz-p3d-audit` killer #1, **Detection**): every non-proxy face of the collision LOD belongs to a `ComponentNN` selection, every component is closed and convex (Rule 1), and every face has a reading (no degenerate face); short of them the check is unresolved and confirms nothing. For each non-proxy face of each component, compute `outward = face_centroid - component_centroid` and `cross_n = normalize(cross(v1-v0, v2-v0))`. On a closed, convex component this reads each face exactly: a face with `dot(outward, cross_n) > 0` is wound OUTWARD — engine raycast `ObjIntersectFire` treats it as back-facing, raycast misses — and one such face is an inverted face: read every face, not most. Expected DayZ state: `dot(outward, cross_n) < 0` (cross product INWARD), matching vanilla LFPG `gate_and` and DayZ engine's expected face orientation. Symptom triplet when that Rule 12 reversal is required but not applied: bullets pass through + hologram falls to ground when stacking + items can't be placed on top — all three simultaneously means cross-product is OUTWARD on collision LODs. Fix (killer #1, **Fix**): `face.vertices.reverse()` on each face that reads outward and only on those, `proxy:*` faces excluded (NOT Visual — this fix targets collision LODs; Visual has its own winding criterion because per-vertex declared normals govern lighting while face winding governs backface culling): a component wound outward reads outward on every face and is reversed whole, and a healthy component on the same LOD is left alone. Set the stored normals of the reversed faces as killer #1's **Fix** says, and never reverse a collision LOD to match the Visual LOD (`dayz-p3d-audit` "Absolute winding check", rule 7). Implementation: no function in this skill runs the check, and `audit_p3d.py` does not either (killer #1, **Detection**). `check_face_winding` in `references/py3d-direct-generation.md` compares vertex order with stored normals on `model.lods[0]` only, which is Check A's question, and the loop in that file's section "Face Winding Order Fix (per source path — see SKILL.md Rule 12)" reverses every face of every LOD, which is right only to undo a whole-model reversal. *(Narrowed 2026-10-02, dayz-p3d-audit killer #1: read every face, not most — on a convex, closed component (Rule 1) the per-face reading is exact and one face pointing away is an inverted face — and reverse only the faces that read outward. The Fix above, every face of every collision LOD, is right only when every face reads outward; on a LOD with one healthy and one outward component it moves the inversion onto the healthy one (measured offline 2026-10-02, synthetic boxes, py3d 1.8.0).)* *(Aligned 2026-10-02 with the note above and killer #1: this rule read “For each face in each collision LOD, compute `outward = face_centroid - component_centroid` and `cross_n = normalize(cross(v1-v0, v2-v0))`. If `dot(outward, cross_n) > 0` for most faces, the cross-product is OUTWARD — engine raycast `ObjIntersectFire` treats faces as back-facing, raycast misses.” and “Fix: `for face in lod.faces: face.vertices.reverse()` on every collision LOD (NOT Visual — this fix targets collision LODs; Visual has its own winding criterion because per-vertex declared normals govern lighting while face winding governs backface culling). Implementation: see section "Face Winding Order Fix (per source path — see SKILL.md Rule 12)" and `check_face_winding` in `references/py3d-direct-generation.md`.” The note above narrows that Fix. `check_face_winding` reads the first LOD only, and only for agreement with the stored normals: on a model whose first LOD is the Memory LOD, with an inward Visual LOD and an outward collision box, it returns no finding, and with that outward box stored first, its normals agreeing, none either (measured offline 2026-10-02, py3d 1.9.0, synthetic 2 m boxes).)*

19. **Wheel proxy `.p3d` Memory LOD MUST contain the 5 anatomical mem-points** — `ce_center`, `ce_radius`, `boundingbox_min`, `boundingbox_max`, `invview`. PhysX uses them to build the wheel collider; without them the engine defaults to the Geometry LOD of the proxy (typically a cube ~0.20³ on procedural wheels) as the collider. Result: wheel collider the size of the hub cube instead of the wheel diameter → `wheelPresent=N` but `contact=0` permanent → suspension raycast misses ground when body lifts more than ~10 cm → bounce divergente, eventually `Will delete object with !finite or outside world coords`. The 5 mem-points follow Croco-vanilla convention (Y vertical/radial, X axial/width, Z radial with INTENTIONAL min/max inversion) — copy literal and scale, do not normalize Z inversion. Full anatomy + scaling rules in `references/vehicle-structural-parity.md` §"Addendum (2026-05-29) — Wheel proxy `.p3d` Memory anatomy (T1-D)". Bake them with py3d using the 6 quirks in `dayz-animation-pipeline/references/py3d-1.0.0-quirks.md`. This invariant is silent in `dayz-p3d-audit` until 2026-05-29 (Silent Killer #11) — older audits may pass a wheel proxy with only `ce_center` as ALL PASSED. Origin: LFQuad bounce 2026-05-29; LL-057 (process — gap diferido sin gate).

20. **Wheel-slot selection must be consistent across visual LODs, FireGeometry, AND `CfgSlots.<slot>.selection`** - for PhysX to seat a wheel (`WheelCountPresent()>0`), the selection named by `config.cpp > CfgSlots > <wheel-slot>.selection` MUST exist in the body's **FireGeometry LOD** and contain the wheel proxy faces, not only in the visual LODs. A procedural pipeline that generates body+config must use ONE consistent `<name>` for all three roles (visual proxy selection, FireGeometry proxy selection, and `CfgSlots.selection`) and NEVER leave the slot selection absent from FireGeometry. If it lives only in visual LODs the slot binds as inventory but the wheel never simulates: `WheelCountPresent()=0` while `WheelCount()=N`, no traction, no spin (steering still animates), no RPT error - a silent killer. This is Rule 7 specialized to wheel slots, with the wheel-sim consequence. Detection (offline): audit check SP-017 in `dayz-p3d-audit`. Mechanism + additive py3d fix: see the `enforce-script-reference` section "Wheel attachment to simulation: CfgSlots.selection to FireGeometry proxy selection". Origin: LFQuad blocker `wheelPresent=0` (LL-030 applied to wheels; LL-044).
21. **Parts contained between LODs are recomputed per LOD** [DESIGN] — a part derived from a container's geometry (liquid surface, content layer inside a tray) must be recomputed from THAT LOD's simplified container profile, never from the full-detail one: on simplified LOD walls the full-detail-derived part pokes through. Gate: every vertex of the contained part lies inside the LOD's wall at its height, checked with a render of the LOD chain read from the p3d, plus a negative control that reproduces the leak. (SP-432, added 2026-09-26; caught on a tray's liquid and powder at LOD 2-4.)

## LOD Resolution Values Quick Reference

**DayZ-modern values** (verified against Mikero DeP3d docs and vanilla items
55galdrum, wooden_case, sea_chest, MilitaryCrate, gate_and). Old Arma 3
values (FireGeo=2e13, ViewGeo=3e13, GeoPhys=4e13) DO NOT work in DayZ —
the engine ignores LODs at those resolutions.

| LOD Type | DayZ Resolution Value | Notes |
|----------|-----------------------|-------|
| Resolution 0 (full detail) | 0.0 | Visual |
| Resolution 1 | 1.0 | Visual |
| Resolution 2 | 4.0 | Visual |
| Resolution 3 | 8.0 | Visual |
| Shadow Volume (close) | 10000.0 (1.0e4) | optional |
| Shadow Volume (far) | 11000.0 (1.1e4) | optional |
| Geometry | 1.0e13 | physics + cursor raycast |
| Memory | 1.0e15 | named points, animation axes |
| LandContact | 2.0e15 | ground placement contact points |
| Roadway | 3.0e15 | walkable surface (stack base) |
| Hitpoints | 5.0e15 | named hit zones for damage |
| ViewGeometry | 6.0e15 | action cursor + occlusion |
| FireGeometry | 7.0e15 | ballistic raycast (bullets) |

## Proven Reference Implementation: LFPG Push Button

A complete working example exists as the LFPG Push Button (worn industrial style):
- 9 LODs, 218KB .p3d file, generated entirely in Python
- 328 vertices at LOD 0 (32-segment cylinders + dome)
- Animated button (translation, 3mm travel)
- LED with 3 material states (green/red/off via hiddenSelections)
- 12 procedural textures (rust, wear, dirt, fingerprints, brushed metal)
- 7 .rvmat files including 3 emissive LED states
- Complete model.cfg, config.cpp, $PBOPREFIX$
- See `references/lfpg-recipes.md` for the full recipe

## Troubleshooting — Common Failures & Quick Fixes

| Symptom | Root Cause | Fix |
|---------|-----------|-----|
| Model spawns sideways/upside-down | Z-up → Y-up conversion not applied | Apply Rule 12's conversion, `(x,y,z)→(x,z,y)` + negate normals, faces in their original order, to FBX/Blender geometry and to a GLB/glTF brought through Blender alike; a glTF read without Blender is Y-up and takes the map "GLB/glTF imports" derives *(aligned 2026-10-03: read "Apply the source-appropriate conversion: FBX/Blender `(x,y,z)→(x,z,y)` + negate normals, faces in their original order, or the GLB-only path (Rule 12)")* |
| Textures render on inside of faces (invisible exterior) | Winding does not match the measured recipe, e.g. faces reversed after the det=-1 map, as the former GLB path did | FBX/Blender and a GLB/glTF brought through Blender: apply `(x,z,y)` + negate normals, faces in their original order (Rule 12); a glTF read without Blender: bake node transforms, reverse mirrored instances, then its own det=-1 map ("GLB/glTF imports"). Verify with `check_face_winding` / offline gates, and check chirality on an asymmetric feature *(aligned 2026-10-03: the cause read "Winding does not match the measured recipe / GLB path" and the fix opened "FBX/Blender: apply `(x,z,y)` + negate normals, faces in their original order (Rule 12). GLB-only: see "GLB/glTF imports".")* |
| Textures look like TV static | Using `random()` per pixel instead of coherent noise | Use OpenSimplex FBM with multi-layer compositing |
| Textures look flat/artificial | Single noise layer, no wear/variation | Add 5-6 layers: base + flow + grain + micro + wear + sparse details |
| Textures appear stretched/misaligned | UVs not generated or exported incorrectly | Run Smart UV Project in Blender before export, verify UV layer exists |
| Model too polygonal/angular | No modifiers applied (bevel, subsurf) | Add Bevel (width 0.002, segments 3) + SubSurf (level 2) before export |
| Collision doesn't work | Geometry LOD is non-convex | Geometry LOD components MUST be convex hulls (Rule 1) |
| Character walks through object | Geometry LOD components have no mass | Set mass ≥ 10 on all Geometry/Fire/View components (Rule 2) |
| Animation doesn't play | Axis memory points missing or misnamed | Verify 2 memory points exist in Memory LOD matching axis name |
| Attachment appears in inventory but invisible on model | Missing one of 3 proxy parts | Need: proxy face+selection in parent, proxy .p3d, CfgNonAIVehicles entry (Rule 14) |
| .p3d file corrupt or crashes Object Builder | Vertex/face data inconsistency | Always read back .p3d with py3d after writing to verify (Rule 11) |
| LOD not recognized by engine | Wrong resolution value | Check LOD resolution table — values must be exact |
| Normal map looks inverted | DirectX vs OpenGL convention | DayZ uses OpenGL convention (green channel = Y-up). Flip green channel if needed |
| Bullets pass through object | Penetration `.rvmat` not assigned to collision LOD faces | Set `face.material = "dz\\data\\data\\penetration\\wood_desk.rvmat"` (or appropriate) on every face in Geometry/FireGeometry/ViewGeometry/HitPoints/Roadway. Verify with `strings <p3d> | grep penetration` (Rule 17) |
| Hologram falls to ground when stacking on object, items can't be placed on top | `autocenter=0` named property missing on collision LODs | Add `lod.properties["autocenter"] = "0"` to ALL collision LODs (FireGeo, ViewGeo, Roadway, Hitpoints, LandContact), not just Geometry. Engine recenters LODs by bbox without it, displacing collision mesh ~20-25cm from visual (Rule 16) |
| Cross-product UNIFORM_NON_FLIPPED (Check A) | Nothing by itself. Check A compares the cross product with the stored normals and cannot tell inward from outward (Rule 18): a correct Rule 12 export reads UNIFORM_NON_FLIPPED (both inward), and so does an inside-out one with both outward — e.g. py3d `P3D.transform()` with the det<0 map, which reversed every face, with the flip back skipped | Decide with a direction check: dayz-p3d-audit "Absolute winding check" (signed volume by winding, negative = the production sign) or the per-component outward check (Rule 18). Only when it reports the cross product on the wrong side: reverse those faces AND negate their normals (both away from the side meant to be seen: killer #1, Rule 12), then check again. On a collision component the wrong side is OUTWARD, and only the faces Rule 18 reads outward are reversed, never the whole LOD (killer #1). On a Visual LOD, first turn back the faces wound against their neighbours (Check B: `dayz-p3d-audit` `references/winding-diagnostics.md`, "From Check B to fix"; py3d `WARN_WINDING_EDGE_INCOHERENT`), then read the signed volume of each closed shell, never the LOD-wide sum: its wrong side is the side meant to be seen, so a negative signed volume is right for a solid seen from outside and a positive one for a room seen from inside ("Absolute winding check" rule 4); an open or double-sided Visual part is not resolved by this sign. Every face of every LOD only to undo a whole-model reversal (the skipped flip-back of the Root Cause column). Never reverse on the Check A label: on a correct export that yields UNIFORM_FLIPPED and an inside-out model. *(Corrected 2026-10-02: this row used to reverse every face whenever Check A read UNIFORM_NON_FLIPPED.)* *(Aligned 2026-10-02 with Rule 18 and "Absolute winding check" rule 4: this cell read “Only when it reports the cross product OUTWARD: reverse every face AND negate the normals (Rule 12: both inward), then check again.” Read literally, that sentence reversed a closed Visual room seen from inside (signed volume +8 to -8), kept 100 % agreement and left py3d with no winding finding (measured offline 2026-10-03, py3d 1.9.0, synthetic 2 m box). The Check B step and the reading per shell follow the order py3d's winding findings give for a Visual LOD (`tools/py3d/py3d/__init__.py`, `_winding_steps`): a face turned with its normal inside a closed solid left that solid's signed volume negative (-8 to -5.33) at 100 % agreement, and only py3d's `WARN_WINDING_EDGE_INCOHERENT` saw it (measured the same way).)* |
| Bullets pass through + hologram falls to ground when stacking + items can't be placed on top — all three simultaneously | Rule 18: cross-product OUTWARD on collision LODs (the faces are not in the MLOD inward order of Rule 12). Check A is insufficient to detect this — must use the per-component outward check (per-component-centroid). | Run Rule 18's per-component check on Geometry/ViewGeometry/FireGeometry and reverse only the non-proxy faces it reads outward, their stored normals set as killer #1's **Fix** says; never a whole LOD to match the Visual LOD. Rule 18 scores closed, convex components only: for a Roadway LOD's walkable faces use dayz-p3d-audit "Absolute winding check" rule 4 instead (production reads `cross_Y < 0` from above). Visual LOD untouched. Verify the check then reads every face INWARD in every component (matches vanilla LFPG gate_and). *(Aligned 2026-10-02 with Rule 18: this cell read “Reverse vertex order on Geometry/Roadway/ViewGeometry/FireGeometry (see "Face Winding Order Fix" in `references/py3d-direct-generation.md`). Visual LOD untouched. Verify the per-component outward check reports cross-product INWARD per component (matches vanilla LFPG gate_and).”)* |
| Damage shows only as color tint, not visible cracks/wear | `_damage.rvmat` Stage3 uses procedural `color()` instead of vanilla overlay texture | Replace Stage3 texture with `dz\\weapons\\data\\weapons_damage_<wood\|metal>_mc.paa` (and `weapons_destruct_<wood\|metal>_mc.paa` for `_destruct.rvmat`). UV transform: `aside={0,4,0}, up={4,0,0}` for 4x tiling. Vanilla pattern: `dz/gear/camping/data/wooden_case_damage.rvmat`. |
| Vehicle launches/bounces at spawn, then deleted (`!finite or outside world coords`) | Geometry built as ONE monolithic component / mass concentrated → low inertia | Rebuild Geometry as many closed convex components + mass on ALL + per-component `autocenter=0` (`vehicle-structural-parity.md` Addendum 2026-05-30; `dayz-p3d-audit` #13) |
| Vehicle doesn't roll / sinks (steers but no roll, `contact=0`) | Wheel hub inside chassis hull, or wheel-proxy Memory anatomy incomplete | Wheel-wells in Geometry so hubs sit OUTSIDE the hull; wheel proxy `.p3d` needs its 5 Memory mem-points (`vehicle-structural-parity.md` Addenda 2026-05-26/29; `dayz-p3d-audit` #11/#12) |
| Vehicle floats / rides too low / belly scrapes | Wheel vertical placement (tire bottom below origin, chassis floor below wheel-center) | Raise wheels AND chassis floor to the ride-height triple together (`dayz-p3d-audit` #12; parity Addendum 2026-05-29) |
| Engine won't start / drowns at spawn | Missing `CarBattery`/`SparkPlug` attachments, or missing `drown_engine` memory point | `vehicle-config-and-modelcfg.md` §15 (won't-start) + in-game Diag menu (Win+Alt → Game→Vehicles→Simulation) |
| `unknown animation source damper` / wheels don't spin or steer | model.cfg ↔ config wiring broken (AnimationSources, animRotation/animTurn/animDamper sources) | `vehicle-config-and-modelcfg.md` §10–13 (the wiring chain) |

## Winding on axis-change + collision LODs + flat-color (added 2026-05-23)

### Rule 13 nuance (history): the det=+1 recipe was a mirror
The Blender/FBX recipe changed three times. 2026-07-06: `(x,z,-y)` (det=+1), "a proper rotation, never reverse" — inside-out in game. 2026-09-26 (SP-432, ticket `fb-20260921-164243-abef`): the same map plus reversed faces and negated normals — validated on five working LFPG models with offline gates only (collision cross INWARD 100%, visual cross opposite to the mapped source normal 100%), and it is a MIRROR: those models are symmetric, so nobody saw it. 2026-09-22 (LL-504) and 2026-10-01 (in-game test of one chiral model): the map is the det=-1 reflection `(x,z,y)`, faces keep their order, normals are negated — Rule 12. Each step fixed the symptom the previous one left; only an asymmetric model shows the last one.

### LL-504 correction (added 2026-09-22, LFDucati T89): a det=+1 map ships a MIRRORED model — the Blender→DayZ map needs det=-1

[EXACT][CLAIM-BLENDER-DAYZ-DET-NEG1] Measured in game (DayZ 1.30.164014 Exp): an exporter that mapped Blender→DayZ with the det=+1 rotation `X = -y` produced a MIRRORED model. Inverting faces and negating normals fixed the inside-out look, but in-game text and logos then read backwards ("92", "ITAJUD"): the shape itself was mirrored, and the mirror was also the real cause of the reversed faces. DayZ is left-handed (X east, Y up, Z north; vehicles face -Z) and Blender right-handed, so conserving shape requires a REFLECTION in the numbers: `X = +y` (det=-1). MLOD convention measured against binarized vanilla: the vertex-order cross points INWARD and the shading normals also point inward; with det=-1 the face order comes out correct on its own, normals get negated, and geometry authored directly in DayZ space (collision boxes) is inverted by the same reflection. Gate: compare your binarized ODOL against the vanilla one with the same reader (tyre tread band, collision boxes) and require readable in-game text. This corrects LL-020 in the opposite sense. Confirmed on 2026-10-01 on DayZDiag 1.29.163709 with one chiral model exported three ways (Rule 12), which now uses this map. Exports made with the old det=+1 recipe (LFPG, LFQuad) are mirrored: harmless on symmetric parts, wrong on anything with text, logos or one-sided geometry.

### Collision LOD winding: every face inward per component, never matched to the Visual LOD (SP-003)
When you GENERATE/EDIT a collision LOD (Geometry/Fire/View), run `audit_p3d.py` and Rule 18's
per-component check BEFORE deploying. Rule 18 decides: with its prerequisites met (every
non-proxy face in a `ComponentNN` selection; every component closed and convex; no degenerate
face), every non-proxy face of every component must read INWARD (Rule 12); otherwise the check
is unresolved and confirms nothing. `audit_p3d.py` does not run Rule 18: through py3d
`P3D.validate()` it compares each collision LOD with the Visual LOD and raises
`ERR_WINDING_INVERTED` when both are uniform and opposite: a trigger for Rule 18, not a
verdict, since it also fires on healthy collision LODs under an inside-out Visual LOD or one
meant to be seen from inside (a room). Reverse only the faces that read outward, and set their
stored normals as killer #1's **Fix** says; never reverse a collision LOD to match the Visual
LOD (`dayz-p3d-audit` killer #1, "Absolute winding check" rule 7).
Surface this proactively when the symptom is "no action /
bullets pass". A walk-through does not point at the winding: in game, 2 m boxes whose
collision LODs were wound outward still stopped the player, the same box with no `ComponentNN`
selection let them through, and a 0.49 m `item_small` kit let them through with either winding
(`dayz-p3d-audit` killers #1 and #2, "Absolute winding check" rule 6). *(Corrected 2026-10-02,
measured in game: the symptom list read "walks through / no action / bullets pass".)*
*(Aligned 2026-10-02 with `dayz-p3d-audit` killer #1 and Rule 18: this section was titled
"Collision LOD winding must match the Visual LOD (SP-003)" and opened: "When you GENERATE/EDIT
a collision LOD (Geometry/Fire/View), compare its winding against the Visual LOD of the same
model (centroid method) BEFORE deploying — they must agree in sign (~100% INWARD in DayZ
left-handed). `audit_p3d.py` does NOT validate this (centroid check disabled for false
positives)." That comparison is the relative check `audit_p3d.py` runs, and matching its
sign is the fix killer #1 forbids.)*

### Flat-color models: per-material .rvmat, NOT a UV-atlas bake (LL-021)
For monochrome / flat-color-per-piece models, use one .rvmat per material with `diffuse[]` =
base color and `texture=""` (zero UV, zero gaps). A UV-atlas bake of many flat materials yields
black holes/smudges. Atlas baking is for models with real texture detail.

### GLB/glTF imports — LL-020 refined (added 2026-06-11; separated from FBX/Blender recipe 2026-09-26; aligned with Rule 12 2026-10-03)

A GLB/glTF exported to spec and brought through Blender takes **Rule 12's recipe**: points and
normals through `(x,y,z)->(x,z,y)` (det=-1), every face in its vertex order (proxy triangles
included), and every normal negated — `py3d.blender_to_dayz(model)`. Do **not** reverse the faces.
A glTF read without Blender needs one more step first (last paragraph). Not checked on this path:
a mirrored node (negative determinant, below) arrives in Blender as an object with negative scale;
read that part's winding in the exported MLOD (signed volume per closed shell, `dayz-characters`
`references/check_dayz_winding.py`) before trusting it.

**glTF front faces are CCW by spec**, as Blender's are (a node whose global transform has a
negative determinant, a mirrored instance, winds them clockwise: glTF 2.0 §3.7.4
"Instantiation"), and Blender's glTF importer turns Y-up into Z-up with a rotation,
`(x,y,z)->(x,-z,y)` (det=+1; Blender 5.1
`io_scene_gltf2/blender/imp/blender_gltf.py:70-72`), which keeps that order: a GLB brought through
Blender is Blender geometry. The det=-1 map turns its vertex-order cross product INWARD, the MLOD
convention; reversing the faces after it turns the cross product OUTWARD, the case that renders
inside-out (`references/py3d-direct-generation.md` §4 "Face Winding Verification"). [EXACT]
Measured in game on two GLB/glTF-origin models:

- A6_MK47 v12c (2026-06-11, binarized): pure swap, normals `−S·n` and `REVERSE_WINDING=False`
  rendered with correct textures (`dayz-weapons` `references/import-rule-v4.md`);
- MercedesAMGLF Phase 2 (2026-06-24): with its faces reversed after its det=-1 map it rendered
  see-through, and with the glTF order kept it rendered solid (`dayz-vehicles`
  `references/vehicle-structural-parity.md`, "glTF→DayZ winding: the offline check can be a
  tautology").

A GLB-sourced mesh exported with the det=+1 rotation `(x,z,-y)` can ship with BOTH defects at
once (reproduced historically on A6_MK47 cycle 1, 2026-06-11):

- textures render on the INNER faces (CCW winding kept), and
- the weapon is MIRRORED laterally (the -y flip puts the weapon's right side at -Z; vanilla
  weapons carry it at **+Z** — verified on the BI sample candygun MLOD: `nabojnicestart` +Z).

Both come from the det=+1 map (LL-504), and Rule 12's det=-1 map removes both. Mirror-detection:
compare one canonical LATERAL point (e.g. `nabojnicestart`) against a vanilla reference — bbox
and audits cannot see a mirror. Weapon proxy-triangle conventions (large 2.0/1.0 legs, per-slot
leg directions, `proxy:\DZ\...001` naming) documented from the Destra3000 sample in
`A6_MK47_dev\HANDOFF.md` §REGLA DE IMPORTS.

The swap expects Z-up input; a glTF read without Blender is Y-up. Derive its det=-1 map from the
asset's own frame (re-derived per asset, as `import-rule-v4.md` says for weapons). Bake each
node's global transform into its mesh first, and reverse the faces of every instance whose global
transform has a negative determinant: glTF winds those clockwise, so after baking they must be
reversed to run counter-clockwise from the visible side again. Then keep that order through the
det=-1 map: any det=-1 map turns a CCW front face's cross product inward (not measured in game
for a direct read). A ripped game asset in a `.glb` can carry its game's frame rather than the
spec's (the measured rip profile keeps the raw winding with a det=+1 map): `dayz-vehicles` decides
it from the measured transform and source lineage.

*(Aligned 2026-10-03 with Rule 12. The section opened "This section is **GLB/glTF-only**. It
does **not** override Rule 12's measured FBX/Blender recipe."; its pipeline read "**GLB-only
pipeline (when the lateral-mirror / CCW issues apply):** pure swap `(x,y,z)->(x,z,y)`
(det=-1) + reverse the vertex order of EVERY face in every LOD — except proxy triangles, whose
vertex order encodes the attachment frame (P0/P1/P2) and must NOT be reversed."; and it closed
"Do **not** treat «any GLB/Blender import» as one shared rule with the FBX/Blender recipe in
Rule 12 — Blender-authored FBX follows the det=-1 map with negated normals; GLB keeps its own
path above (not re-tested against the 2026-10-01 in-game test)." The reversal dates from
A6_MK47's v6-v7 stage — the Pack's v1.3.0 text closed "Status: root cause verified against
spec+vanilla; A6_MK47 v7 in-game re-test pending as final confirmation." — when that project's
builds shipped a stray raw MLOD at the PBO root, the file the engine loaded (`dayz-weapons`
`references/import-rule-v4.md`, W-BUILD1); its first build that loaded the binarized model,
v12c, kept the vertex order.)*

### Decimate COLLAPSE corner normals (docs note)
Decimate COLLAPSE can zero or invert `corner_normals`, surfacing as `WARN_WINDING_NORMAL_MISMATCH`.
When a corner normal is ≈0 or contrary to the face, fall back to the geometric triangle normal.
Docs mention only — no heater script change in this hop.

---

## Mass assembly: `point.mass = None`, NEVER `point.mass = 0.0` (added 2026-06-02)

**Origen**: LFQuad N1.5 cerrado 2026-06-02 (handoff `30_Sessions/2026-06-02-LFQuad-placement-fix-firegeo-mass-CLOSED.md`).

### Regla

When assembling **non-Geometry LODs** (FireGeo 7e15, ViewGeo 6e15, LandContact 2e15, Memory 1e15, Shadow, Visual <1000) via py3d, explicitly use `point.mass = None` for each point. **NEVER** use `point.mass = 0.0`.

Reason: py3d emits `#Mass#` tagg if **ANY** `point.mass` of LOD is ≠ None, **even if exactly `0.0`**. Result: `.p3d` carries a spurious `#Mass#` in non-Geometry LOD. AddonBuilder/binarize bakes the mass of THAT LOD (sum = 0) → deployed ODOL with `CoM=(0,0,0)` and inertia 0 → `ECE_PLACE_ON_SURFACE` positions vehicle at height of CoM = 0 → spawn underground → ejection via PhysX-depenetration.

### Antipattern (producing bug LFQuad N1.5)

```python
# WRONG — produces #Mass# tagg in FireGeo LOD with all zeros
for face in firegeo_lod.faces:
    for v in face.vertices:
        v.point.mass = 0.0      # ← py3d emits #Mass#, binarize uses it, CoM=0
```

### Pattern correcto

```python
# RIGHT — no #Mass# tagg emitted; binarize uses Geometry LOD's #Mass#
for face in firegeo_lod.faces:
    for v in face.vertices:
        v.point.mass = None     # ← py3d skips #Mass# emission for this LOD
```

### Post-assemble verification

Iterate all LODs and ensure that **only the Geometry LOD (res 1e13)** has `lod.mass != None` and `#Mass#` tagg:

```python
for lod in model.lods:
    has_mass = any(p.mass is not None for p in (lod.points if hasattr(lod, "points") else []))
    if lod.resolution == 1e13:
        assert has_mass, "Geometry LOD must have #Mass#"
    else:
        assert not has_mass, f"LOD res={lod.resolution:.0e} must NOT have #Mass#"
```

End-to-end verification (post-binarize): `binarize.exe -always -addon=<dir> <src> <dst> <wildcard>` + read `ModelInfo CoM` of ODOL → must be ≠ (0,0,0) and match CoM of Geometry MLOD.

### Cross-ref

LL-079 (LOD bisection isolated the bug), LL-080 (the durable lesson), check added to `dayz-p3d-audit` (Mass-only-Geometry, added 2026-06-02), reference tool `LFQuad_dev/tools/fix_firegeo_mass.py` (headless patch for .p3d with bug).

<!-- [merged 2026-06-05 from <claude-home>\skills user copy during plugin-canonical migration; almacen: .claude\skills] -->
## Llama Mod Extraction Patterns (rvmat)

Patterns extracted from LM_Planes mod (workshop 3730564764, Llama+Itspete-Here). Aviation-applied but generally reusable.

### Toggle rvmat pattern (lights on/off without duplicating material)

For dashboards, control panels, displays that have an "on" and "off" state, create **two near-identical rvmats** with minimal diff:

```
Controls.rvmat (lights off)           Controls_on.rvmat (lights on)
ambient[]      = {0.9999992,...}     ambient[]      = {0.9999992,...}    SAME
diffuse[]      = {0.9999992,...}     diffuse[]      = {0.9999992,...}    SAME
forcedDiffuse[] = {0,0,0,1}      →   forcedDiffuse[] = {0.1,0.1,0.1,1}   DIFFERS (slight emissive boost)
specular[]     = {0.882,...}         specular[]     = {0.882,...}         SAME
specularPower  = 55                  specularPower  = 55                  SAME

Stage5 SMDI:
  texture = "...controls_SMDI.paa"   texture = "...controls_on_SMDI.paa"  DIFFERS

Everything else identical.
```

**Pattern**: only `forcedDiffuse` (controls overall brightness) + Stage5 SMDI texture (specular/material mask with glowing elements) differ. Toggle via script using vehicle-level `dashboardMatOn`/`dashboardMatOff` properties in CfgVehicles:

```cpp
class LM_Catalina: CarScript {
    dashboardMatOn  = "LM_Planes\LM_Tigermoth\data\Controls_on.rvmat";
    dashboardMatOff = "LM_Planes\LM_Tigermoth\data\Controls.rvmat";
    ...
};
```

(Plus: cross-aircraft material sharing — Catalina references Tigermoth's rvmats directly. DRY across related entities.)

### Glass rvmat distinctives

Transparency requires specific flags + values:

```cpp
ambient[]      = {0.55,0.55,0.55,1};
diffuse[]      = {0.45,0.45,0.45,1};
forcedDiffuse[] = {0,0,0,1};
emmisive[]     = {0,0,0,1};
specular[]     = {0.25,0.25,0.25,1};
specularPower  = 1320;                      // HIGH — glassy reflection
renderFlags[]  = {"noZwrite"};              // MANDATORY for transparency
PixelShaderID  = "Super";
VertexShaderID = "Super";

class Stage1 { texture = "#(argb,8,8,3)color(0.5,0.5,1,1,NOHQ)"; ... };  // procedural normal
class Stage4 { texture = "#(argb,8,8,3)color(1,1,1,1,AS)"; ... };          // procedural AS
class Stage5 { texture = "#(argb,8,8,3)color(1,1,1,1,SMDI)"; ... };        // procedural SMDI
class Stage6 { texture = "#(ai,64,64,1)fresnel(1,0)"; uvSource = "tex1"; ... };  // softer fresnel for glass
class Stage7 { texture = "dz\data\data\env_land_chrome_co.paa"; ... };     // CHROME env map, not env_land
```

Key points:
- `renderFlags[] = {"noZwrite"}` — without this, glass fails to render transparent or causes z-fighting
- `specularPower = 1320` — much higher than opaque materials (typical 50-200)
- Glass usually omits real textures for Stage4/5 (uses procedural `color(...)` placeholders)
- Stage7 uses `env_land_chrome_co.paa` for reflective chrome look, not the regular `env_land_co.paa`
- Stage6 fresnel `(1,0)` softer than standard `(1.1,0.3)` — glass has subtle edge fall-off

### Damage variant rvmat pattern

For materials that should look damaged/scratched/burnt at higher damage levels, create a damage variant with same texture base + overlay:

```cpp
// engine.rvmat (normal)                    engine_scratch_damagex1.rvmat (damaged)
ambient[]    = {0.9999,...};                 ambient[]    = {0.634,...};         REDUCED
diffuse[]    = {0.9999,...};                 diffuse[]    = {0.634,...};         REDUCED
specular[]   = {0.882,...};                  specular[]   = {0.392,...};         REDUCED
specularPower = 55;                          specularPower = 85;                  HIGHER (worn = sharper specular)

// Stage1-2: same base textures
// Stage3 (Macro Color) is the KEY:
Stage3 normal:  "#(argb,8,8,3)color(0,0,0,0,mc)"
Stage3 damaged: "<mod>\<aircraft>\data\damage_layer_mc.paa"   // CUSTOM damage overlay

// Stage2 detail (worn material reuses vanilla DZ texture):
Stage2 damaged: "DZ\data\data\detail_maps\plastic1_512_dt.paa"   // Reuse vanilla
```

**Pattern**: damage variants share base texture set, swap Stage3 (Macro Color) for a damage overlay texture, reduce color values to simulate burn/wear. The damage overlay is a single `damage_layer_mc.paa` per aircraft that overlays scratches/blackening on top of the base diffuse.

Tied to `DamageSystem` zones via `healthLevels[]`:

```cpp
healthLevels[] = {
    {1.0, {}},                                     // pristine: base rvmat
    {0.7, {}},
    {0.5, {}},
    {0.3, {"<path>/engine_scratch.rvmat"}},        // damaged level 1
    {0.0, {"<path>/engine_scratch_damagex1.rvmat"}} // damaged level 2 / destroyed
};
```

### 6-stage rvmat variant (omit env stage)

Standard rvmat has 7 stages (NOHQ, DT, MC, AS, SMDI, Fresnel, env). For cosmetic materials that don't need world environment reflection, omit Stage7 — fits in 6 stages + StageTI:

```
Stage1=NOHQ, Stage2=DT, Stage3=MC, Stage4=SMDI, Stage5=Fresnel, Stage6=env (combined), StageTI=thermal
```

The Pickle.rvmat (Patty_Wagon car body) uses this. Slightly cheaper, valid for simple opaque materials. **Don't use for aircraft canopies, mirrors, or anything reflective** — those need the full env Stage7.

### HDR emissive trick (super-bright lights)

For materials that simulate fluorescent lamps / bright LEDs glowing visibly even in daylight, use values **>1.0** across all stages — engine accepts HDR-range values:

```cpp
ambient[]       = {10,10,10,10};
diffuse[]       = {10,10,10,10};
forcedDiffuse[] = {10,10,10,10};
emmisive[]      = {10,10,10,10};
specular[]      = {10,10,10,10};
specularPower   = 100;
// Plus Stage5 SMDI uvTransform aside/up = {5,0,0}/{0,5,0} (texture scale 5x for finer detail)
```

This produces a glowing material WITHOUT needing PointLight engine objects — purely material-based. Used in `houselightmat.rvmat` (Llama Hangar1 fluorescent lamps). Tradeoff: no actual illumination on nearby surfaces (no dynamic light cast), just the material itself appears very bright. Combine with a real `CarLightBase` / `PointLightBase` if you need surfaces lit too.

For LED screens that need both visible-from-far glow + dynamic light, use this trick + a `PointLightBase` script-controlled to match.

<!-- llama-mod-extraction: findings f_055, f_056, f_057, f_059, f_082 | pbo: LM_Planes | pass: 1 | date: 2026-05-23 | source: workshop 3730564764 LM_Tigermoth/data + LM_Catalina/data + LM_Patty_Wagon/data + LM_Z37_Bumblebee/data + LM_Plane_Assets/LM_Hangar1/data -->

## In-hands frame gate + build discriminator for runtime-posed assets (added 2026-06-12)

Origin: A6_SR2M E5 cycles 1-2 FAIL (LL-146) — every offline gate passed and the weapon
shipped floating/orbiting with idle stance, camera inside the model and an invisible
attached magazine; the corrective build was also delivered blind ("lo veo igual").

1. **In-hands frame gate (BEFORE the first in-game deploy of any weapon/posed asset)**:
   load the debinarized DONOR (the weapon whose anim/pose family the asset inherits —
   vanilla mp5k/akm or the pack gun) into the alignment viewer TOGETHER with the body
   p3d and verify the USE FRAME: bore height (muzzle memory y), eye point, grip-at-origin
   and envelope vs the donor — numerically AND visually. Attachment-seat checks do NOT
   cover this: they validate items against YOUR weapon, never your weapon against the pose.
2. **States anm <-> skeleton pairing (evidence 2026-06-12)**: `w_mp5k_states.anm`
   requires the FULL MP5K skeleton (charging, bullet, mag_release, hide_barrel, firemode
   + shell_parent, axis_rotation_a/b in memory — vanilla mp5k.p3d and the A6 MP5k both
   carry them). Simple 3-bone skeletons (magazine/bolt/trigger) pair with AK-family
   states (`w_AKS74U_states.anm` / akm family) and default rifle reload commands — proven
   in-game by A6_PP19 (PP19 config:55, PP19.c without command remap) and A6_MK47. Match
   the states anm to the skeleton actually built, or build the donor's full skeleton.
3. **Generated-config review must diff for EXTRA fields** vs the vanilla/donor reference,
   not only verify the plan's fields are present: a smuggled weapon-level
   `aimSpeedModifier[]={0.18,...}` caused aim-drag ("weapon orbits the screen") and BOTH
   independent R21 reviewers missed it because their prompts only asked about plan fields.
4. **Every corrective build declares its observable DISCRIMINATOR before the test**
   ("if this build loads, X must visibly change" — an OnDebugSpawn, a print, a value).
   "Looks the same" without a discriminator cannot distinguish wrong-hypothesis from
   stale-build (then triage build identity first: LL-143/LL-145).

## Rules promoted from lessons corpus (added 2026-07-27)

Promoted from `AI/20_Knowledge/lessons-learned.md` to arrive via trigger instead
of depending on someone remembering to look them up. Each rule cites source `LL-NNN`;
complete entry lives there. Do not remove citation: the index detects promotion by it.

- **LL-024** — For curved surface LODs, use Decimate COLLAPSE and protect critical features with an inverted vertex group. Measure tris and feature survival; do not use planar dissolve as reducer without verifying count after triangulating.
- **LL-134** — On attachments with contractual bbox, measure full group in export frame after each feature. If bbox is closed, create relief by stepping down/recessing surrounding area instead of adding proud geometry.
- **LL-372** — Provenance Phase 0 also covers licensing: a packaged README/LICENSE dates from packaging, not today. Date publisher's current announcement; if artifact interior and current source disagree, current source rules.


## Sibling-model frame gate (SP-221, added 2026-08-31)

Before reusing a rotation offset across related models, run the sibling-frame check in
`dayz-p3d-audit`. Compare Memory LOD points and use the thin Geometry LOD axis as the
mounting discriminator. A visual bbox or a shared family name does not establish frame
parity. Treat any axis permutation as a different authoring frame.

## FBX import preflight for Blender 5.x (SP-068, added 2026-08-31)

Run these checks before Path A geometry work:

1. A 3ds Max FBX with custom split normals can crash Blender 5.1.1 in
   `mesh_set_custom_normals`. Import with `use_custom_normals=False`, then recalculate
   normals from the intended smoothing. This is acceptable for a technical pipeline in
   which py3d derives the final data.
2. Blender 5.1 removed `apply_scale_options` and `apply_unit_scale` from the FBX importer.
   `global_scale` and `bake_space_transform` do not repair per-node RSrs scale inheritance.
   Census object families with world-space bboxes (`matrix_world @ vertex`) against the
   root-frame reconstruction, and rebuild only families whose measured anchors validate
   the transform. When a native `.blend` for the same asset exists, prefer its large parts
   and reconstruct only the FBX-only moving families.
3. Import into a factory-clean scene. The same FBX can produce different local axes after
   `open_mainfile()`. Combine native `.blend` data with `bpy.data.libraries.load(..., link=False)`
   instead of importing into an already loaded scene.
4. Before every per-object OBJ export, set `hide_viewport=False` and call
   `hide_set(False)`. With selected-object export, Blender can return `{'FINISHED'}` while
   silently skipping a hidden object. Assert that every expected output file exists after
   its export.

## Editing imported vanilla meshes: rocks (SP-451, added 2026-10-01)

Measured offline in Blender 5.1 headless while reshaping a vanilla rock for a mod (SecretRock R6/R7):

- **[OFFLINE MEASURED] A vanilla rock's visual LOD is a double shell (~0.4 m).** When restoring surface from it,
  keep only the upward-facing faces that are the first hit from above; the inner shell comes back if you do
  not filter. Deleting or replacing with a first-hit filter opens holes on overhanging flanks: leave the flanks
  alone. Cheap check: a fan of horizontal rays cast from inside at several heights — a ray that escapes is a
  hole.
- **[OFFLINE MEASURED] An imported rock mesh is an unwelded triangle soup with flipped faces and cracks.**
  Offsetting the surface along vertex normals (to build an inner face) pushed ~2 % of it outside, and
  ray-parity inside tests are unreliable with cracks. To test "is inside the rock", rasterize the outer
  contour per z slice (flood fill from outside + Euclidean distance transform).
- **[OFFLINE MEASURED] Building an inner face from a cracked rock: use a height field.** Each column rises until
  it sits at a minimum distance from the nearest surface (`BVHTree.find_nearest`), capped by the top seen from
  above. Filling 2D slices from outside leaked above ~14.9 m on R6, and displacing the mesh pushed vertices out.

## Generic DCC visual-winding profile (SP-071, added 2026-08-31)

Blender/FBX-authored geometry follows Rule 12 instead (det=-1 map, face order kept, normals negated): a
winding-preserving det=+1 transform mirrors the model. A GLB/glTF brought through Blender follows Rule 12
too, and one read directly follows section "GLB/glTF imports". An OBJ is in the frame its exporter wrote.
Exported from Blender with Forward Y / Up Z it keeps Blender's own coordinates
(`references/blender-headless.md`) and follows Rule 12 as it stands; in any other frame of a right-handed
source with counter-clockwise front faces, derive a det=-1 map from that frame as for a glTF read directly,
keep the face order, negate the normals, and check chirality on an asymmetric feature. [OFFLINE MEASURED
2026-10-03] Blender's default axes write `(x, z, -y)`; from them the map is `(x,y,z)->(x,y,-z)`, which on
the model of Rule 12's in-game test writes the same bytes as Rule 12's export. Another exporter's frame
was not measured in game. Reverse the vertices of every **visual** MLOD face only for a source whose own
lineage was measured to need it: an in-game A/B against the all-visual-faces-flipped variant plus a
chirality check, as `ai-3d-to-dayz` SP-071 says. Every lineage the Pack records with that reversal under a
det=+1 map started from Blender coordinates, where any det=+1 map mirrors the model: SP-071's one
calibration, the LFHeli OH-1 (2026-07-19: an artist's Blender model exported as OBJ, mapped with
`x'=x, y'=z, z'=-y` and judged by which side rendered), LFInfectedBig (read mirrored in game:
`dayz-characters` `references/character-rigging.md` §6) and the SP-432 recipe ("Rule 13 nuance
(history)"). *(Aligned 2026-10-03 with Rule 12, measured in game 2026-10-01: from "An OBJ is in the
frame" to here, this paragraph read "For other raw OBJ imports whose measured axis transform preserves
the source winding, reverse the vertices of every **visual** MLOD face before binarization, and check
chirality on an asymmetric feature.")* *(Aligned 2026-10-03: this sentence read "For other raw OBJ or
glTF imports whose measured axis transform preserves the source winding".)*
Never include proxy triangles: their winding encodes the proxy frame. This is the generic
DCC profile only. Imported vehicles continue to use the domain profile in Path A, where
preserve-versus-flip is decided from the measured transform and source lineage.

A pre-binarize census of `dot(cross(v1-v0, v2-v0), mean_stored_normal)` per visual LOD, excluding
`abs(dot) < 1e-9`, reads whether the stored normals agree with the winding, not which side renders. Its
thresholds, at least 95% negative `SOLID` and positive-dominant `INVERTED`, come from SP-071's one
calibration, the OH-1 pipeline, which stored the normals outward against an inward winding; a Rule 12
export stores both inward. [OFFLINE MEASURED 2026-10-03] On the MLODs of the Rule 12 in-game test, rebuilt
byte for byte by `tools/py3d/tests/test_s7_blender_to_dayz.py`, the export that rendered solid and read
correctly reads 0% negative, `INVERTED` by those thresholds; the same export with every face reversed
(winding outward: inside-out) and SP-071's own recipe, the det=+1 map with reversed faces and outward
normals (a mirror), both read 100% negative, `SOLID`. Read a Rule 12 export with `dayz-p3d-audit`
instead: a healthy single-sided MLOD reads about 100% agreement ("Absolute winding check", rule 1), and
the direction comes from the signed volume by winding of each closed shell, even at 100% agreement, never
from the sum over the LOD: one shell reversed with its normals negated keeps 100% agreement and a negative
sum (Check A in its `references/winding-diagnostics.md`). Open sheets and double-sided parts take the
visibility battery of "From Check B to fix"; isolate a mixed result per part before any bulk fix
("Absolute winding check", rule 2). Do not apply that threshold
to ODOL, to MLOD produced by conversion from ODOL, or as a ratio comparison against a
vanilla model: stored-normal conventions differ by origin. The final gate remains an
in-game A/B of the candidate and an all-visual-faces-flipped variant. That A/B cannot see a mirror, since
both variants share the map: check chirality too. *(Aligned 2026-10-03 with Rule 12 and the census
measured offline: this paragraph opened "Use a pre-binarize census of
`dot(cross(v1-v0, v2-v0), mean_stored_normal)` per visual LOD, excluding `abs(dot) < 1e-9`, as a cheap
predictor. In the calibrated generic-DCC profile, at least 95% negative is `SOLID`, positive-dominant is
`INVERTED`, and an intermediate result requires a per-piece mixed-winding check.")*

## Spawn remains an engine gate (SP-216, added 2026-08-31)

A clean py3d reload, digest, index check, winding check, or Geometry LOD comparison does
not prove that a generated `.p3d` will spawn. Run `binarize` first to classify capacity and
other conversion failures, then require an in-game spawn before calling the model ready.
If conversion passes but spawn still fails, use the batched LOD-variant method documented
in `dayz-p3d-audit`; file patching does not reload binary `.p3d` files.

## Vehicle proxy constructor preflight (SP-120, added 2026-08-31)

`LOD.add_proxy(path, index, origin, rotation=None, scale=0.001)` defaults describe a tiny,
upright worn-item proxy. They are not a verified vehicle proxy convention. For a scripted
vehicle assembly, always pass `rotation` and `scale` explicitly, then measure the emitted
triangle with `py3d.derive_proxy_frame()` against a known-good control. Three measured
vehicle controls used frame rows `((-1,0,0),(0,0,1),(0,1,0))` and metre-scale triangles;
the default identity frame differs by 180 degrees of yaw and its triangle is roughly
1000 times smaller. This finding is offline-tested only: it does not replace the in-game
seat-orientation gate.

## Budget ratios use the geometry that ships (SP-134, added 2026-08-31)

Do not extrapolate resolved-vertex or normal-triplet budgets from a decimated mesh to a
different source mesh. Edge collapse breaks normal sharing and can inflate triplets per
triangle. Measure the ratio on the exact geometry that will ship. When the source asset
contains an authored LOD ladder, prefer and measure that ladder instead of planning splits
from a decimated proxy; this also preserves the rule that visual decimation is user-gated.

## Cookware models: origin at the centre of the underside (SP-430, added 2026-09-26)

[EXACT] Put a custom cookware's origin at the centre of the underside, Y up, like vanilla `fryingpan.p3d` and `cookingpot.p3d` (their bbox starts at Y 0.000). Vanilla heat-source anchor heights, measured offline on the vanilla models after ODOL->MLOD conversion (DayZ 1.30.164014 Exp): gas stove `cookingpot.001` at 0.084 m; fireplace `direct_cooking_a/b/c` at 0.624 m (over the stone oven) and tripod pot at 0.607 m; FireplaceIndoor 0.343 m; OvenIndoor 0.844 m.

## Selection-safe py3d surgery (SP-362, added 2026-08-31)

py3d selection membership follows `Point` and `Face` object identity. It is not a stored
integer-index map. During section replacement:

- mutate `lod.points[:]`, `lod.faces[:]`, and `lod.facenormals[:]` in place; do not rebind
  those lists;
- keep every surviving `Point` and `Face` object, and prune deleted objects from every
  selection before writing;
- remap the actual integer fields, `Vertex.point_index` and `Vertex.normal_index`;
- append new normals directly to `lod.facenormals` after synchronization, rather than to a
  detached temporary list.

The writer's identity guard should fail loudly on a dead selection key. A successful write
still requires an index-resolution gate and a zero-drift check for surviving proxies.

## `validate()` ERR_AXIS_SELECTION_MISSING is a heuristic, not an engine rule (SP-439, added 2026-09-28)

[EXACT] `py3d.P3D.validate()` flags every memory selection ending in `_axis` that has no homonymous visual selection (heuristic ported from `audit_p3d.py`). Several animations of the same bone with different axes cannot satisfy it: `model.cfg` links `axis=` and `selection=` independently, so there it is a false positive, not an engine rule (measured: renaming the axes from `clab_*_axis` to `axis_clab_*` took the model from 3 ERROR to 0 ERROR, 2026-09-28). [DESIGN] Name such axes `axis_<name>` to keep the validator quiet.

## Orientation metrics for open geometry (SP-166, added 2026-08-31)

Before trusting an aggregate orientation metric, translate the model by several metres and
measure again. A changed verdict proves that the metric is not valid for that geometry.
Classify closure before choosing the oracle: signed volume is valid for closed components;
open sheets require a visibility test.

For open geometry, render from a direction battery with and without culling and compare the
nearest surface with the surface that is actually drawn. Count a defect only when the depth
gap exceeds a calibrated distance. A useful report separates `<2 mm`, `2 mm-2 cm`,
`2-10 cm`, and `>10 cm`; a zero-gap pixel count confuses thin sheet thickness with visible
backfaces. Calibrate both green and inverted controls before acting on the result.

A coincident-twin tolerance must be smaller than the thinnest sheet in the model, and the
actual separation must be measured before labelling a pair as intentional double-sided
geometry. Finally, flip a suspect component and remeasure it: if the defect disappears it
was winding; if it moves to the other side, the sheet needs two-sided geometry rather than
another flip.

**Instrument and seal for see-through triage (added 2026-09-24, LFDucati T96).** [DESIGN] To see what the game draws, render with Blender Workbench `show_backface_culling` (rasterizes and discards backfaces before depth, like the engine) — use it to locate see-through pixels, not to adjudicate winding signs (SP-179 in `dayz-vehicles`). Cycles with a transparent backface also skips the coincident twin, and per-face red overlays come out mottled on double layers: neither adjudicates what the player sees. [EXACT] Flipping a two-sided sheet only moves the hole (measured: 313 tank-lip faces flipped, the front lip then vanished from above); the seal that never removes what was visible is an inverted twin carrying its own UVs and normals in the same animation selections. Twins cost vertices — respect the per-model resolved budget (`dayz-p3d-audit` SP-359), not a fixed 65,535. For a real hole (side views show nothing behind), close the gap and back it with a dark inset sheet. After any mesh fix, compare before/after from the owner's viewpoint, not only the face counts.

## Closed collision-LOD winding (SP-169, added 2026-08-31)

For a closed collision component, use right-handed signed volume
`sum(dot(v0, cross(v1, v2))) / 6` and calibrate against a known-good vanilla control. The
measured DayZ convention is negative volume. Emit each face so
`cross(v1-v0, v2-v0)` points inward, and store the negated exterior normal. A generator
must measure its new box and fail closed unless the volume is negative.

Do not infer this result from agreement between stored normals and geometric cross products,
and do not treat a generic `py3d.validate()` result as the signed-volume gate. Proxy
triangles and relative winding checks can make that aggregate non-discriminating. Closed
collision volume and open-sheet visibility are separate profiles.

## Three-point binary handoff identity (SP-172, added 2026-08-31)

Every corrected binary handoff records SHA-256 for three points: the deployed artifact, the
repair input, and the repair output. Require `deployed == repair input` before deployment;
a small input-to-output delta says nothing about an undeclared deployed-to-input delta.
Also compare per-model PBO size changes with untouched controls and stop when the size change
does not fit the declared edit. Select backups by a stamped name or hash, not by mtime,
because file copies can preserve the source timestamp.

## Visual face texture/material contract (SP-195, added 2026-08-31)

A py3d builder must populate both `face.material` and `face.texture` for visual faces. A
material-only model can pass structural offline gates and still render white, especially in
proxy-split models where `hiddenSelections` repaint the host but not its body proxies.

Enforce this per-face biconditional only for visual LODs (`resolution < 1e4`):

```text
material != ""  <=>  texture == derive(material)
material == ""  <=>  texture == ""
derive(M) = replace \\materials\\ with \\textures\\ and .rvmat with _co.paa
```

Proxy triangles naturally occupy the empty/empty case. Non-visual penetration materials
may legitimately have an empty texture and stay outside this gate. Preserve path casing in
evidence, verify every referenced `.rvmat` and `_co.paa` exists on disk, and keep a negative
fixture that renames one referenced texture and must turn the gate red.

## Client-local cable segment model (SP-441, added 2026-09-28)

[EXACT] Build the unit cable segment with py3d as an open cylinder, 1 m over +Z, centred, radius 0.01, visual LODs of 8/5/3 sides, no collision geometry, every visual face in selection `camo`, faces wound INWARD (measured in game, DayZDiag 1.29, LFPowerGrid cables). Give the model a mod-prefixed name: `CfgModels` classes are global across mods. Vanilla props cannot serve as segments (arbitrary pivots; `proxies/metalwire.p3d` is a ring).

## Per-delivery artist-return amendment (SP-297, added 2026-08-31)

This section narrows the reusable-script rule in SP-246. Never apply an involutive correction
(rotation, mirror, or sign change) blindly to every file in an artist batch. Derive each
proxy frame and classify it against the deployed artifact as `SAME`, `R*Ry(180)`, or
`OTHER`; execute the correction only for the measured class and verify frame and anchor
against the deployed model.

An artist return can also retain the complete non-visual contract. In that case, a surgical
reconstruction must fail closed instead of adding a second contract. Normalize selection and
material-path casing from a canonical map read from the deployed artifact; do not invent
canonical names. In particular, restore `Component01` exactly when the reference requires it.

Reimport gates must express invariants, not freeze one delivery's topology. For symmetric
parts, require both halves to match each other in component count, face count, and material
distribution, plus minimum floors that reject an incomplete half. Treat optional structures
as warnings. After changing the gate, rerun it on the previous delivery and require the
previous production output to remain byte-identical.

## Identify a derived artifact's source by fingerprint, not filename (LL-405, added 2026-09-01)

To decide whether a UV unwrap reached the game, the question was which mesh feeds
`LFQuad_body.p3d`. `obj\body_lod0.obj` was opened because it is named lod0, it did not match
(82,266 verts against the p3d LOD0's 22,004), and the conclusion drawn was that the source was
stale and the work had to be redone. Both halves were false.

**LOD numbering was offset by two**: res=0 comes from `body_lod2.obj` (22,105 v / 38,960 f
against 22,004 / 38,832) and res=1 from `body_lod3.obj`; `lod0` and `lod1` are higher-detail
versions that are never shipped. And the file called stale was the **newer** of the two.

One command settled it: count faces per material in each candidate and compare against the p3d —
11 of 14 materials matched to the digit, and the three that did not were off by handfuls, which
is the assembler cleaning up.

Before deciding anything about a derived artifact, identify its input by a **content
fingerprint** — per-material or per-selection face counts, class counts, a hash of a stable
block — never by a filename that only encodes someone's intent. A name is a claim about
provenance; a count is evidence. It is cheap enough that there is no reason to skip it, and the
failure it prevents is redoing work that was already correct.

## DayZ 1.30 Exp (build 1.30.164014)

The LOD resolution table, Rules 12–20, artist-return preflight (SP-246), and the py3d
assembly path still hold. What 1.30 changes for this skill is engine/config contract
around proxies, houses, wells, and unbinarized-mesh cache. Details and `[EXACT]`
blocks: [dayz-1-30-model-pipeline.md](references/dayz-1-30-model-pipeline.md).

### What changes

- **Inventory-attachment proxy on dedicated server.** (until 1.29: missing proxy in the final visual LOD could make the server report the parent origin.) (since 1.30 Exp [CHANGELOG]: server reads the View Geometry LOD.) Duplicate the proxy triangle in ViewGeometry.
- **`-cacheP3D=0/1`.** Sibling `.p3dcache` next to unbinarized `.p3d` for faster Game/Buldozer relaunches ([CHANGELOG]).
- **`disableSimulation` on house-based entities.** Config key [CHANGELOG]; script natives `Entity.DisableSimulation` / `GetIsSimulationDisabled` in `exp\scripts\scripts\3_Game\Entities\Entity.c:3-6`. Config assignment not found in extracted configs (`[UNVERIFIED]`).
- **`liquid_source`.** ViewGeometry selection for well/pump liquid actions (`UAMisc.LIQUID_SOURCE_SELECTION` in `ActionConstants.c:180`).
- **`StaticObjectType` + `CfgNonAIVehicles` `VegetationSounds`.** Loader is exact (`StaticObjectType.c:1-15`); a shipping bush example is not in this extract (`[UNVERIFIED]`).
- **`.agr` → `.agf` is NOT this skill.** Object `model.cfg` is unchanged; character graphs belong to `dayz-animation-pipeline`.

### What breaks for a 1.29 mesh/config

1. Attachment proxies only in Resolution LODs can sit at the parent origin on dedicated server.
2. Custom wells without ViewGeometry `liquid_source` will not accept wash/drink/fill.
3. Duplicate **members** inside one config class now fail binarize (`dayz-pbo-build`).

### Migration checklist

- [ ] Proxy triangle + `proxy:…p3d.NNN` in visual LODs **and** ViewGeometry.
- [ ] Custom well/pump: ViewGeometry selection `liquid_source`.
- [ ] Static houses with no tick: consider `disableSimulation` (config [CHANGELOG]; runtime `DisableSimulation`).
- [ ] Unbinarized mesh loop: `-cacheP3D=1` on Diag/Buldozer.
- [ ] Character/infected graphs: `.agf` via Workbench — not `model.cfg` here.

Files in this skill

  • SKILL.md68.3 KB
  • references/animations.md24.2 KB
  • references/answeroverflow-2026-05-17.md4.3 KB
  • references/blender-headless.md27.3 KB
  • references/blender-workflow.md12.2 KB
  • references/config-and-packing.md9.6 KB
  • references/dayz-1-30-model-pipeline.md7.1 KB
  • references/lfpg-recipes.md23.5 KB
  • references/lods-and-geometry.md10.6 KB
  • references/memory-and-selections.md14 KB
  • references/multi-rvmat-section-texturing.md6.9 KB
  • references/procedural-textures.md29.7 KB
  • references/py3d-direct-generation.md39 KB
  • references/vehicle-config-and-modelcfg.md460 B
  • references/vehicle-structural-parity.md484 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.