[lane F] Round 0 scaffold: docs, contracts, lane charters, bootable stub world

GDD v1 (flow-locked hybrid movement, 5 biomes + secret, boss roster),
TECH contracts (world API, level schema v0, bus events, manifest law),
ART_BIBLE (synthetic scanner look), PIPELINE (MODELBEAST-first, fal gated),
PROCESS (PROCITY lane/round law), charters A-F + ROUND1 instructions.
Seed code: shell + boot + core (rng/bus/flags) + stub world (peristalsis
shader tube, 1 draw / 102k tris, contract-complete) + qa.sh (GREEN).
Verified in-browser; evidence docs/shots/laneF/round0_stub_tube.png.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jing 2026-07-16 01:28:35 +10:00
commit 34f84ab162
42 changed files with 85788 additions and 0 deletions

11
.claude/launch.json Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "guts",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "8140", "--directory", "web"],
"port": 8140
}
]
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
node_modules/
pipeline/.genraw/
pipeline/.genprops/
pipeline/.genaudio/
pipeline/_normalized/
docs/shots/**/*.mov
docs/shots/**/*.mp4

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# GUTS — a fantastic voyage through the alimentary canal
Browser flight-combat game (three.js, zero build step). You pilot **ENDO-1**, a microscopic
probe, mouth → rectum through five anatomically-grounded biomes: ride peristalsis down the
esophagus, cross the acid sea of the stomach, thread the villi forest of the small intestine,
survive the gas swamps of the colon. *Fantastic Voyage* × *Star Fox* × *Descent*.
- **Design:** [docs/GDD.md](docs/GDD.md) · **Look:** [docs/ART_BIBLE.md](docs/ART_BIBLE.md)
- **Architecture + contracts:** [docs/TECH.md](docs/TECH.md)
- **Asset generation:** [docs/PIPELINE.md](docs/PIPELINE.md)
- **How we work (lanes/rounds):** [docs/PROCESS.md](docs/PROCESS.md) — **read this first if you are a lane**
- **Current round:** [docs/LANES/ROUND1_INSTRUCTIONS.md](docs/LANES/ROUND1_INSTRUCTIONS.md)
## Run it
```bash
cd web && python3 -m http.server 8140
# http://localhost:8140 — the game
# http://localhost:8140/?stub=1&dbg=1 — stub world + debug HUD (works from day 0)
```
No bundler, no npm, no build. ES modules + import map; three.js r175 vendored in `web/vendor/`
(never CDN — the game must run offline). Before you finish a session: `bash tools/qa.sh`.
## The one-paragraph tech pitch
The whole canal is a **spline**. World position is `(s, θ, ρ)` — distance along the canal,
angle and radius in the cross-section. Tube walls are chunked extrusions along
parallel-transport frames; peristalsis and breathing are **vertex-shader displacement**
(cheap, squishy, everywhere); collision is an analytic radius test (no physics engine).
Caverns (mouth, stomach) switch the same controller to free 6DOF. Look is **synthetic
scanner** — dark endoscope void, SEM-style monochrome walls with FLUX-generated detail
textures, emissive-coded enemies. Everything is seeded + deterministic, and every asset is
optional at runtime — the game boots and plays asset-free with procedural fallbacks.
## Hard laws (all lanes)
1. **Determinism.** No `Math.random` outside `js/core/rng.js`. Seeded RNG only. qa.sh greps.
2. **Assets are optional.** Missing texture/model/audio ⇒ procedural fallback, never a crash.
3. **Eyeball everything.** Visual work isn't done until a screenshot is in `docs/shots/laneX/`.
4. **Perf budget:** ≤300 draw calls, 60 fps on M-series / mid laptop. Measure via `window.DBG`.
5. **Free-first.** Local MODELBEAST generation before any paid API; fal.ai is a gated fallback
(John's go). See PIPELINE.md.
6. **Git:** stage only your exact paths, commit + push each session, never `git add .`.
## Deploy
Target: partly.party (static). Not wired yet — when it's time, consult the `deploy-map` skill
and run `ship-check` first. Origin: `ssh://git@100.71.119.27:222/monster/guts.git`.

78
docs/ART_BIBLE.md Normal file
View File

@ -0,0 +1,78 @@
# ART BIBLE — the "synthetic scanner" look
Owner: Lane D (generation) + Lane A (shaders) + F (final eyeball). This settles the
realistic-vs-wireframe question from the concept discussion: **hybrid, decided.**
## The look, in one line
A live feed from an impossible medical instrument: dark endoscope void, walls rendered like
scanning-electron-microscope tissue in a single biome tint with artificial rim light,
everything alive and pulsing; gameplay-relevant things glow.
Why: SEM-monochrome + rim light gives us the gross-out organic detail *and* perfect visual
hierarchy (emissive enemies pop against dark tinted walls) *and* browser-friendly perf (no
real SSS, no fluid sim — fresnel rim + matcap + vertex displacement fake all of it).
## Rendering recipe (Lane A)
- **Walls:** custom ShaderMaterial. Base = biome tint × FLUX detail texture (used as
luminance/AO, tiled along (s,θ) UVs) + **fresnel rim light** (the SEM edge glow, biome
accent color) + optional matcap for wet specular. NO real-time lights needed for walls.
- **Motion:** vertex shader — traveling peristalsis wave `disp = A·pulse(k·s ω·t)` along
inward normal, plus small fbm "breathing". Villi = InstancedMesh cones swaying with the
same phase. Collision uses conservative max amplitude (TECH: `wallRho`).
- **Depth:** exponential fog to the biome void color — the tunnel ahead fades to dark, the
scanner-light radius around the player is your safety bubble (esp. level 5).
- **Post (F wires, A supplies):** vignette + faint scanline/noise overlay (the "feed"),
chromatic aberration pulse on damage, subtle radial blur on boost. All cheap fullscreen.
## Emissive code (readability law — never violate)
| meaning | color |
|---|---|
| hostile | hot amber → red core `#ff5a2a` |
| hostile projectile | white-hot `#ffe9d0` |
| neutral flora / interactive | cyan `#39e6ff` |
| pickups | soft green `#7dffb0` |
| checkpoints/gates | violet `#b06aff` |
| acid / corrosive zones | sickly yellow-green `#c8ff3a` |
## Biome palettes (tint / accent-rim / void-fog)
| biome | tint | rim accent | void | feel |
|---|---|---|---|---|
| oral cavity | pale blue-white `#a8c4d8` | ice `#dff2ff` | `#06090e` | clinical, bright, tutorial-readable |
| esophagus | deep teal `#1e6e64` | mint `#5affd2` | `#02100d` | speed, ribbed rings rushing past |
| stomach | amber `#b0571e` | ember `#ffb13a` | `#120801` | hell-sea; acid glows `#c8ff3a` |
| small intestine | magenta-violet `#7a2a6e` | pink `#ff6ad5` | `#0d0312` | dense alien forest |
| large intestine | murk green `#3a4a26` | bio-lume `#9dff4a` | `#050702` | dark swamp, scanner-light gameplay |
| appendix (secret) | gold `#8a7020` | `#ffe066` | `#0a0800` | treasure room |
## FLUX prompt kit (Lane D — flux_local, 1024², then tile-check + normal-derive)
Style stem, always: `scanning electron microscope micrograph, monochrome, high detail
organic tissue, dark background, dramatic rim lighting, seamless tiling texture`
- esophageal wall: stem + `human esophageal mucosa, longitudinal ribbed folds, wet ridges`
- stomach wall: stem + `gastric mucosa, pitted craters, gastric pits, glistening mucus`
- small intestine: stem + `intestinal villi, dense finger-like projections, forest of fronds`
- colon wall: stem + `colonic mucosa, smooth undulating folds, scattered biofilm patches`
- matcaps: `spherical material study ball, wet translucent organic tissue, subsurface glow,
studio black background` (crop sphere → matcap)
- Texture is authored **grayscale**; the shader applies biome tint. One texture can serve
two biomes at different tints/tilings — generate ~2 per biome, not ten.
- Tiling: generate → wrap-offset check (see PIPELINE) → if seams, mirror-tile or inpaint the
seam with `mflux_image_edit`. **Eyeball every texture in-engine before committing.**
## Hero meshes (round 2+, D): ENDO-1 ship, macrophage, tapeworm head, pyloric guardian
nodes, Blockage chunks. Concept via flux_local (stem: `game asset concept, single object,
centered, black background` + subject) → sf3d draft to sanity-check silhouette → trellis_mac
final → normalize to house GLB law. Blobby organic forms are TRELLIS's sweet spot; thin
fins/antennae or translucency are its weakness → fal.ai Hunyuan fallback list (gated).
## Screens & HUD (Lane E)
HUD is part of the fiction: an instrument overlay. Thin cyan line-work, small caps type,
data-noise ticks. The **gut-map** is the star: a full alimentary-canal silhouette as the
level/progress bar, current position blipping. Damage = feed glitches (aberration, tearing),
not red screen-edges. Title screen: slow fly-through of the esophagus stub + big type.

95
docs/GDD.md Normal file
View File

@ -0,0 +1,95 @@
# GUTS — Game Design Document (v1, round 1)
Owner: Lane C (with F sign-off). This v1 settles the open questions from the original
concept discussion; C refines numbers/encounters in-lane, but the **decisions** below are
integrator-settled for v1.
## Fantasy & tone
You are ENDO-1, a one-pilot micro-probe injected to clear a patient's digestive tract of a
hostile infestation, mouth to exit. Tone: playful-gross medical sci-fi — *Fantastic Voyage*
wonder, *Star Fox* arcade flow, a wink of *Osmos*-y biology-nerd humor. The body is not the
enemy; it's the weather. Enemies are the infestation + an immune system that can't tell
you're friendly.
## DECISION — movement model: "flow-locked" hybrid
Neither pure rail nor pure 6DOF — **the anatomy picks the mode**:
- **Tube mode** (esophagus, intestines): peristalsis is a forward current you cannot fully
fight. Base flow speed comes from the biome (`world.biomeAt(s).flow`); throttle gives
±40%, boost gives short +120% bursts. Full analog movement in the cross-section disc
(mouse/stick aims, ship banks). You steer *within* the tube; the tube carries you. This is
the Star Fox layer: readable, forward, fast.
- **Arena mode** (oral cavity, stomach, boss lairs): the same controller unlocked to free
6DOF, Descent-style, no gravity. Slower, spatial, exploratory. This is where fights and
secrets breathe.
- Transitions are diegetic: sphincters are literal gates between modes.
Rationale: pure rail wastes the stomach; pure 6DOF makes the esophagus a boring corridor.
The hybrid also keeps collision cheap in the 90% case (tube = radius test).
## DECISION — structure: 5 levels + gates + 1 secret
Each level ends at a real anatomical valve, staged as a gate ritual or boss:
| # | level | mode | signature mechanic | exit gate |
|---|---|---|---|---|
| 1 | **Oral Cavity** | arena (tutorial) | dodging molar crush-cycles, saliva tides | **Epiglottis** — timing gate: dive on the swallow |
| 2 | **Esophagus** | tube (speed run) | ride peristalsis rings for boost; **reflux surge** chase finale | **Cardiac sphincter** — outrun the acid wall through it |
| 3 | **Stomach** | arena (the acid sea) | pH as environmental damage; mucus shallows are safe lanes; churn cyclones | **BOSS: Pyloric Guardian** — the sphincter itself |
| 4 | **Small Intestine** | tube (the long maze) | villi forest occludes sightlines; bile slicks; immune hunters | **MINIBOSS: Tapeworm chase** → ileocecal valve |
| 5 | **Large Intestine** | tube→arena (gas swamps) | flammable gas pockets: shooting inside one detonates it (hurts everyone — risk/reward); darkness + your scanner light | **BOSS: The Blockage** — then triumphant ejection finale |
| S | **Appendix** (secret) | arena | hidden lair off the cecum, bonus loot + lore | — |
Target: 610 min/level first playthrough, ~45 min campaign, built to replay for medals.
## Player systems
- **Hull** (HP, pickups only) + **Mucus coat** (regenerating shield; drained continuously by
ambient pH/enzymes — biome-dependent — and by hits). The coat is the pH mechanic: in the
stomach open acid, coat drain is brutal; hugging mucus shallows pauses it.
- **Weapons:** primary **lysozyme cannon** (fast pellet stream, infinite, heat-limited);
secondary **antacid torpedo** (limited ammo, AoE, *also neutralizes an acid zone for ~10 s*
— the dual-use is the interesting bit).
- **Boost** with a short i-frame window at spend; recharges from nutrient orbs.
- **Pickups:** nutrient orbs (score+boost), mucin globs (coat refill), B12 cell (hull),
antacid ammo, **biopsy samples** (3 hidden per level — the collectible).
## Enemies (v1 roster — B implements behaviors, D makes them pretty later)
All emissive-coded (ART_BIBLE): hostile = hot amber/red, projectiles = white-hot,
neutral flora = cyan (turns hostile if shot).
| enemy | biome | behavior archetype |
|---|---|---|
| amylase droplet | mouth | slow floater, corrosive contact aura |
| bolus chunk | esophagus | tumbling debris in the flow (crush) |
| pepsin swarm | stomach | small fast seekers, dissolve on kill |
| churn cyclone | stomach | environmental vortex, flings you |
| macrophage | small int. | big slow hunter; grab → mash to escape; eats other corpses to grow |
| antibody turret (Peyer's patch) | small int. | wall-mounted, fires homing Y-shaped darts |
| gut flora colony | large int. | neutral biofilm; shot ⇒ angry E. coli swarm |
| gas vent | large int. | hazard: thrust-shove + flammable cloud |
Bosses: **Pyloric Guardian** (armored sphincter; destroy mucosal nodes during open-cycles
while dodging acid spouts, arena slowly churns), **Tapeworm** (tube chase; shoot segment
weak-points as it coils around the canal ahead/behind), **The Blockage** (compacted-mass
fortress with embedded turrets and gas pockets; peel it layer by layer, finale = the canal
itself expels you both).
## Scoring & replay
Star-Fox-style medals per level: score (kills × style multiplier, chained kills), par time,
biopsy samples, damageless-gate bonus. Combo meter feeds the music layer (E). Endless
"colonoscopy run" mode is V2 (see V2_IDEAS.md).
## Difficulty & assists
Three difficulties scale coat-drain, enemy density, boss timing windows. Assist toggles
(flow auto-center, aim magnetism) — arcade games live or die on feel accessibility.
## Open for C to design in-lane (not settled)
Exact level lengths/pacing curves, encounter tables per segment, par times, pickup economy,
boss phase timings, secret placement, tutorialization beats of level 1.

View File

@ -0,0 +1,49 @@
# LANE A — World (canal geometry, biome shaders, arenas)
Mission: the alimentary canal itself — a living, pulsing, deterministic tube-world that
implements THE WORLD CONTRACT (TECH.md) exactly, looks like the ART_BIBLE, and stays inside
the perf budget. You own `web/js/world/**` and nothing else.
## Core design (settled)
- **Centreline spline** per level, generated from C's level JSON (`segments[].curviness`,
seeded via `core/rng.js`): sum of seeded low-frequency sine/fbm offsets along the main
axis — smooth, no self-intersection, byte-identical per seed. Arclength-parameterized
lookup table (s → t) so `sample(s)` is O(1)-ish.
- **Parallel-transport frames** (not Frenet — Frenet flips on inflections and will make B's
camera roll-snap; this is the classic tube-racer bug, don't ship it).
- **Tube chunks:** extruded rings, ~2 rings/unit × 6496 radial segs, chunked every ~40
units; stream a window of chunks around `playerS` (build/dispose, leak-clean). Bake
`(s, θ)` into UV2 for the shader.
- **Radius law:** `radius(s) = base · (1 + wobble·fbm(s))`, plus per-biome features (villi
band in small intestine as InstancedMesh cones on the wall — instanced, one draw).
- **Wall shader** (ShaderMaterial, ART_BIBLE recipe): biome tint × detail texture
(luminance, from D via `assets.get`, flat-tint fallback) + fresnel rim + fog-to-void.
**Peristalsis in the vertex shader:** `disp = A·max(0, sin(k·s ω·t))³` traveling wave +
small fbm breathing, along inward normal. Export the *same* phase function to JS
(`world.flowPulse(s, t)`) so B can boost players riding a wave crest and E can pulse audio.
- **Arenas:** displaced icospheres (seeded fbm) with the same wall shader; `arenaAt` gives
B the 6DOF clamp bounds. Stomach arena has an "acid level" plane (animated, emissive
`#c8ff3a`) whose height C's events can move.
- **Collision truth:** `wallRho(s,θ)` = geometric radius max displacement amplitude
skin margin. `collide()` for arena mode + hazards. Cheap, analytic, no physics lib.
## You owe others
- The world contract, frozen after round-1 sign-off (changes after that: propose in NOTES,
F referees). `world.hash()` golden determinism hash + a qa selfcheck.
- `?fly=1` noclip camera so everyone can inspect your work without Lane B.
- `js/world/biomes.js` — the palette/params registry (values from ART_BIBLE; C's JSON
references biomes by id).
## You consume
C's level JSON (schema TECH.md), D's textures via `assets.get` (never hard-require),
F's `core/{rng,bus,flags}.js`, stub as reference implementation of the contract.
## Laws
Determinism (no Math.random, no Date.now in geometry paths — shader time comes from
`world.update(dt)` accumulation). Perf: tube worst-view ≤150 draws (leave headroom for B/E).
Dispose-clean streaming (renderer.info deltas cited in NOTES). Screenshot evidence every
round (`docs/shots/laneA/`), each biome as it lands.

View File

@ -0,0 +1,50 @@
# LANE B — Flight & Combat (controller, weapons, enemies)
Mission: the game *feel*. Flow-locked flight that's silk at 60 fps, readable combat, enemy
behaviors with personality. You own `web/js/flight/**` and `web/js/combat/**`.
## Flight (the hybrid model, GDD-settled)
- Player state lives in **spline space** in tube mode: `(s, x, y)` where (x,y) is the
cross-section disc offset. `ds/dt = biome.flow · throttle + boost`; throttle ∈ [0.6, 1.4],
boost bursts +120% with i-frames. Convert to world space via `world.sample(s)` frame.
Clamp disc position against `world.wallRho(s, θ)` — wall contact = velocity-scaled coat
damage + a shove, never a hard stop (arcade, not sim).
- **Arena mode** (world.modeAt): same inputs drive free 6DOF (velocity-damped, no gravity),
clamped to `arenaAt` bounds. Mode transition = smooth handoff over ~0.5 s at the gate.
- **Input:** mouse-aim + WASD (strafe = disc movement), Space boost, gamepad twin-stick.
Aim reticle leads the ship (Star Fox lag), ship banks into lateral motion.
- **Camera rig:** chase cam on the parallel-transport frame, spring-damped, slight FOV kick
on boost. Camera roll follows frame `nor` — never world-up in tube mode (this is why A
uses parallel transport; if you see roll snapping, file it to A, don't hack it here).
- Riding a peristalsis crest (`world.flowPulse(s,t)` > threshold at your s) grants surf
boost — the esophagus mechanic. Feel numbers documented in NOTES as you tune.
## Combat
- **Weapons:** lysozyme cannon (hitscan-ish fast pellets, heat meter), antacid torpedo
(slow AoE projectile; emits `level:neutralize {s, radius, duration}` on the bus — A/C
consume for acid zones). Pooled projectiles, InstancedMesh, one draw per weapon type.
- **Damage model** (GDD): mucus coat (regen, ambient drain = `biome.coatDrain`) over hull.
Emit `player:damage {amount, kind}`; E renders the feedback, you own the numbers.
- **Enemy framework:** `combat/enemies.js` registry — `spawn(type, {s, theta, rho})` from
C's level events (listen `level:event` type `spawn`). Behaviors are small state machines
updated in one pass; visuals = emissive primitives (ART_BIBLE colors) with
`assets.get('models', type)` upgrade path when D's GLBs land (swap mesh, keep logic).
- Round-1 archetypes: **floater** (amylase droplet — drifts, contact aura), **seeker**
(pepsin — pursue in spline space, cheap: chase in (s,x,y)), **turret** (wall-mounted,
homing darts with turn-rate limit — dodgeable by design). More per GDD roster later.
- Hit tests: sphere-vs-sphere in world space, broad-phase by s-bucket. No physics engine.
## You owe / consume
Owe: `createPlayer(…)` + `createCombat(…)` factories (TECH convention), bus events as
specced, feel-tuning writeup in NOTES (the numbers ARE the deliverable). Consume: world
contract (build against `?stub=1` until A lands — the stub is the contract), C's events,
E renders your bus emissions, D's models optionally.
## Laws
Everything pooled + disposed (leak-test enter/exit). Determinism: enemy RNG from `core/rng`
streams. ≤80 draws budget for all combat visuals. Playability > realism, always. Screenshot
+ short capture evidence (`docs/shots/laneB/`); cite `window.DBG` numbers in NOTES.

View File

@ -0,0 +1,48 @@
# LANE C — Levels & Encounters (data, pacing, bosses)
Mission: turn the GDD into playable levels — you are the game designer lane. You own
`web/js/levels/**` (JSON + registry/loader) and `docs/GDD.md` refinements.
## What you build
- **Level JSONs** (schema TECH.md — you own the schema from round 1 sign-off onward; evolve
it via NOTES + F referee, never silently). One file per level, `L1_mouth``L5_colon`
+ `LS_appendix`. Everything data-driven: if you need a new event `type` or enemy param,
spec it in NOTES for B/A and stub it in data first.
- **`levels/index.js`**: registry, loader, and a **schema selfcheck** (every biome id exists
in A's registry, events sorted by s, s within length, checkpoint before every boss…) that
qa.sh runs headlessly via `node`.
- **Encounter design:** density curves (breather → pressure → spike → gate), teach-then-test
(every mechanic appears safe before it appears lethal), par times, pickup economy,
biopsy-sample hiding spots (visible-but-costly law: secrets are seen, reaching them is
the challenge).
- **Boss scripts** (`levels/bosses/*.js` — logic-as-data where possible, small state
machines where not; B owns the enemy framework you plug into): Pyloric Guardian, Tapeworm
chase, The Blockage. Spec each as: phases, telegraphs, windows, failure pressure.
- **Difficulty scaling:** a single `difficulty.js` table (coat-drain ×, density ×, window ×)
— GDD §Difficulty. No per-level hand-tuned forks.
## Design laws
- **Anatomy is the level designer.** Every hazard/setpiece maps to something real (GDD
table) — the game's charm is that it's *actually* the digestive tract. When inventing,
check the anatomy first; reality is weirder and better (bile from the duodenum, Peyer's
patches, the appendix as a secret room are all real).
- **Readable at speed.** Tube levels are experienced at 1020 u/s: telegraph everything
≥2 s upstream (E gives you `audio:cue` + HUD pings — spec what you need in NOTES).
- **Checkpoints are sphincter-themed** where anatomy allows; death costs ≤30 s of progress.
- Tutorialization: level 1 teaches move/shoot/coat in arena safety; the epiglottis gate is
the exam.
## You owe / consume
Owe: level JSONs + registry + selfcheck; encounter tables per level in NOTES (they double
as B's spawn-behavior wishlist); boss specs early (round 12) so A can build lair arenas
and D can concept hero meshes. Consume: A's biome registry, B's enemy/event vocabulary,
GDD as your spec (refine it in place — it's yours now, banner what you change).
## Verification
You can't screenshot data — your evidence is **playthrough notes**: boot each level
(`?lvl=L2_esophagus&stub=1` before A lands), fly it, record pacing against your curve, note
where it lies. Cite times/densities measured, not intended. qa selfcheck stays green.

View File

@ -0,0 +1,46 @@
# LANE D — Assets (FLUX textures, GLB meshes, audio, manifest)
Mission: make it gorgeous, for free. You own `pipeline/**`, `web/assets/**`, and
`web/js/core/assets.js`. **PIPELINE.md is your bible; ART_BIBLE.md is your spec.**
Everything you ship is optional at runtime — you make GUTS beautiful, not functional.
## Ground rules (hard-won, PROCITY scars)
- **Free-first, always.** flux_local / sf3d / trellis_mac on the m3ultra box
(`m3ultra@100.89.131.57`, repo cloned there). fal.ai only via `FAL_LIST.md` + John's go,
after ≤2 local attempts. Check 3GOD depot + `~/Documents/3D=models/` before generating.
- **Port, don't reinvent:** PROCITY's `gen_skins.py --local`, `gen_props.py`, `gen_audio.py`,
`normalize.py`, `glb_stat.py` (`~/Documents/PROCITY/pipeline/`) are proven against these
exact operators. Adapt them; keep their idempotent/resumable, `--dry-run`-first shape.
- **Eyeball law:** contact sheets for every batch; view textures in-engine on the stub tube;
thumbnail every GLB; delete rejects before harvest. Never "should look right".
- **House GLB law:** metres, +Y up, sensible origin, ≤5k tris (decimate TRELLIS's 2577k),
WebP ≤1024, no Draco. `normalize.py` enforces; you verify.
- One GPU job at a time on the beast; long TRELLIS runs get a heartbeat (`jobs` convention)
if >10 min.
## Deliverables by area
- **Textures:** grayscale SEM-style tiling walls (2/biome), matcaps, derived normal maps
(`derive_maps.py`, numpy Sobel — no model), acid/decal sprites. ART_BIBLE prompt kit.
- **Meshes (round 2+):** ENDO-1 ship, GDD enemy/boss heroes. concept → sf3d draft →
trellis_mac → normalize → thumb → manifest. Enemies must read at a glance: silhouette
first, detail second; check thumbs at 128 px — if it doesn't read there, it won't in-game.
- **Audio:** procedural numpy pack (PIPELINE §Audio) — beds per biome, sfx set, stingers.
OGG+M4A dual, ≤10 MB shipped, spectrogram + loop-seam evidence.
- **`assets.js` + `manifest.json`:** the loader contract (TECH.md) — null on miss, never
throw; `?localassets=0` empty-boot path; `build_manifest.py` + `validate_manifest.py`
(qa hook).
## You owe / consume
Owe: the manifest contract + validator; contact sheets/thumbs in `docs/shots/laneD/`;
provenance + prompt log per asset (committed batch JSONs — regeneration must be possible);
FAL_LIST.md when candidates appear. Consume: ART_BIBLE (F/A arbitrate look disputes), C's
boss specs for hero concepts, A's UV/tiling conventions (ask in NOTES before assuming).
## Verification
In-engine screenshots of your textures on the stub tube (`?dbg=1`), thumbs for meshes,
spectrograms for audio, `validate_manifest.py` 0 errors in qa. Numbers (gen-time, file
sizes, tri counts) measured and logged in NOTES.

View File

@ -0,0 +1,47 @@
# LANE E — UI / HUD / Audio engine
Mission: the instrument you fly by. Diegetic scanner HUD, screens, and the WebAudio engine
that plays D's pack (or synthesizes fallbacks). You own `web/js/ui/**` and `web/js/audio/**`.
## HUD (ART_BIBLE §Screens: thin cyan line-work, small caps, data-noise ticks)
- Core readouts: mucus-coat + hull bars, heat meter, torpedo count, speed/flow indicator,
combo meter, score. All driven by **bus events only** (TECH §Bus) — zero imports from
B's internals; if a signal is missing, request the event in NOTES, don't reach in.
- **The gut-map** (the star): full alimentary-canal silhouette as the campaign progress
bar — current level lit, player blip moving along it, checkpoints ticked. One SVG/canvas
path, hand-drawn once, used everywhere (title, HUD corner, level-complete).
- Telegraphs: upstream-hazard pings for C (spec: `level:event` lookahead), boss health
when `boss:start`.
- **Damage = feed corruption** (aberration/tearing via F's post hooks + your overlay
glitches), not red vignette. Death/respawn = feed drop + re-acquire sequence. Cheap DOM/
canvas overlay preferred over three.js sprites — it's an instrument overlay, let it be 2D.
- Screens: title (over `?fly=1`-style esophagus drift), pause, level-complete medal card
(score/par/samples — GDD §Scoring), game-over, minimal settings (difficulty, assists,
volume, `?mute` respect).
## Audio engine (`js/audio/engine.js`)
- WebAudio graph: bed bus (crossfade per `biomeAt` changes), sfx bus (pooled buffers),
stinger duck. Autoplay-unlock on first input (browser law).
- Consumes D's manifest audio (`ogg` with `m4a` fallback via `canPlayType`). **Procedural
fallback law:** no pack ⇒ synthesized WebAudio beeps/noise bursts so the game is never
silent-broken (`assets.get` null path).
- The heartbeat: a low pulse bed whose **tempo maps to danger** (combo/boss/coat-low —
define the danger scalar from bus events, document the mapping). This is the audio
identity of the game; treat it as a headline feature.
- Sync hook: `world.flowPulse` phase available for pulse-aligned squelches (nice-to-have,
round 2+).
## You owe / consume
Owe: `createHUD/createScreens/createAudio` factories (TECH convention), the event-needs
list in NOTES (B and C build to it), volume/mute persistence (localStorage). Consume: bus
events, D's audio manifest, ART_BIBLE, F's post-processing hooks for damage fx.
## Laws
60 fps law applies to the HUD too — no per-frame DOM layout thrash (mutate via transforms/
canvas; measure with `?dbg=1`). Everything disposes (screens swap clean). Screenshots of
every screen + HUD states in `docs/shots/laneE/`; audio evidence = the engine playing D's
spectrogram-verified pack + a written fallback-mode check.

View File

@ -0,0 +1,34 @@
# LANE F — Integration (Fable, the integrator)
Mission: everything between the lanes. Shell, core, contracts, QA gate, round instructions,
final eyeball. F is run by the coordinating session (Fable), not an Opus lane — lanes never
edit F territory: `web/index.html`, `web/js/boot.js`, `web/js/core/{rng,bus,flags}.js`,
`web/js/stub/**`, `tools/qa.sh`, `docs/LANES/ROUND*_INSTRUCTIONS.md`.
## Standing duties
- **Round loop:** read every lane's NOTES + progress + shots after a round; verify claims
by booting the game (eyeball law applies to the integrator most of all); write
`ROUND<N+1>_INSTRUCTIONS.md` with integrator decisions spelled out; banner the old round
SUPERSEDED.
- **Shell wiring:** lanes offer factory snippets in NOTES (`### → Lane F`); F pastes into
boot.js behind flags, updates the TECH flags table.
- **Contract referee:** world contract freeze, schema evolution, bus-event naming, budget
arbitration (who spends the draw calls).
- **QA gate:** grow `tools/qa.sh` — each round's regressions become next round's gates
(PROCITY pattern: selfchecks accumulate; keep it fast and dependency-free).
- **Stub parity:** `world_stub.js` must track the world contract exactly — it's the
contract's executable spec and every lane's fallback.
- Integration passes: cross-lane bugs (dispose leaks at seams, event races, budget
overruns) are F's to fix or assign.
## Verification duties
Boot matrix per round: default, `?stub=1`, `?localassets=0`, `?fly=1`, each `?lvl=`.
Zero console errors. DBG numbers recorded in `docs/progress/f-progress.md`. Screenshots
of integration state per round in `docs/shots/laneF/`.
## Ship duties (later)
Deploy to partly.party via `deploy-map` skill; `ship-check` before anything is exposed;
version tags on round completion (`v0.<round>`).

View File

@ -0,0 +1,94 @@
# GUTS — Round 1 lane instructions (from Fable, integrator)
Date: 2026-07-16 · Repo state: F's round-0 scaffold is committed on `main` — docs complete
(README/PROCESS/GDD/TECH/ART_BIBLE/PIPELINE + charters), shell boots the **stub world**
(straight pulsing tube, flat shading) with a fly camera, `tools/qa.sh` GREEN. Everything
below builds against that scaffold. Read PROCESS.md §A-lane-session before starting.
## Integrator decisions (settled — don't relitigate in-round)
1. **Movement is the flow-locked hybrid** (GDD). **Look is synthetic scanner** (ART_BIBLE).
Wireframe-only and full-flesh-realism are both rejected.
2. **The world contract in TECH.md is v1.** Round 1 is its shakedown: A implements it, B
consumes it (via stub first). Friction → NOTES immediately; F freezes it at round end.
3. **Level schema v0 in TECH.md is C's to ratify** — C may amend it this round (it's young),
but must update the stub's sample level + notify A/B via NOTES in the same session.
4. **No mesh generation this round** (D). Textures + audio proof first; hero meshes need
C's boss specs and concept sign-off (round 2). Exception: sf3d drafts to *test* the
concept loop are fine (free, seconds).
5. **Vertical slice target = level 2 (esophagus)**, not level 1: the tube is the core tech
risk and the esophagus is the purest tube. Mouth/arena tutorial comes after the spine
works. Round 2 goal: fly the esophagus with 2 enemy types, HUD live, textured walls.
6. Dev port **8140**; evidence law and exact-path git staging are in force everywhere.
## Lane A — the canal, v0
Build `js/world/` per your charter, priority order:
1. Spline + parallel-transport frames + arclength LUT from level JSON (consume
`js/levels/L2_esophagus.json`; the stub's inline level shows the shape). Seeded, and
`world.hash()` + selfcheck wired for qa.
2. Chunked tube geometry with (s,θ) UVs, streaming window, dispose-clean (cite
renderer.info deltas in NOTES).
3. Wall ShaderMaterial: biome tint + fresnel rim + fog (flat-tint fallback path — D's
textures may land mid-round; integrate via `assets.get` if present).
4. Peristalsis vertex displacement + `world.flowPulse(s,t)` JS mirror.
5. `?fly=1` noclip camera. Screenshot set: esophagus straights, curves, a wave crest.
Stretch: stomach arena icosphere v0. Don't touch: player, HUD, levels' content values.
## Lane B — flight + first blood, on the stub
Work entirely against `?stub=1` (contract-identical); swap to A's world late in the round
if it's landed, but **don't block on A**.
1. `createPlayer`: tube-mode controller (s,x,y), mouse+WASD+gamepad, banking chase cam,
boost + i-frames, wall-graze damage + shove. Feel numbers documented in NOTES.
2. Weapons: lysozyme cannon (pooled, instanced, heat) + hit tests vs sphere targets.
3. Enemy framework + 3 archetypes (floater/seeker/turret) as emissive primitives, spawned
from `level:event` (the stub level includes sample spawns; C's L2 JSON is your real feed).
4. Damage/coat model + all bus emissions per TECH §Bus (E builds against these — emit
faithfully even with no listener).
Evidence: capture or screenshot sequence + DBG numbers. Arena-mode controller = round 2.
## Lane C — GDD ownership + the data spine
1. Ratify/amend level schema v0 (TECH.md) — you own it from now; sync stub sample + NOTES.
2. `js/levels/index.js` registry + headless selfcheck (qa.sh already calls
`node js/levels/index.js --selfcheck` — make it real).
3. **`L2_esophagus.json` complete** (the vertical-slice level): segments, flow, encounter
curve with B's three archetypes, reflux-surge finale as a `hazard` event, checkpoints,
par time. Fly it on the stub; pacing notes in NOTES.
4. Draft `L1_mouth.json` + `L3_stomach.json` skeletons (biomes/lengths/arenas only).
5. **Boss specs** (Pyloric Guardian, Tapeworm, Blockage) as one-pagers in your NOTES —
phases/telegraphs/windows — so A can shape lairs and D can concept in round 2.
## Lane D — pipeline online + first texture pack + audio proof
On the m3ultra box (clone the repo there; PIPELINE.md has the verified operator map):
1. Port PROCITY's script shapes: `gen_textures.py` (--dry-run/--local/--harvest),
`derive_maps.py`, `build_manifest.py`, `validate_manifest.py`.
2. **First pack:** esophagus ×2, stomach ×2, small-intestine ×2 walls (grayscale, tiling,
ART_BIBLE prompts) + 1 wet-tissue matcap → normals → WebP → manifest. Contact sheet +
in-engine stub screenshots to `docs/shots/laneD/`.
3. `js/core/assets.js` per the manifest contract (null-on-miss, `?localassets=0`).
4. **Audio proof (small):** port `gen_audio.py` shape; synthesize 1 esophagus bed (loop-
clean) + 4 sfx (pellet, hit-squelch, boost, pickup). Spectrogram evidence. Full pack =
round 2.
## Lane E — instrument panel v0
1. HUD overlay (DOM/canvas): coat/hull bars, heat, speed/flow, score — bus-driven only,
against B's emissions (they land this round; stub-emit fake events for dev via a
`?fakebus=1` helper you own).
2. **Gut-map v1**: draw the alimentary silhouette once (SVG path), show it as progress bar
with player blip. This is the game's signature UI — spend the taste budget here.
3. Title screen (over the stub drift) + pause + respect `?shots=1`/`?mute=1`.
4. `js/audio/engine.js`: bed crossfade + sfx pool + autoplay unlock + **procedural fallback
beeps** (D's pack may land mid-round; handle both paths).
Screenshots of every state to `docs/shots/laneE/`.
## Lane F (Fable) — done in round 0, then on-call
Scaffold committed. During the round: answer contract questions in NOTES daily-ish; end of
round: boot matrix, verify evidence, freeze world contract, write ROUND2.
## Round-2 preview (so you can aim): textured esophagus slice playable start-to-finish —
A's world + B's flight/enemies + C's L2 + D's pack + E's HUD, integrated by F, tagged v0.2.

76
docs/PIPELINE.md Normal file
View File

@ -0,0 +1,76 @@
# PIPELINE — asset generation (Lane D)
Everything generates **free and local** on the m3ultra box via MODELBEAST; fal.ai is a paid,
gated fallback. Verified live 2026-07-16: `m3ultra@100.89.131.57` (tailnet, key auth),
`~/Documents/MODELBEAST/server/operators/` = `flux_local` (FLUX.2-klein-4B text→image on MPS,
~6 s/img, weights cached), `mflux_image_edit` (img2img/inpaint), `sf3d` (image→GLB, seconds,
draft quality), `trellis_mac` (TRELLIS image→GLB `1024_cascade`, ~38 min, quality),
`seedvr2_upscale`. Venvs: `~/Documents/MODELBEAST/venvs/{mflux,rmbg,sf3d}`.
Run pipeline scripts **on the m3ultra box** (clone this repo there) or drive over ssh the
PROCITY way — copy the working patterns from `~/Documents/PROCITY/pipeline/` (`gen_skins.py
--local` drives `flux_local/run.py` directly, no server, one GPU job at a time; `gen_props.py`
chains concept→trellis; `gen_audio.py` is the procedural audio precedent; `normalize.py` is
the Blender house-law enforcer; `glb_stat.py` the dependency-free GLB parser). Don't reinvent
these — port them.
## Textures (round 1)
1. `pipeline/gen_textures.py --dry-run` lists the pack (prompts live in the script, style
stems from ART_BIBLE). `--local` → flux_local → `pipeline/.genraw/` at 1024².
2. **Tile check:** script renders each result wrap-offset by 50% into a contact sheet —
seams are instantly visible. Fix seams by mirror-tiling or `mflux_image_edit` inpaint
(≤2 attempts, then pick a different prompt).
3. **Normals:** `pipeline/derive_maps.py` — grayscale → height (blur pyramid) → Sobel normal
map, plus WebP conversion (≤1024, quality ~82). Pure numpy/PIL, no model.
4. `--harvest``web/assets/gen/*.webp` + entries in `web/assets/manifest.json`
(`build_manifest.py`), thumbs contact sheet → `docs/shots/laneD/`.
5. **Eyeball in-engine** (boot with `?dbg=1`, check tiling scale on the stub tube) before
committing. Delete rejects from `.genraw/` — committed junk is forever.
## Meshes (round 2+, after concept sign-off)
concept (flux_local, ART_BIBLE stem) → **sf3d draft** (seconds — iterate the concept until
the silhouette reads) → **trellis_mac** final → `normalize.py` (Blender: metres, +Y up,
origin, decimate to ≤5k tris — TRELLIS outputs run 2577k, always decimate — WebP ≤1024,
no Draco) → thumbnail → manifest. **Eyeball every thumbnail.**
Known TRELLIS weaknesses (PROCITY scars): transparent/glassy surfaces, thin wires/fins →
don't burn attempts; straight to the fal list.
## fal.ai fallback (the only money in the project)
Rules: **≤2 local attempts** per asset, then it goes on `pipeline/FAL_LIST.md` with the
concept image attached — **John personally approves and runs the batch** (key is his; never
committed, never in env on shared boxes). Cost ~10¢/gen billed 30¢ (MeshGod law). Expected
fal candidates: tapeworm head (long thin body), anything translucent. Check the
3GOD depot (`digalot.fyi/3god`) + `~/Documents/3D=models/` first — never regenerate what
exists.
## Audio (round 1 proof, round 2 full pack)
Procedural synthesis in numpy → WAV → ffmpeg OGG/Opus + M4A/AAC dual-ship (the PROCITY
`gen_audio.py` approach — $0, deterministic, seamless loops, parody-safe). GUTS pack:
biome ambience beds (wet cavernous drones, muffled heartbeat — tempo maps to danger, E's
hook), sfx (pellet, torpedo, squelch hits, boost, gate, pickup chime, alarm), boss stingers.
Beds `loudnorm` 16 LUFS, sfx peak-normalized, budget ≤10 MB shipped. Verification =
spectrogram sheet + numeric loop-seam check into `docs/shots/laneD/`; final ear-check is
John's.
## Licensing
Everything generated here is ours (🟢 web-ok). Any sourced mesh must be Sketchfab CC0/CC-BY
or CGTrader royalty-free; **no ActorCore in web builds** (house licensing zones). No humanoid
characters planned, so the Mixamo fleet rule shouldn't apply — if a humanoid ever appears,
use the character_kit base meshes, never a new rig.
## Artifacts
Git-ignored (regenerable): `pipeline/.genraw/`, `pipeline/.genprops/`, `pipeline/.genaudio/`,
`pipeline/_normalized/`. Committed: scripts, prompt/batch JSONs, `FAL_LIST.md`,
`web/assets/**` shipped files, `docs/shots/laneD/` evidence.
| step | cost |
|---|---|
| textures, normals, audio, drafts, TRELLIS meshes | **free** (local MPS) |
| fal.ai fallback meshes | ~30¢/asset, gated on John, expected ≤$3 total |

56
docs/PROCESS.md Normal file
View File

@ -0,0 +1,56 @@
# PROCESS — lanes, rounds, and how not to step on each other
This repo is built by parallel Claude sessions ("lanes"), each with a fixed territory,
coordinated in numbered rounds by an integrator (Fable, = Lane F). The workflow is inherited
from PROCITY, where it shipped a whole town in 12 rounds — follow it exactly.
## The cast
| lane | territory | charter |
|---|---|---|
| **A — World** | canal geometry, biome shaders, arenas | [LANE_A_WORLD.md](LANES/LANE_A_WORLD.md) |
| **B — Flight & Combat** | controls, collision response, weapons, enemies | [LANE_B_FLIGHT.md](LANES/LANE_B_FLIGHT.md) |
| **C — Levels & Encounters** | level data, encounter design, bosses, pacing | [LANE_C_LEVELS.md](LANES/LANE_C_LEVELS.md) |
| **D — Assets** | FLUX textures, GLB meshes, audio pack, manifest | [LANE_D_ASSETS.md](LANES/LANE_D_ASSETS.md) |
| **E — UI/HUD/Audio-engine** | HUD, menus, gut-map, WebAudio engine | [LANE_E_UIHUD.md](LANES/LANE_E_UIHUD.md) |
| **F — Integration** (Fable) | shell (`index.html`, `boot.js`), core/, contracts, QA, round instructions | [LANE_F_INTEGRATION.md](LANES/LANE_F_INTEGRATION.md) |
## A lane session, step by step
1. Read, in order: `README.md``docs/TECH.md` → your lane charter → the **current**
`docs/LANES/ROUND<N>_INSTRUCTIONS.md` (highest N not marked SUPERSEDED) → your own
`docs/LANES/LANE_<X>_NOTES.md` and any "→ Lane <X>" sections in the other lanes' NOTES.
2. `git pull` first. Check `git status`; if another lane's uncommitted work is present, touch
nothing of theirs.
3. Do the round's work **inside your territory only** (ownership map in TECH.md §Ownership).
If you need a change in someone else's file, write the request in your NOTES under a
`### → Lane <X>` heading (include exact code if you can — see PROCITY LANE_B_NOTES for the
house style: offer the wiring, let the owner paste it).
4. Evidence: screenshots into `docs/shots/lane<x>/` (use `?shots=1` for clean frames). For
audio: spectrogram sheet + numeric loop-seam check. Numbers you claim (draw calls, fps,
gen-times) must be **measured**, and say how you measured them.
5. Update `docs/LANES/LANE_<X>_NOTES.md` (what landed, measured results, contract offers,
open questions) and append a dated entry to `docs/progress/<x>-progress.md`.
6. `bash tools/qa.sh` — leave it green.
7. Commit **only your exact paths** (`git add <file> <file>` — never `git add .` / `-a`),
message `[lane X] <what>`, and **push**.
## Rounds
- F writes `ROUND<N>_INSTRUCTIONS.md` after reviewing all lanes' NOTES + progress + shots.
Old round files get a `⚠ SUPERSEDED` banner — never action a superseded round.
- Rounds are sized so a lane session finishes comfortably in one context window. If you're
running long: land what's solid, document the rest as "next round" in NOTES, push.
- Blocked on another lane? Build against the **stub** (`?stub=1`, `js/stub/world_stub.js`)
or the contract in TECH.md; never idle-wait, never implement the other lane's side.
- Integrator decisions in round instructions are **settled** — don't relitigate in-lane;
argue in NOTES for next round if you must.
## Git specifics
- Remote: `ssh://git@100.71.119.27:222/monster/guts.git`, single branch `main`.
- Lanes may share one working tree (PROCITY style) or run from separate clones — either way
the exact-path staging rule and pull-before-work rule are what keep it safe.
- Generated artifacts that are re-derivable (raw gen output, staging GLBs, WAV scratch) are
git-ignored; shipped assets (`web/assets/**` webp/ogg/m4a/glb) are committed. See
`.gitignore` + PIPELINE.md §Artifacts.

134
docs/TECH.md Normal file
View File

@ -0,0 +1,134 @@
# TECH — architecture, contracts, ownership
Stack: vanilla three.js **r175** (vendored, `web/vendor/`), ES modules + import map, zero
build step, `python3 -m http.server` to run. No physics engine, no framework. This is house
style (proven in PROCITY) — keep it.
## Module layout & ownership
```
web/
index.html F shell + import map + flags table wiring
js/boot.js F creates renderer/scene/loop, wires lane factories, window.DBG
js/core/rng.js F mulberry32 seeded RNG (THE only randomness source)
js/core/bus.js F event bus (on/off/emit)
js/core/flags.js F URL flag parsing
js/core/assets.js D manifest loader + `depot:`/local resolution + fallback law
js/stub/world_stub.js F straight-tube world implementing the world contract
js/world/** A createWorld() — spline, tube chunks, arenas, biome shaders
js/flight/** B createPlayer() — controller, camera rig, collision response
js/combat/** B weapons, damage, enemy framework + behaviors
js/levels/*.json C level data (schema below)
js/levels/index.js C level registry + loader + schema selfcheck
js/ui/** E HUD, menus, gut-map, screens
js/audio/** E WebAudio engine (plays D's pack; procedural fallback)
assets/** D manifest.json + generated webp/glb/ogg/m4a + thumbs
pipeline/** D generation scripts (run on the m3ultra box)
tools/qa.sh F the gate
docs/** all (own lane's files only)
```
Never edit outside your column. Shell wiring requests → your NOTES (`### → Lane F`), with
the exact snippet to paste (PROCITY house style).
## Lane factory convention
Every lane exports pure factories, constructed by boot, no side effects at import time:
```js
export function createX({ scene, world, bus, rng, flags, assets }) {
return { update(dt, ctx){}, dispose(){}, /* lane-specific API */ };
}
```
Default-off behind flags until F wires them on. Dispose must actually free GPU resources
(geometries, materials, textures) — enter/exit cycles are leak-tested.
## THE WORLD CONTRACT (A implements; stub implements it today; frozen after round-1 sign-off)
Spline-space: `s` = arclength along the canal centreline (units), `θ` = angle in the
cross-section (rad), `ρ` = radial distance from centreline. Units are metres in three.js
terms; fiction scale ≈ 1 unit : 1 mm. +Y up, right-handed.
```js
const world = await createWorld(levelData, { rng, quality });
world.length // total arclength
world.group // THREE.Group (boot adds to scene)
world.sample(s) // → { pos, tan, nor, bin, radius } (parallel-transport frame)
world.project(pos) // → { s, theta, rho } (world → spline space, player-local fast path)
world.wallRho(s, theta) // → max safe ρ (radius minus conservative displacement amplitude)
world.collide(pos, r) // → null | { push:Vector3, kind:'wall'|'hazard', biome }
world.biomeAt(s) // → { id, palette, fog, flow, coatDrain } (see biomes.js registry)
world.modeAt(s) // → 'tube' | 'arena'
world.arenaAt(s) // → null | { center:Vector3, radius } (bounds for 6DOF clamp)
world.update(dt, playerS) // stream chunks, advance shader time
```
Determinism law: same `levelData.seed` ⇒ byte-identical geometry. A maintains a golden
hash (`world.hash()` over sampled frames) checked by qa.
## Level data schema v0 (C owns; F drafted; sign-off in round 1)
```jsonc
{
"id": "L2_esophagus", "name": "Esophagus", "seed": 20260716,
"segments": [ // concatenated along s
{ "biome": "esophagus", "length": 600,
"radius": { "base": 10, "wobble": 0.25 }, // wobble: seeded radius variation 0..1
"curviness": 0.6, // 0 straight .. 1 writhing
"flow": 14 } // base current, units/s
],
"arenas": [ { "at": 580, "radius": 90, "biome": "stomach" } ],
"events": [ // consumed by B (spawns) & E (telegraphs) via bus
{ "s": 40, "type": "spawn", "enemy": "bolus_chunk", "count": 6 },
{ "s": 120, "type": "hazard", "kind": "reflux_surge", "speed": 22 },
{ "s": 300, "type": "checkpoint" },
{ "s": 560, "type": "boss", "id": "pyloric_guardian" }
]
}
```
## Bus events (append here when adding; F referees name collisions)
`player:spawn {s}` · `player:damage {amount, kind}` · `player:death` · `player:boost` ·
`enemy:spawn {id,type}` · `enemy:die {id,type,s}` · `pickup {kind}` · `level:event {…}` (raw
C events as player crosses their `s`) · `level:checkpoint {s}` · `level:complete {stats}` ·
`boss:start {id}` / `boss:phase` / `boss:end` · `combo {n}` · `audio:cue {name}`.
## Asset manifest contract (D owns `assets.js` + `manifest.json`)
```jsonc
{ "textures": { "wall_esophagus_a": { "url": "gen/wall_esophagus_a.webp",
"normal": "gen/wall_esophagus_a_n.webp", "tile": [3, 8] } },
"matcaps": { "tissue_wet": { "url": "gen/matcap_tissue.webp" } },
"models": { "macrophage": { "url": "models/macrophage.glb", "tris": 4200 } },
"audio": { "beds": { "stomach": {"ogg": "...", "m4a": "..."} }, "sfx": { … } } }
```
Law: **every entry is optional at runtime.** `assets.get('textures','x')` returns `null` on
miss and the consumer must have a procedural fallback (flat palette color, primitive mesh,
synthesized beep). The game must be fully playable with an empty `assets/gen/`.
House GLB law (from PROCITY): metres, +Y up, sensible origin, **≤5k tris**, WebP textures
≤1024, no Draco. `?localassets=0` simulates empty-manifest boot for fallback testing.
## Flags (F owns this table; lanes request rows via NOTES)
| flag | owner | effect |
|---|---|---|
| `?stub=1` | F | world stub instead of Lane A world |
| `?lvl=<id>` | C | boot straight into a level |
| `?seed=<n>` | F | override level seed |
| `?fly=1` | A | noclip fly camera (no player) |
| `?dbg=1` | F | perf HUD + `window.DBG` helpers |
| `?mute=1` | E | no audio |
| `?shots=1` | F | hide HUD/debug for clean screenshots |
| `?localassets=0` | D | boot with empty manifest (fallback test) |
## Perf & QA budgets
≤300 draw calls worst view · ≤500k tris in frustum · 60 fps M-series & mid laptops ·
level load <2 s · zero console errors · dispose-clean on level exit (renderer.info deltas).
`window.DBG` (F provides): `{draws, tris, fps, shot(name)}` — cite it when claiming numbers.
`tools/qa.sh` gates: `node --check` every js file · no `Math.random` outside `core/rng.js` ·
level JSONs parse + required keys · vendor present · (grows per round).

12
docs/V2_IDEAS.md Normal file
View File

@ -0,0 +1,12 @@
# V2_IDEAS — parked, do not action in v1 rounds
- **Colonoscopy Run** — endless procedural tube, daily seed, leaderboard.
- **Boss rush** + speedrun mode with in-fiction timer ("procedure duration").
- **Co-op** (two probes; one flies, one operates the instrument turret?).
- **Realism mode** — the rejected full-flesh look as an unlockable shader toggle.
- **Photo/endoscopy mode** — freeze, free cam, scanline filters, export frame.
- **Sequel organ systems:** BLOOD (circulatory — currents + immune heat), AIRWAYS (lungs).
- Peristalsis rhythm game micro-mode at the esophagus (ride waves to a beat).
- WebXR cockpit view.
- Gut-flora reputation system (don't shoot the biome, it helps you in level 5).
- Mobile/touch controls.

View File

@ -0,0 +1,21 @@
# Lane F (Fable) — progress
## 2026-07-16 — Round 0: scaffold
Repo initialized from empty. Landed:
- Full doc set: README, PROCESS (lane/round law, inherited from PROCITY), GDD v1 (movement
= flow-locked hybrid; 5 levels + secret; enemy roster; bosses), TECH (world contract,
level schema v0, bus events, manifest contract, flags, budgets), ART_BIBLE (synthetic
scanner look, palettes, emissive code, FLUX prompt kit), PIPELINE (MODELBEAST operator
map — verified live on m3ultra@100.89.131.57 2026-07-16: flux_local, mflux_image_edit,
sf3d, trellis_mac, seedvr2_upscale; fal.ai gated fallback), V2_IDEAS.
- Lane charters AF + ROUND1_INSTRUCTIONS.
- Seed code: index.html shell + boot.js (world pick w/ stub fallback, drift/fly debug cam,
window.DBG, resize/loop), core/{rng,bus,flags}.js, stub/world_stub.js (executable world
contract: analytic centreline, parallel-transport-style frames, inward tube mesh with
peristalsis vertex shader + fresnel rim + fog, project/collide/wallRho/hash).
- three r175 vendored from PROCITY (module + core + addons).
- tools/qa.sh: node --check, Math.random grep, level JSON checks, selfcheck/manifest hooks,
vendor gate.
Verification: qa.sh GREEN; booted `?stub=1&dbg=1` in browser — see docs/shots/laneF/.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

47
tools/qa.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
# tools/qa.sh (Lane F) — the gate. Run before you finish a session; leave it GREEN.
# Dependency-free: bash + node + python3. Gates accumulate each round.
set -u
cd "$(dirname "$0")/.."
FAIL=0
red() { printf '\033[31m✗ %s\033[0m\n' "$1"; FAIL=1; }
green() { printf '\033[32m✓ %s\033[0m\n' "$1"; }
# 1. every JS module parses
JSBAD=0
while IFS= read -r -d '' f; do
node --check "$f" 2>/dev/null || { node --check "$f"; JSBAD=1; }
done < <(find web/js -name '*.js' -print0)
[ "$JSBAD" = 0 ] && green "node --check: all web/js modules parse" || red "JS syntax errors"
# 2. determinism law: Math.random only in core/rng.js
RAND=$(grep -rn 'Math\.random' web/js --include='*.js' | grep -v 'core/rng.js' || true)
if [ -n "$RAND" ]; then red "Math.random outside core/rng.js:"; echo "$RAND"; else green "determinism: no stray Math.random"; fi
# 3. level JSONs parse + minimal keys
LVLBAD=0
for f in web/js/levels/*.json; do
[ -e "$f" ] || continue
python3 - "$f" <<'PY' || LVLBAD=1
import json, sys
d = json.load(open(sys.argv[1]))
assert d.get("id") and isinstance(d.get("seed"), int) and d.get("segments"), "missing id/seed/segments"
PY
done
[ "$LVLBAD" = 0 ] && green "level JSONs valid" || red "level JSON errors"
# 4. Lane C selfcheck (when it exists)
if [ -f web/js/levels/index.js ]; then
node web/js/levels/index.js --selfcheck && green "levels selfcheck" || red "levels selfcheck failed"
fi
# 5. Lane D manifest validation (when it exists)
if [ -f pipeline/validate_manifest.py ]; then
python3 pipeline/validate_manifest.py && green "manifest valid" || red "manifest errors"
fi
# 6. vendor present (the game must run offline)
[ -f web/vendor/three.module.js ] && green "vendor: three r175 present" || red "web/vendor/three.module.js missing"
[ "$FAIL" = 0 ] && printf '\033[32mQA GREEN\033[0m\n' || printf '\033[31mQA RED\033[0m\n'
exit $FAIL

26
web/index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GUTS</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #02100d; }
#game canvas { display: block; }
#ui { position: fixed; inset: 0; pointer-events: none; } /* Lane E's layer */
#dbg { display: none; position: fixed; left: 10px; bottom: 10px; color: #39e6ff;
font: 12px/1.4 ui-monospace, monospace; text-shadow: 0 0 4px #000;
pointer-events: none; z-index: 10; }
</style>
<script type="importmap">
{ "imports": { "three": "./vendor/three.module.js",
"three/addons/": "./vendor/addons/" } }
</script>
</head>
<body>
<div id="game"></div>
<div id="ui"></div>
<div id="dbg"></div>
<script type="module" src="./js/boot.js"></script>
</body>
</html>

117
web/js/boot.js Normal file
View File

@ -0,0 +1,117 @@
// boot.js (Lane F) — the shell. Creates renderer/scene/loop, picks the world (real or
// stub), hosts the debug camera + DBG. Lanes: request wiring via NOTES, don't edit here.
import * as THREE from 'three';
import { parseFlags } from './core/flags.js';
import { createRng } from './core/rng.js';
import { createBus } from './core/bus.js';
import { createWorld as createStubWorld, STUB_LEVEL } from './stub/world_stub.js';
const flags = parseFlags();
const bus = createBus();
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.getElementById('game').appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
async function pickWorld() {
if (!flags.stub) {
try {
const mod = await import('./world/index.js'); // Lane A's world, when it lands
const level = await loadLevel(flags.lvl);
return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed) });
} catch (e) {
console.info('[boot] Lane A world not available, using stub —', e.message);
}
}
return createStubWorld(STUB_LEVEL, { rng: createRng(flags.seed ?? STUB_LEVEL.seed) });
}
async function loadLevel(id) {
const mod = await import('./levels/index.js'); // Lane C's registry, when it lands
return mod.getLevel(id);
}
const world = await pickWorld();
scene.add(world.group);
scene.background = new THREE.Color(world.biomeAt(0).palette.void);
// --- debug drift/fly camera (placeholder until Lane B's player rig; ?fly=1 = manual) ---
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
const keys = new Set();
addEventListener('keydown', (e) => keys.add(e.code));
addEventListener('keyup', (e) => keys.delete(e.code));
addEventListener('mousedown', () => (dragging = true));
addEventListener('mouseup', () => (dragging = false));
addEventListener('mousemove', (e) => {
if (!dragging) return;
look.yaw -= e.movementX * 0.003;
look.pitch = THREE.MathUtils.clamp(look.pitch - e.movementY * 0.003, -1.2, 1.2);
});
function updateCamera(dt) {
if (flags.fly) {
if (keys.has('KeyW')) sCam += speed * 2 * dt;
if (keys.has('KeyS')) sCam -= speed * 2 * dt;
} else {
sCam += speed * dt; // title-screen auto-drift
}
sCam = ((sCam % world.length) + world.length) % world.length;
const f = world.sample(sCam), ahead = world.sample(Math.min(sCam + 12, world.length));
camera.position.copy(f.pos).addScaledVector(f.nor, -f.radius * 0.25);
const target = ahead.pos.clone()
.addScaledVector(f.bin, Math.sin(look.yaw) * 8)
.addScaledVector(f.nor, Math.sin(look.pitch) * 8);
camera.up.copy(f.nor);
camera.lookAt(target);
}
// --- DBG (?dbg=1) — cite these numbers in NOTES; DBG.shot('name') saves a frame ---
const dbgEl = document.getElementById('dbg');
let frames = 0, fpsT = 0, fps = 0;
renderer.info.autoReset = false; // three resets info at render end — cache per-frame instead
const stats = { draws: 0, tris: 0 };
window.DBG = {
get draws() { return stats.draws; },
get tris() { return stats.tris; },
get fps() { return fps; },
world,
shot(name = 'shot') {
const a = document.createElement('a');
a.download = `${name}.png`;
a.href = renderer.domElement.toDataURL('image/png');
a.click();
},
};
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let last = performance.now();
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
world.update(dt, sCam);
updateCamera(dt);
renderer.render(scene, camera);
stats.draws = renderer.info.render.calls;
stats.tris = renderer.info.render.triangles;
renderer.info.reset();
frames++; fpsT += dt;
if (fpsT >= 0.5) {
fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
if (flags.dbg && !flags.shots)
dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${sCam.toFixed(0)} · ${world.level?.id ?? '?'} ${world.hash?.() ?? ''}`;
}
requestAnimationFrame(frame);
}
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
requestAnimationFrame(frame);
export { scene, camera, renderer, bus, flags, world }; // read-only handles for lanes

17
web/js/core/bus.js Normal file
View File

@ -0,0 +1,17 @@
// core/bus.js (Lane F) — event bus. Event names are a contract: TECH.md §Bus.
export function createBus() {
const map = new Map();
return {
on(name, fn) {
if (!map.has(name)) map.set(name, new Set());
map.get(name).add(fn);
return () => map.get(name)?.delete(fn);
},
off(name, fn) { map.get(name)?.delete(fn); },
emit(name, payload) {
const set = map.get(name);
if (set) for (const fn of [...set]) fn(payload);
},
};
}

18
web/js/core/flags.js Normal file
View File

@ -0,0 +1,18 @@
// core/flags.js (Lane F) — URL flags. The table of record lives in TECH.md; add rows there.
export function parseFlags(search = location.search) {
const p = new URLSearchParams(search);
const on = (k) => p.has(k) && p.get(k) !== '0';
return {
stub: on('stub'),
fly: on('fly'),
dbg: on('dbg'),
shots: on('shots'),
mute: on('mute'),
fakebus: on('fakebus'),
localassets: p.get('localassets') !== '0', // ?localassets=0 => empty-manifest boot
lvl: p.get('lvl') || null,
seed: p.has('seed') ? (parseInt(p.get('seed'), 10) >>> 0) : null,
raw: p,
};
}

31
web/js/core/rng.js Normal file
View File

@ -0,0 +1,31 @@
// core/rng.js (Lane F) — THE only randomness source in GUTS. qa.sh greps for violators.
// mulberry32: tiny, fast, deterministic. Same seed => same sequence, forever.
export function mulberry32(seed) {
let a = seed >>> 0;
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;
};
}
// Named sub-streams so lanes don't perturb each other's sequences:
// rng('world'), rng('enemies'), ... each is an independent deterministic stream.
export function createRng(seed) {
const streams = new Map();
function hash(str) {
let h = 2166136261 >>> 0;
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return h >>> 0;
}
const rng = (stream = 'main') => {
if (!streams.has(stream)) streams.set(stream, mulberry32((seed ^ hash(stream)) >>> 0));
return streams.get(stream);
};
rng.seed = seed;
rng.float = (stream, min = 0, max = 1) => min + rng(stream)() * (max - min);
rng.int = (stream, min, max) => Math.floor(rng.float(stream, min, max + 1));
return rng;
}

166
web/js/stub/world_stub.js Normal file
View File

@ -0,0 +1,166 @@
// stub/world_stub.js (Lane F) — executable spec of THE WORLD CONTRACT (docs/TECH.md).
// A straight-ish pulsing esophagus tube, analytic everywhere. Lane A replaces this with
// js/world/index.js; until then every lane builds against this. Keep contract-identical.
import * as THREE from 'three';
export const STUB_LEVEL = {
id: 'STUB', name: 'Stub Esophagus', seed: 20260716,
segments: [{ biome: 'esophagus', length: 400, radius: { base: 10, wobble: 0.15 }, curviness: 0.3, flow: 14 }],
arenas: [],
events: [
{ s: 60, type: 'spawn', enemy: 'floater', count: 4 },
{ s: 140, type: 'spawn', enemy: 'seeker', count: 3 },
{ s: 200, type: 'checkpoint' },
{ s: 260, type: 'spawn', enemy: 'turret', count: 2 },
],
};
const BIOME = { // mirrors ART_BIBLE esophagus row; A's biomes.js is the real registry
id: 'esophagus', flow: 14, coatDrain: 0.5,
palette: { tint: 0x1e6e64, rim: 0x5affd2, void: 0x02100d },
fog: 0.028,
};
const WAVE_A = 0.9; // peristalsis displacement amplitude (units, inward)
const WAVE_K = 0.22; // spatial frequency (rad/unit along s)
const WAVE_W = 3.0; // angular speed (rad/s)
const SKIN = 0.6; // collision safety margin
export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
const seg = levelData.segments[0];
const LEN = seg.length, R = seg.radius.base;
const AX = seg.curviness * 8, AY = seg.curviness * 5; // gentle analytic centreline
const KX = 0.020, KY = 0.031;
// --- centreline & frames (analytic; s ≈ arclength for these gentle amplitudes) ---
const centre = (s) => new THREE.Vector3(AX * Math.sin(s * KX), AY * Math.sin(s * KY), -s);
const tangent = (s) => new THREE.Vector3(AX * KX * Math.cos(s * KX), AY * KY * Math.cos(s * KY), -1).normalize();
function sample(s) {
const tan = tangent(s);
const up = new THREE.Vector3(0, 1, 0);
const nor = up.clone().addScaledVector(tan, -up.dot(tan)).normalize(); // Gram-Schmidt (stable: tube never goes vertical)
const bin = new THREE.Vector3().crossVectors(tan, nor);
return { pos: centre(s), tan, nor, bin, radius: radiusAt(s) };
}
const radiusAt = (s) => R * (1 + seg.radius.wobble * Math.sin(s * 0.05) * Math.sin(s * 0.013 + 2.1));
const flowPulse = (s, t) => Math.pow(Math.max(0, Math.sin(WAVE_K * s - WAVE_W * t)), 3);
// --- geometry: rings along s, uv = (theta/2pi, s), inward-facing ---
// seam ring vertex is duplicated (RADIAL+1 columns) so uv.x runs 0..1 without wrapping
// backwards across the last quad (visible streak otherwise).
const RADIAL = 64, COLS = RADIAL + 1, STEP = 0.5;
const rings = Math.floor(LEN / STEP) + 1;
const pos = new Float32Array(rings * COLS * 3);
const inward = new Float32Array(rings * COLS * 3);
const uv = new Float32Array(rings * COLS * 2);
let p = 0, q = 0;
for (let i = 0; i < rings; i++) {
const s = i * STEP, f = sample(s);
for (let j = 0; j < COLS; j++) {
const th = (j / RADIAL) * Math.PI * 2;
const dir = f.nor.clone().multiplyScalar(Math.cos(th)).addScaledVector(f.bin, Math.sin(th));
const v = f.pos.clone().addScaledVector(dir, f.radius);
pos[p] = v.x; pos[p + 1] = v.y; pos[p + 2] = v.z;
inward[p] = -dir.x; inward[p + 1] = -dir.y; inward[p + 2] = -dir.z;
p += 3;
uv[q++] = j / RADIAL; uv[q++] = s;
}
}
const idx = [];
for (let i = 0; i < rings - 1; i++)
for (let j = 0; j < RADIAL; j++) {
const a = i * COLS + j, b = i * COLS + j + 1, c = a + COLS, d = b + COLS;
idx.push(a, c, b, b, c, d); // wound to face inward
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('aInward', new THREE.BufferAttribute(inward, 3));
geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
geo.setIndex(idx);
const mat = new THREE.ShaderMaterial({
side: THREE.FrontSide,
uniforms: {
uTime: { value: 0 },
uTint: { value: new THREE.Color(BIOME.palette.tint) },
uRim: { value: new THREE.Color(BIOME.palette.rim) },
uVoid: { value: new THREE.Color(BIOME.palette.void) },
uFog: { value: BIOME.fog },
},
vertexShader: /* glsl */`
attribute vec3 aInward;
varying vec2 vUv; varying vec3 vN; varying vec3 vView;
uniform float uTime;
void main() {
vUv = uv;
float pulse = pow(max(0.0, sin(${WAVE_K.toFixed(3)} * uv.y - ${WAVE_W.toFixed(2)} * uTime)), 3.0);
float breathe = 0.15 * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831 * 3.0);
vec3 disp = position + aInward * (${WAVE_A.toFixed(2)} * pulse + breathe);
vec4 mv = modelViewMatrix * vec4(disp, 1.0);
vN = normalize(normalMatrix * aInward);
vView = -mv.xyz;
gl_Position = projectionMatrix * mv;
}`,
fragmentShader: /* glsl */`
varying vec2 vUv; varying vec3 vN; varying vec3 vView;
uniform vec3 uTint; uniform vec3 uRim; uniform vec3 uVoid; uniform float uFog; uniform float uTime;
void main() {
// fake SEM detail until Lane D textures land: ridged folds along theta + s striation
float folds = 0.55 + 0.45 * sin(vUv.x * 6.2831 * 9.0 + sin(vUv.y * 0.9) * 2.0);
float stria = 0.85 + 0.15 * sin(vUv.y * 2.2);
vec3 base = uTint * folds * stria * 0.8;
float fres = pow(1.0 - abs(dot(normalize(vN), normalize(vView))), 2.2);
vec3 col = base + uRim * fres * 0.9;
float d = length(vView);
col = mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55));
gl_FragColor = vec4(col, 1.0);
}`,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.frustumCulled = false; // one chunk; A's real world streams + culls properly
const group = new THREE.Group();
group.name = 'world';
group.add(mesh);
let time = 0;
const world = {
level: levelData,
length: LEN,
group,
sample,
flowPulse: (s, t = time) => flowPulse(s, t),
project(v) {
const s = THREE.MathUtils.clamp(-v.z, 0, LEN);
const f = sample(s);
const rel = v.clone().sub(f.pos);
return { s, theta: Math.atan2(rel.dot(f.bin), rel.dot(f.nor)), rho: rel.length() };
},
wallRho: (s, _theta) => radiusAt(s) - WAVE_A - SKIN,
collide(v, r = 0) {
const { s, theta, rho } = world.project(v);
const max = world.wallRho(s, theta) - r;
if (rho <= max) return null;
const f = sample(s);
const dir = f.nor.clone().multiplyScalar(Math.cos(theta)).addScaledVector(f.bin, Math.sin(theta));
return { push: dir.multiplyScalar(max - rho), kind: 'wall', biome: BIOME.id };
},
biomeAt: (_s) => BIOME,
modeAt: (_s) => 'tube',
arenaAt: (_s) => null,
update(dt, _playerS) { time += dt; mat.uniforms.uTime.value = time; },
hash() {
let h = 2166136261 >>> 0;
for (let s = 0; s <= LEN; s += 10) {
const v = sample(s).pos;
for (const x of [v.x, v.y, v.z, radiusAt(s)]) {
h ^= Math.round(x * 1000) & 0xffff; h = Math.imul(h, 16777619);
}
}
return (h >>> 0).toString(16);
},
dispose() { geo.dispose(); mat.dispose(); },
};
return world;
}

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long