WallpaperMachine
당신이 사랑하는 배경화면, 맥에서 새롭게 만나보세요
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A first-person web walkthrough of the apartment, built from the Blender model. Visitors walk around with the keyboard and mouse, or by touch on phones and tablets, open doors (the glazed ones slide), drawers and cupboards, flip the light switches and lamps, watch TV and put a record on, sit on the sofas and chairs and lie on the bed. Visitors who are there at the same time see each other and share the flat.
It has two halves: blender/ turns the .blend into web assets (headless Blender, run through the Makefile), and web/ is the viewer (Vite, TypeScript, three.js).
/Applications/Blender.app (use another location with BLENDER=/path/to/Blender make ...)basisu and dwebp for the export's texture compression: brew install basis_universal webpcd web && npm ci (the postinstall step copies the Draco decoder and the Basis transcoder)For which changes need Blender and which don't, and the full edit → export → check → publish workflow, see docs/changing-the-model.md.
~/Documents/Untitled.blend in Blender.cp ~/Documents/Untitled.blend .Good to know:
blender/tools/model_details.py added the drawers and cupboards that open (with insides), the French door leaves, lamp colour temperatures, bulbs, LED strips and sockets. It has run once on Untitled.blend and refuses to run again; edit those things by hand from now on, following the conventions below.blender/tools/sliding_doors.py then rebuilt the glazed doors as the real ones are, sliding: a fixed panel and a leaf that slides over it. It also tagged the turntable's platter to spin. It too has run once and refuses to run again.blender/tools/loose_contents.py removed the baked glasses, plates and cutlery from the drawers and cupboards and wrote where their insides are to web/src/prop-frames.json (see Loose objects below). It has run once and refuses to run again.blender/tools/music_studio.py turned Quarto 03, the workshop, into a music studio: the cubby shelving, the wardrobe and the pegboard went; a digital piano and its bench, a synth and monitors on the workbench (the 3D printer kept at its end), an electric guitar, a bass and two ukuleles on an oak slat wall, acoustic panels, a bass trap and a rug came in. It has run once and refuses to run again.blender/tools/electric_drums.py then stood an electronic drum kit sideways on the studio's rug, the drummer facing the door, with a throne whose backrest sets which way it faces. It has run once and refuses to run again./private/tmp with factory settings, so your add-ons don't load.make export-preview # about 1 minute, low-quality lighting, good for checking changes
make export-final # about 1 to 1.5 hours on the GPU
make export-surroundings # about 30 seconds: only the view through the windows, nothing else changes
make export-seats # seconds: only where visitors can sit and lie (seats.json); add VARIANT=kitchen-wall for that layout too
make export-final VARIANT=kitchen-wall # the kitchen with its wall, as it is now (see Kitchen wall, below)
If the GPU bake hangs, quit Blender and try again, or run on the CPU (much slower): WEB_BAKE_DEVICE=CPU make export-final.
Outputs:
web/public/assets/: what the viewer loads. surroundings.glb is the neighbourhood seen through the windows.build/debug_map.png: a floor plan check. Red = closed door, green = open door, blue = light switches, yellow = spawn point.build/web-export.blend: the baked scene, for inspection.Exports leave web/public/assets/audio/, climate.json and DATA-SOURCES.md in place.
After Blender, the export runs web/scripts/optimize-glb.mjs on the .glb files: identical materials and textures are merged, a mesh's primitives that share a material are joined, and the images become KTX2 GPU textures (base colours ETC1S, normal maps UASTC). Compressed textures take an eighth of the GPU memory of plain RGBA for base colours, and upload without decoding. The lightmaps stay lossless WebP: RGBM doesn't survive block compression (blocks straddle charts of very different light), so they keep their shrink on phones.
The export checks itself. If something is wrong it prints EXPORT CHECKS FAILED with the reasons and leaves the previous assets untouched.
Keep these names in Blender, or the export won't find things:
Floor - <Room>. Ceiling lights: Ceiling light <Room>.Door N (A / B) lintel and Glazed Door N (A / B) lintel, where A and B are the rooms on either side.Door N infill (a filled-in opening), Door leaf N, and the glazed parts (glass, mullion, rail).Cam_Entrada is where visitors start. Cam_Sala, Cam_Kitchen and Cam_Bedroom are viewpoints (used for ?view= and the reference renders).Window N (Exterior / Room) glass and Glazed Door N (Exterior / Room) glass; the viewer measures the sunlight entering through them.Portal … are window fill and count as sky light; every other lamp is switched with its room's ceiling light, unless it's in a lamp group (below).Glazed Door N (A / B) fixed panel, with … glass, both part of the room and solid to walk into) and one leaf, Door leaf Nb, tagged web_move = "slide" with web_dir and web_dist. It sits 5.8 cm in front of the panel so they pass. Its glass is … glass.001 with a web_leaf custom property naming the leaf it joins. Both glass names keep the window pattern so the sun still comes in. (Two leaves at one opening, Door leaf Na and Nb without web_move, still make French doors.)Custom properties (Object Properties > Custom Properties):
web_lamp = "<name>" on a light, and on the meshes around it (shade, base, bulb), makes a lamp that switches on its own: click it in the walkthrough. Its glowing materials glow with it. Lamps with the same name in one room switch together.web_seat = "sit" | "lie" on a mesh: visitors can sit on it (a sofa cushion, chair, stool or bench) or lie on it (a bed). The export finds the seat's height, which way it faces (away from its back, toward the table a stool is pulled up to, or away from the wall behind a bench; a bed's head is at its pillows) and one spot per 55 cm across it (80 cm on a bed). The model's own furniture is matched by name instead (SEATS in blender/pipeline/config.py); the toilets are left out on purpose.web_instrument = "piano" | "synth" | "electric guitar" | "bass guitar" | "ukulele" | "drums" on a mesh: visitors can play it (see Music studio, below). It stays in the baked room; the manifest lists its name, room, box (for clicking) and the middle it sounds from. Its name in the model is how the others' browsers know which one is played, so two of a kind need different names. With web_prop too, it is a loose thing played in hand (the manifest names its node), and its box is where it hangs.web_screen = "tv" | "monitor" | "laptop" on a screen mesh: it shows a generated picture when switched on. The TV's bias light is lamp group tv and follows the TV (on, or showing a game).web_move makes a mesh open. "slide" with web_dir (a world direction, e.g. [1, 0, 0]) and web_dist (m) for a drawer; "hinge" with web_pivot (a world point on the hinge line), web_axis (the hinge direction) and web_angle (radians, right-handed about the axis) for a door or flap; "spin" with web_pivot, web_axis and web_rpm for the turntable's platter, which turns while the record plays. On a Door leaf, "slide" makes a sliding door. Give the carcass behind it a hollow (a boolean against a hidden cutter, as the script does) so there is an inside to see.web_prop = "<name>" on a mesh makes it a loose thing visitors can pick up, carry, put down and throw (the prompt calls it by that name); the export keeps it out of the baked rooms and the collision and exports it as its own node. web_tips = true | false says whether it falls over when it lands hard (default: tall and narrow ones do; it must be true/false or 1/0). Long flat things (knives, tools, books) are set down lying and never tip, whatever web_tips says. tagged the flat's small things on surfaces once and refuses to run again.The glasses, mugs, plates, bowls, cutlery, spice jars, food containers, oil and wine bottles, books, glasses cases, screwdrivers and filament spools in the drawers and cupboards are made in the browser (web/src/prop-models.ts), not in Blender: visitors pick them up (click), put them down on any surface facing up (click where the ghost shows; the held thing's ghost shows where it'll go), throw them (hold the right button or F, longer is harder) and drop them (Q); on touch, tap to pick up and put down, and Throw and Drop buttons show while holding. Everyone in the flat sees it, and things stay where they're left until the flat empties.
web/src/prop-frames.json, written once by blender/tools/loose_contents.py (which also removed the baked stand-ins there, and has run once and refuses to run again). Moving or resizing one of those drawers or cupboards in Blender means updating its entry by hand, or the objects will float or sink.web_prop in the model and are loose too, with the same pick up, put down and throw; they start where the model has them, and a thing modelled on another one rests on it (web/src/surface-props.ts).LAYOUTS in web/src/prop-places.ts, one per fill. Throws, light, the hand: CONFIG.props (web/src/config.ts).When the automatic choice is wrong, adjust the tables in blender/pipeline/config.py:
EXPECTED_DOOR_COUNT: how many interactive doors the export check expects.SWITCH_OVERRIDES: move a room's light switch (door, side, and face: outside to put it on the other room's wall).DOOR_OVERRIDES: fix a door leaf's hinge side or swing direction (sign alone also works for a leaf modelled closed in its doorway, as the French doors are).QUALITY: bake resolution and samples for preview and final.SURROUNDINGS_COLLECTION_PREFIXES: which collections make the view through the windows.SEATS, SEAT_SPACING: which furniture visitors can sit or lie on, and how many fit.The audio lives in web/public/assets/audio/: doors, switches and footsteps from Kenney's CC0 packs, birdsong and music from CC0 OpenGameArt uploads, and two generated room-tone loops. CREDITS.md there lists every source. To rebuild the files (needs ffmpeg), download the sources it lists into one folder, unzip each Kenney zip into a folder named after it (kenney_rpg-audio, kenney_impact-sounds, kenney_ui-audio), and run web/scripts/build-audio.sh <folder>. The music is off until the visitor starts the turntable in the Sala; it plays from the top each time and fades out when the record stops. Levels, fades, footstep spacing and which rooms sound outdoors or like a kitchen are in CONFIG.audio and CONFIG.roomAmbience (web/src/config.ts).
The sun is live: its position comes from the NOAA solar equations for the flat's coordinates and the chosen date and time (Lisbon time), and its strength and the sky's from a typical day of each month (web/public/assets/climate.json, sources in web/public/assets/DATA-SOURCES.md). The day layer of the bake is sky light only; the sun, its shadows (from the flat, the building and neighbours within 200 m) and the light it bounces into each sunlit room are added in the browser.
SITE in blender/pipeline/config.py (Blender's origin and the compass bearing of +Y).cd web && node scripts/fetch-climate.mjs 41.117327 -8.653914.make render-refs SUN=2026-12-21T10:00 renders the viewpoints lit by the same sun into build/ref/.CONFIG.sun in web/src/config.ts (bounce, skyGain, ambient, shadow bias, colours, time-lapse speed); CONFIG.lightSpill keeps a little of each room's lamp light with the lights off; CONFIG.exposure sets the overall brightness.CONFIG.flatLighting.web/src/mirrors.ts): any thin slab with the Mirror material.web/src/sky.ts) has a gradient, the sun's disc and glow where the sun really is, and drifting clouds. Cloud cover follows each month's average from climate.json, scaled by CONFIG.sky.cloudScale. Colours and the disc's size are in CONFIG.sky.The flat runs on Philips Hue, so the walkthrough has Hue's own scenes in the sun panel's Lights menu, for the room you're in or the whole home; H steps through them. The white scenes (Bright, Read, Concentrate, Energize, Relax, Dimmed, Nightlight) set each light's brightness and colour temperature with the bridge's own values (bri 1–254, ct in mireds); the colour scenes (Savanna sunset, Tropical twilight, Arctic aurora, Spring blossom, Galaxy) give the room's lights a palette of colours in turn (hue 0–65535, sat 0–254), and Off switches everything off (HUE_SCENES in web/src/hue.ts). The baked lamp light is dimmed and re-tinted live, relative to the colour temperature it was baked at (CONFIG.hue.bakedKelvin); deep colours are capped at CONFIG.hue.colourGain times the bake per channel so they don't wash out to white. Clicking a lamp or a switch afterwards keeps its dimmed level, and the other visitors see the scenes too (visitors on an older version see colour scenes' brightness but not their colours).
Linking the real bridge comes later: fill in CONFIG.hue (the bridge's address, an app key, and each room id's Hue room) and give main.ts a HueLink that sends applyScene(rooms, scene) to the bridge. Until then scenes change only the walkthrough.
cd web && npm run dev # http://localhost:5173
Controls: WASD to walk, Shift to walk faster, mouse to look, click to open doors, drawers and cupboards and to use switches, lamps, screens and the turntable (a click on the TV opens its panel: the remote, and draw and guess), click the bottle of red on the kitchen counter to drink (see Wine, below), click an instrument in the music studio (Quarto 03) to play it (see Music studio, below), click the rifle rack on the office's balcony (the Varanda) to pick up a rifle and the red button beside it to start a round (see Shooting range, below); with the rifle in hand a click fires; click a sofa, chair, stool or bench to sit on it or the bed to lie on it (moving gets you up), C sits on the floor or gets you up (or, at a game board, asks first), click a chair at the chessboard, the backgammon case or the Go board on the dining table to play, L toggles the current room's light, H steps through the Hue scenes in the current room, M mutes sound, G shows who's here, T opens or closes the TV's panel, , and . move the time back and forward 15 minutes (hold to keep going), Shift + , and . move the date a week, P plays the day (one hour of sun every two seconds), R turns the real sun and sky on or off, K puts the kitchen wall up or takes it down, and Esc pauses. The sun panel at the bottom left is always on screen. Press Tab to free the mouse and use it while you keep walking with WASD; press Tab again, or click the view, to look around. It also works while paused.
On phones and tablets (no mouse): drag on the left half of the screen to walk (a stick appears under your thumb; push past its ring to walk faster), drag on the right half to look around, and tap doors, switches, lamps, screens, the turntable, seats and the bed. Tap a chair at the chessboard, the backgammon case or the Go board on the dining table to play, and the wine bottle to drink. Tap the rifle rack on the balcony for a rifle; a Fire button shows while you hold it. Tap the TV for its remote and to draw and guess. Both thumbs work at once. The buttons at the top right open the sun panel, show who's here, toggle the room's light, mute, sit on the floor (or get up) and pause. Android goes fullscreen on start; iPhones can't. Tuning (stick size, dead zone, look speed, tap timing) is in CONFIG.touch. To try it on a desktop, use the browser's device toolbar with a phone selected and reload.
Handy URL options:
?room=friends: meet only the people who open the same ?room= link (see Visitors together, below). ?solo explores alone.?view=Cam_Sala&autostart: jump to a viewpoint and skip the start screen.?sun=2026-12-21T10:00: open at that moment (Lisbon time). It combines with ?view= and autostart.?kitchen=wall: open with the kitchen wall up, as it is now.?debug=lightmap: show the lighting alone, without textures.?stats: frame time (and the GPU's own, where the browser can measure it), draw calls, triangles, shader programs and texture memory in the top left corner.?autostart&bench: the same walk every time, for before-and-after numbers: the spawn point, each viewpoint and the middle of each room, turning a full circle at each. The report shows in the corner and in the console (window.benchReport). Hidden browser tabs slow their frames down, so keep the tab in view; the GPU times are right either way.?nocull: draw every room, not only the ones in view (see Lighting, below), to compare.Tests and comparisons:
make test-blender # export pipeline tests
make web-test # viewer tests
make render-refs # Cycles reference renders in build/ref/ to compare against the viewer
The model as saved is the plan: the kitchen opened up into the Sala, with the island, coffee station and pendants (collection Kitchen - open, and the Kitchen opening boolean on Walls). The flat as it is now has the wall, with units along it (Kitchen - closed). Kitchen - shared is in both.
The walkthrough shows either: the checkbox in the sun panel or K switches. Each is lit by its own bake, so the wall version is a second export into web/public/assets/variants/kitchen-wall/ (make export-final VARIANT=kitchen-wall, as long as the main one). It shares the climate, sound and neighbourhood with the main export, and the main export leaves variants/ alone. Switching reloads the page into the other version; where you stand, the time, and the doors, lights and screens carry over. Run both exports after changing the model, so the two stay in step.
What a variant changes is VARIANTS in blender/pipeline/config.py: the collections to show and hide, the modifiers to remove, and the floor faces to drop under the closed opening (otherwise the Cozinha's floor would still reach the Sala's under the wall, and the export would treat them as one open-plan room).
On the Varanda, the office's balcony, a rifle rack stands at the railing with a big red button beside it (web/src/range-*.ts). Click the rack for a rifle of your own (click it again, or step off the balcony, to put it back); with the rifle in hand a click fires along the crosshair, iron sights, no zoom, a shot every 0.6 s. The red button starts a round for everyone holding a rifle: a 3-2-1 countdown, then 60 s of clay pigeons (10 points), gold clays (30) and drones (50) flying over the street, 12–30 m beyond the railing, then the final scores for 10 s. Everyone competes for the same targets: each browser makes the same flights from the round's seed, each shooter's browser judges its own shots at once, and every browser gives a target to the earliest hit, so all agree on the scores. Anyone in the flat sees the targets, the shatters and the others' shots. Visitors on an older version see no targets or shots. Sizes, speeds, points and the mix are in CONFIG.range; the shot's sound (made in code, no file) is CONFIG.audio.shot. The design note is docs/superpowers/specs/2026-10-28-balcony-shooting-range-design.md.
People who open the walkthrough at the same time see each other as little pastel toy animals after the wooden reindeer on the sofa (a reindeer, bear, bunny or fox in its visitor's own colour, with googly eyes and a face of its own, web/src/creatures.ts), with the name they typed on the start screen above it (remembered per browser; blank reads "Guest"), and a "With …" line under the room name. The flat is shared: doors, drawers and cupboards, lights, screens and the turntable are in the same state for everyone, and whoever arrives later gets the flat as the others left it. The sun's date and time are not shared: they stay each visitor's own.
Whoever arrived first is the host: every browser works it out alone from the arrival time each visitor sends in its hello, earliest first and ties to the lower peer id, so all agree without a server. The arrival time is kept per room in the tab's sessionStorage, so a host who reloads stays host; one who closes the tab hands over to the next to have arrived. For now the host only wears a crown; the party's music, lights and TV controls will go to the host. G (or a click on the "With …" line with the mouse free, or the people button on touch) opens "Who's here": everyone in arrival order with their animal's colour, a 👑 on the host, the room they're in and 🔊 while they talk. The design note is docs/superpowers/specs/2026-10-28-party-host-guest-list-design.md.
There is no server. The browsers connect to each other with WebRTC through Trystero, which finds the other visitors through public Nostr relays. Everyone on the site shares one room by default; ?room=<name> gives a group a room of their own, and ?solo stays alone. Trystero loads only after the start click, and if it can't load or a network blocks WebRTC, the walkthrough carries on alone (a warning in the console). Visitors whose networks block a direct connection (strict NAT, mobile carriers, some offices) are relayed through Cloudflare's TURN service, once it is set up (see Deploy); without it they explore alone. It connects everyone to everyone, so it suits a handful of people, not a crowd. Reference renders (?autostart) never join.
Visitors can talk. The start screen's "Talk with your voice" checkbox (on by default, remembered) asks for the mic on the start click; without it, or if the mic is refused, they still hear the others. A voice comes from its animal's mouth, quieter with distance and muffled by walls and closed doors, and the animal's mouth moves with it; the "With …" line marks whoever is talking. V turns the mic off and on (a mic button on touch), M mutes everyone's voices with the other sounds, and pausing turns the mic off. Everyone's mic goes to everyone, so it suits a handful of people. Headphones are recommended: on speakers, the others may hear themselves echoed back. Tuning is in CONFIG.multiplayer.voice; the design note is docs/superpowers/specs/2026-10-27-proximity-voice-design.md.
Visitors sit and lie down where the others can see it: on a sofa, chair, stool or bench their animal sits leaning back with its legs out in front, swinging its front ones; on the bed it lies on its back, head on the pillows and feet in the air; C sits it on the floor. A seat someone is on is taken ("Someone is here"); a sofa cushion seats two and the bed sleeps two. Getting up puts you a step in front of the seat, or as near it as you can stand on floor you can walk to from where you sat down (never in a pocket boxed in by furniture); when even that is far from the seat, as for a stool pulled up to a desk, back where you sat down from. Visitors still on a version from before seats see a seated visitor standing where they last stood. Eye heights, the lying view and timing are in CONFIG.seats, and the animals' sitting and lying in avatars.ts.
Two visitors can play chess at the dining table. The board stands between the two chairs at the table's end; the chair on the window side plays white. Sitting in one opens a panel with the board, your side at the bottom (the mouse is freed, as with Tab): click a piece to see where it can go, then a square to move it there; a pawn reaching the last rank asks what it becomes. Resign and Offer draw work on your move (an offer lapses when the other player moves instead of accepting). A game starts when both chairs are taken and ends by its rules (checkmate, stalemate, threefold repetition, the 50-move rule, insufficient material), by a resignation or an agreed draw, or when a player leaves the chair, which loses it; a closed or timed-out tab leaves it too. While the panel is open the movement keys don't move you: Stand up (or C) asks "Stand up and resign?" during a game. The final position stays on the table until the next game; to play the same person again, one of you stands up and sits back down. Everyone else sees the game on the table in 3D as it's played, and whoever arrives later gets it with the flat's state. There is still no server: every browser checks the moves with chess.js (BSD-2-Clause), each change sends the whole game in a game message, and every browser works out a player leaving from the empty chair. Visitors on an older version see the players sitting but no board. Tuning (where the boards stand, the table's height, the slide, the colours) is in CONFIG.games; the design note is docs/superpowers/specs/2026-10-28-dining-table-games-design.md.
Backgammon is played at the middle pair of chairs, in a small travel case between the fruit bowl and the books; the window side plays white again. The panel shows the board from your chair, with a Roll button, the dice (used ones faded) and both pip counts. White presses Roll for the opening, one die each (a tie is rolled again), and the higher die starts, playing both dice. On your turn press Roll, then click a checker (or the bar) and one of the marked points (or the tray, to bear off), one die at a time: doubles give four moves, checkers on the bar enter first, a point with two of the other side's checkers is blocked and a single one is hit, you must use both dice if you can (the larger if only one fits), and the turn passes by itself when the dice are used up or none can be. A game ends when a side has borne off all 15, as a single game, a gammon (the loser has borne off none) or a backgammon (the loser still has a checker on the bar or in the winner's home board); there is no doubling cube. Resigning, standing up and leaving work as in chess. The rules are our own (web/src/backgammon-game.ts); dice are rolled in the roller's browser, which is fine among friends.
Go is played on a 9×9 board at the third pair of chairs, on the table's rounded end right of the runner; the window side plays white, so black, across the table, moves first. The panel shows the board from your chair: on your move the points you may play show a faint stone under the pointer, and a click places yours there; Pass gives up your move. Stones left without liberties are captured, a stone may not take its own last liberty unless it captures, and a ko can't be retaken straight away. The line under the board counts the prisoners each side has taken and says when the other player passed. Two passes in a row end the game, counted by area: each side's stones plus the empty points only its stones surround, and 7 komi for white. Stones count as they stand, so capture dead stones before you pass. The result reads like "White wins by 4", with the count under it; an equal score is a draw. Resigning, standing up and leaving work as in chess. The rules are our own (web/src/go-game.ts); a game's moves are written as points, D4, with the columns A to J skipping I.
Visitors can play draw and guess on the Sala's TV, best with three to six. A click on the TV (or T, when you're not at a game board or paused) opens its panel, with the mouse freed: the remote (On/Off and Next channel, shared with everyone as before) and the game's lobby. Press Join to play; with two or more joined, any player presses Start. Each player draws once, in the order they arrived: the drawer picks one of three words (one is picked for them after 15 s) and has 80 seconds to draw it on the panel's canvas, with eight colours, two pen sizes, an eraser, Undo and Clear (mouse, pen or touch). Everyone in the flat sees the drawing live on the TV, with the time, the blanks and the scores beside it; the players also see it in their panel, with the word as blanks (one letter shows at half time and another at three quarters, never more than half the word) and a box for guesses (Enter sends). Case, accents, spaces and hyphens don't matter. A right guess scores 300 points at the start of the drawing time down to 100 at its end, and the drawer 50; a guess one letter off tells only its guesser it's close; wrong guesses show in the feed. The turn ends when the time is up or everyone has the word, the word shows for 5 s with the turn's points, and the next player draws; after the last turn the final scores show for 15 s, ties sharing a place. You can join during a game (you guess straight away and draw at the end), and leave with Leave or by closing the tab: your score stays on the board, and with fewer than two players left the game ends. While a game runs the TV shows it whatever its channel, even switched off, and the remote waits ("A game is on"). The panel closes with Close, Esc or T and opens again any time; the game goes on. While it's open the movement keys don't move you, and typing a guess never triggers the walkthrough's keys. There is still no server: the drawer's browser holds the game (the word never leaves it before the reveal), judges the guesses sent to it alone, and hands the game to the next drawer; if the drawer's tab goes (or is hidden for 15 s), the player who arrived first ends that turn early (points already won stay) and passes it on. It all travels in a draw message, and visitors on an older version see the TV's channel. The words are in web/src/draw-words.ts, the timing and points in CONFIG.draw; the design note is docs/superpowers/specs/2026-10-28-tv-draw-and-guess-design.md.
Quarto 03 is a music studio (blender/tools/music_studio.py, the drums blender/tools/electric_drums.py), and its piano, synth, electronic drum kit, electric guitar, bass and two ukuleles can be played.
The piano and the synth are played from a panel: click or tap one and a panel opens at the bottom with its keys and eight chords, the mouse is freed for it, and the keys play instead of walking. The home row plays white keys from C (A S D F G H J K L ; '), the row above the black ones (W E T Y U O P), Z and X change octave, and 1 to 8 play the chords C G Am F Dm Em D E. A note lasts while its key is held. The panel's keys and chords work with the mouse or a finger too. Esc or Close closes it; while it's open, only M (mute) keeps its usual job.
The drum kit stands on the rug, its throne facing the door: click the throne (its backrest shows over the kit) to sit at it, and click the kit for its panel of pads, laid out as the drummer sees the kit. The keys follow the kit: Space (or B) the kick, F the snare and V a side stick, J the hi-hat with U open and N the pedal (the right hand crossing over, as a drummer's does), E and R the toms, I the floor tom, W the crash, O the ride and P its bell. A drum rings out by itself, and closing the hi-hat stops it ringing open. Each hit is a General MIDI drum note, so it travels to the others like any note. The pads work with the mouse or fingers, several at once.
The guitar, the bass and the ukuleles hang on the slat wall as loose things (web_prop, see Loose objects): click one to take it off its hanger. It's held across the body, neck up to the left and strings turned up to you, and you keep walking with WASD while you play it with the keys on the right: H J K L ; ' are the white keys from C, U I P [ ] the black ones and \ the B; 1 to 8 strum the chords, Space or a click on nothing strums the last one again (Shift+Space strums up), Z and X change octave. While one is in hand those keys are its, so H, K, L and P don't switch scenes, the kitchen wall, the light or the day. Its strings ring out by themselves, one note a string, and a chord is strummed string by string. Click its hanger to hang it back; Q drops it and F throws it like any loose thing, and the others see it in your animal's paws. On a touch screen its panel shows at the top while you hold it, with Drop to let go.
The sound is made in the browser, with nothing to download (web/src/instrument-sound.ts): plucked strings by Karplus–Strong synthesis, the piano from its partials (slightly inharmonic, two strings a hair apart, a hammer's thud), the synth from detuned sawtooths through a resonant filter, the drums from falling sines (the kick and toms), noise (the snare's wires, a stick's attack) and clashing square waves (the hi-hat and cymbals, with the ride's ping). Samples are made on first use and an instrument's notes are made ahead when its panel opens. Everyone in the flat hears what is played, from the instrument (or the hands holding it), fading with distance as a voice does: each note is a small music message (web/src/instruments.ts). Visitors on a version from before the studio hear nothing. Ranges, starting octaves, strum speed, how far an instrument carries, how many notes sound at once and how a guitar is held are in CONFIG.instruments, and the level in CONFIG.audio.instruments.
A bottle of red stands on the kitchen's back counter between the sink and the hob, with a glass beside it, in both kitchens (web/src/wine.ts). Click or tap it to take a sip: the bottle lifts and tips your way. Each sip makes you a little drunker (web/src/drunk.ts); four sips is fully drunk, and more only makes it last longer. Drunk, your view wobbles (it rolls and sways, so aiming gets harder too) and blurs, and it wears off steadily: about three minutes from fully drunk to sober. Drink too fast, more than ten sips within two minutes, and you black out: the view fades to black, you sink to the floor, and a few seconds later you come to sitting there, still fully drunk (moving gets you up). The blur is a CSS filter on the canvas, so it adds no render pass, and there is none while sober. The bottle never runs out and isn't shared, but how drunk you are travels with your pose (an 8th number while drunk), so the others see your animal sway; visitors on a version from before the wine see a drunk visitor's animal stand still until it sobers up. Reference views have no bottle. Tuning (where it stands, how much a sip adds, how fast it wears off, the wobble, the blur, the blackout and the animals' sway) is in CONFIG.wine; the design note is docs/superpowers/specs/2026-10-28-wine-bottle-design.md.
Tuning is in CONFIG.multiplayer (web/src/config.ts): the room, how often poses are sent, how long a silent visitor stays, the colours, and the creatures' size, walk, bounce, googly eyes, blinking and how they dim with the room's light. The design note is docs/superpowers/specs/2026-10-25-multiplayer-design.md.
The repo is self-contained: the exported assets in web/public/assets/ (about 36 MB) and the source Untitled.blend are committed, so a host can build straight from GitHub.
Vercel: import the GitHub repo and set Root Directory to web (Vercel usually suggests it). web/vercel.json installs with npm ci, builds, and serves dist/. Leave the other build settings alone. Every push to main redeploys.
TURN for multiplayer (optional): in the Cloudflare dashboard, under Realtime → TURN Server, create a TURN key (the free tier covers 1,000 GB a month), and add its key ID and API token to the Vercel project's environment variables as CLOUDFLARE_TURN_KEY_ID and CLOUDFLARE_TURN_API_TOKEN, then redeploy. web/api/turn.ts (a Vercel function) hands each visitor credentials that expire after a day, so the token never reaches the browser. Without the variables, /api/turn answers an empty list and visitors connect directly only. Only connections that can't be made directly go through TURN; they stay end-to-end encrypted. Local npm run dev has no /api, so it never uses TURN.
Updating the live site: re-export (make export-final), then commit the changed files in web/public/assets/ (and Untitled.blend) and push.
Building locally: cd web && npm run build writes the static site to web/dist/. Any static host works. Serve it from a URL ending in /, because asset paths are relative.
File names are stable across exports. Vercel revalidates static files on every visit, so that's fine there. On another CDN, purge the cache after re-exporting, or use short cache times for assets/manifest.json, assets/*.glb (including surroundings.glb) and assets/lightmaps/*.
Each room's wall switch turns on its ceiling light and every lamp without a lamp group; lamps with one (web_lamp) switch on their own. Lamps use real colour temperatures (2700 to 4000 K). Each group is baked on its own: a room's wall switch as a full colour layer, everything else (its lamps, an open-plan neighbour's lamps, and the light from a lit room through each of its doors) as 8-bit luminance layers three to a texture (RGB), with the light's colour as a tint (blender/pipeline/packs.py, layers.py). The light through a door follows how far it's open and how bright the room behind it is.
Drawers and cupboard doors are baked open, so their insides are lit; the rooms are baked with them taken away, so the hollow carcasses get light inside and no open front leaves a shadow behind.
Lit bulbs, shades and ceiling diffusers get a soft halo (src/halos.ts, CONFIG.halos): one depth-tested sprite each, much cheaper than a full-screen bloom. The TV's and monitor's light is baked like a lamp's, for an all-white picture (SCREEN_LIGHT_WATTS in blender/pipeline/config.py); the viewer scales it by the picture's brightness and tints it with its colour.
Surfaces:
blender/pipeline/proc_materials.py), plus a normal map where the material has relief (a Bump or Normal Map node into its BSDF: the tiles' grout, the floor's plank seams, the brick). A patch under FLAT_ALBEDO_MAX_AREA (a knob, a hinge) gets the material's mean colour instead of a texture of its own.SHARED_TILES in blender/pipeline/config.py, made in tiles.py): Oak, on about 120 pieces of furniture, is a 1024 px grain at 0.6 m a tile, box-projected with the grain along each piece's length. One material instead of a hundred, and the same grain everywhere.src/details.ts, CONFIG.details, matched by name): plaster's mottling, fabric's weave and slubs, plain wood's grain, as a base colour multiplier and a normal map, box-projected at real size.src/lightmaps.ts, src/reflections.ts). Each room has two reflection probes, the room seen from its middle, rendered by the export (probes.py) once lit by the sky and once by the wall switch, mixed as the lightmaps are and projected onto the room's box, so a floor reflects the window where it is. The reflection is as bright as the baked light where the surface is against the light at the probe, so a cupboard's inside doesn't mirror the lit room. Without probes (an older export) surfaces reflect three's stand-in room environment at the local light level. CONFIG.reflections scales them.src/contact-shadow.ts, CONFIG.contactShadow): they're drawn without the baked light, and without it they float.Only what's in view is drawn: the export works out which rooms each room can see, and through which doors (visibility.py, rays from a grid of eye points through glass and door leaves), and the viewer draws a room, with its doors, drawers, screens, bulbs, mirrors and loose things, only while one of those sets of doors is all open (src/visibility.ts). A mirror draws what its own room sees. A frame that redraws the sun's shadow map draws everything, since what's hidden casts no shadow.
Keep the frame cheap: every live light (three.js light) is paid for by every pixel of every lit surface, and a full-screen pass costs about as much as one. Bake what can be baked. And keep it smooth: stutters, not slow frames, are what feel laggy. At load every shader is compiled in the background (src/warmup.ts), including the screens' pictures, the halos and the variants a mirror needs, and lightmaps go up to the GPU two per frame instead of when a room first comes into view. Mirrors draw at half resolution (CONFIG.mirrorScale). Drawers and cupboards don't redraw the sun's shadow map as they move; doors still do. While the time-lapse plays, the shadows are redrawn each time the sun has moved CONFIG.sun.shadowStep degrees, not every frame, and at night the sun leaves the scene's lights altogether, so no surface pays for its light and shadow lookups (both shader variants are compiled at load). Textures get anisotropic filtering (CONFIG.anisotropy), so floors seen along the room stay sharp.
CONFIG.touch). A touch laptop, or an iPad with a trackpad, counts as a desktop.CONFIG.sun.skyGain, so ceilings near windows read a little bright and up-facing outdoor surfaces (balcony floor, sills) a little dark.VISIBILITY_SPACING or raise VISIBILITY_RAYS in blender/pipeline/config.py if one does.blender/tools/surface_props.pyExterior - neighbourhood (and its sub-collections). It is too big to lightmap, so it goes to surroundings.glb. The viewer lights it live with the same sun and sky as the flat (no shadows between buildings). When only the neighbourhood changed, make export-surroundings is enough.