agentleFS
willy92winswilly92wins/DayZ-Modding-Knowledge-Pack/skills/dayz-3d-viewer/SKILL.md

dayz-3d-viewer

Skill20 starsChanged 42 days agoMITView on GitHub

Use when: 3D viewer, p3d viewer, PAA to PNG, glTF export, GLB convert, model preview, texture conversion. P3D/PAA/RVMAT → Three.js HTML/GLB. Not structural audits: dayz-p3d-audit; not interactive Recipe edit: dayz-p3d-inspector.

  • Installs packages

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All in this skill's scripts/ directory (plugin projection):

    line 107
  2. Very specific:

    NO blob URLs. This is critical because Claude's artifact sandbox blocks all network requests including fetch(), XMLHttpRequest, and …

    line 124
  3. Distinctive:

    PNG → PAA encoding: Not implemented. Would need DXT compression + PAA header assembly.

    line 303
  4. Distinctive:

    NOT to the slider handler.

    line 369

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

DayZ 3D Viewer — P3D + PAA + RVMAT → Interactive Three.js

Converts DayZ mod assets into interactive 3D viewers for chat and web. Battle-tested with real LFPG Push Button model (328 verts, 640 faces, 4 materials, DXT1+LZO textures).

The executable lives in the pack tool tools/dayz-3d-viewer. This skill is the playbook; do not look for scripts/ or a vendored py3d wheel here. In the Cowork plugin projection these were scripts/*.py plus a vendored py3d wheel; in this pack they map to python -m dayz_3d_viewer (see Commands).

Three.js version lanes - READ BEFORE PINNING A VERSION

Measured 2026-09-01 by HTTP probe and headless render (Chrome 151, no permissive flags). These are facts about the library, not preferences, and they apply to every viewer any DayZ skill emits - not only this one.

Loading laneWorks from file:// (double-click)Max three version
UMD <script src=".../three.min.js">yes0.160.0
ESM + importmap -> https CDNyes0.185.1 (latest)
ESM + importmap -> local vendored filesno (CORS)http only
  • build/three.min.js (UMD): HTTP 200 up to 0.160.0, 404 from 0.161.0. A viewer on the UMD lane has no upgrade path at all - it is a dead branch, not merely an old version.
  • examples/js/ (non-module OrbitControls / TransformControls): 200 at 0.147.0, 404 from 0.148.0. That is why legacy viewers sit at 0.147 - a hard limit, not a preference.
  • ESM + importmap against the https CDN does work from file://, with no browser flags. Notes claiming otherwise were wrong. What fails from file:// is importing local module files. Do not vendor three to make a viewer offline: vendored modules need http.
  • three.module.js is no longer a single bundle - it re-exports from three.core.js (0.185.1 = 650 KB + 1.44 MB). Vendoring only the first gives a silent 404 and a blank page.
  • r160 -> r185.1 is behaviourally neutral for this skill's API surface (raw BufferGeometry, MeshStandardMaterial, OrbitControls, Raycaster, shadowMap, tone mapping): identical raycast distance, identical outputColorSpace, zero pageerrors, zero deprecation warnings. The only deprecation that lands nearby is PCFSoftShadowMap (r182, use PCFShadowMap), still working.

Emit new viewers on the ESM + importmap -> CDN lane. Drop to UMD only for a measured reason, knowing it caps that viewer at 0.160.0 permanently.

Install Dependencies

Pack (tools/py3d + tools/dayz-3d-viewer):

pip install -e tools/py3d
pip install -e "tools/dayz-3d-viewer[all]"
# Preferred when python-lzo will not compile (Windows Py3.14+ / sandbox):
#   pip install lzokay
#   python -m dayz_3d_viewer install-lzo-shim
# CRITICAL: py3d = the pack DayZ fork >= 1.6.0 (tools/py3d).
# NEVER `pip install py3d` (PyPI = point-cloud lib) NOR git+upstream (without guards).
python -c "import py3d; assert getattr(py3d,'IS_DAYZ_FORK',False) and tuple(map(int,py3d.__version__.split('.')))>=(1,6,0), (py3d.__version__, py3d.__file__)"

Plugin projection (scripts/ + vendored wheel in this skill - D2=B):

apt-get install -y liblzo2-dev
pip install pygltflib python-lzo pillow numpy opensimplex --break-system-packages
# Preferred when python-lzo will not compile (Windows Py3.14+ / sandbox):
#   pip install lzokay --break-system-packages
#   python scripts/install_lzo_shim.py   # installs the lzokay-based shim (import lzo keeps working)
# CRITICAL: py3d = the pack DayZ fork >= 1.6.0 (tools/py3d).
# NEVER `pip install py3d` (PyPI = point-cloud lib) NOR git+upstream (without guards).
# The installer resolves wheels/py3d_dayz-*-py3-none-any.whl, falls back to
# /sessions/*/mnt/*/_tools/py3d/dist/ when mounted, fails closed when neither is
# present, and refuses to guess when legacy py3d wheels sit beside it.
python3 scripts/install_py3d.py
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__)"

Commands

All via python -m dayz_3d_viewer:

CommandPurpose
paa-to-pngPAA → PNG converter (DXT1/5/3, RGBA, LumAlpha, with LZO/LZSS)
p3d-to-glbP3D MLOD → glTF/GLB converter (geometry + textures + RVMAT materials)
parse-rvmatRVMAT parser (colors, specular, emissive, texture stage mapping)
build-viewerFull orchestration (glb + HTML; embedded or web)
install-lzo-shimInstalls an lzokay-based lzo shim — preferred route when python-lzo will not compile (Windows Py3.14+/sandbox)

Public library names are unchanged: convert_paa_to_png, parse_rvmat, extract_geometry_for_viewer, generate_viewer_html, p3d_to_glb, run_pipeline.

Scripts

In the Cowork plugin projection these were scripts/*.py. In this pack they map to python -m dayz_3d_viewer (see Commands above). Historical names:

ScriptPack command
paa_to_png.pypaa-to-png
p3d_to_gltf.pyp3d-to-glb
rvmat_parser.pyparse-rvmat
viewer_template.py / pipeline.pybuild-viewer
install_lzo_shim.pyinstall-lzo-shim

All in this skill's scripts/ directory (plugin projection):

ScriptPurpose
paa_to_png.pyPAA → PNG converter (DXT1/5/3, RGBA, LumAlpha, with LZO/LZSS)
p3d_to_gltf.pyP3D MLOD → glTF/GLB converter (geometry + textures + RVMAT materials)
rvmat_parser.pyRVMAT parser (colors, specular, emissive, texture stage mapping)
viewer_template.pyThree.js viewer generator (embedded + web modes)
pipeline.pyFull orchestration (all of the above in sequence)
install_lzo_shim.pyInstalls an lzokay-based lzo shim — preferred route when python-lzo will not compile (Windows Py3.14+/sandbox)

Two Viewer Modes

Embedded (for Claude chat / sandboxed iframes)

Bakes geometry as Float32Array + textures as data:image/png;base64 directly into HTML. Uses raw THREE.BufferGeometry + THREE.MeshStandardMaterial — NO GLTFLoader, NO fetch, NO blob URLs. This is critical because Claude's artifact sandbox blocks all network requests including fetch(), XMLHttpRequest, and URL.createObjectURL().

Pack:

from dayz_3d_viewer import extract_geometry_for_viewer, generate_viewer_html

geo = extract_geometry_for_viewer('model.p3d', texture_map={'base_co': 'base_co.png'}, rvmat_data=rvmats)
generate_viewer_html(model_name='My Model', mode='embedded', geometry_data=geo, output_path='viewer.html')

Plugin projection:

from viewer_template import extract_geometry_for_viewer, generate_viewer_html

geo = extract_geometry_for_viewer('model.p3d', texture_map={'base_co': 'base_co.png'}, rvmat_data=rvmats)
generate_viewer_html(model_name='My Model', mode='embedded', geometry_data=geo, output_path='viewer.html')

Web (for deployment)

References external .glb file via GLTFLoader.load(). Separate files, cacheable.

generate_viewer_html(model_name='My Model', mode='web', glb_url='model.glb', output_path='viewer.html')

Generated HTML loads three.js 0.160.0 from jsDelivr. That pin is the current default, not a ceiling - see "Three.js version lanes" above before changing it. It is not embedded; a render needs a network. See tools/dayz-3d-viewer/README.md.

Recommended Workflow (Chat)

This is the proven workflow for when user uploads .p3d + .paa + .rvmat files:

Pack:

from dayz_3d_viewer import convert_paa_to_png, parse_rvmat
from dayz_3d_viewer import extract_geometry_for_viewer, generate_viewer_html
import os

# 1. Convert PAA textures to PNG
convert_paa_to_png('base_co.paa', 'base_co.png', verbose=True)

# 2. Parse RVMAT files
rvmat_data = {}
for f in ['housing.rvmat', 'button.rvmat', 'led_off.rvmat']:
    rvmat_data[os.path.splitext(f)[0]] = parse_rvmat(f)

# 3. Extract geometry with materials + textures baked in
texture_map = {'base_co': 'base_co.png'}  # stem -> png_path
geo = extract_geometry_for_viewer('model.p3d', texture_map=texture_map, rvmat_data=rvmat_data)

# 4. Generate embedded viewer (works in Claude chat)
generate_viewer_html(
    model_name='LFPG Push Button',
    mode='embedded',
    geometry_data=geo,
    output_path='viewer.html',
)

Plugin projection:

import sys, os
sys.path.insert(0, '/path/to/skill/scripts')
from paa_to_png import convert_paa_to_png
from rvmat_parser import parse_rvmat
from viewer_template import extract_geometry_for_viewer, generate_viewer_html

# 1. Convert PAA textures to PNG
convert_paa_to_png('base_co.paa', 'base_co.png', verbose=True)

# 2. Parse RVMAT files
rvmat_data = {}
for f in ['housing.rvmat', 'button.rvmat', 'led_off.rvmat']:
    rvmat_data[os.path.splitext(f)[0]] = parse_rvmat(f)

# 3. Extract geometry with materials + textures baked in
texture_map = {'base_co': 'base_co.png'}  # stem -> png_path
geo = extract_geometry_for_viewer('model.p3d', texture_map=texture_map, rvmat_data=rvmat_data)

# 4. Generate embedded viewer (works in Claude chat)
generate_viewer_html(
    model_name='LFPG Push Button',
    mode='embedded',
    geometry_data=geo,
    output_path='/mnt/user-data/outputs/viewer.html',
)

Pipeline Script (Alternative)

For CLI or batch processing, pack build-viewer / plugin pipeline.py orchestrates everything:

python -m dayz_3d_viewer build-viewer model.p3d --textures ./textures --rvmats ./materials --name "My Model" --mode embedded -v
python scripts/pipeline.py model.p3d --textures ./textures --rvmats ./materials --name "My Model" --mode embedded -v

The pipeline auto-discovers assets: reads P3D → finds referenced texture/material paths → searches provided directories → converts PAA → parses RVMAT → builds viewer.

PAA Format Support

TypeCodeCompressionStatus
DXT10xFF01Raw or LZO✅ Tested with real files
DXT50xFF05Raw or LZO✅ Full
DXT30xFF03Raw or LZO✅ Full
RGBA 4:4:4:40x4444LZSS✅ Full
RGBA 5:5:5:10x1555LZSS✅ Full
LuminanceAlpha0x8080LZSS✅ Full
RGBA 8:8:8:80x8888LZSS✅ Full
Index Palette0x4747LZSS/RLE❌ Not yet (very rare)

PAA Internals

  • File starts with 2-byte type magic, then optional GGAT tags (OFFS, AVGC, MAXC, FLAG, SWIZ)
  • Mipmaps stored largest-first, each with 2-byte width + 2-byte height + 3-byte data size
  • DXT data passed to Pillow's DDS decoder via synthetic DDS header
  • LZO flag: top bit of width field set → python-lzo decompression before DDS decode
  • Non-DXT formats use BI's LZSS variant (ring buffer 4096, init 0x20)
  • End of mipmaps: 6 zero bytes

The SWIZ tag is stored and not applied. Proxy triangles export as visible geometry. Resolution 1100 (ViewPilot) is classified as shadow, not as a view-pilot kind. Details: tools/dayz-3d-viewer/KNOWN-ISSUES.md.

P3D Format Support

FormatSupportNotes
MLOD (unbinarized)✅ FullVia py3d (KoffeinFlummi). Your generated models are MLOD.
ODOL (binarized)✅ via external converterVanilla game .p3d files. ODOL input → pre-process with an external ODOL→MLOD converter (not distributed with this pack), then run the normal pipeline.

RVMAT Material Mapping

The RVMAT parser extracts and maps to PBR:

  • diffuse[] → base color (fallback when no texture)
  • forcedDiffuse[] → overrides diffuse if non-zero (used for LED tinting)
  • specular[] → metallic factor (avg * 2, clamped)
  • specularPower → roughness (1 - power/100)
  • emmisive[] → emissive color + intensity (BI values like {0,40,0} normalized to 0-1)
  • Texture stages: Stage1=normal _nohq, Stage3=specular _smdi (parsed but only _co applied in viewer)

Known Gotchas (Fixed)

  1. Backslash paths on Linux: P3D stores texture paths with Windows backslashes (lfpg_button\data\textures\base_co.paa). os.path.basename() on Linux doesn't split these. Fix: always normalize with .replace('\\', '/') before os.path.basename().
  2. GLTFLoader in sandbox: Claude's artifact iframe blocks fetch() and Request.clone(). GLB loading via loader.load() or even loader.parse() fails because the loader internally resolves buffer/image URIs via fetch. Fix: bypass GLTFLoader entirely for embedded mode, build geometry from raw typed arrays.
  3. PyPI py3d collision: pip install py3d installs a point cloud visualization library, NOT the DayZ P3D library. Install the pack fork (pip install -e tools/py3d) or the DayZ fork wheel vendored in this skill via python3 scripts/install_py3d.py (wheels/py3d_dayz-*.whl; fallback _tools/py3d/dist/ if mounted), which fails closed instead of installing the wrong py3d, and assert py3d.IS_DAYZ_FORK.
  4. LZO on DXT mipmaps: Arma2+ PAA files compress large mipmaps (256+) with LZO. Indicated by top bit of width field. Requires liblzo2-dev + python-lzo, or lzokay plus python -m dayz_3d_viewer install-lzo-shim (plugin projection: python scripts/install_lzo_shim.py).

Viewer Features

  • Orbit controls (LMB rotate, RMB pan, scroll zoom)
  • Wireframe overlay toggle
  • Grid toggle
  • Auto-rotate toggle
  • Background color cycling (5 presets)
  • Camera reset
  • Model stats HUD (vertices, triangles, materials)
  • ACES filmic tone mapping
  • 4-light setup (key, fill, rim, ambient)
  • Responsive sizing
  • PBR materials with metallic/roughness from RVMAT
  • Emissive materials for LEDs

Limitations & Future Work

  • ODOL input: pre-process with an external ODOL→MLOD converter (not distributed with this pack), then run the normal pipeline. (The previously documented cfgconvert -txt workaround was wrong — CfgConvert converts config.bin↔cpp, not .p3d.)
  • Normal maps: Parsed from RVMAT but not yet applied in Three.js viewer.
  • PNG → PAA encoding: Not implemented. Would need DXT compression + PAA header assembly.
  • Multiple LOD viewer: Currently shows only best visual LOD. Could add LOD switcher.

States, Animations & LEDs (Phase 3 — Planned)

Full implementation plan in references/states-animations-leds.md. Summary:

What it enables: Interactive viewers with animated buttons/levers/doors/knobs, LED state toggles (off/green/red), and material swaps — matching in-game behavior.

Data sources needed per model:

  • .p3d — selections (vertex groups) + Memory LOD axes
  • model.cfg → CfgModels class → sections[] + Animations{} (type, axis, offset/angle)
  • config.cpp → CfgVehicles class → hiddenSelections[] + alternative materials

Implementation phases:

  • Phase A: config_parser.py + modelcfg_parser.py (parse BI config format)
  • Phase B: Extend geometry extraction with per-selection vertex groups + axis data
  • Phase C: Material preset system (auto-detect LED alternatives by filename pattern)
  • Phase D: Viewer UI (animation sliders + state toggle buttons + PointLights for LEDs)
  • Phase E: Web mode sidecar JSON for animation data

Key insight: P3D selections overlap — a face can be in both an animation group AND a material-swap group (e.g., LED moves with button AND changes color independently). The viewer must build a THREE.Group hierarchy where swappable meshes are children of animated groups.

Fit editor + flat-color materials (added 2026-05-23)

Interactive "fit editor" mode for Memory-LOD points (SP-006)

Beyond the inspection viewer, a reusable "fit editor" places Memory-LOD points that are NOT tunable in-game (crewDriver, pos_driver/_dir, animation axes, cameras) — baked points cost a rebuild per tweak, so dial them in OFFLINE first (R5). Pattern: a Three.js viewer that loads the LOD0 of the .p3d(s) with geometry embedded as base64 (no fetch), plus articulated mannequin(s) at scale (pelvis-origin local pose) driven by sliders (X/Y/Z/yaw), fixed reference markers (handlebar grips, get-in points), and a live copyable coord readout. Supports multiple occupants (driver+codriver, distinct colors) to fit without overlap. The real seated pose comes from the animation; the mannequin is a geometric reference. Reference impl: build_viewer3.py (p3d → per-material groups + base64 loader) + build_viewer4.py (buildRider(root,mat), sliders, readout). No-browser verify (when Chromium will not fit on disk): parse the embedded JSON payload + node --check the inline script.

Flat-color models: per-material .rvmat, NOT a UV-atlas bake (LL-021)

For monochrome / flat-color-per-piece models (no real texture detail), generate one .rvmat per material with diffuse[] = base color (from the .mtl) and texture=""; zero UV, zero gaps. A UV-atlas bake of many flat materials produces black holes / circles / smudges (tiny islands over a black background; any UV drift samples the background). Atlas baking is for models with real texture detail.

(added 2026-06-05) Self-test of an HTML viewer with preview MCP (without Puppeteer/disk) (SP-024)

To self-test a Three.js viewer without Puppeteer (Chromium fills sandbox disk): serve the HTML with a static http.server + the preview MCP: preview_start, then preview_eval for state/DATA/THREE, preview_console_logs level=error, preview_screenshot. Verifies render + clean console + state without downloading Chromium. Caveat: screenshot can flake (timeout) with the rAF loop -> preview_eval of state is the robust verification. Origin: SP-024, LFQuad wheel-well tuner 2026-06-01. Cross-ref cowork-entorno-y-tooling-gotchas.

Fit editor: the two defects that destroy user placement (added 2026-09-09)

Both hit the "fit editor" pattern of SP-006 above, both were shipped, and each cost an entire placement cycle in LFQuad3. Wire their gates BEFORE delivering the viewer.

1. Copiable readout must regenerate on EVERY change, and only on relevant ones. Shipped defect: dump() was wired to startup, reset, and a mirror button, but NOT to the slider handler. The user positioned everything, saw it looked correct on screen, copied the textbox... and the box still held values from LOAD TIME. Nothing warns you, and the placement is unrecoverable because it was never written anywhere. Worse: the timestamp was printed INSIDE dump(), so the stale text bore the page open time and read as freshly generated. Gate in both directions: (a) move a control via code, verify that the text CHANGED and brings the new value, restore and verify that it returns identical; (b) change a control that should NOT alter placement (which mesh is previewed, the camera) and verify that the readout remains the SAME. Direction (b) is not optional: a viewer that rewrites numbers when changing mesh is the same bug with reversed sign.

2. THREE.BoxHelper decouples for TWO independent reasons that look identical. (a) Operates in WORLD coordinates: attaching it to transformed holder applies that transformation a second time. It belongs in the SCENE. (b) update() reads object.matrixWorld, which three only recalculates during render(); calling it right after moving the object uses the matrix from the PREVIOUS frame, so the bounding box permanently lags one step behind -- the visible symptom is a box lagging while dragging. Call object.updateMatrixWorld(true) BEFORE helper.update(), and route all movement and all rotation through that single function so nowhere can bypass it. Fixing only (a) leaves the viewer visibly broken all the same, which is how it shipped the first time. Gate catching both at once: take the DRAWN helper corners (its position attribute transformed by its own matrixWorld, so test does not depend on who its parent is) and compare them with mesh world bbox. Negative controls, both mandatory: the old path must give a deviation equal to what you moved (measured 0.55 m for a 0.55 m movement) and attaching it to holder a large one (1.13 m). EXPECTED non-zero, do not chase it: BoxHelper draws the AABB of the transformed local AABB, not that of transformed vertices, so with rotations not multiples of 90 degrees it remains legitimately LOOSER than the mesh (measured +5 to +16 mm across nine parts). Verify that it ENCLOSES the mesh, never that it matches. Origin: LFQuad3 placement viewer, 2026-09-08/09.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.