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
In this repository47 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- 3d-generation-harnessSkill
- ai-3d-to-dayzSkill
- ardy-motion-generationSkill
- blender-animationSkill
- blender-assemblySkill
- blender-visual-reviewSkill
- dayz-ai-patternsSkill
- dayz-animation-pipelineSkill
- dayz-aviationSkill
- dayz-basebuildingSkill
- dayz-charactersSkill
- dayz-clothingSkill
- dayz-doorsSkill
- dayz-environment-hazardsSkill
- dayz-feature-specSkill
- dayz-mcp-verifySkill
- dayz-model-pipelineSkill
- dayz-mod-workflowSkill
- dayz-motorbikesSkill
- dayz-p3d-auditSkill
- dayz-p3d-inspectorSkill
- dayz-particlesSkill
- dayz-pbo-buildSkill
- dayz-pbo-reverse-engineeringSkill
- dayz-persistenceSkill
- dayz-physics-engineSkill
- dayz-preflightSkill
- dayz-proxy-alignSkill
- dayz-realistic-animation-directorSkill
- dayz-sound-systemSkill
- dayz-test-ingameSkill
- dayz-texture-pipelineSkill
- dayz-ui-developmentSkill
- dayz-undergroundSkill
- dayz-vehiclesSkill
- dayz-weaponsSkill
- enforce-script-referenceSkill
- mixamo-retargetSkill
- ovh-cloudSkill
- rigorous-data-auditSkill
- rip-vehicle-importSkill
- systematic-debuggingSkill
- task-observerSkill
- uv-clean-atlasSkill
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
- Very specific: line 107
All in this skill's scripts/ directory (plugin projection):
- Very specific: line 124
NO blob URLs. This is critical because Claude's artifact sandbox blocks all network requests including fetch(), XMLHttpRequest, and …
- Distinctive: line 303
PNG → PAA encoding: Not implemented. Would need DXT compression + PAA header assembly.
- Distinctive: line 369
NOT to the slider handler.
≈ “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.
Files
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 lane | Works from file:// (double-click) | Max three version |
UMD <script src=".../three.min.js"> | yes | 0.160.0 |
ESM + importmap -> https CDN | yes | 0.185.1 (latest) |
ESM + importmap -> local vendored files | no (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 fromfile://is importing local module files. Do not vendor three to make a viewer offline: vendored modules need http. three.module.jsis no longer a single bundle - it re-exports fromthree.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 isPCFSoftShadowMap(r182, usePCFShadowMap), 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:
| Command | Purpose |
paa-to-png | PAA → PNG converter (DXT1/5/3, RGBA, LumAlpha, with LZO/LZSS) |
p3d-to-glb | P3D MLOD → glTF/GLB converter (geometry + textures + RVMAT materials) |
parse-rvmat | RVMAT parser (colors, specular, emissive, texture stage mapping) |
build-viewer | Full orchestration (glb + HTML; embedded or web) |
install-lzo-shim | Installs 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:
| Script | Pack command |
paa_to_png.py | paa-to-png |
p3d_to_gltf.py | p3d-to-glb |
rvmat_parser.py | parse-rvmat |
viewer_template.py / pipeline.py | build-viewer |
install_lzo_shim.py | install-lzo-shim |
All in this skill's scripts/ directory (plugin projection):
| Script | Purpose |
paa_to_png.py | PAA → PNG converter (DXT1/5/3, RGBA, LumAlpha, with LZO/LZSS) |
p3d_to_gltf.py | P3D MLOD → glTF/GLB converter (geometry + textures + RVMAT materials) |
rvmat_parser.py | RVMAT parser (colors, specular, emissive, texture stage mapping) |
viewer_template.py | Three.js viewer generator (embedded + web modes) |
pipeline.py | Full orchestration (all of the above in sequence) |
install_lzo_shim.py | Installs 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 -vThe pipeline auto-discovers assets: reads P3D → finds referenced texture/material paths → searches provided directories → converts PAA → parses RVMAT → builds viewer.
PAA Format Support
| Type | Code | Compression | Status |
| DXT1 | 0xFF01 | Raw or LZO | ✅ Tested with real files |
| DXT5 | 0xFF05 | Raw or LZO | ✅ Full |
| DXT3 | 0xFF03 | Raw or LZO | ✅ Full |
| RGBA 4:4:4:4 | 0x4444 | LZSS | ✅ Full |
| RGBA 5:5:5:1 | 0x1555 | LZSS | ✅ Full |
| LuminanceAlpha | 0x8080 | LZSS | ✅ Full |
| RGBA 8:8:8:8 | 0x8888 | LZSS | ✅ Full |
| Index Palette | 0x4747 | LZSS/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-lzodecompression 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
| Format | Support | Notes |
| MLOD (unbinarized) | ✅ Full | Via py3d (KoffeinFlummi). Your generated models are MLOD. |
| ODOL (binarized) | ✅ via external converter | Vanilla 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_coapplied in viewer)
Known Gotchas (Fixed)
- 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('\\', '/')beforeos.path.basename(). - GLTFLoader in sandbox: Claude's artifact iframe blocks
fetch()andRequest.clone(). GLB loading vialoader.load()or evenloader.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. - PyPI py3d collision:
pip install py3dinstalls 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 viapython3 scripts/install_py3d.py(wheels/py3d_dayz-*.whl; fallback_tools/py3d/dist/if mounted), which fails closed instead of installing the wrong py3d, and assertpy3d.IS_DAYZ_FORK. - 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, orlzokaypluspython -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 -txtworkaround 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 axesmodel.cfg→CfgModelsclass →sections[]+Animations{}(type, axis, offset/angle)config.cpp→CfgVehiclesclass →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.
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
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.

