[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:
commit
34f84ab162
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal 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
8
.gitignore
vendored
Normal 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
49
README.md
Normal 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
78
docs/ART_BIBLE.md
Normal 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
95
docs/GDD.md
Normal 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: 6–10 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.
|
||||
49
docs/LANES/LANE_A_WORLD.md
Normal file
49
docs/LANES/LANE_A_WORLD.md
Normal 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 × 64–96 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.
|
||||
50
docs/LANES/LANE_B_FLIGHT.md
Normal file
50
docs/LANES/LANE_B_FLIGHT.md
Normal 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.
|
||||
48
docs/LANES/LANE_C_LEVELS.md
Normal file
48
docs/LANES/LANE_C_LEVELS.md
Normal 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 10–20 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 1–2) 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.
|
||||
46
docs/LANES/LANE_D_ASSETS.md
Normal file
46
docs/LANES/LANE_D_ASSETS.md
Normal 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 25–77k),
|
||||
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.
|
||||
47
docs/LANES/LANE_E_UIHUD.md
Normal file
47
docs/LANES/LANE_E_UIHUD.md
Normal 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.
|
||||
34
docs/LANES/LANE_F_INTEGRATION.md
Normal file
34
docs/LANES/LANE_F_INTEGRATION.md
Normal 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>`).
|
||||
94
docs/LANES/ROUND1_INSTRUCTIONS.md
Normal file
94
docs/LANES/ROUND1_INSTRUCTIONS.md
Normal 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
76
docs/PIPELINE.md
Normal 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`, ~3–8 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 25–77k, 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
56
docs/PROCESS.md
Normal 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
134
docs/TECH.md
Normal 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
12
docs/V2_IDEAS.md
Normal 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.
|
||||
21
docs/progress/f-progress.md
Normal file
21
docs/progress/f-progress.md
Normal 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 A–F + 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/.
|
||||
BIN
docs/shots/laneF/round0_stub_tube.png
Normal file
BIN
docs/shots/laneF/round0_stub_tube.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
47
tools/qa.sh
Executable file
47
tools/qa.sh
Executable 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
26
web/index.html
Normal 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
117
web/js/boot.js
Normal 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
17
web/js/core/bus.js
Normal 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
18
web/js/core/flags.js
Normal 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
31
web/js/core/rng.js
Normal 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
166
web/js/stub/world_stub.js
Normal 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;
|
||||
}
|
||||
270
web/vendor/addons/controls/PointerLockControls.js
vendored
Normal file
270
web/vendor/addons/controls/PointerLockControls.js
vendored
Normal file
@ -0,0 +1,270 @@
|
||||
import {
|
||||
Controls,
|
||||
Euler,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
const _euler = new Euler( 0, 0, 0, 'YXZ' );
|
||||
const _vector = new Vector3();
|
||||
|
||||
/**
|
||||
* Fires when the user moves the mouse.
|
||||
*
|
||||
* @event PointerLockControls#change
|
||||
* @type {Object}
|
||||
*/
|
||||
const _changeEvent = { type: 'change' };
|
||||
|
||||
/**
|
||||
* Fires when the pointer lock status is "locked" (in other words: the mouse is captured).
|
||||
*
|
||||
* @event PointerLockControls#lock
|
||||
* @type {Object}
|
||||
*/
|
||||
const _lockEvent = { type: 'lock' };
|
||||
|
||||
/**
|
||||
* Fires when the pointer lock status is "unlocked" (in other words: the mouse is not captured anymore).
|
||||
*
|
||||
* @event PointerLockControls#unlock
|
||||
* @type {Object}
|
||||
*/
|
||||
const _unlockEvent = { type: 'unlock' };
|
||||
|
||||
const _PI_2 = Math.PI / 2;
|
||||
|
||||
/**
|
||||
* The implementation of this class is based on the [Pointer Lock API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API}.
|
||||
* `PointerLockControls` is a perfect choice for first person 3D games.
|
||||
*
|
||||
* ```js
|
||||
* const controls = new PointerLockControls( camera, document.body );
|
||||
*
|
||||
* // add event listener to show/hide a UI (e.g. the game's menu)
|
||||
* controls.addEventListener( 'lock', function () {
|
||||
*
|
||||
* menu.style.display = 'none';
|
||||
*
|
||||
* } );
|
||||
*
|
||||
* controls.addEventListener( 'unlock', function () {
|
||||
*
|
||||
* menu.style.display = 'block';
|
||||
*
|
||||
* } );
|
||||
* ```
|
||||
*
|
||||
* @augments Controls
|
||||
*/
|
||||
class PointerLockControls extends Controls {
|
||||
|
||||
/**
|
||||
* Constructs a new controls instance.
|
||||
*
|
||||
* @param {Camera} camera - The camera that is managed by the controls.
|
||||
* @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
|
||||
*/
|
||||
constructor( camera, domElement = null ) {
|
||||
|
||||
super( camera, domElement );
|
||||
|
||||
/**
|
||||
* Whether the controls are locked or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
* @default false
|
||||
*/
|
||||
this.isLocked = false;
|
||||
|
||||
/**
|
||||
* Camera pitch, lower limit. Range is '[0, Math.PI]' in radians.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 0
|
||||
*/
|
||||
this.minPolarAngle = 0;
|
||||
|
||||
/**
|
||||
* Camera pitch, upper limit. Range is '[0, Math.PI]' in radians.
|
||||
*
|
||||
* @type {number}
|
||||
* @default Math.PI
|
||||
*/
|
||||
this.maxPolarAngle = Math.PI;
|
||||
|
||||
/**
|
||||
* Multiplier for how much the pointer movement influences the camera rotation.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.pointerSpeed = 1.0;
|
||||
|
||||
// event listeners
|
||||
|
||||
this._onMouseMove = onMouseMove.bind( this );
|
||||
this._onPointerlockChange = onPointerlockChange.bind( this );
|
||||
this._onPointerlockError = onPointerlockError.bind( this );
|
||||
|
||||
if ( this.domElement !== null ) {
|
||||
|
||||
this.connect( this.domElement );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
connect( element ) {
|
||||
|
||||
super.connect( element );
|
||||
|
||||
this.domElement.ownerDocument.addEventListener( 'mousemove', this._onMouseMove );
|
||||
this.domElement.ownerDocument.addEventListener( 'pointerlockchange', this._onPointerlockChange );
|
||||
this.domElement.ownerDocument.addEventListener( 'pointerlockerror', this._onPointerlockError );
|
||||
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
this.domElement.ownerDocument.removeEventListener( 'mousemove', this._onMouseMove );
|
||||
this.domElement.ownerDocument.removeEventListener( 'pointerlockchange', this._onPointerlockChange );
|
||||
this.domElement.ownerDocument.removeEventListener( 'pointerlockerror', this._onPointerlockError );
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.disconnect();
|
||||
|
||||
}
|
||||
|
||||
getObject() {
|
||||
|
||||
console.warn( 'THREE.PointerLockControls: getObject() has been deprecated. Use controls.object instead.' ); // @deprecated r169
|
||||
|
||||
return this.object;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the look direction of the camera.
|
||||
*
|
||||
* @param {Vector3} v - The target vector that is used to store the method's result.
|
||||
* @return {Vector3} The normalized direction vector.
|
||||
*/
|
||||
getDirection( v ) {
|
||||
|
||||
return v.set( 0, 0, - 1 ).applyQuaternion( this.object.quaternion );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera forward parallel to the xz-plane. Assumes camera.up is y-up.
|
||||
*
|
||||
* @param {number} distance - The signed distance.
|
||||
*/
|
||||
moveForward( distance ) {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
// move forward parallel to the xz-plane
|
||||
// assumes camera.up is y-up
|
||||
|
||||
const camera = this.object;
|
||||
|
||||
_vector.setFromMatrixColumn( camera.matrix, 0 );
|
||||
|
||||
_vector.crossVectors( camera.up, _vector );
|
||||
|
||||
camera.position.addScaledVector( _vector, distance );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera sidewards parallel to the xz-plane.
|
||||
*
|
||||
* @param {number} distance - The signed distance.
|
||||
*/
|
||||
moveRight( distance ) {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
const camera = this.object;
|
||||
|
||||
_vector.setFromMatrixColumn( camera.matrix, 0 );
|
||||
|
||||
camera.position.addScaledVector( _vector, distance );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the pointer lock.
|
||||
*
|
||||
* @param {boolean} [unadjustedMovement=false] - Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead.
|
||||
* Setting it to true will disable mouse acceleration.
|
||||
*/
|
||||
lock( unadjustedMovement = false ) {
|
||||
|
||||
this.domElement.requestPointerLock( {
|
||||
unadjustedMovement
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the pointer lock.
|
||||
*/
|
||||
unlock() {
|
||||
|
||||
this.domElement.ownerDocument.exitPointerLock();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// event listeners
|
||||
|
||||
function onMouseMove( event ) {
|
||||
|
||||
if ( this.enabled === false || this.isLocked === false ) return;
|
||||
|
||||
const camera = this.object;
|
||||
_euler.setFromQuaternion( camera.quaternion );
|
||||
|
||||
_euler.y -= event.movementX * 0.002 * this.pointerSpeed;
|
||||
_euler.x -= event.movementY * 0.002 * this.pointerSpeed;
|
||||
|
||||
_euler.x = Math.max( _PI_2 - this.maxPolarAngle, Math.min( _PI_2 - this.minPolarAngle, _euler.x ) );
|
||||
|
||||
camera.quaternion.setFromEuler( _euler );
|
||||
|
||||
this.dispatchEvent( _changeEvent );
|
||||
|
||||
}
|
||||
|
||||
function onPointerlockChange() {
|
||||
|
||||
if ( this.domElement.ownerDocument.pointerLockElement === this.domElement ) {
|
||||
|
||||
this.dispatchEvent( _lockEvent );
|
||||
|
||||
this.isLocked = true;
|
||||
|
||||
} else {
|
||||
|
||||
this.dispatchEvent( _unlockEvent );
|
||||
|
||||
this.isLocked = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onPointerlockError() {
|
||||
|
||||
console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' );
|
||||
|
||||
}
|
||||
|
||||
export { PointerLockControls };
|
||||
4958
web/vendor/addons/loaders/GLTFLoader.js
vendored
Normal file
4958
web/vendor/addons/loaders/GLTFLoader.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
361
web/vendor/addons/postprocessing/EffectComposer.js
vendored
Normal file
361
web/vendor/addons/postprocessing/EffectComposer.js
vendored
Normal file
@ -0,0 +1,361 @@
|
||||
import {
|
||||
Clock,
|
||||
HalfFloatType,
|
||||
NoBlending,
|
||||
Vector2,
|
||||
WebGLRenderTarget
|
||||
} from 'three';
|
||||
import { CopyShader } from '../shaders/CopyShader.js';
|
||||
import { ShaderPass } from './ShaderPass.js';
|
||||
import { ClearMaskPass, MaskPass } from './MaskPass.js';
|
||||
|
||||
/**
|
||||
* Used to implement post-processing effects in three.js.
|
||||
* The class manages a chain of post-processing passes to produce the final visual result.
|
||||
* Post-processing passes are executed in order of their addition/insertion.
|
||||
* The last pass is automatically rendered to screen.
|
||||
*
|
||||
* This module can only be used with {@link WebGLRenderer}.
|
||||
*
|
||||
* ```js
|
||||
* const composer = new EffectComposer( renderer );
|
||||
*
|
||||
* // adding some passes
|
||||
* const renderPass = new RenderPass( scene, camera );
|
||||
* composer.addPass( renderPass );
|
||||
*
|
||||
* const glitchPass = new GlitchPass();
|
||||
* composer.addPass( glitchPass );
|
||||
*
|
||||
* const outputPass = new OutputPass()
|
||||
* composer.addPass( outputPass );
|
||||
*
|
||||
* function animate() {
|
||||
*
|
||||
* composer.render(); // instead of renderer.render()
|
||||
*
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class EffectComposer {
|
||||
|
||||
/**
|
||||
* Constructs a new effect composer.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
|
||||
* be used as the internal read and write buffers. If not given, the composer creates
|
||||
* the buffers automatically.
|
||||
*/
|
||||
constructor( renderer, renderTarget ) {
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @type {WebGLRenderer}
|
||||
*/
|
||||
this.renderer = renderer;
|
||||
|
||||
this._pixelRatio = renderer.getPixelRatio();
|
||||
|
||||
if ( renderTarget === undefined ) {
|
||||
|
||||
const size = renderer.getSize( new Vector2() );
|
||||
this._width = size.width;
|
||||
this._height = size.height;
|
||||
|
||||
renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
|
||||
renderTarget.texture.name = 'EffectComposer.rt1';
|
||||
|
||||
} else {
|
||||
|
||||
this._width = renderTarget.width;
|
||||
this._height = renderTarget.height;
|
||||
|
||||
}
|
||||
|
||||
this.renderTarget1 = renderTarget;
|
||||
this.renderTarget2 = renderTarget.clone();
|
||||
this.renderTarget2.texture.name = 'EffectComposer.rt2';
|
||||
|
||||
/**
|
||||
* A reference to the internal write buffer. Passes usually write
|
||||
* their result into this buffer.
|
||||
*
|
||||
* @type {WebGLRenderTarget}
|
||||
*/
|
||||
this.writeBuffer = this.renderTarget1;
|
||||
|
||||
/**
|
||||
* A reference to the internal read buffer. Passes usually read
|
||||
* the previous render result from this buffer.
|
||||
*
|
||||
* @type {WebGLRenderTarget}
|
||||
*/
|
||||
this.readBuffer = this.renderTarget2;
|
||||
|
||||
/**
|
||||
* Whether the final pass is rendered to the screen (default framebuffer) or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.renderToScreen = true;
|
||||
|
||||
/**
|
||||
* An array representing the (ordered) chain of post-processing passes.
|
||||
*
|
||||
* @type {Array<Pass>}
|
||||
*/
|
||||
this.passes = [];
|
||||
|
||||
/**
|
||||
* A copy pass used for internal swap operations.
|
||||
*
|
||||
* @private
|
||||
* @type {ShaderPass}
|
||||
*/
|
||||
this.copyPass = new ShaderPass( CopyShader );
|
||||
this.copyPass.material.blending = NoBlending;
|
||||
|
||||
/**
|
||||
* The intenral clock for managing time data.
|
||||
*
|
||||
* @private
|
||||
* @type {Clock}
|
||||
*/
|
||||
this.clock = new Clock();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the internal read/write buffers.
|
||||
*/
|
||||
swapBuffers() {
|
||||
|
||||
const tmp = this.readBuffer;
|
||||
this.readBuffer = this.writeBuffer;
|
||||
this.writeBuffer = tmp;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given pass to the pass chain.
|
||||
*
|
||||
* @param {Pass} pass - The pass to add.
|
||||
*/
|
||||
addPass( pass ) {
|
||||
|
||||
this.passes.push( pass );
|
||||
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given pass at a given index.
|
||||
*
|
||||
* @param {Pass} pass - The pass to insert.
|
||||
* @param {number} index - The index into the pass chain.
|
||||
*/
|
||||
insertPass( pass, index ) {
|
||||
|
||||
this.passes.splice( index, 0, pass );
|
||||
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given pass from the pass chain.
|
||||
*
|
||||
* @param {Pass} pass - The pass to remove.
|
||||
*/
|
||||
removePass( pass ) {
|
||||
|
||||
const index = this.passes.indexOf( pass );
|
||||
|
||||
if ( index !== - 1 ) {
|
||||
|
||||
this.passes.splice( index, 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
|
||||
*
|
||||
* @param {number} passIndex - The pass index.
|
||||
* @return {boolean} Whether the the pass for the given index is the last pass in the pass chain.
|
||||
*/
|
||||
isLastEnabledPass( passIndex ) {
|
||||
|
||||
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
|
||||
|
||||
if ( this.passes[ i ].enabled ) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes all enabled post-processing passes in order to produce the final frame.
|
||||
*
|
||||
* @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
|
||||
* its own time delta value.
|
||||
*/
|
||||
render( deltaTime ) {
|
||||
|
||||
// deltaTime value is in seconds
|
||||
|
||||
if ( deltaTime === undefined ) {
|
||||
|
||||
deltaTime = this.clock.getDelta();
|
||||
|
||||
}
|
||||
|
||||
const currentRenderTarget = this.renderer.getRenderTarget();
|
||||
|
||||
let maskActive = false;
|
||||
|
||||
for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
|
||||
|
||||
const pass = this.passes[ i ];
|
||||
|
||||
if ( pass.enabled === false ) continue;
|
||||
|
||||
pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
|
||||
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
|
||||
|
||||
if ( pass.needsSwap ) {
|
||||
|
||||
if ( maskActive ) {
|
||||
|
||||
const context = this.renderer.getContext();
|
||||
const stencil = this.renderer.state.buffers.stencil;
|
||||
|
||||
//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
|
||||
stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
|
||||
|
||||
this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
|
||||
|
||||
//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
|
||||
stencil.setFunc( context.EQUAL, 1, 0xffffffff );
|
||||
|
||||
}
|
||||
|
||||
this.swapBuffers();
|
||||
|
||||
}
|
||||
|
||||
if ( MaskPass !== undefined ) {
|
||||
|
||||
if ( pass instanceof MaskPass ) {
|
||||
|
||||
maskActive = true;
|
||||
|
||||
} else if ( pass instanceof ClearMaskPass ) {
|
||||
|
||||
maskActive = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.renderer.setRenderTarget( currentRenderTarget );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal state of the EffectComposer.
|
||||
*
|
||||
* @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
|
||||
* the one from the constructor. If set, it is used to setup the read and write buffers.
|
||||
*/
|
||||
reset( renderTarget ) {
|
||||
|
||||
if ( renderTarget === undefined ) {
|
||||
|
||||
const size = this.renderer.getSize( new Vector2() );
|
||||
this._pixelRatio = this.renderer.getPixelRatio();
|
||||
this._width = size.width;
|
||||
this._height = size.height;
|
||||
|
||||
renderTarget = this.renderTarget1.clone();
|
||||
renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
|
||||
|
||||
}
|
||||
|
||||
this.renderTarget1.dispose();
|
||||
this.renderTarget2.dispose();
|
||||
this.renderTarget1 = renderTarget;
|
||||
this.renderTarget2 = renderTarget.clone();
|
||||
|
||||
this.writeBuffer = this.renderTarget1;
|
||||
this.readBuffer = this.renderTarget2;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
|
||||
* this method honors the current pixel ration.
|
||||
*
|
||||
* @param {number} width - The width in logical pixels.
|
||||
* @param {number} height - The height in logical pixels.
|
||||
*/
|
||||
setSize( width, height ) {
|
||||
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
|
||||
const effectiveWidth = this._width * this._pixelRatio;
|
||||
const effectiveHeight = this._height * this._pixelRatio;
|
||||
|
||||
this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
|
||||
this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
|
||||
|
||||
for ( let i = 0; i < this.passes.length; i ++ ) {
|
||||
|
||||
this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
|
||||
* Setting the pixel ratio will automatically resize the composer.
|
||||
*
|
||||
* @param {number} pixelRatio - The pixel ratio to set.
|
||||
*/
|
||||
setPixelRatio( pixelRatio ) {
|
||||
|
||||
this._pixelRatio = pixelRatio;
|
||||
|
||||
this.setSize( this._width, this._height );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the composer is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this.renderTarget1.dispose();
|
||||
this.renderTarget2.dispose();
|
||||
|
||||
this.copyPass.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { EffectComposer };
|
||||
194
web/vendor/addons/postprocessing/MaskPass.js
vendored
Normal file
194
web/vendor/addons/postprocessing/MaskPass.js
vendored
Normal file
@ -0,0 +1,194 @@
|
||||
import { Pass } from './Pass.js';
|
||||
|
||||
/**
|
||||
* This pass can be used to define a mask during post processing.
|
||||
* Meaning only areas of subsequent post processing are affected
|
||||
* which lie in the masking area of this pass. Internally, the masking
|
||||
* is implemented with the stencil buffer.
|
||||
*
|
||||
* ```js
|
||||
* const maskPass = new MaskPass( scene, camera );
|
||||
* composer.addPass( maskPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class MaskPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new mask pass.
|
||||
*
|
||||
* @param {Scene} scene - The 3D objects in this scene will define the mask.
|
||||
* @param {Camera} camera - The camera.
|
||||
*/
|
||||
constructor( scene, camera ) {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* The scene that defines the mask.
|
||||
*
|
||||
* @type {Scene}
|
||||
*/
|
||||
this.scene = scene;
|
||||
|
||||
/**
|
||||
* The camera.
|
||||
*
|
||||
* @type {Camera}
|
||||
*/
|
||||
this.camera = camera;
|
||||
|
||||
/**
|
||||
* Overwritten to perform a clear operation by default.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.clear = true;
|
||||
|
||||
/**
|
||||
* Overwritten to disable the swap.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.needsSwap = false;
|
||||
|
||||
/**
|
||||
* Whether to inverse the mask or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.inverse = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a mask pass with the configured scene and camera.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
||||
|
||||
const context = renderer.getContext();
|
||||
const state = renderer.state;
|
||||
|
||||
// don't update color or depth
|
||||
|
||||
state.buffers.color.setMask( false );
|
||||
state.buffers.depth.setMask( false );
|
||||
|
||||
// lock buffers
|
||||
|
||||
state.buffers.color.setLocked( true );
|
||||
state.buffers.depth.setLocked( true );
|
||||
|
||||
// set up stencil
|
||||
|
||||
let writeValue, clearValue;
|
||||
|
||||
if ( this.inverse ) {
|
||||
|
||||
writeValue = 0;
|
||||
clearValue = 1;
|
||||
|
||||
} else {
|
||||
|
||||
writeValue = 1;
|
||||
clearValue = 0;
|
||||
|
||||
}
|
||||
|
||||
state.buffers.stencil.setTest( true );
|
||||
state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
|
||||
state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
|
||||
state.buffers.stencil.setClear( clearValue );
|
||||
state.buffers.stencil.setLocked( true );
|
||||
|
||||
// draw into the stencil buffer
|
||||
|
||||
renderer.setRenderTarget( readBuffer );
|
||||
if ( this.clear ) renderer.clear();
|
||||
renderer.render( this.scene, this.camera );
|
||||
|
||||
renderer.setRenderTarget( writeBuffer );
|
||||
if ( this.clear ) renderer.clear();
|
||||
renderer.render( this.scene, this.camera );
|
||||
|
||||
// unlock color and depth buffer and make them writable for subsequent rendering/clearing
|
||||
|
||||
state.buffers.color.setLocked( false );
|
||||
state.buffers.depth.setLocked( false );
|
||||
|
||||
state.buffers.color.setMask( true );
|
||||
state.buffers.depth.setMask( true );
|
||||
|
||||
// only render where stencil is set to 1
|
||||
|
||||
state.buffers.stencil.setLocked( false );
|
||||
state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
|
||||
state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
|
||||
state.buffers.stencil.setLocked( true );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This pass can be used to clear a mask previously defined with {@link MaskPass}.
|
||||
*
|
||||
* ```js
|
||||
* const clearPass = new ClearMaskPass();
|
||||
* composer.addPass( clearPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class ClearMaskPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new clear mask pass.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* Overwritten to disable the swap.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.needsSwap = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the clear of the currently defined mask.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
|
||||
|
||||
renderer.state.buffers.stencil.setLocked( false );
|
||||
renderer.state.buffers.stencil.setTest( false );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MaskPass, ClearMaskPass };
|
||||
138
web/vendor/addons/postprocessing/OutputPass.js
vendored
Normal file
138
web/vendor/addons/postprocessing/OutputPass.js
vendored
Normal file
@ -0,0 +1,138 @@
|
||||
import {
|
||||
ColorManagement,
|
||||
RawShaderMaterial,
|
||||
UniformsUtils,
|
||||
LinearToneMapping,
|
||||
ReinhardToneMapping,
|
||||
CineonToneMapping,
|
||||
AgXToneMapping,
|
||||
ACESFilmicToneMapping,
|
||||
NeutralToneMapping,
|
||||
CustomToneMapping,
|
||||
SRGBTransfer
|
||||
} from 'three';
|
||||
import { Pass, FullScreenQuad } from './Pass.js';
|
||||
import { OutputShader } from '../shaders/OutputShader.js';
|
||||
|
||||
/**
|
||||
* This pass is responsible for including tone mapping and color space conversion
|
||||
* into your pass chain. In most cases, this pass should be included at the end
|
||||
* of each pass chain. If a pass requires sRGB input (e.g. like FXAA), the pass
|
||||
* must follow `OutputPass` in the pass chain.
|
||||
*
|
||||
* The tone mapping and color space settings are extracted from the renderer.
|
||||
*
|
||||
* ```js
|
||||
* const outputPass = new OutputPass();
|
||||
* composer.addPass( outputPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class OutputPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new output pass.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* The pass uniforms.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
this.uniforms = UniformsUtils.clone( OutputShader.uniforms );
|
||||
|
||||
/**
|
||||
* The pass material.
|
||||
*
|
||||
* @type {RawShaderMaterial}
|
||||
*/
|
||||
this.material = new RawShaderMaterial( {
|
||||
name: OutputShader.name,
|
||||
uniforms: this.uniforms,
|
||||
vertexShader: OutputShader.vertexShader,
|
||||
fragmentShader: OutputShader.fragmentShader
|
||||
} );
|
||||
|
||||
// internals
|
||||
|
||||
this._fsQuad = new FullScreenQuad( this.material );
|
||||
|
||||
this._outputColorSpace = null;
|
||||
this._toneMapping = null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the output pass.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer, writeBuffer, readBuffer/*, deltaTime, maskActive */ ) {
|
||||
|
||||
this.uniforms[ 'tDiffuse' ].value = readBuffer.texture;
|
||||
this.uniforms[ 'toneMappingExposure' ].value = renderer.toneMappingExposure;
|
||||
|
||||
// rebuild defines if required
|
||||
|
||||
if ( this._outputColorSpace !== renderer.outputColorSpace || this._toneMapping !== renderer.toneMapping ) {
|
||||
|
||||
this._outputColorSpace = renderer.outputColorSpace;
|
||||
this._toneMapping = renderer.toneMapping;
|
||||
|
||||
this.material.defines = {};
|
||||
|
||||
if ( ColorManagement.getTransfer( this._outputColorSpace ) === SRGBTransfer ) this.material.defines.SRGB_TRANSFER = '';
|
||||
|
||||
if ( this._toneMapping === LinearToneMapping ) this.material.defines.LINEAR_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === ReinhardToneMapping ) this.material.defines.REINHARD_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === CineonToneMapping ) this.material.defines.CINEON_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === ACESFilmicToneMapping ) this.material.defines.ACES_FILMIC_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === AgXToneMapping ) this.material.defines.AGX_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === NeutralToneMapping ) this.material.defines.NEUTRAL_TONE_MAPPING = '';
|
||||
else if ( this._toneMapping === CustomToneMapping ) this.material.defines.CUSTOM_TONE_MAPPING = '';
|
||||
|
||||
this.material.needsUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
if ( this.renderToScreen === true ) {
|
||||
|
||||
renderer.setRenderTarget( null );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
} else {
|
||||
|
||||
renderer.setRenderTarget( writeBuffer );
|
||||
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the pass is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this.material.dispose();
|
||||
this._fsQuad.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { OutputPass };
|
||||
189
web/vendor/addons/postprocessing/Pass.js
vendored
Normal file
189
web/vendor/addons/postprocessing/Pass.js
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
import {
|
||||
BufferGeometry,
|
||||
Float32BufferAttribute,
|
||||
OrthographicCamera,
|
||||
Mesh
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* Abstract base class for all post processing passes.
|
||||
*
|
||||
* This module is only relevant for post processing with {@link WebGLRenderer}.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
class Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new pass.
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
/**
|
||||
* This flag can be used for type testing.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
* @default true
|
||||
*/
|
||||
this.isPass = true;
|
||||
|
||||
/**
|
||||
* If set to `true`, the pass is processed by the composer.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.enabled = true;
|
||||
|
||||
/**
|
||||
* If set to `true`, the pass indicates to swap read and write buffer after rendering.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.needsSwap = true;
|
||||
|
||||
/**
|
||||
* If set to `true`, the pass clears its buffer before rendering
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.clear = false;
|
||||
|
||||
/**
|
||||
* If set to `true`, the result of the pass is rendered to screen. The last pass in the composers
|
||||
* pass chain gets automatically rendered to screen, no matter how this property is configured.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.renderToScreen = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the pass.
|
||||
*
|
||||
* @abstract
|
||||
* @param {number} width - The width to set.
|
||||
* @param {number} height - The width to set.
|
||||
*/
|
||||
setSize( /* width, height */ ) {}
|
||||
|
||||
/**
|
||||
* This method holds the render logic of a pass. It must be implemented in all derived classes.
|
||||
*
|
||||
* @abstract
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
|
||||
|
||||
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the pass is no longer used in your app.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
dispose() {}
|
||||
|
||||
}
|
||||
|
||||
// Helper for passes that need to fill the viewport with a single quad.
|
||||
|
||||
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
|
||||
|
||||
// https://github.com/mrdoob/three.js/pull/21358
|
||||
|
||||
class FullscreenTriangleGeometry extends BufferGeometry {
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
|
||||
this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const _geometry = new FullscreenTriangleGeometry();
|
||||
|
||||
|
||||
/**
|
||||
* This module is a helper for passes which need to render a full
|
||||
* screen effect which is quite common in context of post processing.
|
||||
*
|
||||
* The intended usage is to reuse a single full screen quad for rendering
|
||||
* subsequent passes by just reassigning the `material` reference.
|
||||
*
|
||||
* This module can only be used with {@link WebGLRenderer}.
|
||||
*
|
||||
* @augments Mesh
|
||||
*/
|
||||
class FullScreenQuad {
|
||||
|
||||
/**
|
||||
* Constructs a new full screen quad.
|
||||
*
|
||||
* @param {?Material} material - The material to render te full screen quad with.
|
||||
*/
|
||||
constructor( material ) {
|
||||
|
||||
this._mesh = new Mesh( _geometry, material );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the instance is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this._mesh.geometry.dispose();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the full screen quad.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
*/
|
||||
render( renderer ) {
|
||||
|
||||
renderer.render( this._mesh, _camera );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The quad's material.
|
||||
*
|
||||
* @type {?Material}
|
||||
*/
|
||||
get material() {
|
||||
|
||||
return this._mesh.material;
|
||||
|
||||
}
|
||||
|
||||
set material( value ) {
|
||||
|
||||
this._mesh.material = value;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Pass, FullScreenQuad };
|
||||
182
web/vendor/addons/postprocessing/RenderPass.js
vendored
Normal file
182
web/vendor/addons/postprocessing/RenderPass.js
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
import {
|
||||
Color
|
||||
} from 'three';
|
||||
import { Pass } from './Pass.js';
|
||||
|
||||
/**
|
||||
* This class represents a render pass. It takes a camera and a scene and produces
|
||||
* a beauty pass for subsequent post processing effects.
|
||||
*
|
||||
* ```js
|
||||
* const renderPass = new RenderPass( scene, camera );
|
||||
* composer.addPass( renderPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class RenderPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new render pass.
|
||||
*
|
||||
* @param {Scene} scene - The scene to render.
|
||||
* @param {Camera} camera - The camera.
|
||||
* @param {?Material} [overrideMaterial=null] - The override material. If set, this material is used
|
||||
* for all objects in the scene.
|
||||
* @param {?(number|Color|string)} [clearColor=null] - The clear color of the render pass.
|
||||
* @param {?number} [clearAlpha=null] - The clear alpha of the render pass.
|
||||
*/
|
||||
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* The scene to render.
|
||||
*
|
||||
* @type {Scene}
|
||||
*/
|
||||
this.scene = scene;
|
||||
|
||||
/**
|
||||
* The camera.
|
||||
*
|
||||
* @type {Camera}
|
||||
*/
|
||||
this.camera = camera;
|
||||
|
||||
/**
|
||||
* The override material. If set, this material is used
|
||||
* for all objects in the scene.
|
||||
*
|
||||
* @type {?Material}
|
||||
* @default null
|
||||
*/
|
||||
this.overrideMaterial = overrideMaterial;
|
||||
|
||||
/**
|
||||
* The clear color of the render pass.
|
||||
*
|
||||
* @type {?(number|Color|string)}
|
||||
* @default null
|
||||
*/
|
||||
this.clearColor = clearColor;
|
||||
|
||||
/**
|
||||
* The clear alpha of the render pass.
|
||||
*
|
||||
* @type {?number}
|
||||
* @default null
|
||||
*/
|
||||
this.clearAlpha = clearAlpha;
|
||||
|
||||
/**
|
||||
* Overwritten to perform a clear operation by default.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.clear = true;
|
||||
|
||||
/**
|
||||
* If set to `true`, only the depth can be cleared when `clear` is to `false`.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.clearDepth = false;
|
||||
|
||||
/**
|
||||
* Overwritten to disable the swap.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.needsSwap = false;
|
||||
this._oldClearColor = new Color();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a beauty pass with the configured scene and camera.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
||||
|
||||
const oldAutoClear = renderer.autoClear;
|
||||
renderer.autoClear = false;
|
||||
|
||||
let oldClearAlpha, oldOverrideMaterial;
|
||||
|
||||
if ( this.overrideMaterial !== null ) {
|
||||
|
||||
oldOverrideMaterial = this.scene.overrideMaterial;
|
||||
|
||||
this.scene.overrideMaterial = this.overrideMaterial;
|
||||
|
||||
}
|
||||
|
||||
if ( this.clearColor !== null ) {
|
||||
|
||||
renderer.getClearColor( this._oldClearColor );
|
||||
renderer.setClearColor( this.clearColor, renderer.getClearAlpha() );
|
||||
|
||||
}
|
||||
|
||||
if ( this.clearAlpha !== null ) {
|
||||
|
||||
oldClearAlpha = renderer.getClearAlpha();
|
||||
renderer.setClearAlpha( this.clearAlpha );
|
||||
|
||||
}
|
||||
|
||||
if ( this.clearDepth == true ) {
|
||||
|
||||
renderer.clearDepth();
|
||||
|
||||
}
|
||||
|
||||
renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
|
||||
|
||||
if ( this.clear === true ) {
|
||||
|
||||
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
|
||||
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
|
||||
|
||||
}
|
||||
|
||||
renderer.render( this.scene, this.camera );
|
||||
|
||||
// restore
|
||||
|
||||
if ( this.clearColor !== null ) {
|
||||
|
||||
renderer.setClearColor( this._oldClearColor );
|
||||
|
||||
}
|
||||
|
||||
if ( this.clearAlpha !== null ) {
|
||||
|
||||
renderer.setClearAlpha( oldClearAlpha );
|
||||
|
||||
}
|
||||
|
||||
if ( this.overrideMaterial !== null ) {
|
||||
|
||||
this.scene.overrideMaterial = oldOverrideMaterial;
|
||||
|
||||
}
|
||||
|
||||
renderer.autoClear = oldAutoClear;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { RenderPass };
|
||||
134
web/vendor/addons/postprocessing/ShaderPass.js
vendored
Normal file
134
web/vendor/addons/postprocessing/ShaderPass.js
vendored
Normal file
@ -0,0 +1,134 @@
|
||||
import {
|
||||
ShaderMaterial,
|
||||
UniformsUtils
|
||||
} from 'three';
|
||||
import { Pass, FullScreenQuad } from './Pass.js';
|
||||
|
||||
/**
|
||||
* This pass can be used to create a post processing effect
|
||||
* with a raw GLSL shader object. Useful for implementing custom
|
||||
* effects.
|
||||
*
|
||||
* ```js
|
||||
* const fxaaPass = new ShaderPass( FXAAShader );
|
||||
* composer.addPass( fxaaPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class ShaderPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new shader pass.
|
||||
*
|
||||
* @param {Object|ShaderMaterial} [shader] - A shader object holding vertex and fragment shader as well as
|
||||
* defines and uniforms. It's also valid to pass a custom shader material.
|
||||
* @param {string} [textureID='tDiffuse'] - The name of the texture uniform that should sample
|
||||
* the read buffer.
|
||||
*/
|
||||
constructor( shader, textureID = 'tDiffuse' ) {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* The name of the texture uniform that should sample the read buffer.
|
||||
*
|
||||
* @type {string}
|
||||
* @default 'tDiffuse'
|
||||
*/
|
||||
this.textureID = textureID;
|
||||
|
||||
/**
|
||||
* The pass uniforms.
|
||||
*
|
||||
* @type {?Object}
|
||||
*/
|
||||
this.uniforms = null;
|
||||
|
||||
/**
|
||||
* The pass material.
|
||||
*
|
||||
* @type {?ShaderMaterial}
|
||||
*/
|
||||
this.material = null;
|
||||
|
||||
if ( shader instanceof ShaderMaterial ) {
|
||||
|
||||
this.uniforms = shader.uniforms;
|
||||
|
||||
this.material = shader;
|
||||
|
||||
} else if ( shader ) {
|
||||
|
||||
this.uniforms = UniformsUtils.clone( shader.uniforms );
|
||||
|
||||
this.material = new ShaderMaterial( {
|
||||
|
||||
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
|
||||
defines: Object.assign( {}, shader.defines ),
|
||||
uniforms: this.uniforms,
|
||||
vertexShader: shader.vertexShader,
|
||||
fragmentShader: shader.fragmentShader
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
// internals
|
||||
|
||||
this._fsQuad = new FullScreenQuad( this.material );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the shader pass.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
|
||||
|
||||
if ( this.uniforms[ this.textureID ] ) {
|
||||
|
||||
this.uniforms[ this.textureID ].value = readBuffer.texture;
|
||||
|
||||
}
|
||||
|
||||
this._fsQuad.material = this.material;
|
||||
|
||||
if ( this.renderToScreen ) {
|
||||
|
||||
renderer.setRenderTarget( null );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
} else {
|
||||
|
||||
renderer.setRenderTarget( writeBuffer );
|
||||
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
|
||||
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the pass is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
this.material.dispose();
|
||||
|
||||
this._fsQuad.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ShaderPass };
|
||||
491
web/vendor/addons/postprocessing/UnrealBloomPass.js
vendored
Normal file
491
web/vendor/addons/postprocessing/UnrealBloomPass.js
vendored
Normal file
@ -0,0 +1,491 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
Color,
|
||||
HalfFloatType,
|
||||
MeshBasicMaterial,
|
||||
ShaderMaterial,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
Vector3,
|
||||
WebGLRenderTarget
|
||||
} from 'three';
|
||||
import { Pass, FullScreenQuad } from './Pass.js';
|
||||
import { CopyShader } from '../shaders/CopyShader.js';
|
||||
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
|
||||
|
||||
/**
|
||||
* This pass is inspired by the bloom pass of Unreal Engine. It creates a
|
||||
* mip map chain of bloom textures and blurs them with different radii. Because
|
||||
* of the weighted combination of mips, and because larger blurs are done on
|
||||
* higher mips, this effect provides good quality and performance.
|
||||
*
|
||||
* When using this pass, tone mapping must be enabled in the renderer settings.
|
||||
*
|
||||
* Reference:
|
||||
* - [Bloom in Unreal Engine]{@link https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/}
|
||||
*
|
||||
* ```js
|
||||
* const resolution = new THREE.Vector2( window.innerWidth, window.innerHeight );
|
||||
* const bloomPass = new UnrealBloomPass( resolution, 1.5, 0.4, 0.85 );
|
||||
* composer.addPass( bloomPass );
|
||||
* ```
|
||||
*
|
||||
* @augments Pass
|
||||
*/
|
||||
class UnrealBloomPass extends Pass {
|
||||
|
||||
/**
|
||||
* Constructs a new Unreal Bloom pass.
|
||||
*
|
||||
* @param {Vector2} [resolution] - The effect's resolution.
|
||||
* @param {number} [strength=1] - The Bloom strength.
|
||||
* @param {number} radius - The Bloom radius.
|
||||
* @param {number} threshold - The luminance threshold limits which bright areas contribute to the Bloom effect.
|
||||
*/
|
||||
constructor( resolution, strength = 1, radius, threshold ) {
|
||||
|
||||
super();
|
||||
|
||||
/**
|
||||
* The Bloom strength.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.strength = strength;
|
||||
|
||||
/**
|
||||
* The Bloom radius.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.radius = radius;
|
||||
|
||||
/**
|
||||
* The luminance threshold limits which bright areas contribute to the Bloom effect.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.threshold = threshold;
|
||||
|
||||
/**
|
||||
* The effect's resolution.
|
||||
*
|
||||
* @type {Vector2}
|
||||
* @default (256,256)
|
||||
*/
|
||||
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
|
||||
|
||||
/**
|
||||
* The effect's clear color
|
||||
*
|
||||
* @type {Color}
|
||||
* @default (0,0,0)
|
||||
*/
|
||||
this.clearColor = new Color( 0, 0, 0 );
|
||||
|
||||
/**
|
||||
* Overwritten to disable the swap.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.needsSwap = false;
|
||||
|
||||
// internals
|
||||
|
||||
// render targets
|
||||
this.renderTargetsHorizontal = [];
|
||||
this.renderTargetsVertical = [];
|
||||
this.nMips = 5;
|
||||
let resx = Math.round( this.resolution.x / 2 );
|
||||
let resy = Math.round( this.resolution.y / 2 );
|
||||
|
||||
this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
|
||||
this.renderTargetBright.texture.generateMipmaps = false;
|
||||
|
||||
for ( let i = 0; i < this.nMips; i ++ ) {
|
||||
|
||||
const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
|
||||
renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
|
||||
renderTargetHorizontal.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetsHorizontal.push( renderTargetHorizontal );
|
||||
|
||||
const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
|
||||
renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
|
||||
renderTargetVertical.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetsVertical.push( renderTargetVertical );
|
||||
|
||||
resx = Math.round( resx / 2 );
|
||||
|
||||
resy = Math.round( resy / 2 );
|
||||
|
||||
}
|
||||
|
||||
// luminosity high pass material
|
||||
|
||||
const highPassShader = LuminosityHighPassShader;
|
||||
this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms );
|
||||
|
||||
this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
|
||||
this.highPassUniforms[ 'smoothWidth' ].value = 0.01;
|
||||
|
||||
this.materialHighPassFilter = new ShaderMaterial( {
|
||||
uniforms: this.highPassUniforms,
|
||||
vertexShader: highPassShader.vertexShader,
|
||||
fragmentShader: highPassShader.fragmentShader
|
||||
} );
|
||||
|
||||
// gaussian blur materials
|
||||
|
||||
this.separableBlurMaterials = [];
|
||||
const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
|
||||
resx = Math.round( this.resolution.x / 2 );
|
||||
resy = Math.round( this.resolution.y / 2 );
|
||||
|
||||
for ( let i = 0; i < this.nMips; i ++ ) {
|
||||
|
||||
this.separableBlurMaterials.push( this._getSeparableBlurMaterial( kernelSizeArray[ i ] ) );
|
||||
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
|
||||
|
||||
resx = Math.round( resx / 2 );
|
||||
|
||||
resy = Math.round( resy / 2 );
|
||||
|
||||
}
|
||||
|
||||
// composite material
|
||||
|
||||
this.compositeMaterial = this._getCompositeMaterial( this.nMips );
|
||||
this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
|
||||
this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
|
||||
this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
|
||||
this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
|
||||
this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
|
||||
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
|
||||
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
|
||||
|
||||
const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
|
||||
this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
|
||||
this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
|
||||
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
|
||||
|
||||
// blend material
|
||||
|
||||
this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );
|
||||
|
||||
this.blendMaterial = new ShaderMaterial( {
|
||||
uniforms: this.copyUniforms,
|
||||
vertexShader: CopyShader.vertexShader,
|
||||
fragmentShader: CopyShader.fragmentShader,
|
||||
blending: AdditiveBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true
|
||||
} );
|
||||
|
||||
this._oldClearColor = new Color();
|
||||
this._oldClearAlpha = 1;
|
||||
|
||||
this._basic = new MeshBasicMaterial();
|
||||
|
||||
this._fsQuad = new FullScreenQuad( null );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the GPU-related resources allocated by this instance. Call this
|
||||
* method whenever the pass is no longer used in your app.
|
||||
*/
|
||||
dispose() {
|
||||
|
||||
for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {
|
||||
|
||||
this.renderTargetsHorizontal[ i ].dispose();
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {
|
||||
|
||||
this.renderTargetsVertical[ i ].dispose();
|
||||
|
||||
}
|
||||
|
||||
this.renderTargetBright.dispose();
|
||||
|
||||
//
|
||||
|
||||
for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) {
|
||||
|
||||
this.separableBlurMaterials[ i ].dispose();
|
||||
|
||||
}
|
||||
|
||||
this.compositeMaterial.dispose();
|
||||
this.blendMaterial.dispose();
|
||||
this._basic.dispose();
|
||||
|
||||
//
|
||||
|
||||
this._fsQuad.dispose();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the pass.
|
||||
*
|
||||
* @param {number} width - The width to set.
|
||||
* @param {number} height - The width to set.
|
||||
*/
|
||||
setSize( width, height ) {
|
||||
|
||||
let resx = Math.round( width / 2 );
|
||||
let resy = Math.round( height / 2 );
|
||||
|
||||
this.renderTargetBright.setSize( resx, resy );
|
||||
|
||||
for ( let i = 0; i < this.nMips; i ++ ) {
|
||||
|
||||
this.renderTargetsHorizontal[ i ].setSize( resx, resy );
|
||||
this.renderTargetsVertical[ i ].setSize( resx, resy );
|
||||
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
|
||||
|
||||
resx = Math.round( resx / 2 );
|
||||
resy = Math.round( resy / 2 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the Bloom pass.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {WebGLRenderTarget} writeBuffer - The write buffer. This buffer is intended as the rendering
|
||||
* destination for the pass.
|
||||
* @param {WebGLRenderTarget} readBuffer - The read buffer. The pass can access the result from the
|
||||
* previous pass from this buffer.
|
||||
* @param {number} deltaTime - The delta time in seconds.
|
||||
* @param {boolean} maskActive - Whether masking is active or not.
|
||||
*/
|
||||
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
|
||||
|
||||
renderer.getClearColor( this._oldClearColor );
|
||||
this._oldClearAlpha = renderer.getClearAlpha();
|
||||
const oldAutoClear = renderer.autoClear;
|
||||
renderer.autoClear = false;
|
||||
|
||||
renderer.setClearColor( this.clearColor, 0 );
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
|
||||
|
||||
// Render input to screen
|
||||
|
||||
if ( this.renderToScreen ) {
|
||||
|
||||
this._fsQuad.material = this._basic;
|
||||
this._basic.map = readBuffer.texture;
|
||||
|
||||
renderer.setRenderTarget( null );
|
||||
renderer.clear();
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
// 1. Extract Bright Areas
|
||||
|
||||
this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
|
||||
this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
|
||||
this._fsQuad.material = this.materialHighPassFilter;
|
||||
|
||||
renderer.setRenderTarget( this.renderTargetBright );
|
||||
renderer.clear();
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
// 2. Blur All the mips progressively
|
||||
|
||||
let inputRenderTarget = this.renderTargetBright;
|
||||
|
||||
for ( let i = 0; i < this.nMips; i ++ ) {
|
||||
|
||||
this._fsQuad.material = this.separableBlurMaterials[ i ];
|
||||
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
|
||||
renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
|
||||
renderer.clear();
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
|
||||
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
|
||||
renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
|
||||
renderer.clear();
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
inputRenderTarget = this.renderTargetsVertical[ i ];
|
||||
|
||||
}
|
||||
|
||||
// Composite All the mips
|
||||
|
||||
this._fsQuad.material = this.compositeMaterial;
|
||||
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
|
||||
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
|
||||
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
|
||||
|
||||
renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
|
||||
renderer.clear();
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
// Blend it additively over the input texture
|
||||
|
||||
this._fsQuad.material = this.blendMaterial;
|
||||
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
|
||||
|
||||
if ( this.renderToScreen ) {
|
||||
|
||||
renderer.setRenderTarget( null );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
} else {
|
||||
|
||||
renderer.setRenderTarget( readBuffer );
|
||||
this._fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
// Restore renderer settings
|
||||
|
||||
renderer.setClearColor( this._oldClearColor, this._oldClearAlpha );
|
||||
renderer.autoClear = oldAutoClear;
|
||||
|
||||
}
|
||||
|
||||
// internals
|
||||
|
||||
_getSeparableBlurMaterial( kernelRadius ) {
|
||||
|
||||
const coefficients = [];
|
||||
|
||||
for ( let i = 0; i < kernelRadius; i ++ ) {
|
||||
|
||||
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
|
||||
|
||||
}
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
defines: {
|
||||
'KERNEL_RADIUS': kernelRadius
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
'colorTexture': { value: null },
|
||||
'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
|
||||
'direction': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`#include <common>
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 invSize;
|
||||
uniform vec2 direction;
|
||||
uniform float gaussianCoefficients[KERNEL_RADIUS];
|
||||
|
||||
void main() {
|
||||
float weightSum = gaussianCoefficients[0];
|
||||
vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
|
||||
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
|
||||
float x = float(i);
|
||||
float w = gaussianCoefficients[i];
|
||||
vec2 uvOffset = direction * invSize * x;
|
||||
vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
|
||||
vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
|
||||
diffuseSum += (sample1 + sample2) * w;
|
||||
weightSum += 2.0 * w;
|
||||
}
|
||||
gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
_getCompositeMaterial( nMips ) {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
defines: {
|
||||
'NUM_MIPS': nMips
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
'blurTexture1': { value: null },
|
||||
'blurTexture2': { value: null },
|
||||
'blurTexture3': { value: null },
|
||||
'blurTexture4': { value: null },
|
||||
'blurTexture5': { value: null },
|
||||
'bloomStrength': { value: 1.0 },
|
||||
'bloomFactors': { value: null },
|
||||
'bloomTintColors': { value: null },
|
||||
'bloomRadius': { value: 0.0 }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`varying vec2 vUv;
|
||||
uniform sampler2D blurTexture1;
|
||||
uniform sampler2D blurTexture2;
|
||||
uniform sampler2D blurTexture3;
|
||||
uniform sampler2D blurTexture4;
|
||||
uniform sampler2D blurTexture5;
|
||||
uniform float bloomStrength;
|
||||
uniform float bloomRadius;
|
||||
uniform float bloomFactors[NUM_MIPS];
|
||||
uniform vec3 bloomTintColors[NUM_MIPS];
|
||||
|
||||
float lerpBloomFactor(const in float factor) {
|
||||
float mirrorFactor = 1.2 - factor;
|
||||
return mix(factor, mirrorFactor, bloomRadius);
|
||||
}
|
||||
|
||||
void main() {
|
||||
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
|
||||
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
|
||||
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
|
||||
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
|
||||
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 );
|
||||
UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 );
|
||||
|
||||
export { UnrealBloomPass };
|
||||
49
web/vendor/addons/shaders/CopyShader.js
vendored
Normal file
49
web/vendor/addons/shaders/CopyShader.js
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
/** @module CopyShader */
|
||||
|
||||
/**
|
||||
* Full-screen copy shader pass.
|
||||
*
|
||||
* @constant
|
||||
* @type {ShaderMaterial~Shader}
|
||||
*/
|
||||
const CopyShader = {
|
||||
|
||||
name: 'CopyShader',
|
||||
|
||||
uniforms: {
|
||||
|
||||
'tDiffuse': { value: null },
|
||||
'opacity': { value: 1.0 }
|
||||
|
||||
},
|
||||
|
||||
vertexShader: /* glsl */`
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
uniform float opacity;
|
||||
|
||||
uniform sampler2D tDiffuse;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vec4 texel = texture2D( tDiffuse, vUv );
|
||||
gl_FragColor = opacity * texel;
|
||||
|
||||
|
||||
}`
|
||||
|
||||
};
|
||||
|
||||
export { CopyShader };
|
||||
65
web/vendor/addons/shaders/LuminosityHighPassShader.js
vendored
Normal file
65
web/vendor/addons/shaders/LuminosityHighPassShader.js
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import {
|
||||
Color
|
||||
} from 'three';
|
||||
|
||||
/** @module LuminosityHighPassShader */
|
||||
|
||||
/**
|
||||
* Luminosity high pass shader.
|
||||
*
|
||||
* @constant
|
||||
* @type {ShaderMaterial~Shader}
|
||||
*/
|
||||
const LuminosityHighPassShader = {
|
||||
|
||||
name: 'LuminosityHighPassShader',
|
||||
|
||||
uniforms: {
|
||||
|
||||
'tDiffuse': { value: null },
|
||||
'luminosityThreshold': { value: 1.0 },
|
||||
'smoothWidth': { value: 1.0 },
|
||||
'defaultColor': { value: new Color( 0x000000 ) },
|
||||
'defaultOpacity': { value: 0.0 }
|
||||
|
||||
},
|
||||
|
||||
vertexShader: /* glsl */`
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform vec3 defaultColor;
|
||||
uniform float defaultOpacity;
|
||||
uniform float luminosityThreshold;
|
||||
uniform float smoothWidth;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vec4 texel = texture2D( tDiffuse, vUv );
|
||||
|
||||
float v = luminance( texel.xyz );
|
||||
|
||||
vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
|
||||
|
||||
float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
|
||||
|
||||
gl_FragColor = mix( outputColor, texel, alpha );
|
||||
|
||||
}`
|
||||
|
||||
};
|
||||
|
||||
export { LuminosityHighPassShader };
|
||||
100
web/vendor/addons/shaders/OutputShader.js
vendored
Normal file
100
web/vendor/addons/shaders/OutputShader.js
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
/** @module OutputShader */
|
||||
|
||||
/**
|
||||
* Performs tone mapping and color space conversion for
|
||||
* FX workflows.
|
||||
*
|
||||
* Used by {@link OutputPass}.
|
||||
*
|
||||
* @constant
|
||||
* @type {ShaderMaterial~Shader}
|
||||
*/
|
||||
const OutputShader = {
|
||||
|
||||
name: 'OutputShader',
|
||||
|
||||
uniforms: {
|
||||
|
||||
'tDiffuse': { value: null },
|
||||
'toneMappingExposure': { value: 1 }
|
||||
|
||||
},
|
||||
|
||||
vertexShader: /* glsl */`
|
||||
precision highp float;
|
||||
|
||||
uniform mat4 modelViewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
attribute vec3 position;
|
||||
attribute vec2 uv;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D tDiffuse;
|
||||
|
||||
#include <tonemapping_pars_fragment>
|
||||
#include <colorspace_pars_fragment>
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
gl_FragColor = texture2D( tDiffuse, vUv );
|
||||
|
||||
// tone mapping
|
||||
|
||||
#ifdef LINEAR_TONE_MAPPING
|
||||
|
||||
gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( REINHARD_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( CINEON_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( ACES_FILMIC_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( AGX_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( NEUTRAL_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#elif defined( CUSTOM_TONE_MAPPING )
|
||||
|
||||
gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );
|
||||
|
||||
#endif
|
||||
|
||||
// color space
|
||||
|
||||
#ifdef SRGB_TRANSFER
|
||||
|
||||
gl_FragColor = sRGBTransferOETF( gl_FragColor );
|
||||
|
||||
#endif
|
||||
|
||||
}`
|
||||
|
||||
};
|
||||
|
||||
export { OutputShader };
|
||||
53
web/vendor/addons/shaders/VignetteShader.js
vendored
Normal file
53
web/vendor/addons/shaders/VignetteShader.js
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
/** @module VignetteShader */
|
||||
|
||||
/**
|
||||
* Based on [PaintEffect postprocess from ro.me]{@link http://code.google.com/p/3-dreams-of-black/source/browse/deploy/js/effects/PaintEffect.js}.
|
||||
*
|
||||
* @constant
|
||||
* @type {ShaderMaterial~Shader}
|
||||
*/
|
||||
const VignetteShader = {
|
||||
|
||||
name: 'VignetteShader',
|
||||
|
||||
uniforms: {
|
||||
|
||||
'tDiffuse': { value: null },
|
||||
'offset': { value: 1.0 },
|
||||
'darkness': { value: 1.0 }
|
||||
|
||||
},
|
||||
|
||||
vertexShader: /* glsl */`
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader: /* glsl */`
|
||||
|
||||
uniform float offset;
|
||||
uniform float darkness;
|
||||
|
||||
uniform sampler2D tDiffuse;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
|
||||
// Eskil's vignette
|
||||
|
||||
vec4 texel = texture2D( tDiffuse, vUv );
|
||||
vec2 uv = ( vUv - vec2( 0.5 ) ) * vec2( offset );
|
||||
gl_FragColor = vec4( mix( texel.rgb, vec3( 1.0 - darkness ), dot( uv, uv ) ), texel.a );
|
||||
|
||||
}`
|
||||
|
||||
};
|
||||
|
||||
export { VignetteShader };
|
||||
1432
web/vendor/addons/utils/BufferGeometryUtils.js
vendored
Normal file
1432
web/vendor/addons/utils/BufferGeometryUtils.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
490
web/vendor/addons/utils/SkeletonUtils.js
vendored
Normal file
490
web/vendor/addons/utils/SkeletonUtils.js
vendored
Normal file
@ -0,0 +1,490 @@
|
||||
import {
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
Matrix4,
|
||||
Quaternion,
|
||||
QuaternionKeyframeTrack,
|
||||
SkeletonHelper,
|
||||
Vector3,
|
||||
VectorKeyframeTrack
|
||||
} from 'three';
|
||||
|
||||
/**
|
||||
* @module SkeletonUtils
|
||||
* @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
|
||||
*/
|
||||
|
||||
function getBoneName( bone, options ) {
|
||||
|
||||
if ( options.getBoneName !== undefined ) {
|
||||
|
||||
return options.getBoneName( bone );
|
||||
|
||||
}
|
||||
|
||||
return options.names[ bone.name ];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retargets the skeleton from the given source 3D object to the
|
||||
* target 3D object.
|
||||
*
|
||||
* @param {Object3D} target - The target 3D object.
|
||||
* @param {Object3D} source - The source 3D object.
|
||||
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
|
||||
*/
|
||||
function retarget( target, source, options = {} ) {
|
||||
|
||||
const quat = new Quaternion(),
|
||||
scale = new Vector3(),
|
||||
relativeMatrix = new Matrix4(),
|
||||
globalMatrix = new Matrix4();
|
||||
|
||||
options.preserveBoneMatrix = options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true;
|
||||
options.preserveBonePositions = options.preserveBonePositions !== undefined ? options.preserveBonePositions : true;
|
||||
options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
|
||||
options.hip = options.hip !== undefined ? options.hip : 'hip';
|
||||
options.hipInfluence = options.hipInfluence !== undefined ? options.hipInfluence : new Vector3( 1, 1, 1 );
|
||||
options.scale = options.scale !== undefined ? options.scale : 1;
|
||||
options.names = options.names || {};
|
||||
|
||||
const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
|
||||
bones = target.isObject3D ? target.skeleton.bones : getBones( target );
|
||||
|
||||
let bone, name, boneTo,
|
||||
bonesPosition;
|
||||
|
||||
// reset bones
|
||||
|
||||
if ( target.isObject3D ) {
|
||||
|
||||
target.skeleton.pose();
|
||||
|
||||
} else {
|
||||
|
||||
options.useTargetMatrix = true;
|
||||
options.preserveBoneMatrix = false;
|
||||
|
||||
}
|
||||
|
||||
if ( options.preserveBonePositions ) {
|
||||
|
||||
bonesPosition = [];
|
||||
|
||||
for ( let i = 0; i < bones.length; i ++ ) {
|
||||
|
||||
bonesPosition.push( bones[ i ].position.clone() );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( options.preserveBoneMatrix ) {
|
||||
|
||||
// reset matrix
|
||||
|
||||
target.updateMatrixWorld();
|
||||
|
||||
target.matrixWorld.identity();
|
||||
|
||||
// reset children matrix
|
||||
|
||||
for ( let i = 0; i < target.children.length; ++ i ) {
|
||||
|
||||
target.children[ i ].updateMatrixWorld( true );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < bones.length; ++ i ) {
|
||||
|
||||
bone = bones[ i ];
|
||||
name = getBoneName( bone, options );
|
||||
|
||||
boneTo = getBoneByName( name, sourceBones );
|
||||
|
||||
globalMatrix.copy( bone.matrixWorld );
|
||||
|
||||
if ( boneTo ) {
|
||||
|
||||
boneTo.updateMatrixWorld();
|
||||
|
||||
if ( options.useTargetMatrix ) {
|
||||
|
||||
relativeMatrix.copy( boneTo.matrixWorld );
|
||||
|
||||
} else {
|
||||
|
||||
relativeMatrix.copy( target.matrixWorld ).invert();
|
||||
relativeMatrix.multiply( boneTo.matrixWorld );
|
||||
|
||||
}
|
||||
|
||||
// ignore scale to extract rotation
|
||||
|
||||
scale.setFromMatrixScale( relativeMatrix );
|
||||
relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
|
||||
|
||||
// apply to global matrix
|
||||
|
||||
globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
|
||||
|
||||
if ( target.isObject3D ) {
|
||||
|
||||
if ( options.localOffsets ) {
|
||||
|
||||
if ( options.localOffsets[ bone.name ] ) {
|
||||
|
||||
globalMatrix.multiply( options.localOffsets[ bone.name ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
globalMatrix.copyPosition( relativeMatrix );
|
||||
|
||||
}
|
||||
|
||||
if ( name === options.hip ) {
|
||||
|
||||
globalMatrix.elements[ 12 ] *= options.scale * options.hipInfluence.x;
|
||||
globalMatrix.elements[ 13 ] *= options.scale * options.hipInfluence.y;
|
||||
globalMatrix.elements[ 14 ] *= options.scale * options.hipInfluence.z;
|
||||
|
||||
if ( options.hipPosition !== undefined ) {
|
||||
|
||||
globalMatrix.elements[ 12 ] += options.hipPosition.x * options.scale;
|
||||
globalMatrix.elements[ 13 ] += options.hipPosition.y * options.scale;
|
||||
globalMatrix.elements[ 14 ] += options.hipPosition.z * options.scale;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( bone.parent ) {
|
||||
|
||||
bone.matrix.copy( bone.parent.matrixWorld ).invert();
|
||||
bone.matrix.multiply( globalMatrix );
|
||||
|
||||
} else {
|
||||
|
||||
bone.matrix.copy( globalMatrix );
|
||||
|
||||
}
|
||||
|
||||
bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
|
||||
|
||||
bone.updateMatrixWorld();
|
||||
|
||||
}
|
||||
|
||||
if ( options.preserveBonePositions ) {
|
||||
|
||||
for ( let i = 0; i < bones.length; ++ i ) {
|
||||
|
||||
bone = bones[ i ];
|
||||
name = getBoneName( bone, options ) || bone.name;
|
||||
|
||||
if ( name !== options.hip ) {
|
||||
|
||||
bone.position.copy( bonesPosition[ i ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( options.preserveBoneMatrix ) {
|
||||
|
||||
// restore matrix
|
||||
|
||||
target.updateMatrixWorld( true );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retargets the animation clip of the source object to the
|
||||
* target 3D object.
|
||||
*
|
||||
* @param {Object3D} target - The target 3D object.
|
||||
* @param {Object3D} source - The source 3D object.
|
||||
* @param {AnimationClip} clip - The animation clip.
|
||||
* @param {module:SkeletonUtils~RetargetOptions} options - The options.
|
||||
* @return {AnimationClip} The retargeted animation clip.
|
||||
*/
|
||||
function retargetClip( target, source, clip, options = {} ) {
|
||||
|
||||
options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
|
||||
|
||||
// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
|
||||
options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
|
||||
options.names = options.names || [];
|
||||
|
||||
if ( ! source.isObject3D ) {
|
||||
|
||||
source = getHelperFromSkeleton( source );
|
||||
|
||||
}
|
||||
|
||||
const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
|
||||
delta = clip.duration / ( numFrames - 1 ),
|
||||
convertedTracks = [],
|
||||
mixer = new AnimationMixer( source ),
|
||||
bones = getBones( target.skeleton ),
|
||||
boneDatas = [];
|
||||
|
||||
let positionOffset,
|
||||
bone, boneTo, boneData,
|
||||
name;
|
||||
|
||||
mixer.clipAction( clip ).play();
|
||||
|
||||
// trim
|
||||
|
||||
let start = 0, end = numFrames;
|
||||
|
||||
if ( options.trim !== undefined ) {
|
||||
|
||||
start = Math.round( options.trim[ 0 ] * options.fps );
|
||||
end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;
|
||||
|
||||
mixer.update( options.trim[ 0 ] );
|
||||
|
||||
} else {
|
||||
|
||||
mixer.update( 0 );
|
||||
|
||||
}
|
||||
|
||||
source.updateMatrixWorld();
|
||||
|
||||
//
|
||||
|
||||
for ( let frame = 0; frame < end; ++ frame ) {
|
||||
|
||||
const time = frame * delta;
|
||||
|
||||
retarget( target, source, options );
|
||||
|
||||
for ( let j = 0; j < bones.length; ++ j ) {
|
||||
|
||||
bone = bones[ j ];
|
||||
name = getBoneName( bone, options ) || bone.name;
|
||||
boneTo = getBoneByName( name, source.skeleton );
|
||||
|
||||
if ( boneTo ) {
|
||||
|
||||
boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
|
||||
|
||||
if ( options.hip === name ) {
|
||||
|
||||
if ( ! boneData.pos ) {
|
||||
|
||||
boneData.pos = {
|
||||
times: new Float32Array( end ),
|
||||
values: new Float32Array( end * 3 )
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
if ( options.useFirstFramePosition ) {
|
||||
|
||||
if ( frame === 0 ) {
|
||||
|
||||
positionOffset = bone.position.clone();
|
||||
|
||||
}
|
||||
|
||||
bone.position.sub( positionOffset );
|
||||
|
||||
}
|
||||
|
||||
boneData.pos.times[ frame ] = time;
|
||||
|
||||
bone.position.toArray( boneData.pos.values, frame * 3 );
|
||||
|
||||
}
|
||||
|
||||
if ( ! boneData.quat ) {
|
||||
|
||||
boneData.quat = {
|
||||
times: new Float32Array( end ),
|
||||
values: new Float32Array( end * 4 )
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
boneData.quat.times[ frame ] = time;
|
||||
|
||||
bone.quaternion.toArray( boneData.quat.values, frame * 4 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( frame === end - 2 ) {
|
||||
|
||||
// last mixer update before final loop iteration
|
||||
// make sure we do not go over or equal to clip duration
|
||||
mixer.update( delta - 0.0000001 );
|
||||
|
||||
} else {
|
||||
|
||||
mixer.update( delta );
|
||||
|
||||
}
|
||||
|
||||
source.updateMatrixWorld();
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < boneDatas.length; ++ i ) {
|
||||
|
||||
boneData = boneDatas[ i ];
|
||||
|
||||
if ( boneData ) {
|
||||
|
||||
if ( boneData.pos ) {
|
||||
|
||||
convertedTracks.push( new VectorKeyframeTrack(
|
||||
'.bones[' + boneData.bone.name + '].position',
|
||||
boneData.pos.times,
|
||||
boneData.pos.values
|
||||
) );
|
||||
|
||||
}
|
||||
|
||||
convertedTracks.push( new QuaternionKeyframeTrack(
|
||||
'.bones[' + boneData.bone.name + '].quaternion',
|
||||
boneData.quat.times,
|
||||
boneData.quat.values
|
||||
) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mixer.uncacheAction( clip );
|
||||
|
||||
return new AnimationClip( clip.name, - 1, convertedTracks );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are
|
||||
* correctly associated with their bones. Bones are also cloned, and must be descendants of the
|
||||
* object passed to this method. Other data, like geometries and materials, are reused by reference.
|
||||
*
|
||||
* @param {Object3D} source - The 3D object to clone.
|
||||
* @return {Object3D} The cloned 3D object.
|
||||
*/
|
||||
function clone( source ) {
|
||||
|
||||
const sourceLookup = new Map();
|
||||
const cloneLookup = new Map();
|
||||
|
||||
const clone = source.clone();
|
||||
|
||||
parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
|
||||
|
||||
sourceLookup.set( clonedNode, sourceNode );
|
||||
cloneLookup.set( sourceNode, clonedNode );
|
||||
|
||||
} );
|
||||
|
||||
clone.traverse( function ( node ) {
|
||||
|
||||
if ( ! node.isSkinnedMesh ) return;
|
||||
|
||||
const clonedMesh = node;
|
||||
const sourceMesh = sourceLookup.get( node );
|
||||
const sourceBones = sourceMesh.skeleton.bones;
|
||||
|
||||
clonedMesh.skeleton = sourceMesh.skeleton.clone();
|
||||
clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
|
||||
|
||||
clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
|
||||
|
||||
return cloneLookup.get( bone );
|
||||
|
||||
} );
|
||||
|
||||
clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
|
||||
|
||||
} );
|
||||
|
||||
return clone;
|
||||
|
||||
}
|
||||
|
||||
// internal helper
|
||||
|
||||
function getBoneByName( name, skeleton ) {
|
||||
|
||||
for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) {
|
||||
|
||||
if ( name === bones[ i ].name )
|
||||
|
||||
return bones[ i ];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getBones( skeleton ) {
|
||||
|
||||
return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function getHelperFromSkeleton( skeleton ) {
|
||||
|
||||
const source = new SkeletonHelper( skeleton.bones[ 0 ] );
|
||||
source.skeleton = skeleton;
|
||||
|
||||
return source;
|
||||
|
||||
}
|
||||
|
||||
function parallelTraverse( a, b, callback ) {
|
||||
|
||||
callback( a, b );
|
||||
|
||||
for ( let i = 0; i < a.children.length; i ++ ) {
|
||||
|
||||
parallelTraverse( a.children[ i ], b.children[ i ], callback );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retarget options of `SkeletonUtils`.
|
||||
*
|
||||
* @typedef {Object} module:SkeletonUtils~RetargetOptions
|
||||
* @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not.
|
||||
* @property {number} [fps] - The FPS of the clip.
|
||||
* @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names.
|
||||
* @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`.
|
||||
* @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end.
|
||||
* @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not.
|
||||
* @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not.
|
||||
* @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not.
|
||||
* @property {string} [hip='hip'] - The name of the source's hip bone.
|
||||
* @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence.
|
||||
* @property {number} [scale=1] - The scale.
|
||||
**/
|
||||
|
||||
export {
|
||||
retarget,
|
||||
retargetClip,
|
||||
clone,
|
||||
};
|
||||
57362
web/vendor/three.core.js
vendored
Normal file
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
17990
web/vendor/three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user