dayz-model-pipeline · git:20260926.04cea6f · 2026-09-26 · sha256 d1950876afc2cc42

dayz-model-pipeline git:20260926.04cea6fA

Immutable. This exact content never changes and is served at /api/v1/blob/d1950876afc2cc42.

---
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).
- **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`
- **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** (codec KoffeinFlummi + guards anti-corrupcion,
> constantes LOD DayZ, `validate()`, proxies, recipe y CLI `python -m py3d`),
> pack distribution `py3d-dayz` via `pip install -e tools/py3d`:
>
> ```bash
> SKILL_DIR=<dir de esta skill>
> # py3d DayZ fork >= 1.6.0 (`pip install -e tools/py3d`).
> # NUNCA `pip install py3d` (PyPI = point-cloud lib) NI git+upstream (sin 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 / flip 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 — SP-071 remains the generic-DCC visual-cull guide. 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.
- 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)
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 winding for FBX/Blender-authored meshes follows the measured recipe, not det alone** — Blender exports with Z as up-axis; DayZ uses Y-up. One axis map, two source cases:
    - **FBX / Blender-authored geometry with outward normals (primary LFPG path):** axis `(x,y,z)→(x,z,-y)` (det=+1 position map) applied to ALL vertices AND normals in ALL LODs, **plus** reverse every face vertex order (except proxy triangles, whose vertex order encodes the attachment frame) **and** negate normals after the same axis map. Rationale (ticket `fb-20260921-164243-abef`, measured on 5 working LFPG p3d): working DayZ meshes store `cross(p1-p0,p2-p0)` **opposite** to the source outward normal mapped as `(fn[0], fn[2], -fn[1])`. Pure «det=+1 ⇒ never reverse» is **false** for this authored path. Offline gates (docs only — heater recipe *aún sin probar en juego*; do **not** claim in-game proof): (a) collision LODs — per-component centroid → cross **INWARD 100%**; (b) visual — cross vs source outward mapped to DayZ → **opposite 100%** (centroid is NOT the visual gate); (c) cross ≡ declared vertex-normal mean 100% (sanity). Detail: section "Rule 13 nuance" below.
    - **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: verify post-assembly with `check_face_winding` / the offline gates above.
13. *(Merged into Rule 12 — winding for FBX/Blender follows the measured invert+negate recipe; determinant alone is insufficient. 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** — The winding reversal required by Rule 12 for det<0 source transforms can be silently skipped without Check A noticing, because Check A (`cross(e1,e2)` vs declared normal) returns UNIFORM_NON_FLIPPED in BOTH the broken and rotated-but-not-flipped states. 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`). 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. 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: `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 (MANDATORY after axis rotation)" and `check_face_winding` in `references/py3d-direct-generation.md`.

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).

## 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 the source-appropriate conversion: FBX/Blender `(x,y,z)→(x,z,-y)` + invert faces + negate normals, or GLB-only pure swap `(x,z,y)` + reverse (Rule 12) |
| Textures render on inside of faces (invisible exterior) | Winding does not match the measured recipe / GLB path | FBX/Blender: apply `(x,z,-y)` + invert faces + negate normals (Rule 12 measured recipe). GLB-only: pure-swap `(x,z,y)` + reverse (except proxy triangles). Verify with `check_face_winding` / offline gates |
| 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 in collision LODs | Winding reversal required by Rule 12 for a det<0 source transform was not applied | Run `for lod in model.lods: for face in lod.faces: face.vertices.reverse()` once on all LODs. Verify expected state post-fix is UNIFORM_FLIPPED (cross-product opposite to declared normals) |
| 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 winding reversal required by Rule 12 for a det<0 source transform was skipped). Check A is insufficient to detect this — must use the per-component outward check (per-component-centroid). | 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: determinant alone is insufficient for FBX/Blender→DayZ (LL-020 / abef 2026-09-21)
A PROPER rotation (determinant = +1) preserves mathematical handedness of the *position map*,
but that does **not** mean «do NOT reverse winding» for Blender/FBX-authored meshes going into
DayZ. Measured working LFPG p3d (ticket `fb-20260921-164243-abef`) require axis
`(x,y,z)→(x,z,-y)` **plus** invert face vertex order (except proxy triangles) **plus** negate
normals after the same axis map — target is cross **opposite** source outward (visual gate),
and collision cross **INWARD** via per-component centroid. Determinant alone does not decide
the reverse; the measured recipe does. Reflections (det<0) still flip handedness and typically
need a reverse, but do **not** re-introduce an unconditional «det=+1 → never reverse» Blender
rule (contradicted by measurement). Never assume: verify with `check_face_winding` and the
offline gates in Rule 12. (Heater recipe *aún sin probar en juego* — do not claim in-game proof.)

### Collision LOD winding must match the Visual LOD (SP-003)
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). Surface this proactively when the symptom is "walks through /
no action / bullets pass".

### 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)

This section is **GLB/glTF-only**. It does **not** override Rule 12's measured FBX/Blender recipe.

**glTF front faces are CCW by spec.** 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).

**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. 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.

Do **not** treat «any GLB/Blender import» as one shared rule with the FBX/Blender measured
recipe in Rule 12 — Blender-authored FBX follows invert+negate on `(x,z,-y)`; GLB keeps its own
optional pure-swap path above.

### 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

Cuando ensambles **LODs no-Geometry** (FireGeo 7e15, ViewGeo 6e15, LandContact 2e15, Memory 1e15, Shadow, Visual <1000) por py3d, usa explícitamente `point.mass = None` para cada punto. **NUNCA** uses `point.mass = 0.0`.

Razón: py3d emite el tagg `#Mass#` si **ALGUNA** `point.mass` del LOD es ≠ None, **aunque sea exactamente `0.0`**. Resultado: el `.p3d` lleva un `#Mass#` espurio en el LOD no-Geometry. AddonBuilder/binarize hornea la masa de ESE LOD (suma = 0) → ODOL desplegado con `CoM=(0,0,0)` e inercia 0 → `ECE_PLACE_ON_SURFACE` posiciona el vehículo a la altura del CoM = 0 → spawn bajo tierra → eyección por PhysX-depenetración.

### Antipattern (productor de 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
```

### Verificación post-assemble

Iterar todos los LODs y asegurar que **solo el Geometry LOD (res 1e13)** tiene `lod.mass != None` y tagg `#Mass#`:

```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#"
```

Verificación end-to-end (post-binarize): `binarize.exe -always -addon=<dir> <src> <dst> <wildcard>` + leer `ModelInfo CoM` del ODOL → debe ser ≠ (0,0,0) y coincidir con el CoM del Geometry MLOD.

### Cross-ref

LL-079 (bisección de LODs aisló el bug), LL-080 (la lección durable), check añadido a `dayz-p3d-audit` (Mass-only-Geometry, added 2026-06-02), tool de referencia `LFQuad_dev/tools/fix_firegeo_mass.py` (patch headless para .p3d con el 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).

## Reglas promovidas del corpus de lecciones (added 2026-07-27)

Promovidas desde `AI/20_Knowledge/lessons-learned.md` para que lleguen por trigger en vez
de depender de que alguien recuerde buscarlas. Cada regla cita su `LL-NNN` de origen;
la entrada completa vive allí. No quites la cita: el índice detecta la promoción por ella.

- **LL-024** — Para LODs de superficies curvas, usa Decimate COLLAPSE y protege las features críticas con un vertex group invertido. Mide tris y supervivencia de la feature; no uses planar dissolve como reductor sin verificar el conteo tras triangular.
- **LL-134** — En attachments con bbox contractual, mide el grupo completo en el frame de exportación después de cada feature. Si el bbox está cerrado, crea relieve rebajando el entorno mediante escalón/recess en vez de añadir geometría proud.
- **LL-372** — Phase 0 de procedencia también cubre licencia: un README/LICENSE empaquetado data del empaquetado, no de hoy. Fecha el anuncio vigente del publicador; si el interior del artefacto y la fuente actual discrepan, manda la fuente actual.


## 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.

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

For a raw Blender, OBJ, glTF, or FBX import whose measured axis transform preserves the
source winding, reverse the vertices of every **visual** MLOD face before binarization.
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.

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. 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.

## 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.

## 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.

## 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.

## 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.

## 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.