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>
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.genraw/
|
||||
saves/
|
||||
node_modules/
|
||||
.env
|
||||
49
README.md
Normal 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."** A–E 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 A–E into one seed-to-town game + QA gates | (after A–E) |
|
||||
|
||||
## 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
@ -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 ~2–3; 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 A–E 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`).
|
||||
90
docs/LANES/LANE_A_CITYGEN.md
Normal 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 N–S through the origin,
|
||||
28m corridor (road 10m + verandah'd footpaths). Continuous retail frontage: narrow lots
|
||||
(6–9m frontage, 12–20m deep), zero side setbacks, 1–2 storeys, occasional 3-storey corner
|
||||
anchor. This is where record/toy/book/video shops cluster.
|
||||
2. **Cross streets** every 80–140m; 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 (3–5m 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 2–4
|
||||
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: 5–7 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 9–5, 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).
|
||||
89
docs/LANES/LANE_B_STREETSCAPE.md
Normal 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()` (1441–1515). 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.
|
||||
71
docs/LANES/LANE_C_INTERIORS.md
Normal 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.2–4.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 door→counter always exists.
|
||||
- Runs with zero assets (fallback colours) and zero network.
|
||||
74
docs/LANES/LANE_D_CITIZENS.md
Normal 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 ~258–393 (`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.55–1.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 (25–70m)**: 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 0–200 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
@ -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 (2–3), arcade interior frontage, dept-store anchor (wide),
|
||||
warehouse/roller-door, residential terrace/cottage fronts (3–4), 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.
|
||||
46
docs/LANES/LANE_F_INTEGRATION.md
Normal file
@ -0,0 +1,46 @@
|
||||
# LANE F — INTEGRATION & QA (run after A–E 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 E1–E4 override tables), venue
|
||||
district + gig system crossover with 90sDJsim.
|
||||
95
docs/RESEARCH.md
Normal 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` ~258–393:
|
||||
- `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 (E1–E4) 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.
|
||||
BIN
web/assets/gen/facade-arcade-tile.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
web/assets/gen/facade-besser.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
web/assets/gen/facade-boutique.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
web/assets/gen/facade-brickveneer.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
web/assets/gen/facade-cashtheists.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
web/assets/gen/facade-corrugated.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
web/assets/gen/facade-deco-pastel.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
web/assets/gen/facade-djsim-dept.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
web/assets/gen/facade-djsim-milkbar.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
web/assets/gen/facade-djsim-opshop.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
web/assets/gen/facade-djsim-pawn.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
web/assets/gen/facade-djsim-pub.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
web/assets/gen/facade-djsim-record.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
web/assets/gen/facade-djsim-video.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
web/assets/gen/facade-federation.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
web/assets/gen/facade-fibro-blue.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
web/assets/gen/facade-grimy.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
web/assets/gen/facade-market.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
web/assets/gen/facade-redbrick.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
web/assets/gen/facade-sandstone.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
web/assets/gen/facade-stripmall.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
web/assets/gen/facade-stucco-pink.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
web/assets/gen/facade-terrazzo.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
web/assets/gen/facade-timber-teal.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
web/assets/gen/facade-weatherboard.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
web/assets/gen/ground-asphalt1.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
web/assets/gen/ground-asphalt2.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
web/assets/gen/ground-brickpave.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
web/assets/gen/ground-djsim-footpath.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
web/assets/gen/ground-djsim-road.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
web/assets/gen/ground-footpath1.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
web/assets/gen/ground-footpath2.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
web/assets/gen/ground-grass.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
web/assets/gen/ground-gravel.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
web/assets/gen/ground-reddust.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
web/assets/gen/sky-cold-front.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
web/assets/gen/sky-endless-blue.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
web/assets/gen/sky-golden-arvo.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
web/assets/gen/sky-grey-drizzle.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
web/assets/gen/sky-heat-haze.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
web/assets/gen/sky-high-cirrus.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
web/assets/gen/sky-monsoon.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
web/assets/gen/sky-outback-dusk.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
web/assets/gen/sky-spring-puffy.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
web/assets/gen/sky-summer-storm.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
web/assets/gen/tex-awning-blue.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
web/assets/gen/tex-awning-green.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/assets/gen/tex-awning-red.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
web/assets/gen/tex-boards-polished.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
web/assets/gen/tex-boutique-wall.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/assets/gen/tex-brick.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
web/assets/gen/tex-carpet-greygreen.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
web/assets/gen/tex-carpet-mustard.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
web/assets/gen/tex-carpet-swirl.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
web/assets/gen/tex-grimy-wall.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
web/assets/gen/tex-lino-check.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
web/assets/gen/tex-lino-cork.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
web/assets/gen/tex-pegboard.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
web/assets/gen/tex-render.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
web/assets/gen/tex-velvet-curtain.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
web/assets/gen/tex-wood-counter.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
web/assets/gen/wall-damask-mauve.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
web/assets/gen/wall-diamond-green.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
web/assets/gen/wall-floral-cream.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
web/assets/gen/wall-floral-gold.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
web/assets/gen/wall-geo-orange.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
web/assets/gen/wall-stripe-sage.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/assets/gen/wall-trellis-blue.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
web/assets/gen/wall-woodchip-white.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
36
web/js/core/canvas.js
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
270
web/vendor/addons/controls/PointerLockControls.js
vendored
Normal 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
361
web/vendor/addons/postprocessing/EffectComposer.js
vendored
Normal 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 };
|
||||
194
web/vendor/addons/postprocessing/MaskPass.js
vendored
Normal 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 };
|
||||
138
web/vendor/addons/postprocessing/OutputPass.js
vendored
Normal 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
@ -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 };
|
||||
182
web/vendor/addons/postprocessing/RenderPass.js
vendored
Normal 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 };
|
||||
134
web/vendor/addons/postprocessing/ShaderPass.js
vendored
Normal 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 };
|
||||
491
web/vendor/addons/postprocessing/UnrealBloomPass.js
vendored
Normal 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
@ -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 };
|
||||
65
web/vendor/addons/shaders/LuminosityHighPassShader.js
vendored
Normal 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 };
|
||||
100
web/vendor/addons/shaders/OutputShader.js
vendored
Normal 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 };
|
||||
53
web/vendor/addons/shaders/VignetteShader.js
vendored
Normal 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 };
|
||||
1432
web/vendor/addons/utils/BufferGeometryUtils.js
vendored
Normal file
490
web/vendor/addons/utils/SkeletonUtils.js
vendored
Normal 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,
|
||||
};
|
||||