# Build your own 3D museum for your NFTs *An implementation outline, not code. Everything an AI coding tool needs to know on day one so the build takes a weekend, not a summer. Any layout, any library, any host.* We tried this once and failed. Lights, GIFs on walls, a CDN, a hang edited by hand: months, and a black screen on half the machines. The second attempt copied the structure of a page we admired and then learned, one bug at a time, what is below. The code is the cheap part. This is the expensive part, written down so you can skip it. --- ## The idea A first-person walk through rooms you designed, with your works on the walls at the size you chose, in the order you chose. Not a grid of thumbnails. A place. The rooms can be anything: one big hall, a ring around a courtyard, a house with a floor per collection, a long axis to one work, a cave. What matters is that the layout says something about the collection, and that you can change it in minutes without touching code. ## What you get - One web page. No build step for the page (one offline script prepares the art), no framework, no backend. A WebGL library, a JSON manifest of works, a folder of images and videos. Runs from any static host: GitHub Pages, Netlify, Vercel, Cloudflare, a bucket, your own server. - Walk with keyboard and mouse, or a thumbstick on a phone. Walk up to a work and a key opens its card: title, artist, collection, contract, token, links to the chain. - Videos play on the walls. Animated works animate. Generative works that are code run live. - Light and dark theme. A catalogue page with links back into the museum. - A plan you edit in a text file. A script places the works, a validator says what you got wrong. ## Decisions only you can make 1. **The organising idea.** What does a visitor understand by walking it? Chronology, a room per artist, a floor per chain, a procession toward one work, a cabinet of moods. Pick one; it decides the shape of the building. 2. **The rooms.** Six to twenty, 10 to 20 m a side, walls 10 to 13 m high and no ceiling so a 7 m work has air. Which connects to which. Where you start. 3. **The heights.** A work's height in metres is the only sizing decision per work. 2 to 3 m for most, 4 to 7 m for the ones that tower, more only for the one or two the collection is about. 4. **The order.** First room, last room, what faces each door. A visitor walks a story whether you wrote one or not. 5. **Your rules.** Two or three things the layout must never do, written down, because the validator will enforce them. Ours: nothing in the line of sight from the entrance to the centrepiece, nothing early on the way taller than the centrepiece deserves. Write these as a plan file before any code exists: one section per room with its position in metres, its doors, its sign text, and the works on each wall in order, a height each. Everything is derived from it. ## The build, in order Each step is a prompt to your AI tool and leaves something that runs. **1. Manifest.** From the wallet, a JSON file keyed by title: file, aspect, artist, collection, contract, token. Originals stay outside the repo. **2. Plan and placer.** The plan file by hand. A script that reads it and computes every work's position. A script that goes the other way, so a round trip changes nothing. **3. Validator.** Before the viewer. Fails on a work in a door, above the wall top, overlapping a neighbour, a plan out of date with the manifest, and your rules. **4. Viewer.** Walls, floor, frames, pictures, captions, signs. Zero lights (below). **5. Walking.** Then cards, motion, live works, wayfinding, splash, catalogue, tests, in that order. Details below. ## The hacks, specifically This is the part that took the time. Numbers are ours and work; start from them. **Rendering: no lights at all.** - Every surface is an unlit material (three.js: `MeshBasicMaterial`), tone mapping off, sRGB output, pixel ratio capped at 1.5, field of view 68 degrees. - Depth comes from a palette: in dark, background `#030303`, floor `#050505`, walls `#0b0b0b`, pillars `#1a1a1a`, near frames `#f4f4f4`, far frames `#333`. In light, a warm off-white plaster `#e2ded6` on a putty background `#b4afa6` with a dark floor `#4a4641`. Never paper-white; it is flat and blinding. - Linear fog in the background colour from 18 m to a little past your longest view (ours 82 m). The centrepiece is exempt from fog so it glows at the end. - A mirror floor: everything that hangs is added twice, the twin under a group scaled `[1, -1, 1]`, and the floor is a translucent plane at opacity 0.82 drawn last. Walls run 13 m below the floor so a reflection never shows the next room. - A painted light wash instead of lights: a few lines added to the wall shader that darken the bottom metre (ambient occlusion, factor 0.85) and the top above 3.6 m (factor 0.8). It reads as a lit room. It costs nothing. - A vignette overlay in CSS on top. That is the whole lighting model. Every version with real lights broke: 52 spotlights gave a black screen, a hemisphere light a white void, one lost flag turned it black. - All walls are merged into one mesh, all pillars into one, the floor into one. Ten rooms are three draw calls. **Textures.** - Each still is rebuilt to the wall it hangs on: height in metres times 200 pixels per metre, rounded up to a power of two, clamped between 256 and 2048, never upscaled. A 2.4 m work is a 512 px texture; a 7 m work is 2048. - JPEG at quality 88, progressive. PNG only with real transparency or under 768 KB. Five generative PNGs at 2048 px were 24 MB of a 53 MB first visit until this rule. - Stills get mipmaps and maximum anisotropy. Video and sprite textures get neither. - Static SVGs stay vector, cleaned to a square viewBox. Animated SVGs do not exist on walls (next section). **Motion: never a GIF.** - Every GIF and animated SVG becomes three files: an H.264 mp4 at a power-of-two size, a 4-by-4 sprite sheet as WebP at quality 82, and a poster JPEG. - The 5 nearest animated works within 22 m play as video; the choice is re-made every 250 ms, nearest wins, and the work you are looking at is pinned. Everything else flips its sprite sheet at 8 frames a second by moving the texture offset: one upload, ever. - Every video element is torn down when it leaves the set. A page that keeps 20 videos alive loses its WebGL context on a laptop. - Videos are silent, except works you flag. Their audio fades in as you approach: full within 4 m, gone at 16 m. - Animated SVGs are rendered to frames with a headless SVG renderer and go through the same pipeline. At least one browser refuses animated SVG as an image by every route. - When a collection's chain metadata points every token at one promo video, you will build the wrong footage eventually. Record a frame hash of the approved file and fail the build on anything else. **Live code works.** A generative work that is a web page runs in a hidden iframe behind the canvas, 960 px wide, and its canvas is copied to the wall texture at 30 fps. Only the 2 nearest within 14 m run; the rest are paused with the sketch's own stop call and show their poster. **Walking and collision.** Eye height 1.65 m, walk 4.2 m/s, run 8.5, jump 4.6 with gravity 14. Walkable zones are the room rectangles inset by a 0.35 m body radius, plus door rectangles bridging them; a point is walkable if it is in any zone. Movement is split into 0.12 m sub-steps so nobody tunnels a 0.3 m wall. Pointer lock on desktop; the first click locks. Mouse deltas over 400 px in one event are dropped: some browsers report a pointer warp as a jump. **The plan and the placer.** Works are spread along each wall between doors and sign pillars with at least 0.6 m between frames, 0.3 m from a corner, 0.3 m from a door edge, frames 0.14 m wide. One row is centre-hung at 2.0 m for works up to 2.4 m, 2.6 m for taller ones, never lower than 0.7 m at the bottom edge. Extra rows stack from 0.75 m with 0.75 m between for captions. On a wall broken by doors every split of the ordered list across the stretches is tried and the most even wins. The one work in the end room is centred, bottom edge at 1.0 m. **Captions and signs** are drawn to canvas textures with the page's own web fonts after they load, and rebuilt on a theme switch. A sign is an eyebrow line, a big name, one sentence; on first entry to a room it also appears once in the middle of the screen for 2.4 s. **Boot and survival.** - An inline script runs before any module: checks for WebGL2 and import map support, creates the canvas and its WebGL context once, and gives that canvas to the 3D library. Some browsers hand out one context and refuse the next. A 15 s timer explains itself if the engine never loads. - The library is served from the site, version in the folder name, cached for a year. A CDN on the critical path fails behind ad blockers and office proxies. - Import map addresses start with `./` or `/`. A bare path makes the browser drop the map silently. - Every art URL carries an 8-character content hash, and the art folder is cached for a year. Browsers serve stale images in ways no header prevents. - The global error handler ignores errors from other files and everything after the visitor is inside. Wallet extensions throw on every page. - The splash enables its button when the walls exist, not when the art is loaded. Behind it the camera drifts slowly so the page moves before the click. **Deep links and wayfinding.** `?work=` lands you in front of a work: start 3.5 m back and level, glide in over 1.8 s once inside. A strip of dots on the screen edge marks the rooms in order; two keys jump between them with the same landing. No map is needed when rooms have names and an order. **Tests without a browser.** - A smoke test runs the page's script in a plain JavaScript runtime against a stub for the 3D library and the DOM. It catches a typo, an undefined name, a bad import map: everything that leaves a black screen. - A test for the video budget runs the actual motion code with fake distances. - A journey mode in the page walks every work, logs frame rate per room and per work, counts context losses, screenshots as it goes, and posts the report to a tiny local server. You read a file instead of asking a friend what they saw. ## Hosting Two branches. One holds everything; one is generated from it and holds only the files the page loads at runtime: the page, the manifest, the art, the fonts, the library. A script does the copy and refuses a file over 25 MB. Any static host serves the second branch. If you want to stay anonymous, every file and every commit is public eventually; put nothing identifying in either. Before you host a work's files publicly, check its licence: owning a token usually does not include the right to re-host its media. ## How to use this with an AI tool Give it this document and your plan file. Ask for step 1, run it, look. Then step 2, and so on. Make it write the validator before the viewer and run it after every change. When something looks wrong, do not let it reach for a light. When it wants a framework, a build step, or a CDN, say no. When it is done, walk the museum yourself once, slowly, with a friend on a different machine. The outline is the asset. The building is yours.