MichaelYeh507/Unpictured

Python

1

8 commits

updated Oct 3, 2026

See the code

See what people are saying

SourceMessageScoreDate

A world model turns my photo into a 360° panorama but never says where the photo ended up, so I find it with brute-force edge matching (r/computervision)

World Labs' Marble turns a photo into a 360° panorama and then a 3D world, and nothing in its docs says where the original photo ends up. My game computes everything from the source camera, so I needed that pose. The matcher is deliberately dumb. It cuts flat perspective views out of the…

1

Oct 4, 2026

Unpictured: a first-person game that turns your photos into dreams you can walk through, where the level is everything the camera never saw (r/coolgithubprojects)

I'm building a game where each level starts inside a real photo. World Labs' Marble turns the photo into a 3D Gaussian splat world you can walk through, and the game is about the gap between what the camera saw and what the model made up. The dream steals a few objects from the photo and scatters…

3

Oct 4, 2026

README

Unpictured

Step into everything the photo left out.

A first-person game that turns photos into dreams you can walk through. Early development.

Worlds are generated from photos with the World Labs Marble API and rendered in the browser with three.js.

Layout

  • web/: the game client (Vite, TypeScript, three.js, Spark)
  • core/: engine-agnostic TypeScript (no three.js, Spark or DOM), such as coordinate frames
  • pipeline/: Python tools that generate and prepare world packages
  • tests/: language-neutral test vectors that every engine's code must pass

Develop

Requires Node 26 and uv.

npm install
npm run dev      # the game at http://localhost:5173
npm run check    # every check that CI runs

Generated worlds, photos and API keys are never committed.

Generate a world

Put a World Labs API key in a file outside the repo (one line, WLT_API_KEY=...), then from pipeline/:

uv run --env-file <key file> python -m unpictured_pipeline credits                 # balance, free
uv run --env-file <key file> python -m unpictured_pipeline generate photo.jpg      # dry run: price
uv run --env-file <key file> python -m unpictured_pipeline generate photo.jpg --yes

Two to four photos of one place make a single world. Give each photo's direction in the same order (0 front, 90 right, 180 back, 270 left):

uv run --env-file <key file> python -m unpictured_pipeline generate front.jpg back.jpg --azimuth 0 --azimuth 180

To find where each photo sits in its world, run locate (free and local, about 10 seconds per photo). It matches each photo against the world's panorama and writes camera.json:

uv run python -m unpictured_pipeline locate --name <name>

To walk around a world, run npm run dev and open http://localhost:5173/?world=<name>, where <name> is a folder in worlds/. Drag to look, W A S D to move, E up, Q down. The viewer loads the most detailed splat file the world has. Draft worlds have no metric data, so they show at raw scale with a notice on screen. When a world has camera.json, the viewer draws each photo's frame in yellow, starts looking through the first photo, and O lays the photo over the world. N measures where a person can walk from the photo spot and colors that floor: blue inside a photo, pink unpictured, gray out of reach, with the areas on screen.

Phones need a lighter splat file: add &splats=500k or &splats=100k to the address (&splats=full_res asks for the most detailed). If the world lacks that size, the viewer takes the next smaller file, or its smallest file when nothing is that small. The status line names the file it loaded.

generate defaults to the cheap draft model and refuses to pass a daily cap (UNPICTURED_DAILY_CAP_USD, default $3). Every paid call is logged to one file per user, ~/.unpictured/cost_log.jsonl (UNPICTURED_COST_LOG to move it), so every clone shares the same daily total. It uploads an upright JPEG copy with all metadata removed, since phone photos usually carry GPS. Packages land in worlds/.

MichaelYeh507/Unpictured

Python

1

8 commits

updated Oct 3, 2026

See the code

See what people are saying

SourceMessageScoreDate

A world model turns my photo into a 360° panorama but never says where the photo ended up, so I find it with brute-force edge matching (r/computervision)

World Labs' Marble turns a photo into a 360° panorama and then a 3D world, and nothing in its docs says where the original photo ends up. My game computes everything from the source camera, so I needed that pose. The matcher is deliberately dumb. It cuts flat perspective views out of the…

1

Oct 4, 2026

Unpictured: a first-person game that turns your photos into dreams you can walk through, where the level is everything the camera never saw (r/coolgithubprojects)

I'm building a game where each level starts inside a real photo. World Labs' Marble turns the photo into a 3D Gaussian splat world you can walk through, and the game is about the gap between what the camera saw and what the model made up. The dream steals a few objects from the photo and scatters…

3

Oct 4, 2026

README

Unpictured

Step into everything the photo left out.

A first-person game that turns photos into dreams you can walk through. Early development.

Worlds are generated from photos with the World Labs Marble API and rendered in the browser with three.js.

Layout

  • web/: the game client (Vite, TypeScript, three.js, Spark)
  • core/: engine-agnostic TypeScript (no three.js, Spark or DOM), such as coordinate frames
  • pipeline/: Python tools that generate and prepare world packages
  • tests/: language-neutral test vectors that every engine's code must pass

Develop

Requires Node 26 and uv.

npm install
npm run dev      # the game at http://localhost:5173
npm run check    # every check that CI runs

Generated worlds, photos and API keys are never committed.

Generate a world

Put a World Labs API key in a file outside the repo (one line, WLT_API_KEY=...), then from pipeline/:

uv run --env-file <key file> python -m unpictured_pipeline credits                 # balance, free
uv run --env-file <key file> python -m unpictured_pipeline generate photo.jpg      # dry run: price
uv run --env-file <key file> python -m unpictured_pipeline generate photo.jpg --yes

Two to four photos of one place make a single world. Give each photo's direction in the same order (0 front, 90 right, 180 back, 270 left):

uv run --env-file <key file> python -m unpictured_pipeline generate front.jpg back.jpg --azimuth 0 --azimuth 180

To find where each photo sits in its world, run locate (free and local, about 10 seconds per photo). It matches each photo against the world's panorama and writes camera.json:

uv run python -m unpictured_pipeline locate --name <name>

To walk around a world, run npm run dev and open http://localhost:5173/?world=<name>, where <name> is a folder in worlds/. Drag to look, W A S D to move, E up, Q down. The viewer loads the most detailed splat file the world has. Draft worlds have no metric data, so they show at raw scale with a notice on screen. When a world has camera.json, the viewer draws each photo's frame in yellow, starts looking through the first photo, and O lays the photo over the world. N measures where a person can walk from the photo spot and colors that floor: blue inside a photo, pink unpictured, gray out of reach, with the areas on screen.

Phones need a lighter splat file: add &splats=500k or &splats=100k to the address (&splats=full_res asks for the most detailed). If the world lacks that size, the viewer takes the next smaller file, or its smallest file when nothing is that small. The status line names the file it loaded.

generate defaults to the cheap draft model and refuses to pass a daily cap (UNPICTURED_DAILY_CAP_USD, default $3). Every paid call is logged to one file per user, ~/.unpictured/cost_log.jsonl (UNPICTURED_COST_LOG to move it), so every clone shares the same daily total. It uploads an upright JPEG copy with all metadata removed, since phone photos usually carry GPS. Packages land in worlds/.

Languages

Python

52.0%

TypeScript

47.3%