custom-obj-models
Validate import, normalization, and rendering of custom `.obj` actor models in qmap
New in this cycle: MagicaVoxel .obj actor models — palette-aware import and lighting studies
Why
The current actor meshes are intentionally simple geometric stand-ins. They have been good enough for layout, grouping, and flow experiments, but they do not answer the next visual question: how well does qmap hold up when actors are represented by real 3D assets with irregular silhouettes, custom proportions, and more demanding lighting?
This cycle exists to prove the full path from a hand-authored .obj file to an in-scene qmap actor. It should also answer a second design question: what combination of material treatment and lighting makes custom models feel readable in qmap's isometric scene without losing the crisp, diagrammatic quality of the map.
This is adjacent to:
c2.entity-meshes, which established the current primitive-mesh vocabularyc14.actor-expansion, which expanded actor types but still used procedural geometry
Objective
One runnable experiment that:
- Loads at least one custom
.objfile into the scene as an actor mesh - Supports a round-trip asset workflow where the supplied test model can be swapped and reloaded without changing scene logic
- Compares a small set of lighting presets in the same scene
- Compares at least three color/material strategies for imported models
- Identifies the minimum reusable abstraction needed to bring custom-model actors into mainline qmap without destabilizing existing primitive actors
Scope
dev/experiments/c23.custom-obj-models/ExperimentsApp.vue— dedicated model-import and look-dev experimentdev/experiments/c23.custom-obj-models/shared/— asset manifest, fixture actors, lighting/material presets, and helpersdev/experiments/c23.custom-obj-models/assets/— local test.objfiles and optional companion.mtlfiles or texturesinbox/vox-exports-2/— source MagicaVoxel export set provided for import validationsrc/entities/— only if the experiment proves a reusableCustomModelMeshor equivalent factory seam is warrantedcycles/c23.custom-obj-models/— cycle metadata
Deliverables
- Runnable experiment
dev/experiments/c23.custom-obj-models/ExperimentsApp.vue
- Asset manifest + fixtures
- One or more actors configured to load the supplied test model
- Lighting preset definitions
- Material/color strategy definitions
- Cycle metadata
cycles/c23.custom-obj-models/CYCLE.mdcycles/c23.custom-obj-models/initial-prompt.md
Optional if the experiment proves it out:
- Reusable model mesh abstraction
- Example:
src/entities/CustomModelMesh.js
- Example:
- Configurable lighting seam
- Example: experiment-only wrapper first, then a narrow
QMapprop if it earns promotion
- Example: experiment-only wrapper first, then a narrow
Non-Goals
- Full asset-pipeline generalization for every 3D format
- Skeletal animation, rigging, or animated model playback
- Production-ready asset compression or optimization tooling
- Replacing all existing actor types with imported models during this cycle
- Final art direction for the entire product
Asset Contract To Validate
The experiment should validate a small, explicit contract for imported models. An actor should be able to declare something close to:
{
id: 'actor-1',
type: 'app',
label: 'Custom App',
model: {
kind: 'obj',
src: '/dev/experiments/c23.custom-obj-models/assets/test-model.obj',
mtl: null,
scale: 1,
recenter: true,
normalizeSize: true,
rotation: [0, 0, 0],
offset: [0, 0, 0],
materialMode: 'override',
},
}
The exact shape can change, but this cycle should settle the important decisions:
- where the model descriptor lives
- how scale/origin normalization works
- when source materials are respected vs overridden
- how imported meshes inherit qmap state colors and opacity changes
Provided Asset Set
Primary source assets for this cycle now live in:
inbox/vox-exports-2/
Current inventory:
Abstract Monsters.obj+.mtl+.pngBob Wire (Cowboy).obj+.mtl+.pngBob Wire (Sheriff).obj+.mtl+.pngEye Monsters.obj+.mtl+.pngHelpers.obj+.mtl+.pngIntern.obj+.mtl+.pngManager.obj+.mtl+.pngMonsters.obj+.mtl+.pngMouse&Pointer.obj+.mtl+.pngPuzzle.obj+.mtl+.pngReceptionist.obj+.mtl+.pngRobot.obj+.mtl+.png
Deferred variant exports now present in the same folder but intentionally excluded from the stable 12-asset manifest for this pass:
Intern Type1/2/3Receptionist Hair/BaldMousePointerAbs Mon Green/Purple/WhiteEye Mon Green/Purple/RedMonster Green/Purple/Red
Observed characteristics of this export set:
- Exporter: MagicaVoxel
- Material shape: one
palettematerial per model - Texture shape: one
256x1RGBA palette PNG per model - Geometry character: low-poly voxel surfaces with very few UV rows and only six normal directions
- Filename character: several assets include spaces and parentheses, so path handling must be tested directly rather than normalized away
Initial rendering implications:
- preserve palette crispness with nearest-neighbor texture filtering
- treat
.mtl+ palette texture loading as part of the baseline path, not a stretch goal - normalize scale and origin per asset because sizes vary significantly across the set
- verify whether imported assets sit on the ground plane consistently; at least one asset (
Robot.obj) appears slightly elevated abovey = 0
Asset-Specific Rendering Plan
This cycle should not treat the provided files as generic placeholders. Use the set intentionally, in passes that answer different questions.
Pass 1: Baseline Import Pair
Start with two simpler human-scale assets:
Manager.objBob Wire (Sheriff).obj
Why these first:
- compact geometry
- lower face counts
- easy to visually inspect for orientation, palette sampling, and label placement
- good first test for whether the imported-model wrapper feels comparable to existing actor scale
Goal of this pass:
- prove
OBJLoader+MTLLoader+ palette PNG loading - confirm upright orientation and ground contact
- verify nearest-filter palette rendering
Pass 2: Hero Character Pair
Promote to more detailed representative assets:
Receptionist.objRobot.obj
Why these next:
- more visually distinctive silhouettes
- higher geometric complexity
- likely good candidates for presentation-grade screenshots
- strong test for lighting, especially readable front/side planes and shadow noise
Goal of this pass:
- tune normalization and scale rules on larger assets
- compare source-material vs override-material strategies
- validate whether the current label offset logic still works on taller meshes
Pass 3: Stylized / Non-Humanoid Pair
Use expressive shape tests:
Puzzle.objMouse&Pointer.obj
Why these matter:
- less character-like silhouettes
- likely useful for non-human actor types such as tools, devices, or abstract system objects
- good test of whether qmap can support custom-model actors beyond a single visual family
Goal of this pass:
- verify that custom models still read as qmap actors when the silhouette is unusual
- test whether outline/edge support becomes necessary for flatter or more horizontal forms
Pass 4: Stress / Texture Cohesion Set
Use the monster family as a batch:
Abstract Monsters.objEye Monsters.objMonsters.obj
Optional additions if useful:
Helpers.objIntern.objBob Wire (Cowboy).obj
Why these last:
- they provide several related assets with different massing and palette usage
- they are well suited for multi-actor scenes where we compare coherence across several imported meshes at once
Goal of this pass:
- test scene readability when multiple custom assets coexist
- compare whether source palettes create too much scene noise
- evaluate grouped scenes rather than one-off hero placement
Asset Normalization Rules To Prove
The provided assets suggest a concrete normalization checklist:
- Load
.mtlbefore.objso palette textures resolve cleanly. - Apply nearest filtering to palette textures to preserve voxel sharpness.
- Compute bounding box per asset and recenter in
x/z. - Drop the asset to the stage by aligning min
yto ground level. - Normalize by target height or max dimension so
Manager,Robot, andInterncan share the same actor slot without manual scene edits. - Preserve an actor-level override for rotation, because exporter axes may still differ on future drops.
Experiment Structure For These Assets
The experiment should include three small controls specific to the provided asset set:
- asset picker: switch among the twelve supplied exports
- render mode:
source,override,hybrid tint - lighting preset: baseline, studio, dramatic, flat-readable
Optional but useful:
- compare mode: primitive placeholder vs imported asset
- multi-asset scene toggle: one hero actor vs several imported actors together
The asset manifest for this experiment should store, per file:
- source paths for
.obj,.mtl, and.png - display label
- intended actor type for qmap
- any per-asset rotation/offset overrides discovered during setup
- notes on whether the asset is a baseline, hero, or stress-test candidate
Runtime Session Follow-on
The next c23 branch should duplicate the c22 runtime-session experiment and use it as the first integration sandbox for imported models inside a real qmap topology.
Goal:
- keep c22's successful runtime-session controls and fixtures intact
- create a second c23 page dedicated to runtime-session model substitution
- replace primitive runtime actors incrementally rather than all at once
Recommended implementation path:
Phase A: Baseline Duplicate
- duplicate the c22 runtime-session experiment into a second c23 page
- keep all current session fixtures, grouping controls, and configurator wiring
- add a visible model-substitution plan panel to the page
Primary output:
- a stable before/after lab where runtime-session behavior can be compared without disturbing c22
Phase B: Root Process Substitution
- add model-backed actor support at the actor level
- replace only the root process first
- initial candidate:
Manager.obj
Why first:
- one actor only
- easiest to visually validate scale, orientation, and selection behavior
- already matches the conservative runtime/inventory integration that is currently shipping
Phase C: Child Process Substitution
- replace child processes selectively, not every runtime actor at once
- initial candidates:
Puzzle.objfor app-like subprocessesHelpers.objfor lighter utility/process roles
Questions to answer:
- do imported subprocess models remain legible when grouped densely?
- do labels still anchor clearly when several model-backed actors occupy one rug?
Phase D: External Host Substitution
- test a small host family using the monster assets
- initial candidates:
Monsters.objEye Monsters.objAbstract Monsters.obj
This is intentionally stylized and may or may not survive promotion. The point is to stress silhouette diversity in the runtime-session map, not to lock final semantics.
Phase E: File Actor Decision
- keep file actors primitive in the first integration pass
- revisit file substitution only after root/process/host model behavior is proven
Reason:
- files are the densest actor class in c22 fixtures
- primitive file meshes currently support readability better than arbitrary imported forms
- model-backed files could easily reduce map legibility before we learn anything useful
Runtime Session Model Mapping Proposal
First-pass mapping:
- Root process (
role: root) →Manager.obj - Child process (
type: app) →Puzzle.obj - Optional utility subprocess variant →
Helpers.obj - External host (
type: external-host) → monster family (Monsters,Eye Monsters,Abstract Monsters) - File (
type: file) → keep existing primitive file mesh for now
Follow-on candidate to revisit after the refreshed asset set settles:
Robot.objas a more dramatic root-process hero model if it proves readable without destabilizing the rug composition
Success criteria for this follow-on:
- the duplicated c23 runtime-session page matches c22 behavior before model substitution begins
- root process substitution works without breaking selection or layout
- at least one non-root imported actor type remains readable under entity-type grouping
- the experiment ends with a clear recommendation on which runtime actor classes should stay primitive
Replay-oriented synthetic live-session work has been split into c24.synthetic-live-session-replay so this cycle can stay focused on imported model loading, look-dev, and conservative runtime-session substitution.
Visual Study Areas
Lighting
Test a small number of intentionally different presets rather than exposing a dozen unrelated knobs. Initial candidates:
- Current baseline: ambient + directional
- Soft studio: hemisphere + key + fill
- Dramatic diagram: low ambient + strong key + rim
- Flat readable: bright ambient-forward lighting for maximum label/model clarity
For each preset, judge:
- silhouette legibility
- face readability at qmap's normal camera angle
- compatibility with labels and dataflows
- whether shadows help or just add noise
Material / Color Strategy
Test at least these approaches:
- Source material: use model-authored material/color if present
- qmap override: replace with qmap-controlled
MeshStandardMaterial - Hybrid tint: preserve value/roughness feel but tint to actor/state color
Questions to answer:
- Can imported models still communicate actor type/state as clearly as primitives?
- Does one shared material improve diagram coherence?
- Do per-model source colors fight the rest of the map?
Key Questions
- What is the thinnest integration seam: experiment-local loader, reusable
CustomModelMesh, or extension tocreateEntity()? - How should
.objmodels be normalized so different authoring tools do not produce wildly different scale/origin results? - Should qmap treat imported meshes as fully authored objects, or as geometry that still receives qmap-owned materials and state styling?
- Which lighting preset preserves readability best from the default isometric camera?
- Does the main
QMap.vuelighting setup need to become configurable, or should custom-model lighting live in a specialized wrapper/experiment first? - What round-trip workflow is easiest when the user iterates on the same test model repeatedly?
Success Criteria
- A supplied
.objfile loads reliably in a qmap experiment - The imported model can be swapped for a revised version without changing core experiment code
- At least one material strategy preserves qmap state readability (
default,active,ignored,highlighted) - At least one lighting preset produces clearly readable silhouettes and surfaces at normal zoom
- The cycle ends with a concrete recommendation for the reusable abstraction, if any
- Existing primitive actors remain untouched unless a narrow, low-risk extension is clearly justified
Implementation Plan
Phase 1: Loader Spike
- Add one experiment-local path for loading
.obj+.mtl+ palette PNG assets - Load
Manager.objinto an otherwise minimal scene first - Add
Bob Wire (Sheriff).objimmediately after as a second sanity check - Normalize its origin, scale, and upright orientation
- Capture the exact metadata needed for reliable reloads
Primary output:
- proof that the MagicaVoxel asset set can be loaded, centered, scaled, and rendered consistently
Phase 2: Actor Integration
- Replace one primitive actor with
Manager.objorReceptionist.objinside a normal qmap actor/dataflow scene - Preserve existing label placement, selection, and state transitions
- Decide whether imported-model actors should wrap
BaseMesh, extend it, or sit beside it
Primary output:
- one imported model functioning as a first-class qmap actor
Phase 3: Material Strategy Study
- Implement source, override, and hybrid material modes for the palette-textured assets
- Verify state coloring/opacity transitions still read clearly
- Compare whether edges/outlines are needed for irregular geometry
Primary output:
- a recommended default material strategy for diagram contexts
Phase 4: Lighting Study
- Implement a small preset switcher in the experiment
- Compare readability across the same scene and camera using
Receptionist,Robot,Puzzle, and one monster asset - Decide whether shadows, rim light, or hemisphere lighting materially improve the result
Primary output:
- one recommended lighting preset and one acceptable fallback
Phase 5: Round-Trip Workflow
- Make the supplied asset set easy to replace in-place with revised exports from the same toolchain
- Verify reloading behavior during iteration
- Document any naming, axis, palette, or export conventions that make the loop smoother
Primary output:
- a repeatable designer-to-qmap asset workflow
Phase 6: Promotion Decision
- Decide what, if anything, should move from experiment code into
src/ - Keep promoted API surface narrow
- Park anything that is interesting but not yet stable
Primary output:
- explicit recommendation: experiment-only, narrow promotion, or no promotion yet
Notes For Implementation
- Start with experiment-local code first; promote only after the shape is proven
- Prefer
OBJLoader+MTLLoaderfrom Three's examples before inventing a custom parser - Keep a primitive fallback path for comparison
- Default to qmap-owned materials unless source materials clearly outperform them
- For this asset set, treat
.mtlsupport as baseline because the palette PNG is part of the authored look - Expose a small set of lighting presets, not raw light-object editing UI
- Use
inbox/vox-exports-2/as the primary validation asset set
Visual record
13 captures — heroes, page slices, and full-page renders. Click to view.












