PROCITY scaffold: spec, research, 6 Opus lane prompts, core modules, vendored three r175, 69 inherited skins

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jing 2026-07-14 10:46:40 +10:00
commit a702a69f1e
99 changed files with 85389 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
*.pyc
__pycache__/
.genraw/
saves/
node_modules/
.env

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# PROCITY
A standalone, procedurally generated, fully walkable 90s-Australian shopping town — every
building enterable (the Vuntra City trick), every interior seeded and themed: record stores,
op shops, toy shops, book barns, video rental, pawnbrokers, milk bars, market stalls. Built on
the proven house stack (three.js r175 vendored, plain JS, no build step) and the asset DNA of
90sDJsim + thriftgod. v1 is the *system*; the GODVERSE/BaseGod real-item content plugs in later.
## Run
```
cd web && python3 -m http.server 8130
# http://localhost:8130 (game shell — Lane B)
# http://localhost:8130/map.html (2D plan debugger — Lane A)
```
## Read first
- **`docs/CITY_SPEC.md`** — the treaty: schema, units, seeds, budgets, file ownership.
- **`docs/RESEARCH.md`** — what the parent repos already solved, with exact paths to port from.
## The lanes (parallel Opus 4.8 sessions)
Each lane is a self-contained prompt. To run one, start an Opus session in this repo and say:
**"Read docs/LANES/LANE_X_….md and execute it."** AE are parallel-safe (disjoint files,
standalone test pages); F integrates after they land.
| lane | mission | owns |
|---|---|---|
| [A — CITYGEN](docs/LANES/LANE_A_CITYGEN.md) | seed → CityPlan data (streets, blocks, lots, shops) + 2D map | `js/citygen`, `js/core/registry.js`, `map.html` |
| [B — STREETSCAPE](docs/LANES/LANE_B_STREETSCAPE.md) | chunk-streamed instanced 3D town + game shell | `index.html`, `js/world` |
| [C — INTERIORS](docs/LANES/LANE_C_INTERIORS.md) | every door opens: seeded themed shop interiors | `js/interiors`, `interior_test.html` |
| [D — CITIZENS](docs/LANES/LANE_D_CITIZENS.md) | rigged-near / impostor-far NPCs + shopkeepers | `js/citizens`, `citizens_test.html`, `models/` |
| [E — ASSETS](docs/LANES/LANE_E_ASSETS.md) | audit ultra's library, normalize GLBs, skins, manifest | `pipeline/`, `assets/` |
| [F — INTEGRATION](docs/LANES/LANE_F_INTEGRATION.md) | wire AE into one seed-to-town game + QA gates | (after AE) |
## Already in the repo
- `web/vendor/` — three.js r175 + addons (vendored from 90sDJsim; no CDN at runtime).
- `web/assets/gen/` — 69 generated texture skins (25 shopfront facades, 10 skies, 10 grounds,
8 wallpapers, 16 interior surfaces) inherited from thriftgod + 90sDJsim, style-locked.
- `web/js/core/` — frozen shared modules: `prng.js` (seeded-everything law), `loaders.js`
(fail-soft promise-cached GLB/texture + 3GOD depot), `canvas.js` (text planes).
## House laws (short form — CITY_SPEC has the fine print)
Seeded everything, `Math.random` banned in gen code · the game must run with zero assets ·
instancing from day one · canvas text, no font files · characters = shared base rigs + one
clip bank, canonical sources live outside this repo · 🟢 web-ok licensing only.

189
docs/CITY_SPEC.md Normal file
View File

@ -0,0 +1,189 @@
# PROCITY — CITY_SPEC (the shared contract)
*Every lane reads this first. If a lane needs to change something in here, it changes this file
in the same commit and says so loudly in the commit message — this doc is the treaty.*
## What we are building
A standalone, procedurally generated, fully walkable Australian shopping town — the Vuntra City
trick (every building enterable, interiors generated on demand) applied not to cyberpunk towers
but to **record stores, op shops, toy shops, book barns, video rental, pawnbrokers, milk bars,
market stalls**. 90s-Australia aesthetic, low-poly + generated texture skins. Content (real item
data, lore, economy) plugs in later via the GODVERSE/BaseGod feeds — v1 is the *system*: plan →
streets → facades → doors that open → themed interiors → NPCs walking around.
## Engine decision (settled — do not relitigate)
**three.js r175, vendored, plain JS ES modules, importmap, zero build step, zero npm.**
This is the proven house stack (90sDJsim + thriftgod both ship on it), it runs great on Apple
Silicon, and it lets us lift working code (fittings kit, rig stack, dig.js) verbatim. Unreal was
considered and parked: nothing in v1 needs it, and the asset pipeline (GLB, web-ok licensing,
3GOD depot) is web-native. Serve with `cd web && python3 -m http.server 8130`.
- `three` and `three/addons/` resolve via importmap to `web/vendor/` (copied from 90sDJsim).
- No DRACO requirement (house GLBs ship uncompressed/webp-optimized); DRACOLoader is vendored if needed.
- No external CDNs at runtime. Everything local or from `https://digalot.fyi/3god` (CORS-enabled).
## Units, axes, determinism
- **Metres. +Y up. Ground plane is XZ.** City origin (0,0) = centre of the main square.
- **Right-handed three.js defaults.** Buildings face their street; a facade's outward normal
points at its street edge. GLB convention: origin at base, facing **Z**, real-world scale.
- **Seeded everything, `Math.random()` is banned in generation code.**
`web/js/core/prng.js` is the only randomness source:
- `seedFor(citySeed, kind, id)` → stable uint32 (xmur3 hash of `"seed:kind:id"`).
- `rng(citySeed, kind, id)` → mulberry32 stream.
- Same citySeed ⇒ byte-identical city, forever. This is the thriftgod/90sDJsim house rule and
it's what makes the authored-override layer possible later.
## The three-layer architecture
```
LAYER 1 CityPlan (pure data, no THREE) Lane A
LAYER 2 Streetscape (chunked 3D realization) Lane B
LAYER 3 Interiors (on-demand room scenes) Lane C
Citizens (NPCs over layers 2+3) Lane D
Assets (skins + GLB depot) Lane E
```
### Layer 1 — CityPlan (JSON-serializable, generated in <100ms)
The whole town as lightweight data, generated up front from `citySeed`, dumpable to JSON.
Schema v1 (Lane A owns it; extend, don't break):
```js
{
version: 1,
citySeed: 20261990, // uint32
name: "…", // generated town name
size: { w: 1024, d: 1024 }, // metres, city core v1 = 1km²
districts: [ { id, kind, cx, cz } ],
// kind: 'mainstreet' | 'arcade' | 'backstreets' | 'warehouse' | 'residential' | 'market'
streets: {
nodes: [ { id, x, z } ],
edges: [ { id, a, b, width, kind } ] // kind: 'main' | 'side' | 'lane' | 'arcade'
},
blocks: [ { id, district, poly: [[x,z],…] } ],
lots: [ { id, block, x, z, w, d, ry, frontEdge, use } ],
// use: 'shop' | 'anchor' | 'house' | 'yard' | 'stall' | 'infill'
shops: [ { id, lot, type, name, sign, seed, facadeSkin, storeys, hours: [open, close] } ]
// type: see SHOP TYPES below
}
```
Chunk key: `cx = floor(x/64)`, `cz = floor(z/64)`. **Chunk size 64m.** Lane A ships a
`chunkIndex(plan)` helper: chunk key → { lots, shops, edges } touching it.
### Layer 2 — Streetscape (Lane B)
Realizes CityPlan chunks around the player. Hard requirements the two parent games skipped:
- **Contiguous geometry** — no teleport seams; you can walk the whole core.
- **Chunk streaming** — build/dispose chunk Groups at radius ~23; never rebuild the world.
- **InstancedMesh from day one** for: building shells, awning boxes, verandah posts, street
furniture, trees, streetlights. Canvas signage batched into a **sign atlas texture** per chunk
(one CanvasTexture, many UV-mapped planes) — not one canvas per sign.
- Facade formula (proven in both games): instanced box shell + front `PlaneGeometry` with a
`facade-*.jpg` skin + parametric awning/verandah + sign band + door + window planes.
Skins are generated with **blank signboards** — the game overlays the shop name.
### Layer 3 — Interiors (Lane C)
Interiors are **not** street geometry. Walking into a doorway triggers
`enterShop(shop)` → separate room Group built from `shop.seed` (thriftgod archetypes + the
90sDJsim `fittings.js` parametric kit), themed by `shop.type`. Leaving restores the street
exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at
night in v1; interior-mapping shader is a stretch goal).
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
| type | facade pool | interior archetype | fittings mix |
|---|---|---|---|
| `record` | timber-teal, arcade-tile, djsim-record | crates + bins | record bins, crates, counter, listening corner |
| `opshop` | weatherboard, fibro-blue, djsim-opshop | racks + shelves | clothes racks, bric-a-brac shelving, book wall |
| `toy` | stucco-pink, deco-pastel | shelves + glass case | cube shelves, display tables, glass case |
| `book` | federation, sandstone | halls of shelves | bookshelves, spinner racks, armchair |
| `video` | stripmall, djsim-video | aisle shelves | VHS shelving, returns slot, poster wall |
| `pawn` | besser, grimy, djsim-pawn | counter-forward | glass cabinets, wall hooks, barred window |
| `milkbar` | djsim-milkbar, brickveneer | counter + freezer | counter, fridge, magazine rack |
| `dept` (anchor) | terrazzo, djsim-dept | grand hall | mixed sections, escalator prop |
| `stall` (market) | facade-market | open stall | trestle tables, crates |
## NPC contract (Lane D)
- **Fleet rule (house law):** 2 rigged base meshes + shared clip bank. Canonical rigs:
`~/Documents/character_kit/` (female `grandma_game.glb` route, male `hum_character.glb`) plus
the 19 peds in `90sDJsim/web/world/models/peds/`. Game-local copies are byte-identical builds —
**copy from character_kit / 90sDJsim, never edit locally.**
- Distance tiers: near <25m = rigged `SkeletonUtils.clone` + AnimationMixer; mid = billboard
impostor (pre-rendered sprite, instanced); far = culled. Walkers path along street-edge
footpath lanes from the CityPlan graph.
- Licensing: 🟢 web-ok only (Mixamo, CC0/CC-BY, CGTrader RF). **No ActorCore in web builds.**
## Performance budget (M-series laptop, 60fps)
- ≤ 300 draw calls in street mode, ≤ 200k tris in a typical view.
- `renderer.setPixelRatio(min(devicePixelRatio, 2))`.
- Textures: skins ≤ 1024px, sign atlases 2048px, total GPU texture < 512MB.
- Chunk build must not hitch: budget 4ms/frame (build incrementally or in idle callbacks).
- Adopt 90sDJsim's `GFX_TIERS` idea later if needed; don't gold-plate now.
## Modes & shell (Lane B owns the shell)
`MODE ∈ { map, street, interior }` — one renderer, one state machine, same as both parent games.
`map` = 2D canvas town directory (Lane A's debug view grows into it). Save/load = localStorage v1.
## File ownership (parallel-safety treaty)
| path | owner |
|---|---|
| `web/js/core/*` (prng, loaders, canvas) | scaffold (frozen — propose changes via CITY_SPEC PR) |
| `web/js/core/registry.js` | Lane A |
| `web/js/citygen/*`, `web/map.html` | Lane A |
| `web/index.html`, `web/js/world/*` | Lane B |
| `web/js/interiors/*`, `web/interior_test.html` | Lane C |
| `web/js/citizens/*`, `web/citizens_test.html`, `web/models/*` | Lane D |
| `pipeline/*`, `web/assets/*` + `web/assets/manifest.json` | Lane E |
| `docs/*` | everyone, additively |
Each lane also ships its own standalone test page so it can be verified without the others.
**Never edit another lane's files.** Integration (Lane F) happens after AE land.
## Infrastructure map
- **This mac** — dev box, Blender + Unreal installed, Apple Silicon (no CUDA — all gen is
cloud-API or tailnet).
- **ultra** `ssh johnking@100.91.239.7` (key auth works, always-on M1 Ultra): MESHGOD Blender
scripts (`~/Documents/MESHGOD/scripts/finish_glb.py`, `render_glb.py`, `media_primitive.py`),
sorted model library `~/Documents/3D=models/` (shop-fittings / street-furniture / furniture —
see its `MANIFEST.md` + `CH_MAPPING.md`), `character_kit/`, `mixamo-fetch/out/` clip bank
(34 clips — never re-download an existing clip). Caveats: no GNU coreutils/timeout/setsid,
system python 3.9, Homebrew at /opt/homebrew.
- **m3ultra** `http://100.89.131.57:11434` — Ollama (`qwen3:235b/32b/8b`, `gemma3:4b` vision,
`gemma4`). Free local LLM for name generation, lore, cataloguing. Use it before any paid API.
- **3GOD depot** `https://digalot.fyi/3god``POST /api/upload?name=<f>.glb`, `GET /a/<f>`,
`GET /api/list` (CORS on). The shared GLB CDN all sibling games use.
- **MeshGod** `https://digalot.fyi/meshgod` — image→GLB generation (~30¢/solid; check the
DealGod canon-3D cache first; media/books use the free parametric skinner).
- **Image gen** — house flow is prompt packs (see `~/Documents/FLOW_PROMPTS.md` format) +
thriftgod's `gen_assets.py` batch pipeline (OpenRouter `google/gemini-3.1-flash-image`,
~$0.004/img, style-bible prefix, resumable) and 90sDJsim's Cloudflare Flux scripts
(creds in `~/Documents/backnforth/.env`).
## Style lock (visual)
Low-poly chunky geometry, flat-ish shading, muted 90s Australian palette, warm cinematic light —
the 90sDJsim POLY lock — skinned with the thriftgod generated-texture kit (69 skins already in
`web/assets/gen/`). Facade prompt template (blank signboards!) is in thriftgod
`gen_assets.py:69-86` — reuse it for new skins so everything matches.
## House patterns to keep (both games proved these)
- Promise-cached loaders; missing asset ⇒ placeholder box, never a crash. The game must run
with zero assets.
- `CanvasTexture` text planes for ALL signage/text — no font files.
- `SkeletonUtils.clone` for anything skinned; canonicalize `mixamorig\d+``mixamorig` so one
clip drives every character; strip position tracks from shared clips (`_rotOnly`).
- Height-normalize rigs off the head bone; plant feet by min bone Y.
- Seeded flat-colour fallbacks under every texture.
- A `shot()` fixed-camera screenshot harness for visual regression (90sDJsim `tools/shots.py`).

View File

@ -0,0 +1,90 @@
# LANE A — CITYGEN (the plan generator)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read
> `docs/CITY_SPEC.md` (the contract) and `docs/RESEARCH.md` (what's already solved) before
> writing any code. You own `web/js/citygen/*`, `web/js/core/registry.js`, and `web/map.html`.
> Do not touch any other lane's files (ownership table in CITY_SPEC). No npm, no build step,
> no TypeScript — plain ES modules, three.js style of the house repos. All randomness through
> `web/js/core/prng.js`. Commit in small, described steps on `main`.
## Mission
Pure-data town generator: `generatePlan(citySeed) → CityPlan` per the schema in CITY_SPEC,
in under 100ms, fully deterministic, JSON-serializable, **no THREE import anywhere in this lane
except the debug map page**. Plus the shop-type registry and a 2D debug map viewer.
## Design brief (the town we want)
Not a Manhattan grid, not noise-soup. A **1km² Australian country-town-meets-inner-suburb core**:
1. **Main street spine** — one gently bent high street running roughly NS through the origin,
28m corridor (road 10m + verandah'd footpaths). Continuous retail frontage: narrow lots
(69m frontage, 1220m deep), zero side setbacks, 12 storeys, occasional 3-storey corner
anchor. This is where record/toy/book/video shops cluster.
2. **Cross streets** every 80140m; the two central ones are "second high streets" (side kind),
the rest taper into `backstreets`.
3. **One arcade** — a covered pedestrian lane (`kind:'arcade'`, width 5) cutting through a
mid-spine block, lined with tiny lots (35m frontage) both sides. Prime record-store turf.
4. **Market square** district — an open block near the origin: `stall` lots in rows, the town's
`dept` anchor on one edge.
5. **Warehouse fringe** (N or S end, seeded): bigger lots, corrugated/besser skins, sparse —
future flea-market/venue territory.
6. **Residential collar** — the outer ring: `house` lots with front yards, and exactly 24
corner-lot `milkbar`s embedded in it (the classic Australian corner shop).
7. Laneways (`kind:'lane'`, width 4) behind main-street blocks for service/back-door flavour.
## Algorithm sketch (suggested, not sacred)
- Spine as a polyline: 57 nodes, x-jitter ±30m. Offset cross streets perpendicular-ish with
±10° jitter. Snap intersections into shared nodes; build the half-edge/block extraction by
walking the planar graph (blocks = faces). Keep it simple — the graph is small (<200 edges);
an O(n²) face walk is fine.
- Subdivide each block's street-facing sides into lots by frontage bands per district kind.
Every lot records `frontEdge` (street edge id) and `ry` (facing rotation) so Lane B knows
which way the facade points. Interior leftover = `yard`/`infill`.
- Assign shop types by district weights (registry-driven), with clustering bonuses (a record
shop next to an opshop next to a bookshop is the vibe — use a per-block shuffle that favours
same-cluster neighbours), then assign `facadeSkin` from the type's pool (seeded), `storeys`,
`hours` (seeded around 95, milkbars longer, one weirdo open till late).
## Shop names & signs
`web/js/citygen/names.js`: seeded generator with per-type patterns and 90s-AU flavour
(e.g. record: "{Word} Records", "Rotation", "{Suburb} Sound Exchange"; opshop: "St {Name}'s
Opportunity Shop", "Vinnies-alike parodies"; milkbar: "{Family} Milk Bar"). 40+ patterns, plus
a `sign` short form (what fits on the signboard). Keep it wordlist-based and deterministic —
**optionally** ship `pipeline/gen_names.py` that asks m3ultra Ollama (`http://100.89.131.57:11434`,
model `qwen3:32b`) to expand the wordlists into `web/js/citygen/wordlists.js` — generation
uses only the checked-in wordlists at runtime (no network calls in the game).
## Registry (`web/js/core/registry.js`)
Export `SHOP_TYPES` exactly covering the table in CITY_SPEC: per type — facade skin pool
(filenames that exist in `web/assets/gen/`), sign style hints, interior archetype id, fittings
mix, district weights, hours profile. This file is the shared vocabulary — every other lane
imports it, so land it early and keep it flat data.
## Debug map (`web/map.html`)
Standalone page, Canvas 2D (steal the thriftgod REFIDEX vibe): draws streets (width-scaled),
blocks, lots coloured by use, shop dots coloured by type with name labels on hover, chunk grid
overlay, seed input box + "regen" + "export JSON" buttons. This page is how humans (and Lane F)
eyeball a seed. Make it pleasant — it becomes the in-game map later.
## Deliverables
1. `web/js/citygen/plan.js``generatePlan(citySeed)`, `chunkIndex(plan)`.
2. `web/js/citygen/names.js` (+ `wordlists.js`).
3. `web/js/core/registry.js`.
4. `web/map.html` debug viewer.
5. `web/js/citygen/selfcheck.js` — run with `node web/js/citygen/selfcheck.js` (plain node, no
deps): asserts determinism (two runs, deep-equal), plan under 100ms, every shop has a lot,
every lot has a valid frontEdge, no overlapping lots within a block, every facadeSkin file
exists (fs check against `web/assets/gen/`), chunkIndex covers every lot, JSON round-trip.
## Acceptance
- `node web/js/citygen/selfcheck.js` prints all-green.
- `web/map.html` renders 5 different seeds that all read as *towns* (spine visible, arcade
present, market square, milkbars in the collar) — screenshot each into `docs/shots/laneA/`.
- Zero `Math.random`, zero THREE imports in citygen (grep-clean).

View File

@ -0,0 +1,89 @@
# LANE B — STREETSCAPE (chunked 3D town + game shell)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read
> `docs/CITY_SPEC.md` and `docs/RESEARCH.md` first, then skim the two source patterns you are
> generalizing: `90sDJsim/web/world/index.html` `buildStrip()` (~line 740) and
> `thriftgod/web/index.html` `buildStreet()` (14411515). You own `web/index.html` and
> `web/js/world/*`. Do not touch other lanes' files. Plain ES modules, vendored three.js via
> importmap (`web/vendor/` — copy the importmap style from 90sDJsim's index.html), no npm,
> no build. All randomness via `core/prng.js`. Commit in small, described steps on `main`.
## Mission
Turn a CityPlan into a walkable, contiguous, chunk-streamed 3D town at 60fps, plus the game
shell (mode state machine, controls, day/night, HUD). This is the lane where PROCITY stops
being two separate street-strip games and becomes a *city*.
**Don't wait for Lane A**: start by hand-writing `web/js/world/fixture_plan.js` — a small
hard-coded CityPlan (2 blocks of main street, one cross street, ~14 shops, valid per the
CITY_SPEC schema). Build everything against the schema; swap in `generatePlan()` when Lane A
lands. If the schema needs a field Lane A didn't foresee, propose it in CITY_SPEC in the same
commit.
## Architecture
- `web/index.html` — shell only: importmap, canvas, HUD divs, mode state machine
(`map | street | interior`), main loop. Keep it thin; logic lives in modules.
- `web/js/world/chunks.js` — the streamer. `ChunkManager(plan, scene)`: on player move,
ensure chunks within radius R (default 3) exist, dispose beyond R+1. Each chunk = one Group
built from `chunkIndex(plan)` data. **Build budget 4ms/frame** — queue chunk builds, one per
frame or via `requestIdleCallback`. Dispose = remove + `geometry.dispose()` for non-shared
geometry + return instanced slots.
- `web/js/world/buildings.js` — the facade kit:
- Shells: **InstancedMesh per chunk** (one box geometry, per-instance matrix + color).
Parapet/verandah posts/awning boxes likewise instanced.
- Facade planes: shops sharing a skin share a material — group facade planes per skin into
merged geometry (`BufferGeometryUtils.mergeGeometries`, vendored) per chunk. ≤25 skin
materials city-wide.
- Sign band: per-chunk **2048px canvas atlas** — draw every sign text once, UV-map planes into
it. One material per chunk for all signage. Steal `textPlane` proportions from the parents
but do NOT create per-sign canvases.
- Doors: plane + `userData.place = {kind:'door', shopId}` for the raycaster. Windows: dark
tinted planes, emissive warm at night.
- Verandahs/awnings over footpath on main street (the classic AU posted verandah — boxes +
posts, instanced), `tex-awning-*.jpg` skins.
- `web/js/world/ground.js` — roads/footpaths/kerbs as merged strips along street edges
(`ground-asphalt*`, `ground-footpath*`, `ground-grass` for yards, `ground-brickpave` for the
arcade + market square). UV along edge length. Intersections: simple quad fill, don't
over-engineer curb geometry.
- `web/js/world/furniture.js` — instanced streetlights, benches, bins, gum trees (cross-plane
billboard trees are fine), bus stops; placed seeded along footpaths. GLB props may load from
the 3GOD depot (promise-cached, placeholder box fallback — pattern in RESEARCH) but every
furniture type MUST have a primitive fallback.
- `web/js/world/lighting.js` — sky dome (`sky-*.jpg` picked by seed, BackSide sphere), 6-segment
day cycle ported from 90sDJsim `applyLighting`: sun direction/intensity, ambient, streetlights
+ window emissives at night. ONE directional shadow map (2048) following the player, or none —
measure. ACES tonemapping, bloom optional at threshold 0.9 (vendored postprocessing exists).
- `web/js/world/player.js` — PointerLockControls walk (WASD + shift-run), eye 1.7m. Collision:
push-out against building AABBs of the current + neighbour chunks and a "stay off private
yards" rule; streets/footpaths/square all freely walkable. No navmesh — rectangles are enough.
- `web/js/world/hud.js` — crosshair, shop-name tooltip via throttled raycast (every 6th frame,
far=6), time-of-day widget, FPS/draw-call debug readout (`renderer.info`), seed display.
- Door interaction: raycast hit + click → dispatch `window.dispatchEvent(new CustomEvent(
'procity:enterShop', {detail:{shopId}}))` and pause street mode. Lane C listens for it; until
Lane C lands, show a "🚪 {name} — interiors coming" toast and keep walking. Interior returns
via `procity:exitShop`.
## Performance gates (hard)
- ≤300 draw calls, ≤200k tris in street view (HUD shows both, live).
- No per-frame allocation storms; reuse vectors/matrices.
- Chunk in/out causes no visible hitch (verify with the HUD frame-time graph).
- `setPixelRatio(min(dpr,2))`; textures ≤1024 (skins already are).
## Deliverables
1. `web/index.html` + all `web/js/world/*` modules above.
2. `web/js/world/fixture_plan.js` (works standalone before Lane A).
3. `shot()` bookmark harness: press `P` → downloads canvas PNG from 3 fixed cameras; save
references into `docs/shots/laneB/`.
4. A `docs/LANES/LANE_B_NOTES.md` with the measured numbers (draw calls, tris, chunk build ms).
## Acceptance
- `cd web && python3 -m http.server 8130` → walk the fixture town for 5 minutes: contiguous,
no seams, chunks stream silently, day/night cycles, signs legible, tooltips work, budget
numbers green on an M-series MacBook.
- Works with `?seed=N` once Lane A's `generatePlan` is importable (guard the import so the page
still runs on fixture data if citygen is absent).
- The page loads and runs with `web/assets/` renamed away (all-fallback mode) — no crashes.

View File

@ -0,0 +1,71 @@
# LANE C — INTERIORS (every door opens)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read
> `docs/CITY_SPEC.md` and `docs/RESEARCH.md` first, then the two sources you are porting:
> `thriftgod/web/index.html` `buildShop()` (line ~1109+, archetypes `SHOP_SHAPES`, wall-slot
> system, seeded decor) and `90sDJsim/web/world/fittings.js` (parametric fittings kit — read it
> whole, it was written to be lifted). You own `web/js/interiors/*` and `web/interior_test.html`.
> Do not touch other lanes' files. Plain ES modules, three.js from `web/vendor/` importmap,
> randomness via `core/prng.js` only. Commit in small, described steps on `main`.
## Mission
The Vuntra move: **every shop door opens into a unique, believable, seeded interior**
generated on demand in <50ms, byte-identical every revisit (`shop.seed`), themed by `shop.type`.
This lane is a standalone library + test page; Lane B wires it to real doors later via the
`procity:enterShop` / `procity:exitShop` events (contract in LANE_B).
## Architecture
- `web/js/interiors/interiors.js` — public API:
```js
buildInterior(shop, THREE) → { group, spawn:{x,z,ry}, exits:[{x,z,w, toStreet:true}],
places:[…userData-tagged interactables], dispose() }
```
Pure function of `shop` (a CityPlan shop record + its lot dims). No global state.
- `web/js/interiors/shell.js` — the room shell: floor/walls/ceiling sized from the lot
(`lot.w × lot.d`, storeys → ceiling height 3.24.5m), seeded from the 5 thriftgod archetypes
(**cosy / gallery / wide / hall / pokey**) adapted to lot proportions, plus door/shopfront
wall with real glazing (you can see a hint of street), back room doorway (blocked v1).
Materials: seeded pick of `wall-*.jpg` wallpapers, `tex-carpet-*`/`tex-lino-*` floors,
`tex-*` counters — all already in `web/assets/gen/`, all with flat-colour fallbacks.
- `web/js/interiors/fittings.js` — port 90sDJsim's kit and extend it. Needed set (see the
registry's fittings mixes): record bins & crates, clothes racks, wall/cube/metal shelving,
bookshelves, VHS aisle shelving, glass display case, counter + till, trestle tables, fridge,
magazine rack, poster/art frames (`art-*` future), pegboard walls. Parametric primitives
first; where a GLB upgrade exists use the manifest (see below) with primitive fallback.
- `web/js/interiors/layout.js` — the placer: per-archetype floor zones (window display / aisles
/ walls / counter-corner), the thriftgod **shuffled wall-slot system** so nothing overlaps,
density from shop seed (some shops crammed, some sparse), guaranteed walkable path from door
to counter (grid-mark occupied cells; verify connectivity with a flood fill — assert it).
- `web/js/interiors/stock.js` — v1 *visual* stock only: product boxes/sleeves/spines as
canvas-texture blocks on shelves (steal the dig.js sleeve-canvas trick — coloured cardboard +
price stickers). Real items are the content phase; leave a `stockAdapter` hook:
`(shop, slotKind) => texture/mesh` so BaseGod data can plug in without touching layout code.
- **GLB upgrades**: read `web/assets/manifest.json` if present (Lane E ships it) mapping fitting
ids → 3GOD depot files; promise-cached loader, placeholder-persists pattern. The test page
must be fully usable with no manifest and no network.
- Windows-from-street problem (Lane B fakes it): you own the *inside-looking-out* — a simple
backdrop plane beyond the glazing (street-ish gradient / `sky-*` slice) sells it in v1.
## Type theming (registry-driven)
Import `SHOP_TYPES` from `core/registry.js`; every type in CITY_SPEC's table gets a distinct
recipe (fittings mix, wallpaper bias, clutter level, counter position, signage-inside flavour).
A `record` shop must *instantly* read different from a `toy` shop from the doorway. Milk bar
counter faces the door; pawn shop is counter-forward with wall hooks; dept anchor is the
"grand hall" archetype with mixed sections.
## Test page (`web/interior_test.html`)
Standalone: seed input, type dropdown, archetype override dropdown, "re-roll", first-person walk
inside (reuse PointerLock pattern), wireframe/occupancy-grid debug toggle, and a "50-room soak"
button that builds+disposes 50 seeded interiors and reports ms/room + leaked geometries
(`renderer.info.memory` before vs after — must return to baseline).
## Acceptance
- Same seed twice → identical room (soak test asserts deep-equal on placement lists).
- All 9 types × 5 archetypes render sensibly (screenshot grid into `docs/shots/laneC/`).
- Build <50ms/room, dispose leak-free, flood-fill path doorcounter always exists.
- Runs with zero assets (fallback colours) and zero network.

View File

@ -0,0 +1,74 @@
# LANE D — CITIZENS (populate the town)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read
> `docs/CITY_SPEC.md` and `docs/RESEARCH.md` first, then the source you are porting:
> `90sDJsim/web/world/index.html` lines ~258393 (`loadRig`, `spawnRig`, `_canon`, `_rotOnly`,
> head-bone height normalization, `upgradeStreetPeople`) — this stack is proven, port it
> faithfully. You own `web/js/citizens/*`, `web/citizens_test.html`, and `web/models/*`.
> Do not touch other lanes' files. Plain ES modules, three.js from `web/vendor/`, randomness
> via `core/prng.js`. Commit in small, described steps on `main` (GLBs are committed — keep
> the set curated, not hoarded).
## Mission
Streets that feel lived-in and shops with keepers, at city scale, on budget. Rigged characters
near, cheap impostors far, deterministic per-citizen identity (same seed → same person walks
the same beat), all built on the **house fleet rule**: shared base meshes + one canonical clip
bank, never per-character rigs.
## Assets (copy, never edit locally)
- Copy the ped fleet from `90sDJsim/web/world/models/peds/` (19 rigged GLBs + clip-only
`walk.glb` / `idle.glb`) into `web/models/peds/`. Byte-identical copies — the canonical
source stays upstream (house law; see `asset-pipeline` conventions).
- Canonical hero rigs if needed: `~/Documents/character_kit/` (female + male). Clip bank:
`~/Documents/mixamo-fetch/out/` (34 clips — sit, lean, look-around, phone etc. may exist;
check before wanting new ones; **never re-download an existing clip**). New clips need John
to run the mixamo-fetch flow — file wishes in `docs/LANES/LANE_D_NOTES.md`, don't block.
- Licensing: 🟢 web-ok only. No ActorCore.
## Architecture
- `web/js/citizens/rigs.js` — the ported stack: promise-cached `loadRig`; `spawnRig()` with
SkeletonUtils.clone, `mixamorig\d+` canonicalization (one walk clip drives all 19 peds),
`_rotOnly` position-track stripping, head-bone height normalization (seeded 1.551.95m),
feet planted by min bone Y, random clip start offsets. Primitive-figure placeholders that
hot-swap when GLBs arrive (`upgradeStreetPeople` pattern) — the town is populated from
frame one.
- `web/js/citizens/sim.js` — the citizen simulation (Vuntra's layered idea, hamlet-scale):
- Deterministic roster per chunk: `rng(citySeed,'citizen',chunkKey+i)` → identity (ped index,
height, walk speed, home/goal habits). Density by district (main street busy at midday,
residential sparse, market square clumps).
- **Near tier (<25m)**: full rig + mixer, walking footpath lanes (offset polylines along
street edges from the CityPlan graph), turning at nodes with seeded choice, idle/loiter
stops at shop windows and benches (bench-sit if a sit clip exists, else stand-loiter).
- **Mid tier (2570m)**: no mixers ticking — instanced/billboard impostors. Generate impostor
sprites at runtime: render each ped GLB once to a small offscreen RT from 4 yaw angles →
one sprite atlas → InstancedMesh quads picking the nearest angle. (This keeps impostors
automatically in sync with the fleet; cache the atlas in-session.)
- **Far**: culled entirely. Promotion/demotion by distance with hysteresis so nobody pops
while you watch (swap only when off-screen or beyond a fade).
- Time-of-day modulation from the shell's day segment (event or global): morning trickle,
lunch rush, night = near-empty streets + the odd wanderer.
- `web/js/citizens/keepers.js` — shopkeepers: one keeper per open shop, standing at the counter
slot (Lane C exposes counter position in its `places` — coordinate via the event payload;
until Lane C lands, test with a mock). Idle clip + a "greet" head-turn toward the player if a
clip allows. Keepers are spawned when an interior builds, disposed with it.
- Budgets: ≤ 24 rigged actives, ≤ 4 mixers updated per frame beyond the nearest 8 (stagger
updates), impostor instancing = 1 draw call per atlas. All mixers pause when tab hidden.
## Test page (`web/citizens_test.html`)
Standalone: a fixture street loop (hard-code a small street graph — do NOT import Lane A/B
files; keep the fixture local), spawn slider 0200 citizens, tier-visualization toggle
(colour by tier), live HUD (rigged count, mixer ms, draw calls, fps), time-of-day slider,
determinism check button (respawn roster, assert identical identities).
## Acceptance
- 200-citizen fixture scene holds 60fps on an M-series MacBook; mixer update <2ms/frame.
- No T-pose ever visible (placeholder → rig swap is seamless); no giants/ants (height
normalization verified across all 19 peds); one shared walk clip animates every ped.
- Same seed → same crowd, twice (assert).
- Screenshots of near/mid tiers + a busy main-street shot into `docs/shots/laneD/`.
- `docs/LANES/LANE_D_NOTES.md`: measured budgets + clip wishlist for the mixamo-fetch run.

100
docs/LANES/LANE_E_ASSETS.md Normal file
View File

@ -0,0 +1,100 @@
# LANE E — ASSETS (skins, GLBs, the depot manifest)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY` with SSH access
> to **ultra** (`ssh johnking@100.91.239.7`, key auth works non-interactively — mind: no GNU
> coreutils/timeout/setsid, system python 3.9, Homebrew at /opt/homebrew). Read
> `docs/CITY_SPEC.md` and `docs/RESEARCH.md` first. You own `pipeline/*`, `web/assets/*`, and
> `web/assets/manifest.json`. Do not touch other lanes' files. **Spending rule: fal/MeshGod
> generations cost real money — batch nothing without listing it in a plan file first and
> keeping v1 to the shortlist below. Prompt-pack image gen is near-free but still list counts.**
> Verify every visual result with your own eyes (render thumbnails / open in browser) — never
> "should look right". Commit in small, described steps on `main`.
## Mission
Feed the other lanes: audit what exists, normalize the best of it into a PROCITY asset set,
publish it (3GOD depot + local), and ship the **manifest** that Lanes B/C/D consume. Then the
gap-fill generation passes (facade skins first, GLB props second). Everything optional at
runtime — the game must run asset-free — so this lane is about making it *gorgeous*, not
making it *work*.
## Step 1 — Audit (no generation yet)
- On ultra, read `~/Documents/3D=models/MANIFEST.md` + `CH_MAPPING.md`, then inventory
`shop-fittings/`, `street-furniture/`, `furniture/`, `props-scenes/` against the fittings
needs in CITY_SPEC's shop-type table and LANE_C's fittings list. Note tri counts, scale
sanity, licensing zone (🟢 web-ok only — anything unclear goes in a QUARANTINE list, not
the manifest).
- Hit `https://digalot.fyi/3god/api/list` — some of this may already be uploaded.
- Output: `pipeline/AUDIT.md` — table of candidate GLBs → PROCITY fitting/furniture id,
status (ready / needs-normalize / missing / quarantined-licence).
## Step 2 — Normalize & publish GLBs
- `pipeline/normalize.py` — runs **on ultra** against Blender + the existing MESHGOD scripts
(`~/Documents/MESHGOD/scripts/finish_glb.py`, `render_glb.py` — read them, extend, don't
fork): enforce house GLB law (metres, +Y up, origin at base, facing Z, ≤5k tris props,
webp textures ≤1024, no Draco), name as `procity_<category>_<name>_01.glb`, render a
256px thumbnail per asset.
- Upload to 3GOD (`POST /api/upload?name=<f>.glb`), thumbnails into `web/assets/thumbs/`.
- Priority order (what Lanes B/C actually place first): record crate + record bin, clothes
rack, metal wire shelf, garage shelves, bookshelf, cube shelf, glass case, counter (balcao),
trestle table, bench + longbench + park bench, market stall, food cart, streetlight, fridge.
## Step 3 — The manifest (`web/assets/manifest.json`)
The single contract file other lanes read:
```json
{
"version": 1,
"depot": "https://digalot.fyi/3god",
"fittings": { "record_bin": {"file":"procity_fit_record_bin_01.glb", "footprint":[1.2,0.8], "thumb":"…"}, … },
"furniture": { "bench": {…}, "streetlight": {…}, … },
"skins": {
"facade": {"weatherboard":{"file":"gen/facade-weatherboard.jpg","types":["opshop"]}, …},
"sky": [...], "ground": {...}, "wall": [...], "interior": {...}
}
}
```
Catalogue the 69 skins already in `web/assets/gen/` (they came from thriftgod + 90sDJsim —
map them to shop types per the registry table). Add a `pipeline/validate_manifest.py`
(plain python3) that checks every referenced file exists locally or HEADs OK on the depot,
footprints are sane, and JSON parses — this runs in Lane F's gate.
## Step 4 — Facade skin expansion (prompt-pack pass)
The 18+7 existing facades are the style anchor. Gaps for a whole town: **toy shop, book shop,
video store, milk bar variants (23), arcade interior frontage, dept-store anchor (wide),
warehouse/roller-door, residential terrace/cottage fronts (34), corner-lot two-face,
market-square edge**. Two routes, in order:
1. Port thriftgod's `gen_assets.py` (OpenRouter `google/gemini-3.1-flash-image`, ~$0.004/img,
resumable `.genraw/` — see `thriftgod/gen_assets.py`) into `pipeline/gen_skins.py` with the
**same style bible prefix** and the **blank-signboard facade template** (`gen_assets.py:69-86`).
~30 images ≈ 12¢. Needs the OpenRouter key (`thriftgod/.env` pattern) — if absent, route 2.
2. Emit `pipeline/SKIN_PROMPTS.md` in the house FLOW_PROMPTS.md format (POLY lock, save-as
filenames) for John to run through the manual flow.
Also: **corner buildings need a second facade face** — add a `-side` variant convention
(`facade-X-side.jpg`) or document that Lane B reuses the front skin on corners (decide, write
it in the manifest README).
## Step 5 — Bespoke hero props (MeshGod, budget-gated)
Shortlist ONLY (≈10 solids ≈ $3): listening booth, till/cash register, VHS rewinder,
glass counter case (if audit finds none), spinner rack, milkshake mixer, arcade cabinet shell,
band-poster easel, giant novelty record (roof ornament), bus stop shelter. Write
`pipeline/MESHGOD_BATCH.md` (manifest-style, the `djsim_home.json` pattern) — **do not run the
batch without John's go**; everything has a primitive fallback anyway. Remember the MeshGod
gotcha: thin wires and flat discs reconstruct badly — those stay in-engine primitives.
## Acceptance
- `pipeline/AUDIT.md` complete; ≥15 normalized GLBs live on 3GOD with thumbnails.
- `web/assets/manifest.json` validates green; Lanes B/C load fittings from it untouched.
- Facade coverage: every shop type in the registry has ≥2 skins mapped; new skins
indistinguishable in style from the originals (side-by-side contact sheet in
`docs/shots/laneE/`).
- A one-page `pipeline/README.md`: how to re-run each step, what it costs, where creds live.

View File

@ -0,0 +1,46 @@
# LANE F — INTEGRATION & QA (run after AE land)
> **Prompt for Opus 4.8.** You are working in `/Users/jing/Documents/PROCITY`. Read
> `docs/CITY_SPEC.md`, then every `docs/LANES/LANE_*_NOTES.md` the other lanes left. You may
> touch any file, but prefer surgical wiring changes over rewrites — each lane's internals are
> its own. This lane turns five verified parts into one game.
## Mission
One URL, one seed, a whole town: `web/index.html?seed=N` → map → walk the streets → open any
door → themed interior with a keeper → back out — 60fps, no seams, no leaks.
## Wiring checklist
1. Swap Lane B's `fixture_plan.js` for Lane A's `generatePlan(seed)`; seed from URL param,
surfaced in HUD; `web/map.html` linked from an in-game map key (M) using the same plan.
2. Wire `procity:enterShop` → Lane C `buildInterior` (mode switch, street scene paused not
disposed, player restored on exit at the door, interior disposed on exit).
3. Wire Lane D: citizens stream with Lane B's chunks (roster per chunk key); keepers spawn from
Lane C's counter `places`; day-segment events drive density.
4. Point Lanes B/C at Lane E's `manifest.json` (skin→type mapping replaces any hardcoded pools;
GLB upgrades appear; primitive fallbacks still exercised via a `?noassets=1` param).
5. Hours: closed shops (CityPlan `hours` vs day segment) get dark windows, CLOSED plate, locked
door toast. One late-night shop per town stays open (Lane A seeds it) — verify it exists.
## QA gates (all must pass before calling it v1)
- **Determinism**: seed 1234 on two machines → identical map PNG, identical first-interior
placement list.
- **Soak**: scripted 10-minute walk (record a waypoint path, drive the camera) crossing ≥30
chunks, entering ≥15 shops: `renderer.info.memory` geometries/textures return to baseline
after each interior; JS heap stable; zero console errors.
- **Budget**: HUD numbers within CITY_SPEC limits at the busiest intersection at midday.
- **Asset-free run**: `?noassets=1` full playthrough — placeholder town, zero crashes.
- **Selfchecks**: `node web/js/citygen/selfcheck.js` + `python3 pipeline/validate_manifest.py`
green; add `tools/qa.sh` that runs both.
- **Shots**: refresh all `docs/shots/*` reference images; add a contact-sheet
`docs/shots/v1_tour/` (10 beauty shots: main street noon, arcade, market square, milkbar
dusk, record-shop interior, night neon, etc.) — these are the Vuntra-style devlog material.
## Then write `docs/V2_IDEAS.md`
Park everything discovered but not built: dig.js wiring + BaseGod real-item stock, interior
window shader, real-OSM plan import (thriftgod `build_city()` route), trams/bus loop, weather,
multiplayer presence, the in-game layout editor (thriftgod E1E4 override tables), venue
district + gig system crossover with 90sDJsim.

95
docs/RESEARCH.md Normal file
View File

@ -0,0 +1,95 @@
# RESEARCH — what the parent repos already solved (port, don't reinvent)
*Condensed technical briefs from full sweeps of both repos (2026-07-14). Exact paths verified.
Read the section for your lane before writing code; open the referenced files and lift patterns.*
## 90sDJsim (`/Users/jing/Documents/90sDJsim`)
- three.js r175 **vendored** at `web/world/vendor/` (copied into PROCITY `web/vendor/`), importmap
in `web/world/index.html`. No build, no npm. Backend = single stdlib-Python `server.py` :8123.
- Docs: `CLAUDE.md`, `DEVMANUAL.md` (single source of truth), `TOWNMAP.md`, `ECONOMY_SPEC.md`.
- **World**: `web/world/index.html` (~3400 lines, all inline). One scene, one `room` Group
swapped per mode. `AREAS` data table (~line 711) declares streets as shop rows; `buildStrip()`
(~740) realizes them: `BoxGeometry(4.8,h,4)` shell + facade JPEG on a `PlaneGeometry(4.7,2.4)`
+ awning box + `textPlane()` canvas signs, seeded per building with `mulberry32(i*77+5)`.
`buildHome()` ~963, `buildVenue()` ~1094. Day/night = 6-segment `applyLighting(segIdx)`.
- **Facade skins**: `web/world/tex/facade_{record,opshop,pawn,pub,video,dept,milkbar}.jpg`
Cloudflare Flux, committed 2026-07-10 (commit `e31e605`), cropped below the sign band. Mapped
by `FACADE` table (~line 276). Copied into PROCITY as `facade-djsim-*.jpg`.
- **NPC stack (the crown jewel — port for Lane D)**, `web/world/index.html` ~258393:
- `models/peds/` = 17 normal + 2 comical rigged GLBs (~2MB each) + clip-only `walk.glb`/`idle.glb`.
- `spawnRig(rig,target,x,z,ry,clipName,height,extClip)`: SkeletonUtils.clone → height-normalize
off the **head bone** → plant feet by min bone Y → mixer.clipAction().play() with random
start offset to desync.
- `_canon = s => s.replace(/mixamorig\d+/g,'mixamorig')` — one clip binds to any character.
- `_rotOnly` strips position tracks + Hips.quaternion from shared clips (prevents scale blowups).
- `upgradeStreetPeople()` — world builds instantly with primitive figures, rigs hot-swap in
when ~40MB of GLBs arrive. Venue crowd: rigged front rows, bobbing box-figures behind.
- **`web/world/fittings.js`** — standalone parametric shop-fitting kit (racks, bins, shelves,
counters, pegboards, canvas-texture garments). Takes THREE as arg, returns placeable Groups.
Written to theme "any shop, any count". Port for Lane C.
- **`web/world/dig.js`** — self-contained crate-riffle minigame (own scene/camera, callback API
`onBuy/getCash/spawnClerk/spawnFittings`). Already ported across 3 games. Content phase.
- Loaders: `loadGLB`/`loadRig`/`loadProp`/`loadTex` promise-cached, fail→null→placeholder stays.
- Perf: **zero instancing/LOD/atlases** (small scenes got away with it — we can't). Tricks worth
keeping: `GFX_TIERS` era-tiered graphics (~line 167), bloom threshold 0.9, single 2048 shadow
map on street only, additive light-cones, `tools/shots.py` screenshot regression harness.
- Gear/prop gen: MeshGod manifests (`MESHGOD/manifests/djsim_home.json` pattern), webp-optimized
~1MB GLBs, **no Draco**. Gotcha: thin wires/flat discs reconstruct badly — do those as
in-engine primitives.
## thriftgod (`/Users/jing/Documents/thriftgod`)
- three.js 0.175 from unpkg importmap in `web/index.html` (2,061 lines = whole game). Backend
`server.py` (1,616 lines, stdlib + psycopg2) :8777; reads `dealgod` DB (1.77M real listings).
- **Real-world city**: `build_city()` (`server.py:433`) — Overpass query for all named AU
secondhand/charity/music/antiques/books/toys shops → `city_source.json` cache → dedupe →
nearest-capital assignment → suburb inference → parody names → seeded hours. ~2,918 shops.
The recipe for later swapping PROCITY's synthetic plan for real geography.
- **Seed + override architecture (the big idea)**: every shop interior is deterministic from
`shop.seed` via mulberry32 (`web/index.html:1063`); Postgres `layout_shop`/`layout_area`/
`layout_asset` tables hold optional hand-authored overrides; NULL = procedural fallback.
In-game editor (E1E4) writes overrides. See `EDITOR_PLAN.md`.
- **Street**: `buildStreet()` (`index.html:1441-1515`) — 4.4×3.4×0.4 box per shop, facade skin
on front face, canvas name sign, OPEN/CLOSED plate, `SphereGeometry(70)` BackSide sky dome
(seasonal `pickSky`), landmark billboard, textured ground planes.
- **Interior**: `buildShop()` (`index.html:1109+`) — 5 archetypes `SHOP_SHAPES`
(cosy/gallery/wide/hall/pokey), seeded wallpaper/carpet/art, shuffled wall-slot system so
decor never overlaps, record bins, shelves with product thumbnails, floor piles, counter+till,
glass case. Port for Lane C.
- **Generated assets**: 557 JPGs in `web/assets/gen/` from `gen_assets.py` (OpenRouter
`google/gemini-3.1-flash-image`, ~$0.004/img, resumable `.genraw/`, style-bible prefix
*"1990s Australian op-shop aesthetic, slightly faded and worn…"*). Facade prompts generate
**blank signboards** (template at `gen_assets.py:69-86`). 69 relevant skins copied into
PROCITY. CORS enabled on live `/assets/*`.
- **3GOD depot loading** (`index.html:1072-1249`): `DEPOT='https://digalot.fyi/3god'`,
`GLB_CACHE[file] ||= loadAsync`, SkeletonUtils.clone, bbox floor-plant, auto-play `/idle/i`
clip. Editor palette from `/api/list`.
- Movement: PointerLockControls + AABB clamps + rotated-rect divider collision (`hitsDivider`,
`index.html:1994`). `/img` caching proxy with SSRF guard (`server.py:1362`) for CORS-free
WebGL textures.
- Item economy (content phase): `mint()` `server.py:792-858` — per-shop-type profiles, gem
bands, curation axis (CBD distance), isolation multiplier, `TABLESAMPLE` over real listings,
rarity from live supply. `dig.js` riffle with real physical sleeve thicknesses.
## Everything else
- **character_kit** (`~/Documents/character_kit/`, also on ultra): female + male canonical rigs,
props (hats/boombox/camera/thermos), `scripts/finish_rig.py`, `export_for_mixamo.py`.
House law: edit here, copy into games, never edit game-local.
- **mixamo-fetch** (`~/Documents/mixamo-fetch/`): `fetch.cjs` drives Mixamo's REST API (token
from logged-in Brave via CDP :9222). 34 clips in `out/`. Search first; never re-download.
Bespoke clips (crate-riffle, DJ) don't exist there — hand-author.
- **ultra 3D=models library** (`johnking@100.91.239.7:~/Documents/3D=models/`):
`shop-fittings/` (record_crate_plastic_01, clothes_rack_01, shelf_metal_wire_01,
garage_shelves_metal_v2, bookshelves, cube_bookcase, wardrobes, many tables, balcao counter…),
`street-furniture/` (bench, longbench, park_bench_01, market_stall_01, food_cart_bakso_01,
**building_shell_01.glb**), `furniture/`, `props-scenes/` (dj_room, turntable), `characters/`,
`animations/`. `MANIFEST.md` + `CH_MAPPING.md` + `_thumbnails/` exist — read them on ultra.
- **GODVERSE brain** (`~/Documents/dealgod/GODVERSE.md`, `docs/GODVERSE_3D_BRIEF.md`): recordgod/
toygod/bookgod/mediagod data feeds, 44k solid-object 3D candidates, 185k vinyl w/ images —
this is the content firehose for later. Not v1.
- **Vuntra City reference** (YouTube devlogs): 20×20km UE5 city, every building enterable,
interiors generated at runtime, million-NPC layered simulation (detailed near / abstract far),
faked GI. We steal the *ideas* at hamlet scale: on-demand interiors, LOD'd simulation,
cheap lighting.

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

36
web/js/core/canvas.js Normal file
View File

@ -0,0 +1,36 @@
// PROCITY core canvas-text helpers — all in-world text is CanvasTexture (no font files).
// NOTE for Lane B: per-sign planes are fine for one-offs (tooltips, plates); street signage at
// scale must go through a per-chunk atlas (CITY_SPEC perf law) — build that in world/, not here.
import * as THREE from 'three';
// Draw text into a canvas → { canvas, w, h } (px). Multi-line via \n.
export function textCanvas(text, { font = 'bold 48px Arial', fg = '#222', bg = null,
padX = 24, padY = 16, lineGap = 1.15 } = {}) {
const lines = String(text).split('\n');
const c = document.createElement('canvas');
const x = c.getContext('2d');
x.font = font;
const lineH = parseInt(font.match(/(\d+)px/)?.[1] || 48, 10) * lineGap;
const w = Math.ceil(Math.max(...lines.map(l => x.measureText(l).width)) + padX * 2);
const h = Math.ceil(lineH * lines.length + padY * 2);
c.width = w; c.height = h;
if (bg) { x.fillStyle = bg; x.fillRect(0, 0, w, h); }
x.font = font; x.fillStyle = fg; x.textBaseline = 'top';
lines.forEach((l, i) => x.fillText(l, padX, padY + i * lineH));
return { canvas: c, w, h };
}
// One-off text plane mesh, height in metres, width follows aspect.
export function textPlane(text, heightM = 0.4, opts = {}) {
const { canvas, w, h } = textCanvas(text, opts);
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry(heightM * (w / h), heightM),
new THREE.MeshBasicMaterial({ map: tex, transparent: !opts.bg })
);
mesh.userData.isTextPlane = true;
return mesh;
}

39
web/js/core/loaders.js Normal file
View File

@ -0,0 +1,39 @@
// PROCITY core loaders — promise-cached, fail-soft (house pattern from both parent games).
// Missing asset ⇒ null / flat colour; callers keep their placeholder. The game runs asset-free.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
export const DEPOT = 'https://digalot.fyi/3god'; // shared GLB CDN: GET /a/<file>, /api/list
const gltfLoader = new GLTFLoader();
const texLoader = new THREE.TextureLoader();
const GLB_CACHE = {}; // url → Promise<gltf|null>
const TEX_CACHE = {}; // url → THREE.Texture (may still be loading; fallback colour beneath)
// loadGLB('gen/x.glb' | 'depot:file.glb' | full URL) → Promise<gltf|null>, cached, never rejects
export function loadGLB(ref) {
const url = ref.startsWith('depot:') ? `${DEPOT}/a/${ref.slice(6)}` : ref;
return (GLB_CACHE[url] ||= gltfLoader.loadAsync(url).catch(err => {
console.warn('[loaders] GLB failed, placeholder stays:', url, err?.message || err);
return null;
}));
}
// loadTex(url, {repeat:[u,v]}) → texture immediately (fills in when loaded), sRGB, aniso 4
export function loadTex(url, opts = {}) {
if (TEX_CACHE[url]) return TEX_CACHE[url];
const t = texLoader.load(url, undefined, undefined,
() => console.warn('[loaders] tex failed, fallback colour stays:', url));
t.colorSpace = THREE.SRGBColorSpace;
t.anisotropy = 4;
if (opts.repeat) { t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(...opts.repeat); }
TEX_CACHE[url] = t;
return t;
}
// fetch + cache the depot listing (editor palettes, Lane E validation)
let _depotList = null;
export async function depotList() {
return (_depotList ||= fetch(`${DEPOT}/api/list`).then(r => r.json()).catch(() => []));
}

46
web/js/core/prng.js Normal file
View File

@ -0,0 +1,46 @@
// PROCITY core PRNG — the only randomness source in generation code (CITY_SPEC law).
// Same citySeed ⇒ byte-identical city. Math.random() is banned outside cosmetic-only FX.
export function xmur3(str) {
let h = 1779033703 ^ str.length;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return function () {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
export function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Stable uint32 for an entity: seedFor(citySeed, 'shop', 42)
export function seedFor(citySeed, kind, id) {
return xmur3(`${citySeed}:${kind}:${id}`)();
}
// Seeded stream for an entity: const r = rng(citySeed,'shop',42); r() → [0,1)
export function rng(citySeed, kind, id) {
return mulberry32(seedFor(citySeed, kind, id));
}
// Helpers over a stream r
export const pick = (r, arr) => arr[(r() * arr.length) | 0];
export const irange = (r, lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); // inclusive ints
export const frange = (r, lo, hi) => lo + r() * (hi - lo);
export function shuffle(r, arr) { // in-place Fisher-Yates
for (let i = arr.length - 1; i > 0; i--) {
const j = (r() * (i + 1)) | 0;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

View File

@ -0,0 +1,270 @@
import {
Controls,
Euler,
Vector3
} from 'three';
const _euler = new Euler( 0, 0, 0, 'YXZ' );
const _vector = new Vector3();
/**
* Fires when the user moves the mouse.
*
* @event PointerLockControls#change
* @type {Object}
*/
const _changeEvent = { type: 'change' };
/**
* Fires when the pointer lock status is "locked" (in other words: the mouse is captured).
*
* @event PointerLockControls#lock
* @type {Object}
*/
const _lockEvent = { type: 'lock' };
/**
* Fires when the pointer lock status is "unlocked" (in other words: the mouse is not captured anymore).
*
* @event PointerLockControls#unlock
* @type {Object}
*/
const _unlockEvent = { type: 'unlock' };
const _PI_2 = Math.PI / 2;
/**
* The implementation of this class is based on the [Pointer Lock API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API}.
* `PointerLockControls` is a perfect choice for first person 3D games.
*
* ```js
* const controls = new PointerLockControls( camera, document.body );
*
* // add event listener to show/hide a UI (e.g. the game's menu)
* controls.addEventListener( 'lock', function () {
*
* menu.style.display = 'none';
*
* } );
*
* controls.addEventListener( 'unlock', function () {
*
* menu.style.display = 'block';
*
* } );
* ```
*
* @augments Controls
*/
class PointerLockControls extends Controls {
/**
* Constructs a new controls instance.
*
* @param {Camera} camera - The camera that is managed by the controls.
* @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
*/
constructor( camera, domElement = null ) {
super( camera, domElement );
/**
* Whether the controls are locked or not.
*
* @type {boolean}
* @readonly
* @default false
*/
this.isLocked = false;
/**
* Camera pitch, lower limit. Range is '[0, Math.PI]' in radians.
*
* @type {number}
* @default 0
*/
this.minPolarAngle = 0;
/**
* Camera pitch, upper limit. Range is '[0, Math.PI]' in radians.
*
* @type {number}
* @default Math.PI
*/
this.maxPolarAngle = Math.PI;
/**
* Multiplier for how much the pointer movement influences the camera rotation.
*
* @type {number}
* @default 1
*/
this.pointerSpeed = 1.0;
// event listeners
this._onMouseMove = onMouseMove.bind( this );
this._onPointerlockChange = onPointerlockChange.bind( this );
this._onPointerlockError = onPointerlockError.bind( this );
if ( this.domElement !== null ) {
this.connect( this.domElement );
}
}
connect( element ) {
super.connect( element );
this.domElement.ownerDocument.addEventListener( 'mousemove', this._onMouseMove );
this.domElement.ownerDocument.addEventListener( 'pointerlockchange', this._onPointerlockChange );
this.domElement.ownerDocument.addEventListener( 'pointerlockerror', this._onPointerlockError );
}
disconnect() {
this.domElement.ownerDocument.removeEventListener( 'mousemove', this._onMouseMove );
this.domElement.ownerDocument.removeEventListener( 'pointerlockchange', this._onPointerlockChange );
this.domElement.ownerDocument.removeEventListener( 'pointerlockerror', this._onPointerlockError );
}
dispose() {
this.disconnect();
}
getObject() {
console.warn( 'THREE.PointerLockControls: getObject() has been deprecated. Use controls.object instead.' ); // @deprecated r169
return this.object;
}
/**
* Returns the look direction of the camera.
*
* @param {Vector3} v - The target vector that is used to store the method's result.
* @return {Vector3} The normalized direction vector.
*/
getDirection( v ) {
return v.set( 0, 0, - 1 ).applyQuaternion( this.object.quaternion );
}
/**
* Moves the camera forward parallel to the xz-plane. Assumes camera.up is y-up.
*
* @param {number} distance - The signed distance.
*/
moveForward( distance ) {
if ( this.enabled === false ) return;
// move forward parallel to the xz-plane
// assumes camera.up is y-up
const camera = this.object;
_vector.setFromMatrixColumn( camera.matrix, 0 );
_vector.crossVectors( camera.up, _vector );
camera.position.addScaledVector( _vector, distance );
}
/**
* Moves the camera sidewards parallel to the xz-plane.
*
* @param {number} distance - The signed distance.
*/
moveRight( distance ) {
if ( this.enabled === false ) return;
const camera = this.object;
_vector.setFromMatrixColumn( camera.matrix, 0 );
camera.position.addScaledVector( _vector, distance );
}
/**
* Activates the pointer lock.
*
* @param {boolean} [unadjustedMovement=false] - Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead.
* Setting it to true will disable mouse acceleration.
*/
lock( unadjustedMovement = false ) {
this.domElement.requestPointerLock( {
unadjustedMovement
} );
}
/**
* Exits the pointer lock.
*/
unlock() {
this.domElement.ownerDocument.exitPointerLock();
}
}
// event listeners
function onMouseMove( event ) {
if ( this.enabled === false || this.isLocked === false ) return;
const camera = this.object;
_euler.setFromQuaternion( camera.quaternion );
_euler.y -= event.movementX * 0.002 * this.pointerSpeed;
_euler.x -= event.movementY * 0.002 * this.pointerSpeed;
_euler.x = Math.max( _PI_2 - this.maxPolarAngle, Math.min( _PI_2 - this.minPolarAngle, _euler.x ) );
camera.quaternion.setFromEuler( _euler );
this.dispatchEvent( _changeEvent );
}
function onPointerlockChange() {
if ( this.domElement.ownerDocument.pointerLockElement === this.domElement ) {
this.dispatchEvent( _lockEvent );
this.isLocked = true;
} else {
this.dispatchEvent( _unlockEvent );
this.isLocked = false;
}
}
function onPointerlockError() {
console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' );
}
export { PointerLockControls };

4958
web/vendor/addons/loaders/GLTFLoader.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,361 @@
import {
Clock,
HalfFloatType,
NoBlending,
Vector2,
WebGLRenderTarget
} from 'three';
import { CopyShader } from '../shaders/CopyShader.js';
import { ShaderPass } from './ShaderPass.js';
import { ClearMaskPass, MaskPass } from './MaskPass.js';
/**
* Used to implement post-processing effects in three.js.
* The class manages a chain of post-processing passes to produce the final visual result.
* Post-processing passes are executed in order of their addition/insertion.
* The last pass is automatically rendered to screen.
*
* This module can only be used with {@link WebGLRenderer}.
*
* ```js
* const composer = new EffectComposer( renderer );
*
* // adding some passes
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
*
* const glitchPass = new GlitchPass();
* composer.addPass( glitchPass );
*
* const outputPass = new OutputPass()
* composer.addPass( outputPass );
*
* function animate() {
*
* composer.render(); // instead of renderer.render()
*
* }
* ```
*/
class EffectComposer {
/**
* Constructs a new effect composer.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
* be used as the internal read and write buffers. If not given, the composer creates
* the buffers automatically.
*/
constructor( renderer, renderTarget ) {
/**
* The renderer.
*
* @type {WebGLRenderer}
*/
this.renderer = renderer;
this._pixelRatio = renderer.getPixelRatio();
if ( renderTarget === undefined ) {
const size = renderer.getSize( new Vector2() );
this._width = size.width;
this._height = size.height;
renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
renderTarget.texture.name = 'EffectComposer.rt1';
} else {
this._width = renderTarget.width;
this._height = renderTarget.height;
}
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.renderTarget2.texture.name = 'EffectComposer.rt2';
/**
* A reference to the internal write buffer. Passes usually write
* their result into this buffer.
*
* @type {WebGLRenderTarget}
*/
this.writeBuffer = this.renderTarget1;
/**
* A reference to the internal read buffer. Passes usually read
* the previous render result from this buffer.
*
* @type {WebGLRenderTarget}
*/
this.readBuffer = this.renderTarget2;
/**
* Whether the final pass is rendered to the screen (default framebuffer) or not.
*
* @type {boolean}
* @default true
*/
this.renderToScreen = true;
/**
* An array representing the (ordered) chain of post-processing passes.
*
* @type {Array<Pass>}
*/
this.passes = [];
/**
* A copy pass used for internal swap operations.
*
* @private
* @type {ShaderPass}
*/
this.copyPass = new ShaderPass( CopyShader );
this.copyPass.material.blending = NoBlending;
/**
* The intenral clock for managing time data.
*
* @private
* @type {Clock}
*/
this.clock = new Clock();
}
/**
* Swaps the internal read/write buffers.
*/
swapBuffers() {
const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}
/**
* Adds the given pass to the pass chain.
*
* @param {Pass} pass - The pass to add.
*/
addPass( pass ) {
this.passes.push( pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
/**
* Inserts the given pass at a given index.
*
* @param {Pass} pass - The pass to insert.
* @param {number} index - The index into the pass chain.
*/
insertPass( pass, index ) {
this.passes.splice( index, 0, pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
/**
* Removes the given pass from the pass chain.
*
* @param {Pass} pass - The pass to remove.
*/
removePass( pass ) {
const index = this.passes.indexOf( pass );
if ( index !== - 1 ) {
this.passes.splice( index, 1 );
}
}
/**
* Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
*
* @param {number} passIndex - The pass index.
* @return {boolean} Whether the the pass for the given index is the last pass in the pass chain.
*/
isLastEnabledPass( passIndex ) {
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
if ( this.passes[ i ].enabled ) {
return false;
}
}
return true;
}
/**
* Executes all enabled post-processing passes in order to produce the final frame.
*
* @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
* its own time delta value.
*/
render( deltaTime ) {
// deltaTime value is in seconds
if ( deltaTime === undefined ) {
deltaTime = this.clock.getDelta();
}
const currentRenderTarget = this.renderer.getRenderTarget();
let maskActive = false;
for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
const pass = this.passes[ i ];
if ( pass.enabled === false ) continue;
pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
if ( pass.needsSwap ) {
if ( maskActive ) {
const context = this.renderer.getContext();
const stencil = this.renderer.state.buffers.stencil;
//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
stencil.setFunc( context.EQUAL, 1, 0xffffffff );
}
this.swapBuffers();
}
if ( MaskPass !== undefined ) {
if ( pass instanceof MaskPass ) {
maskActive = true;
} else if ( pass instanceof ClearMaskPass ) {
maskActive = false;
}
}
}
this.renderer.setRenderTarget( currentRenderTarget );
}
/**
* Resets the internal state of the EffectComposer.
*
* @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
* the one from the constructor. If set, it is used to setup the read and write buffers.
*/
reset( renderTarget ) {
if ( renderTarget === undefined ) {
const size = this.renderer.getSize( new Vector2() );
this._pixelRatio = this.renderer.getPixelRatio();
this._width = size.width;
this._height = size.height;
renderTarget = this.renderTarget1.clone();
renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
}
/**
* Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
* this method honors the current pixel ration.
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
*/
setSize( width, height ) {
this._width = width;
this._height = height;
const effectiveWidth = this._width * this._pixelRatio;
const effectiveHeight = this._height * this._pixelRatio;
this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
for ( let i = 0; i < this.passes.length; i ++ ) {
this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
}
}
/**
* Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
* Setting the pixel ratio will automatically resize the composer.
*
* @param {number} pixelRatio - The pixel ratio to set.
*/
setPixelRatio( pixelRatio ) {
this._pixelRatio = pixelRatio;
this.setSize( this._width, this._height );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the composer is no longer used in your app.
*/
dispose() {
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.copyPass.dispose();
}
}
export { EffectComposer };

View File

@ -0,0 +1,194 @@
import { Pass } from './Pass.js';
/**
* This pass can be used to define a mask during post processing.
* Meaning only areas of subsequent post processing are affected
* which lie in the masking area of this pass. Internally, the masking
* is implemented with the stencil buffer.
*
* ```js
* const maskPass = new MaskPass( scene, camera );
* composer.addPass( maskPass );
* ```
*
* @augments Pass
*/
class MaskPass extends Pass {
/**
* Constructs a new mask pass.
*
* @param {Scene} scene - The 3D objects in this scene will define the mask.
* @param {Camera} camera - The camera.
*/
constructor( scene, camera ) {
super();
/**
* The scene that defines the mask.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
/**
* Whether to inverse the mask or not.
*
* @type {boolean}
* @default false
*/
this.inverse = false;
}
/**
* Performs a mask pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const context = renderer.getContext();
const state = renderer.state;
// don't update color or depth
state.buffers.color.setMask( false );
state.buffers.depth.setMask( false );
// lock buffers
state.buffers.color.setLocked( true );
state.buffers.depth.setLocked( true );
// set up stencil
let writeValue, clearValue;
if ( this.inverse ) {
writeValue = 0;
clearValue = 1;
} else {
writeValue = 1;
clearValue = 0;
}
state.buffers.stencil.setTest( true );
state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
state.buffers.stencil.setClear( clearValue );
state.buffers.stencil.setLocked( true );
// draw into the stencil buffer
renderer.setRenderTarget( readBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
// unlock color and depth buffer and make them writable for subsequent rendering/clearing
state.buffers.color.setLocked( false );
state.buffers.depth.setLocked( false );
state.buffers.color.setMask( true );
state.buffers.depth.setMask( true );
// only render where stencil is set to 1
state.buffers.stencil.setLocked( false );
state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
state.buffers.stencil.setLocked( true );
}
}
/**
* This pass can be used to clear a mask previously defined with {@link MaskPass}.
*
* ```js
* const clearPass = new ClearMaskPass();
* composer.addPass( clearPass );
* ```
*
* @augments Pass
*/
class ClearMaskPass extends Pass {
/**
* Constructs a new clear mask pass.
*/
constructor() {
super();
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
}
/**
* Performs the clear of the currently defined mask.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
renderer.state.buffers.stencil.setLocked( false );
renderer.state.buffers.stencil.setTest( false );
}
}
export { MaskPass, ClearMaskPass };

View File

@ -0,0 +1,138 @@
import {
ColorManagement,
RawShaderMaterial,
UniformsUtils,
LinearToneMapping,
ReinhardToneMapping,
CineonToneMapping,
AgXToneMapping,
ACESFilmicToneMapping,
NeutralToneMapping,
CustomToneMapping,
SRGBTransfer
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { OutputShader } from '../shaders/OutputShader.js';
/**
* This pass is responsible for including tone mapping and color space conversion
* into your pass chain. In most cases, this pass should be included at the end
* of each pass chain. If a pass requires sRGB input (e.g. like FXAA), the pass
* must follow `OutputPass` in the pass chain.
*
* The tone mapping and color space settings are extracted from the renderer.
*
* ```js
* const outputPass = new OutputPass();
* composer.addPass( outputPass );
* ```
*
* @augments Pass
*/
class OutputPass extends Pass {
/**
* Constructs a new output pass.
*/
constructor() {
super();
/**
* The pass uniforms.
*
* @type {Object}
*/
this.uniforms = UniformsUtils.clone( OutputShader.uniforms );
/**
* The pass material.
*
* @type {RawShaderMaterial}
*/
this.material = new RawShaderMaterial( {
name: OutputShader.name,
uniforms: this.uniforms,
vertexShader: OutputShader.vertexShader,
fragmentShader: OutputShader.fragmentShader
} );
// internals
this._fsQuad = new FullScreenQuad( this.material );
this._outputColorSpace = null;
this._toneMapping = null;
}
/**
* Performs the output pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) {
this.uniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure;
// rebuild defines if required
if ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) {
this._outputColorSpace = renderer.outputColorSpace;
this._toneMapping = renderer.toneMapping;
this.material.defines = {};
if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = '';
if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = '';
else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = '';
else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = '';
else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';
else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = '';
else if ( this._toneMapping === NeutralToneMapping ) this.material.defines.NEUTRAL_TONE_MAPPING = '';
else if ( this._toneMapping === CustomToneMapping ) this.material.defines.CUSTOM_TONE_MAPPING = '';
this.material.needsUpdate = true;
}
//
if ( this.renderToScreen === true ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this._fsQuad.dispose();
}
}
export { OutputPass };

189
web/vendor/addons/postprocessing/Pass.js vendored Normal file
View File

@ -0,0 +1,189 @@
import {
BufferGeometry,
Float32BufferAttribute,
OrthographicCamera,
Mesh
} from 'three';
/**
* Abstract base class for all post processing passes.
*
* This module is only relevant for post processing with {@link WebGLRenderer}.
*
* @abstract
*/
class Pass {
/**
* Constructs a new pass.
*/
constructor() {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPass = true;
/**
* If set to `true`, the pass is processed by the composer.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
/**
* If set to `true`, the pass indicates to swap read and write buffer after rendering.
*
* @type {boolean}
* @default true
*/
this.needsSwap = true;
/**
* If set to `true`, the pass clears its buffer before rendering
*
* @type {boolean}
* @default false
*/
this.clear = false;
/**
* If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
* pass chain gets automatically rendered to screen, no matter how this property is configured.
*
* @type {boolean}
* @default false
*/
this.renderToScreen = false;
}
/**
* Sets the size of the pass.
*
* @abstract
* @param {number} width - The width to set.
* @param {number} height - The width to set.
*/
setSize( /* width, height */ ) {}
/**
* This method holds the render logic of a pass. It must be implemented in all derived classes.
*
* @abstract
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*
* @abstract
*/
dispose() {}
}
// Helper for passes that need to fill the viewport with a single quad.
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
// https://github.com/mrdoob/three.js/pull/21358
class FullscreenTriangleGeometry extends BufferGeometry {
constructor() {
super();
this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
}
}
const _geometry = new FullscreenTriangleGeometry();
/**
* This module is a helper for passes which need to render a full
* screen effect which is quite common in context of post processing.
*
* The intended usage is to reuse a single full screen quad for rendering
* subsequent passes by just reassigning the `material` reference.
*
* This module can only be used with {@link WebGLRenderer}.
*
* @augments Mesh
*/
class FullScreenQuad {
/**
* Constructs a new full screen quad.
*
* @param {?Material} material - The material to render te full screen quad with.
*/
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the instance is no longer used in your app.
*/
dispose() {
this._mesh.geometry.dispose();
}
/**
* Renders the full screen quad.
*
* @param {WebGLRenderer} renderer - The renderer.
*/
render( renderer ) {
renderer.render( this._mesh, _camera );
}
/**
* The quad's material.
*
* @type {?Material}
*/
get material() {
return this._mesh.material;
}
set material( value ) {
this._mesh.material = value;
}
}
export { Pass, FullScreenQuad };

View File

@ -0,0 +1,182 @@
import {
Color
} from 'three';
import { Pass } from './Pass.js';
/**
* This class represents a render pass. It takes a camera and a scene and produces
* a beauty pass for subsequent post processing effects.
*
* ```js
* const renderPass = new RenderPass( scene, camera );
* composer.addPass( renderPass );
* ```
*
* @augments Pass
*/
class RenderPass extends Pass {
/**
* Constructs a new render pass.
*
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera.
* @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used
* for all objects in the scene.
* @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.
* @param {?number} [clearAlpha=null] - The clear alpha of the render pass.
*/
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
super();
/**
* The scene to render.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The override material. If set, this material is used
* for all objects in the scene.
*
* @type {?Material}
* @default null
*/
this.overrideMaterial = overrideMaterial;
/**
* The clear color of the render pass.
*
* @type {?(number|Color|string)}
* @default null
*/
this.clearColor = clearColor;
/**
* The clear alpha of the render pass.
*
* @type {?number}
* @default null
*/
this.clearAlpha = clearAlpha;
/**
* Overwritten to perform a clear operation by default.
*
* @type {boolean}
* @default true
*/
this.clear = true;
/**
* If set to `true`, only the depth can be cleared when `clear` is to `false`.
*
* @type {boolean}
* @default false
*/
this.clearDepth = false;
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
this._oldClearColor = new Color();
}
/**
* Performs a beauty pass with the configured scene and camera.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
let oldClearAlpha, oldOverrideMaterial;
if ( this.overrideMaterial !== null ) {
oldOverrideMaterial = this.scene.overrideMaterial;
this.scene.overrideMaterial = this.overrideMaterial;
}
if ( this.clearColor !== null ) {
renderer.getClearColor( this._oldClearColor );
renderer.setClearColor( this.clearColor, renderer.getClearAlpha() );
}
if ( this.clearAlpha !== null ) {
oldClearAlpha = renderer.getClearAlpha();
renderer.setClearAlpha( this.clearAlpha );
}
if ( this.clearDepth == true ) {
renderer.clearDepth();
}
renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
if ( this.clear === true ) {
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
}
renderer.render( this.scene, this.camera );
// restore
if ( this.clearColor !== null ) {
renderer.setClearColor( this._oldClearColor );
}
if ( this.clearAlpha !== null ) {
renderer.setClearAlpha( oldClearAlpha );
}
if ( this.overrideMaterial !== null ) {
this.scene.overrideMaterial = oldOverrideMaterial;
}
renderer.autoClear = oldAutoClear;
}
}
export { RenderPass };

View File

@ -0,0 +1,134 @@
import {
ShaderMaterial,
UniformsUtils
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
/**
* This pass can be used to create a post processing effect
* with a raw GLSL shader object. Useful for implementing custom
* effects.
*
* ```js
* const fxaaPass = new ShaderPass( FXAAShader );
* composer.addPass( fxaaPass );
* ```
*
* @augments Pass
*/
class ShaderPass extends Pass {
/**
* Constructs a new shader pass.
*
* @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as
* defines and uniforms. It's also valid to pass a custom shader material.
* @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample
* the read buffer.
*/
constructor( shader, textureID = 'tDiffuse' ) {
super();
/**
* The name of the texture uniform that should sample the read buffer.
*
* @type {string}
* @default 'tDiffuse'
*/
this.textureID = textureID;
/**
* The pass uniforms.
*
* @type {?Object}
*/
this.uniforms = null;
/**
* The pass material.
*
* @type {?ShaderMaterial}
*/
this.material = null;
if ( shader instanceof ShaderMaterial ) {
this.uniforms = shader.uniforms;
this.material = shader;
} else if ( shader ) {
this.uniforms = UniformsUtils.clone( shader.uniforms );
this.material = new ShaderMaterial( {
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
defines: Object.assign( {}, shader.defines ),
uniforms: this.uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
} );
}
// internals
this._fsQuad = new FullScreenQuad( this.material );
}
/**
* Performs the shader pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
if ( this.uniforms[ this.textureID ] ) {
this.uniforms[ this.textureID ].value = readBuffer.texture;
}
this._fsQuad.material = this.material;
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this._fsQuad.render( renderer );
}
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
this.material.dispose();
this._fsQuad.dispose();
}
}
export { ShaderPass };

View File

@ -0,0 +1,491 @@
import {
AdditiveBlending,
Color,
HalfFloatType,
MeshBasicMaterial,
ShaderMaterial,
UniformsUtils,
Vector2,
Vector3,
WebGLRenderTarget
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
/**
* This pass is inspired by the bloom pass of Unreal Engine. It creates a
* mip map chain of bloom textures and blurs them with different radii. Because
* of the weighted combination of mips, and because larger blurs are done on
* higher mips, this effect provides good quality and performance.
*
* When using this pass, tone mapping must be enabled in the renderer settings.
*
* Reference:
* - [Bloom in Unreal Engine]{@link https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/}
*
* ```js
* const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
* const bloomPass = new UnrealBloomPass( resolution, 1.5, 0.4, 0.85 );
* composer.addPass( bloomPass );
* ```
*
* @augments Pass
*/
class UnrealBloomPass extends Pass {
/**
* Constructs a new Unreal Bloom pass.
*
* @param {Vector2} [resolution] - The effect's resolution.
* @param {number} [strength=1] - The Bloom strength.
* @param {number} radius - The Bloom radius.
* @param {number} threshold - The luminance threshold limits which bright areas contribute to the Bloom effect.
*/
constructor( resolution, strength = 1, radius, threshold ) {
super();
/**
* The Bloom strength.
*
* @type {number}
* @default 1
*/
this.strength = strength;
/**
* The Bloom radius.
*
* @type {number}
*/
this.radius = radius;
/**
* The luminance threshold limits which bright areas contribute to the Bloom effect.
*
* @type {number}
*/
this.threshold = threshold;
/**
* The effect's resolution.
*
* @type {Vector2}
* @default (256,256)
*/
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
/**
* The effect's clear color
*
* @type {Color}
* @default (0,0,0)
*/
this.clearColor = new Color( 0, 0, 0 );
/**
* Overwritten to disable the swap.
*
* @type {boolean}
* @default false
*/
this.needsSwap = false;
// internals
// render targets
this.renderTargetsHorizontal = [];
this.renderTargetsVertical = [];
this.nMips = 5;
let resx = Math.round( this.resolution.x / 2 );
let resy = Math.round( this.resolution.y / 2 );
this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
this.renderTargetBright.texture.generateMipmaps = false;
for ( let i = 0; i < this.nMips; i ++ ) {
const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
renderTargetHorizontal.texture.generateMipmaps = false;
this.renderTargetsHorizontal.push( renderTargetHorizontal );
const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
renderTargetVertical.texture.generateMipmaps = false;
this.renderTargetsVertical.push( renderTargetVertical );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// luminosity high pass material
const highPassShader = LuminosityHighPassShader;
this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms );
this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
this.highPassUniforms[ 'smoothWidth' ].value = 0.01;
this.materialHighPassFilter = new ShaderMaterial( {
uniforms: this.highPassUniforms,
vertexShader: highPassShader.vertexShader,
fragmentShader: highPassShader.fragmentShader
} );
// gaussian blur materials
this.separableBlurMaterials = [];
const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
resx = Math.round( this.resolution.x / 2 );
resy = Math.round( this.resolution.y / 2 );
for ( let i = 0; i < this.nMips; i ++ ) {
this.separableBlurMaterials.push( this._getSeparableBlurMaterial( kernelSizeArray[ i ] ) );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// composite material
this.compositeMaterial = this._getCompositeMaterial( this.nMips );
this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
// blend material
this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );
this.blendMaterial = new ShaderMaterial( {
uniforms: this.copyUniforms,
vertexShader: CopyShader.vertexShader,
fragmentShader: CopyShader.fragmentShader,
blending: AdditiveBlending,
depthTest: false,
depthWrite: false,
transparent: true
} );
this._oldClearColor = new Color();
this._oldClearAlpha = 1;
this._basic = new MeshBasicMaterial();
this._fsQuad = new FullScreenQuad( null );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever the pass is no longer used in your app.
*/
dispose() {
for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {
this.renderTargetsHorizontal[ i ].dispose();
}
for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {
this.renderTargetsVertical[ i ].dispose();
}
this.renderTargetBright.dispose();
//
for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) {
this.separableBlurMaterials[ i ].dispose();
}
this.compositeMaterial.dispose();
this.blendMaterial.dispose();
this._basic.dispose();
//
this._fsQuad.dispose();
}
/**
* Sets the size of the pass.
*
* @param {number} width - The width to set.
* @param {number} height - The width to set.
*/
setSize( width, height ) {
let resx = Math.round( width / 2 );
let resy = Math.round( height / 2 );
this.renderTargetBright.setSize( resx, resy );
for ( let i = 0; i < this.nMips; i ++ ) {
this.renderTargetsHorizontal[ i ].setSize( resx, resy );
this.renderTargetsVertical[ i ].setSize( resx, resy );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
}
/**
* Performs the Bloom pass.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
* destination for the pass.
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
* previous pass from this buffer.
* @param {number} deltaTime - The delta time in seconds.
* @param {boolean} maskActive - Whether masking is active or not.
*/
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
renderer.getClearColor( this._oldClearColor );
this._oldClearAlpha = renderer.getClearAlpha();
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
renderer.setClearColor( this.clearColor, 0 );
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
// Render input to screen
if ( this.renderToScreen ) {
this._fsQuad.material = this._basic;
this._basic.map = readBuffer.texture;
renderer.setRenderTarget( null );
renderer.clear();
this._fsQuad.render( renderer );
}
// 1. Extract Bright Areas
this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
this._fsQuad.material = this.materialHighPassFilter;
renderer.setRenderTarget( this.renderTargetBright );
renderer.clear();
this._fsQuad.render( renderer );
// 2. Blur All the mips progressively
let inputRenderTarget = this.renderTargetBright;
for ( let i = 0; i < this.nMips; i ++ ) {
this._fsQuad.material = this.separableBlurMaterials[ i ];
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
renderer.clear();
this._fsQuad.render( renderer );
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
renderer.clear();
this._fsQuad.render( renderer );
inputRenderTarget = this.renderTargetsVertical[ i ];
}
// Composite All the mips
this._fsQuad.material = this.compositeMaterial;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
renderer.clear();
this._fsQuad.render( renderer );
// Blend it additively over the input texture
this._fsQuad.material = this.blendMaterial;
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this._fsQuad.render( renderer );
} else {
renderer.setRenderTarget( readBuffer );
this._fsQuad.render( renderer );
}
// Restore renderer settings
renderer.setClearColor( this._oldClearColor, this._oldClearAlpha );
renderer.autoClear = oldAutoClear;
}
// internals
_getSeparableBlurMaterial( kernelRadius ) {
const coefficients = [];
for ( let i = 0; i < kernelRadius; i ++ ) {
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
}
return new ShaderMaterial( {
defines: {
'KERNEL_RADIUS': kernelRadius
},
uniforms: {
'colorTexture': { value: null },
'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
'direction': { value: new Vector2( 0.5, 0.5 ) },
'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`#include <common>
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform vec2 invSize;
uniform vec2 direction;
uniform float gaussianCoefficients[KERNEL_RADIUS];
void main() {
float weightSum = gaussianCoefficients[0];
vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
float x = float(i);
float w = gaussianCoefficients[i];
vec2 uvOffset = direction * invSize * x;
vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
diffuseSum += (sample1 + sample2) * w;
weightSum += 2.0 * w;
}
gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
}`
} );
}
_getCompositeMaterial( nMips ) {
return new ShaderMaterial( {
defines: {
'NUM_MIPS': nMips
},
uniforms: {
'blurTexture1': { value: null },
'blurTexture2': { value: null },
'blurTexture3': { value: null },
'blurTexture4': { value: null },
'blurTexture5': { value: null },
'bloomStrength': { value: 1.0 },
'bloomFactors': { value: null },
'bloomTintColors': { value: null },
'bloomRadius': { value: 0.0 }
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`varying vec2 vUv;
uniform sampler2D blurTexture1;
uniform sampler2D blurTexture2;
uniform sampler2D blurTexture3;
uniform sampler2D blurTexture4;
uniform sampler2D blurTexture5;
uniform float bloomStrength;
uniform float bloomRadius;
uniform float bloomFactors[NUM_MIPS];
uniform vec3 bloomTintColors[NUM_MIPS];
float lerpBloomFactor(const in float factor) {
float mirrorFactor = 1.2 - factor;
return mix(factor, mirrorFactor, bloomRadius);
}
void main() {
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
}`
} );
}
}
UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 );
UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 );
export { UnrealBloomPass };

49
web/vendor/addons/shaders/CopyShader.js vendored Normal file
View File

@ -0,0 +1,49 @@
/** @module CopyShader */
/**
* Full-screen copy shader pass.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const CopyShader = {
name: 'CopyShader',
uniforms: {
'tDiffuse': { value: null },
'opacity': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}`
};
export { CopyShader };

View File

@ -0,0 +1,65 @@
import {
Color
} from 'three';
/** @module LuminosityHighPassShader */
/**
* Luminosity high pass shader.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const LuminosityHighPassShader = {
name: 'LuminosityHighPassShader',
uniforms: {
'tDiffuse': { value: null },
'luminosityThreshold': { value: 1.0 },
'smoothWidth': { value: 1.0 },
'defaultColor': { value: new Color( 0x000000 ) },
'defaultOpacity': { value: 0.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform sampler2D tDiffuse;
uniform vec3 defaultColor;
uniform float defaultOpacity;
uniform float luminosityThreshold;
uniform float smoothWidth;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
float v = luminance( texel.xyz );
vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
gl_FragColor = mix( outputColor, texel, alpha );
}`
};
export { LuminosityHighPassShader };

View File

@ -0,0 +1,100 @@
/** @module OutputShader */
/**
* Performs tone mapping and color space conversion for
* FX workflows.
*
* Used by {@link OutputPass}.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const OutputShader = {
name: 'OutputShader',
uniforms: {
'tDiffuse': { value: null },
'toneMappingExposure': { value: 1 }
},
vertexShader: /* glsl */`
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
precision highp float;
uniform sampler2D tDiffuse;
#include <tonemapping_pars_fragment>
#include <colorspace_pars_fragment>
varying vec2 vUv;
void main() {
gl_FragColor = texture2D( tDiffuse, vUv );
// tone mapping
#ifdef LINEAR_TONE_MAPPING
gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb );
#elif defined( REINHARD_TONE_MAPPING )
gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb );
#elif defined( CINEON_TONE_MAPPING )
gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );
#elif defined( ACES_FILMIC_TONE_MAPPING )
gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );
#elif defined( AGX_TONE_MAPPING )
gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );
#elif defined( NEUTRAL_TONE_MAPPING )
gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );
#elif defined( CUSTOM_TONE_MAPPING )
gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );
#endif
// color space
#ifdef SRGB_TRANSFER
gl_FragColor = sRGBTransferOETF( gl_FragColor );
#endif
}`
};
export { OutputShader };

View File

@ -0,0 +1,53 @@
/** @module VignetteShader */
/**
* Based on [PaintEffect postprocess from ro.me]{@link http://code.google.com/p/3-dreams-of-black/source/browse/deploy/js/effects/PaintEffect.js}.
*
* @constant
* @type {ShaderMaterial~Shader}
*/
const VignetteShader = {
name: 'VignetteShader',
uniforms: {
'tDiffuse': { value: null },
'offset': { value: 1.0 },
'darkness': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float offset;
uniform float darkness;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
// Eskil's vignette
vec4 texel = texture2D( tDiffuse, vUv );
vec2 uv = ( vUv - vec2( 0.5 ) ) * vec2( offset );
gl_FragColor = vec4( mix( texel.rgb, vec3( 1.0 - darkness ), dot( uv, uv ) ), texel.a );
}`
};
export { VignetteShader };

File diff suppressed because it is too large Load Diff

490
web/vendor/addons/utils/SkeletonUtils.js vendored Normal file
View File

@ -0,0 +1,490 @@
import {
AnimationClip,
AnimationMixer,
Matrix4,
Quaternion,
QuaternionKeyframeTrack,
SkeletonHelper,
Vector3,
VectorKeyframeTrack
} from 'three';
/**
* @module SkeletonUtils
* @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
*/
function getBoneName( bone, options ) {
if ( options.getBoneName !== undefined ) {
return options.getBoneName( bone );
}
return options.names[ bone.name ];
}
/**
* Retargets the skeleton from the given source 3D object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
*/
function retarget( target, source, options = {} ) {
const quat = new Quaternion(),
scale = new Vector3(),
relativeMatrix = new Matrix4(),
globalMatrix = new Matrix4();
options.preserveBoneMatrix = options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true;
options.preserveBonePositions = options.preserveBonePositions !== undefined ? options.preserveBonePositions : true;
options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
options.hip = options.hip !== undefined ? options.hip : 'hip';
options.hipInfluence = options.hipInfluence !== undefined ? options.hipInfluence : new Vector3( 1, 1, 1 );
options.scale = options.scale !== undefined ? options.scale : 1;
options.names = options.names || {};
const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
bones = target.isObject3D ? target.skeleton.bones : getBones( target );
let bone, name, boneTo,
bonesPosition;
// reset bones
if ( target.isObject3D ) {
target.skeleton.pose();
} else {
options.useTargetMatrix = true;
options.preserveBoneMatrix = false;
}
if ( options.preserveBonePositions ) {
bonesPosition = [];
for ( let i = 0; i < bones.length; i ++ ) {
bonesPosition.push( bones[ i ].position.clone() );
}
}
if ( options.preserveBoneMatrix ) {
// reset matrix
target.updateMatrixWorld();
target.matrixWorld.identity();
// reset children matrix
for ( let i = 0; i < target.children.length; ++ i ) {
target.children[ i ].updateMatrixWorld( true );
}
}
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options );
boneTo = getBoneByName( name, sourceBones );
globalMatrix.copy( bone.matrixWorld );
if ( boneTo ) {
boneTo.updateMatrixWorld();
if ( options.useTargetMatrix ) {
relativeMatrix.copy( boneTo.matrixWorld );
} else {
relativeMatrix.copy( target.matrixWorld ).invert();
relativeMatrix.multiply( boneTo.matrixWorld );
}
// ignore scale to extract rotation
scale.setFromMatrixScale( relativeMatrix );
relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
// apply to global matrix
globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
if ( target.isObject3D ) {
if ( options.localOffsets ) {
if ( options.localOffsets[ bone.name ] ) {
globalMatrix.multiply( options.localOffsets[ bone.name ] );
}
}
}
globalMatrix.copyPosition( relativeMatrix );
}
if ( name === options.hip ) {
globalMatrix.elements[ 12 ] *= options.scale * options.hipInfluence.x;
globalMatrix.elements[ 13 ] *= options.scale * options.hipInfluence.y;
globalMatrix.elements[ 14 ] *= options.scale * options.hipInfluence.z;
if ( options.hipPosition !== undefined ) {
globalMatrix.elements[ 12 ] += options.hipPosition.x * options.scale;
globalMatrix.elements[ 13 ] += options.hipPosition.y * options.scale;
globalMatrix.elements[ 14 ] += options.hipPosition.z * options.scale;
}
}
if ( bone.parent ) {
bone.matrix.copy( bone.parent.matrixWorld ).invert();
bone.matrix.multiply( globalMatrix );
} else {
bone.matrix.copy( globalMatrix );
}
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
bone.updateMatrixWorld();
}
if ( options.preserveBonePositions ) {
for ( let i = 0; i < bones.length; ++ i ) {
bone = bones[ i ];
name = getBoneName( bone, options ) || bone.name;
if ( name !== options.hip ) {
bone.position.copy( bonesPosition[ i ] );
}
}
}
if ( options.preserveBoneMatrix ) {
// restore matrix
target.updateMatrixWorld( true );
}
}
/**
* Retargets the animation clip of the source object to the
* target 3D object.
*
* @param {Object3D} target - The target 3D object.
* @param {Object3D} source - The source 3D object.
* @param {AnimationClip} clip - The animation clip.
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
* @return {AnimationClip} The retargeted animation clip.
*/
function retargetClip( target, source, clip, options = {} ) {
options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
options.names = options.names || [];
if ( ! source.isObject3D ) {
source = getHelperFromSkeleton( source );
}
const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
delta = clip.duration / ( numFrames - 1 ),
convertedTracks = [],
mixer = new AnimationMixer( source ),
bones = getBones( target.skeleton ),
boneDatas = [];
let positionOffset,
bone, boneTo, boneData,
name;
mixer.clipAction( clip ).play();
// trim
let start = 0, end = numFrames;
if ( options.trim !== undefined ) {
start = Math.round( options.trim[ 0 ] * options.fps );
end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;
mixer.update( options.trim[ 0 ] );
} else {
mixer.update( 0 );
}
source.updateMatrixWorld();
//
for ( let frame = 0; frame < end; ++ frame ) {
const time = frame * delta;
retarget( target, source, options );
for ( let j = 0; j < bones.length; ++ j ) {
bone = bones[ j ];
name = getBoneName( bone, options ) || bone.name;
boneTo = getBoneByName( name, source.skeleton );
if ( boneTo ) {
boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
if ( options.hip === name ) {
if ( ! boneData.pos ) {
boneData.pos = {
times: new Float32Array( end ),
values: new Float32Array( end * 3 )
};
}
if ( options.useFirstFramePosition ) {
if ( frame === 0 ) {
positionOffset = bone.position.clone();
}
bone.position.sub( positionOffset );
}
boneData.pos.times[ frame ] = time;
bone.position.toArray( boneData.pos.values, frame * 3 );
}
if ( ! boneData.quat ) {
boneData.quat = {
times: new Float32Array( end ),
values: new Float32Array( end * 4 )
};
}
boneData.quat.times[ frame ] = time;
bone.quaternion.toArray( boneData.quat.values, frame * 4 );
}
}
if ( frame === end - 2 ) {
// last mixer update before final loop iteration
// make sure we do not go over or equal to clip duration
mixer.update( delta - 0.0000001 );
} else {
mixer.update( delta );
}
source.updateMatrixWorld();
}
for ( let i = 0; i < boneDatas.length; ++ i ) {
boneData = boneDatas[ i ];
if ( boneData ) {
if ( boneData.pos ) {
convertedTracks.push( new VectorKeyframeTrack(
'.bones[' + boneData.bone.name + '].position',
boneData.pos.times,
boneData.pos.values
) );
}
convertedTracks.push( new QuaternionKeyframeTrack(
'.bones[' + boneData.bone.name + '].quaternion',
boneData.quat.times,
boneData.quat.values
) );
}
}
mixer.uncacheAction( clip );
return new AnimationClip( clip.name, - 1, convertedTracks );
}
/**
* Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are
* correctly associated with their bones. Bones are also cloned, and must be descendants of the
* object passed to this method. Other data, like geometries and materials, are reused by reference.
*
* @param {Object3D} source - The 3D object to clone.
* @return {Object3D} The cloned 3D object.
*/
function clone( source ) {
const sourceLookup = new Map();
const cloneLookup = new Map();
const clone = source.clone();
parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
sourceLookup.set( clonedNode, sourceNode );
cloneLookup.set( sourceNode, clonedNode );
} );
clone.traverse( function ( node ) {
if ( ! node.isSkinnedMesh ) return;
const clonedMesh = node;
const sourceMesh = sourceLookup.get( node );
const sourceBones = sourceMesh.skeleton.bones;
clonedMesh.skeleton = sourceMesh.skeleton.clone();
clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
return cloneLookup.get( bone );
} );
clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
} );
return clone;
}
// internal helper
function getBoneByName( name, skeleton ) {
for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) {
if ( name === bones[ i ].name )
return bones[ i ];
}
}
function getBones( skeleton ) {
return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
}
function getHelperFromSkeleton( skeleton ) {
const source = new SkeletonHelper( skeleton.bones[ 0 ] );
source.skeleton = skeleton;
return source;
}
function parallelTraverse( a, b, callback ) {
callback( a, b );
for ( let i = 0; i < a.children.length; i ++ ) {
parallelTraverse( a.children[ i ], b.children[ i ], callback );
}
}
/**
* Retarget options of `SkeletonUtils`.
*
* @typedef {Object} module:SkeletonUtils~RetargetOptions
* @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not.
* @property {number} [fps] - The FPS of the clip.
* @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names.
* @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`.
* @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end.
* @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not.
* @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not.
* @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not.
* @property {string} [hip='hip'] - The name of the source's hip bone.
* @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence.
* @property {number} [scale=1] - The scale.
**/
export {
retarget,
retargetClip,
clone,
};

57362
web/vendor/three.core.js vendored Normal file

File diff suppressed because one or more lines are too long

17990
web/vendor/three.module.js vendored Normal file

File diff suppressed because one or more lines are too long