Compare commits

...
Author SHA1 Message Date
ChrisBeWithYouandClaude Opus 4.8 fbc7ca2d9a feat(highway_3d): remove the strike line; sparks-only hit feedback, subtler
Second tester-charrette pass. The strike line (even hit-only/centred from the
last pass) was still too distracting/confusing on a hit, so it's removed
entirely — strings + fret markers already orient the player, and the hit is
fully carried at the gem (bright outline + scale-punch + spark burst) with the
timing-coloured ✓/✗ verdict as the knowledge-of-results channel.

- Deleted the strike-line mesh, its per-frame update, the `strikeLine` setting
  (BG_DEFAULTS / _BG_BOOL_KEYS / setter / settings-load), the settings.html
  toggle, and the now-dead `_strikeLine`/`_ndHitFlash`/`_ndMissFlash` state +
  their verdict-block feeds.
- Made the spark burst subtler now that it's the sole celebration: point size
  1.7→1.0·K, opacity 0.95→0.8, burst count (7+13·hitFx)→(4+7·hitFx), radial
  speed (7+r·20)→(5+r·12)·K, life (0.40+r·0.28)→(0.30+r·0.16)s.
- Toggles for Hit sparks and the ✓/✗ verdict marks already exist in settings
  (kept).

Minimal hit-feedback set now: gem bright + subtle spark (celebration) +
timing-coloured ✓/✗ (the KR) + ambient streak heat. plugin.json -> 3.30.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:51:18 -05:00
ChrisBeWithYouandClaude Opus 4.8 0596f7970c perf(highway): stop the adaptive renderScale from visibly hunting up/down
Alpha testers reported the 3D-highway "quality going up and down to try to
compensate" as passages got busier (#618 charrette). That's core's
load-adaptive render scale (_adaptRenderScale, #654) ping-ponging across the
7-12ms deadband: it downscales when a busy frame blows the budget, then the
now-cheaper frame dips under the low watermark so it upscales, which blows the
budget again — a visible resolution pop on a loop.

Fix: keep downscaling prompt (protect the frame rate), but make UPSCALING lazy
and predictive:
- smaller up-step (x1.06 vs x1.1) on a longer, separate cooldown
  (_AUTO_UPSCALE_COOLDOWN_MS = 2500ms vs the 600ms general adjust cooldown),
  reset on any downscale so we never bounce straight back up;
- a predictive guard: only upscale when the projected cost AFTER the step
  (~cost * step^2, since draw cost tracks pixel count) still clears the high
  budget. The scale settles just inside the deadband instead of oscillating.

No public API change; the user-facing "Min res" floor (_autoScaleMin) is
untouched. Pairs with the in-plugin AA-under-bloom fix in feedBack#618.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:51:22 -05:00
ChrisBeWithYouandClaude Opus 4.8 5111ef9440 feat(highway_3d): act on tester charrette — strike line, fog readability, AA
Addresses the alpha-tester 3D-highway feedback thread via the design panel's
recommendations:

- Strike line (panel rec 1a): now a HIT-ONLY faint "now" line — flashes green
  on a confirmed hit, no red miss branch (misses already show at the gem: red
  wash + ✗). Moved off the bottom edge to the vertical CENTRE of the string
  field, which was the "incorrectly placed" complaint (it read as the board's
  lower border and fused with open-string gems on a miss). Added a "Strike
  line" on/off toggle (`strikeLine`, default on).

- Horizon readability (#2): the note gems + their outlines are now fog-exempt
  (`material.fog = false` on mStr/mGlow/mStrHitOutline/mHitBright/mWhiteOutline/
  mMissOutline), so upcoming notes punch through the distance fog and stay
  legible as they render in — the board, lane, sustains and scenery keep their
  atmospheric fog, so depth is preserved.

- Cinematic lighting softened: cinematic ambient 0.35 -> 0.45 so the dark stage
  doesn't crush note/fret legibility.

- Anti-aliasing under bloom (perf rec): give the bloom EffectComposer a
  multisampled (WebGL2 MSAA x4) HalfFloat render target. The default target had
  no `samples`, so bloom-on bypassed MSAA — the "too HD / jagged on Windows,
  fine on Mac" report (Mac only won via Retina supersampling). This is the
  highest-value, smallest fix for the jaggies.

plugin.json -> 3.29.0. The renderScale quality-oscillation is core
(static/highway.js) and will be a separate feedBack PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:03:01 -05:00
ChrisBeWithYouandClaude Opus 4.8 b04d7fa00d feat(highway_3d): add a "Hit sparks" on/off toggle (note-hit particles)
The on-hit spark burst (the particle effect that fires the instant
note_detect confirms a hit) could previously only be removed by dragging
Hit-feedback intensity to 0 — which also kills the strike-line flash and
the scale-punch. Add a dedicated "Hit sparks" toggle (default on) under
3D Highway settings, in the hit-feedback group beside the intensity
slider, that gates ONLY the spark particles; the strike flash and colour
verdict are unaffected.

Wired the same way as the sibling juice toggles: a `sparks` boolean in
BG_DEFAULTS, in _BG_BOOL_KEYS, a window.h3dBgSetSparks setter, the
per-instance _sparks state + settings re-read, and a guard on the
_sparkBurst spawn. Reuses existing Tailwind utility classes, so
assets/plugin.css is unchanged; plugin.json version bumped to 3.28.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:15:54 -05:00
ChrisBeWithYouandClaude Opus 4.8 ccbb2c476d feat(highway_3d): #5 early/late timing — colour the hit feedback by timing
Surfaces the detector's timing on every hit (the charrette's last item), fully
highway-side: notedetect already dispatches the judgment (timingState/timingError)
on notedetect:hit/miss, so we carry timingState onto the event mark and tint the
hit's spark burst + the ✓ verdict glyph by it — on-time green, early cyan, late
amber. Gracefully falls back to green when no timing is known (pure-provider path),
so it never invents data. Behind the new "Timing feedback" toggle (default on).

Charrette: 8/8 complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq
2026-06-27 17:11:19 -05:00
ChrisBeWithYouandClaude Opus 4.8 e0e6213b1e feat(highway_3d): #4 bloom + ACES — vendored Three.js postprocessing, perf-gated
The single biggest fidelity lever from the charrette. Core had only
three.module.min.js (no postprocessing addons), so this vendors the r170
EffectComposer/RenderPass/UnrealBloomPass/OutputPass + their shader deps into
static/vendor/three/addons/, with every `from 'three'` rewritten to the SAME
vendored three (../../three.module.min.js) so the addons share the plugin's
three instance (a CDN copy would be a second, non-interoperable module).

highway_3d wiring:
- Lazy-loads the addons only when the new `bloom` setting is on (dynamic import),
  builds EffectComposer(RenderPass → UnrealBloomPass(strength .65/radius .5/
  threshold .82 — high so only emissive gems + the hit flash bloom) → OutputPass).
- Render loop uses composer.render() with ACES tone-mapping when bloom is active,
  else the unchanged direct ren.render() with NoToneMapping (bloom-off = today's look).
- Perf-gated: OFF in splitscreen; graceful fallback to direct render if the modules
  or composer fail; composer.setSize on canvas resize; disposed on teardown.
- settings.html: "Glow bloom" toggle (default on).

Verified the import chain resolves + renders via a same-origin module-load test
(EffectComposer built + a bloom frame rendered, three r170).

Charrette status: 7/8 (only #5's early/late timing tint remains — a notedetect
verdict-field change, outside the highway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq
2026-06-27 17:11:19 -05:00
ChrisBeWithYouandClaude Opus 4.8 d92db7ad44 feat(highway_3d): wave 2 — gem scale-punch, streak heat, colorblind verdict marks
- #3 (completion) gem scale-punch: the hit gem briefly grows (1 + 0.22·hitFx·alpha),
  biggest at the strike and easing with the verdict — the per-gem impulse.
- #7 streak heat: a renderer-side consecutive-hit counter eases a 0..1 "heat"
  (plateau at 16) that grows the spark burst + warms the strike-line idle glow;
  a miss eases it back down. Behind the Streak-feedback toggle.
- #6 colorblind verdict marks: a redundant ✓ (hit) / ✗ (miss) glyph on the verdict
  via the existing 2D label overlay, so the green/red pair isn't the only signal —
  notably also covers the provider path (where the timing labels don't show).
- settings.html: Streak-feedback + Accessible-marks toggles.

Deferred: #4 bloom+ACES (needs the Three.js postprocessing addons vendored into
core static/vendor/three/ — not present; warrants its own infra change), and #5's
timing tint (the early/late ±ms labels already render on the event path; surfacing
them on the provider path needs a notedetect verdict field — a cross-plugin item).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq
2026-06-27 17:07:02 -05:00
ChrisBeWithYouandClaude Opus 4.8 88e822dafb feat(highway_3d): hit-feedback juice — cinematic lighting, strike line, sparks, intensity dial
Charrette wave 1 (additive, default-tasteful, all behind settings):
- #8 Hit-feedback settings: hitFx (0..1), cinematic, verdictMarks, timingFx,
  streakFx in BG_DEFAULTS + h3dBgSet* setters + settings.html (intensity slider +
  cinematic toggle). hitFx=0 → colour verdict only.
- #2 Cinematic lighting: ambient 0.85→0.35 + stronger key light when cinematic on,
  so emissive gems have a dark surround to pop against. Live-toggleable.
- #1 Strike line: a glowing bar at the hit line (Z=0) that flashes green on a
  verified hit / red on a miss, eased from the per-frame verdict alpha.
- #3 Hit sparks: a pooled additive Points burst at the gem on a verified hit
  (deduped one burst per note), scaled by hitFx; disposed on teardown.

Staged for wave 2 (after dogfooding): bloom+ACES (#4), colorblind verdict glyphs
(#6), early/late timing tint (#5), streak heat + clean-bar (#7), gem scale-punch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq
2026-06-27 17:07:02 -05:00
15 changed files with 1580 additions and 8 deletions
+1
View File
@@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **3D highway: section + tone HUD cards now default OFF.** The v0.3.0 player chrome carries a persistent "Up Next" pill, making the in-canvas section card redundant by default (it doubled the readout, feedBack feedback); the tone HUD follows the same less-is-more default. Both remain available in Settings → 3D Highway (visibility/position/size unchanged); users who previously toggled either explicitly keep their stored preference — only the untouched default flips. `plugins/highway_3d` v3.24.1. - **3D highway: section + tone HUD cards now default OFF.** The v0.3.0 player chrome carries a persistent "Up Next" pill, making the in-canvas section card redundant by default (it doubled the readout, feedBack feedback); the tone HUD follows the same less-is-more default. Both remain available in Settings → 3D Highway (visibility/position/size unchanged); users who previously toggled either explicitly keep their stored preference — only the untouched default flips. `plugins/highway_3d` v3.24.1.
- **Perf**: replace runtime Tailwind Play CDN with a prebuilt static stylesheet (`static/tailwind.min.css`). The Play CDN's runtime JIT scanned the DOM ~1.8x/sec on the main thread (~37 ms blocking spans), dropping ~26% of frames in long playback sessions with the 3D highway as default. Theme extensions (dark/accent/gold colors, Inter font) move to `tailwind.config.js`; regen via `bash scripts/build-tailwind.sh`. No runtime build step — the generated CSS is committed. Fixes feedBack-desktop#110. - **Perf**: replace runtime Tailwind Play CDN with a prebuilt static stylesheet (`static/tailwind.min.css`). The Play CDN's runtime JIT scanned the DOM ~1.8x/sec on the main thread (~37 ms blocking spans), dropping ~26% of frames in long playback sessions with the 3D highway as default. Theme extensions (dark/accent/gold colors, Inter font) move to `tailwind.config.js`; regen via `bash scripts/build-tailwind.sh`. No runtime build step — the generated CSS is committed. Fixes feedBack-desktop#110.
- **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free. - **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free.
- **Perf**: the load-adaptive render scale (`_adaptRenderScale`, #654) no longer visibly hunts up/down on passages that hover near the frame budget (testers saw "quality going up and down" with the 3D highway). Downscaling stays prompt to protect the frame rate, but **upscaling is now lazy**: a smaller step (×1.06 vs ×1.1) on a longer cooldown (`_AUTO_UPSCALE_COOLDOWN_MS` 2500 ms vs the 600 ms adjust cooldown), reset on any downscale, and gated by a predictive guard — it only upscales when the projected cost *after* the step (≈ cost × step², since draw cost tracks the pixel count) still clears the high budget. The scale therefore settles just inside the 712 ms deadband instead of oscillating across it. No new public API; the `_autoScaleMin` "Min res" floor is unchanged.
### Removed ### Removed
- **`c` library hotkey ("Convert to .sloppak") removed from core.** Core hardcoded a plugin-specific shortcut: a documentation-only `registerShortcut({ key: 'c', scope: 'library' })` no-op plus a `c → button.sloppak-convert-btn` entry in the library keydown handler that fired the Sloppak Converter plugin's button. Per the plugins-own-their-behavior principle, core no longer ships this hotkey — the convert button still works by click, and the Sloppak Converter plugin can register its own `c` shortcut via `window.registerShortcut()` if keyboard access is wanted. The `f` (favorite) and `e` (edit) library hotkeys, which drive core buttons, are unchanged. Help-modal/registry tests in `tests/browser/keyboard-shortcuts.spec.ts` updated to drop the `c` assertions. - **`c` library hotkey ("Convert to .sloppak") removed from core.** Core hardcoded a plugin-specific shortcut: a documentation-only `registerShortcut({ key: 'c', scope: 'library' })` no-op plus a `c → button.sloppak-convert-btn` entry in the library keydown handler that fired the Sloppak Converter plugin's button. Per the plugins-own-their-behavior principle, core no longer ships this hotkey — the convert button still works by click, and the Sloppak Converter plugin can register its own `c` shortcut via `window.registerShortcut()` if keyboard access is wanted. The `f` (favorite) and `e` (edit) library hotkeys, which drive core buttons, are unchanged. Help-modal/registry tests in `tests/browser/keyboard-shortcuts.spec.ts` updated to drop the `c` assertions.
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "highway_3d", "id": "highway_3d",
"name": "3D Highway", "name": "3D Highway",
"version": "3.27.0", "version": "3.30.0",
"type": "visualization", "type": "visualization",
"bundled": true, "bundled": true,
"script": "screen.js", "script": "screen.js",
+171 -5
View File
@@ -1768,7 +1768,7 @@
return _bgBandsCache; return _bgBandsCache;
} }
const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true }; const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true, hitFx: 0.7, sparks: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true };
// User-selectable, persistable bg styles — must mirror settings.html's // User-selectable, persistable bg styles — must mirror settings.html's
// VALID_STYLES. 'venue' is deliberately NOT here: it is an internal effective // VALID_STYLES. 'venue' is deliberately NOT here: it is an internal effective
// style reached only via _venueSceneOverride (the viz-picker Venue flow), so // style reached only via _venueSceneOverride (the viz-picker Venue flow), so
@@ -2115,7 +2115,7 @@
// means (fall back to default rather than silently flipping to // means (fall back to default rather than silently flipping to
// false). Add new boolean keys to BG_DEFAULTS and they pick this // false). Add new boolean keys to BG_DEFAULTS and they pick this
// up via the dispatch below. // up via the dispatch below.
const _BG_BOOL_KEYS = new Set(['reactive', 'showFretOnNote', 'cameraLockLow', 'inlayLabelsVisible', 'sectionLabelsOnHighway', 'sectionHudVisible', 'nutHeadstockVisible', 'tuningLabelsVisible', 'projectionVisible', 'chordDiagramVisible', 'fpsVisible', 'toneHudVisible', 'fretDividersVisible', 'slideArrowApproachVisible', 'slideArrowNeckVisible', 'slideArrowChainPreviewVisible']); const _BG_BOOL_KEYS = new Set(['reactive', 'showFretOnNote', 'cameraLockLow', 'inlayLabelsVisible', 'sectionLabelsOnHighway', 'sectionHudVisible', 'nutHeadstockVisible', 'tuningLabelsVisible', 'projectionVisible', 'chordDiagramVisible', 'fpsVisible', 'toneHudVisible', 'fretDividersVisible', 'slideArrowApproachVisible', 'slideArrowNeckVisible', 'slideArrowChainPreviewVisible', 'sparks', 'cinematic', 'verdictMarks', 'timingFx', 'streakFx', 'bloom']);
function _bgCoerceBool(val, fallback) { function _bgCoerceBool(val, fallback) {
if (val === 'true' || val === '1') return true; if (val === 'true' || val === '1') return true;
if (val === 'false' || val === '0') return false; if (val === 'false' || val === '0') return false;
@@ -2125,7 +2125,7 @@
// hysteresis; zoomSmoothing the zoom dead zone; tiltSmoothing the // hysteresis; zoomSmoothing the zoom dead zone; tiltSmoothing the
// vertical-tilt deadband + correction strength. All three slider- // vertical-tilt deadband + correction strength. All three slider-
// shaped settings share the same parse + clamp behaviour. // shaped settings share the same parse + clamp behaviour.
const _BG_FLOAT_KEYS = new Set(['intensity', 'cameraSmoothing', 'zoomSmoothing', 'tiltSmoothing', 'cameraLockZoom', 'textSize', 'vibrancy', 'glow', 'chordDiagramSize', 'sectionHudSize', 'toneHudSize']); const _BG_FLOAT_KEYS = new Set(['intensity', 'cameraSmoothing', 'zoomSmoothing', 'tiltSmoothing', 'cameraLockZoom', 'textSize', 'vibrancy', 'glow', 'chordDiagramSize', 'sectionHudSize', 'toneHudSize', 'hitFx']);
function _bgCoerce(key, val) { function _bgCoerce(key, val) {
if (_BG_FLOAT_KEYS.has(key)) { if (_BG_FLOAT_KEYS.has(key)) {
const n = parseFloat(val); const n = parseFloat(val);
@@ -2259,6 +2259,13 @@
window.h3dBgSetTextSize = (v) => _bgWriteGlobal('textSize', v); window.h3dBgSetTextSize = (v) => _bgWriteGlobal('textSize', v);
window.h3dBgSetVibrancy = (v) => _bgWriteGlobal('vibrancy', v); window.h3dBgSetVibrancy = (v) => _bgWriteGlobal('vibrancy', v);
window.h3dBgSetGlow = (v) => _bgWriteGlobal('glow', v); window.h3dBgSetGlow = (v) => _bgWriteGlobal('glow', v);
window.h3dBgSetHitFx = (v) => _bgWriteGlobal('hitFx', v);
window.h3dBgSetSparks = (v) => _bgWriteGlobal('sparks', !!v);
window.h3dBgSetCinematic = (v) => _bgWriteGlobal('cinematic', !!v);
window.h3dBgSetVerdictMarks = (v) => _bgWriteGlobal('verdictMarks', !!v);
window.h3dBgSetTimingFx = (v) => _bgWriteGlobal('timingFx', !!v);
window.h3dBgSetStreakFx = (v) => _bgWriteGlobal('streakFx', !!v);
window.h3dBgSetBloom = (v) => _bgWriteGlobal('bloom', !!v);
window.h3dBgSetToneHudVisible = (v) => _bgWriteGlobal('toneHudVisible', !!v); window.h3dBgSetToneHudVisible = (v) => _bgWriteGlobal('toneHudVisible', !!v);
window.h3dBgSetToneHudPosition = (v) => _bgWriteGlobal('toneHudPosition', v); window.h3dBgSetToneHudPosition = (v) => _bgWriteGlobal('toneHudPosition', v);
window.h3dBgSetToneHudSize = (v) => _bgWriteGlobal('toneHudSize', v); window.h3dBgSetToneHudSize = (v) => _bgWriteGlobal('toneHudSize', v);
@@ -3552,6 +3559,19 @@
// linear blend every frame. // linear blend every frame.
let vibrancy = BG_DEFAULTS.vibrancy; let vibrancy = BG_DEFAULTS.vibrancy;
let glowMul = BG_DEFAULTS.glow; let glowMul = BG_DEFAULTS.glow;
let _hitFx = BG_DEFAULTS.hitFx;
let _sparks = BG_DEFAULTS.sparks;
let _cinematic = BG_DEFAULTS.cinematic;
let _verdictMarks = BG_DEFAULTS.verdictMarks;
let _timingFx = BG_DEFAULTS.timingFx;
let _streakFx = BG_DEFAULTS.streakFx;
let _bloom = BG_DEFAULTS.bloom;
let _composer = null, _bloomPass = null, _bloomLoad = null, _bloomW = 0, _bloomH = 0;
let _sparkPts = null, _sparkPos = null, _sparkCol = null, _sparkVel = null, _sparkLife = null;
const _SPARK_N = 256;
const _sparkSeen = new Map(); // note-key -> expiry; one burst per hit
let _juiceLastT = 0; // frame-dt clock for the juice layer
let _streakHits = 0, _streakHeat = 0; // #7 consecutive-hit escalation
let fpsVisible = BG_DEFAULTS.fpsVisible; let fpsVisible = BG_DEFAULTS.fpsVisible;
let fretDividersVisible = BG_DEFAULTS.fretDividersVisible; let fretDividersVisible = BG_DEFAULTS.fretDividersVisible;
let chordDiagramVisible = BG_DEFAULTS.chordDiagramVisible; let chordDiagramVisible = BG_DEFAULTS.chordDiagramVisible;
@@ -5912,10 +5932,23 @@
dirLight = new T.DirectionalLight(0xffffff, 0.8); dirLight = new T.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(40 * K, 120 * K, 80 * K); dirLight.position.set(40 * K, 120 * K, 80 * K);
scene.add(dirLight); scene.add(dirLight);
_applyCinematic();
fretG = new T.Group(); scene.add(fretG); fretG = new T.Group(); scene.add(fretG);
tuningLblG = new T.Group(); scene.add(tuningLblG); tuningLblG = new T.Group(); scene.add(tuningLblG);
noteG = new T.Group(); scene.add(noteG); noteG = new T.Group(); scene.add(noteG);
// Hit sparks (#3): a pooled additive Points cloud; a small burst fires at a
// gem on a verified hit (spawned in the verdict block, advanced in the render loop).
_sparkPos = new Float32Array(_SPARK_N * 3); _sparkCol = new Float32Array(_SPARK_N * 3);
_sparkVel = new Float32Array(_SPARK_N * 3); _sparkLife = new Float32Array(_SPARK_N);
{
const sg = new T.BufferGeometry();
sg.setAttribute('position', new T.BufferAttribute(_sparkPos, 3).setUsage(T.DynamicDrawUsage));
sg.setAttribute('color', new T.BufferAttribute(_sparkCol, 3).setUsage(T.DynamicDrawUsage));
const sm = new T.PointsMaterial({ size: 1.0 * K, vertexColors: true, transparent: true, opacity: 0.8, depthWrite: false, blending: T.AdditiveBlending, sizeAttenuation: true });
_sparkPts = new T.Points(sg, sm); _sparkPts.frustumCulled = false; _sparkPts.renderOrder = 8;
scene.add(_sparkPts);
}
beatG = new T.Group(); scene.add(beatG); beatG = new T.Group(); scene.add(beatG);
lblG = new T.Group(); scene.add(lblG); lblG = new T.Group(); scene.add(lblG);
@@ -6134,6 +6167,13 @@
transparent: true, opacity: 1.0, depthWrite: false, transparent: true, opacity: 1.0, depthWrite: false,
})); }));
mHitBrightArrays = mHitBright.map(m => [m, m, m, m, mEdgeTransparent, mEdgeTransparent]); mHitBrightArrays = mHitBright.map(m => [m, m, m, m, mEdgeTransparent, mEdgeTransparent]);
// Readability (#2 / charrette): the note gems + their outlines punch THROUGH
// the distance fog so upcoming notes stay legible as they render in at the
// horizon. The board, lane, sustains and background scenery keep their
// atmospheric fog — only the note-defining materials are exempted, so the
// highway still reads as deep while the notes never dissolve into the haze.
[mWhiteOutline, mMissOutline].forEach(m => { if (m) m.fog = false; });
[mStr, mGlow, mStrHitOutline, mHitBright].forEach(arr => arr && arr.forEach(m => { if (m) m.fog = false; }));
// Outline materials render at a lower renderOrder than the body. // Outline materials render at a lower renderOrder than the body.
// The body is rendered on top with opacity:1 on hit/miss, which // The body is rendered on top with opacity:1 on hit/miss, which
// fully covers the outline center — only the fringe that extends // fully covers the outline center — only the fringe that extends
@@ -7191,7 +7231,7 @@
color: '#66c7ff', color: '#66c7ff',
}); });
} }
return { s: note.s, f: note.f, noteTime: d.noteTime, labels }; return { s: note.s, f: note.f, noteTime: d.noteTime, labels, timingState: d.timingState || null };
}; };
const _ndPushMark = (arr, d) => { const _ndPushMark = (arr, d) => {
const mark = _ndNormalizeMark(d); const mark = _ndNormalizeMark(d);
@@ -7347,6 +7387,14 @@
textSize = _bgReadSetting(panelKey, 'textSize'); textSize = _bgReadSetting(panelKey, 'textSize');
vibrancy = _bgReadSetting(panelKey, 'vibrancy'); vibrancy = _bgReadSetting(panelKey, 'vibrancy');
glowMul = _bgReadSetting(panelKey, 'glow'); glowMul = _bgReadSetting(panelKey, 'glow');
_hitFx = _bgReadSetting(panelKey, 'hitFx');
_sparks = _bgReadSetting(panelKey, 'sparks');
_cinematic = _bgReadSetting(panelKey, 'cinematic');
_verdictMarks = _bgReadSetting(panelKey, 'verdictMarks');
_timingFx = _bgReadSetting(panelKey, 'timingFx');
_streakFx = _bgReadSetting(panelKey, 'streakFx');
_bloom = _bgReadSetting(panelKey, 'bloom');
_applyCinematic();
fpsVisible = _bgReadSetting(panelKey, 'fpsVisible'); fpsVisible = _bgReadSetting(panelKey, 'fpsVisible');
fretDividersVisible = _bgReadSetting(panelKey, 'fretDividersVisible'); fretDividersVisible = _bgReadSetting(panelKey, 'fretDividersVisible');
chordDiagramVisible = _bgReadSetting(panelKey, 'chordDiagramVisible'); chordDiagramVisible = _bgReadSetting(panelKey, 'chordDiagramVisible');
@@ -7785,6 +7833,85 @@
: d; : d;
return parseInt(s.slice(1), 16); return parseInt(s.slice(1), 16);
} }
// Cinematic lighting (#2): darken ambient so emissive gems have a dark
// surround to pop against; strengthen the key light for modelling.
// Toggle via the 'cinematic' setting so it's directly comparable.
function _applyCinematic() {
if (!ambLight || !dirLight) return;
ambLight.intensity = _cinematic ? 0.45 : 0.85;
dirLight.intensity = _cinematic ? 1.15 : 0.8;
}
// #5 early/late: tint the hit feedback by timing — on-time green, early cyan,
// late amber. Falls back to green when timing is unknown (pure-provider path).
function _timingHex(ts) {
if (!_timingFx || !ts || ts === 'OK') return 0x22ff88;
if (ts === 'EARLY') return 0x35d6ff;
if (ts === 'LATE') return 0xffb84d;
return 0x22ff88;
}
function _sparkBurst(x, y, z, hex, count) {
if (!_sparkPts || count <= 0) return;
const r = ((hex >> 16) & 255) / 255, g = ((hex >> 8) & 255) / 255, b = (hex & 255) / 255;
let made = 0;
for (let i = 0; i < _SPARK_N && made < count; i++) {
if (_sparkLife[i] > 0) continue;
const j = i * 3, ang = Math.random() * Math.PI * 2, sp = (5 + Math.random() * 12) * K;
_sparkPos[j] = x; _sparkPos[j + 1] = y; _sparkPos[j + 2] = z;
_sparkVel[j] = Math.cos(ang) * sp; _sparkVel[j + 1] = (12 + Math.random() * 24) * K; _sparkVel[j + 2] = Math.sin(ang) * sp * 0.55;
_sparkCol[j] = r; _sparkCol[j + 1] = g; _sparkCol[j + 2] = b;
_sparkLife[i] = 0.30 + Math.random() * 0.16; made++;
}
}
function _sparkUpdate(dt) {
if (!_sparkPts) return;
const grav = 55 * K; let any = false;
for (let i = 0; i < _SPARK_N; i++) {
if (_sparkLife[i] <= 0) continue;
const j = i * 3;
_sparkLife[i] -= dt;
if (_sparkLife[i] <= 0) { _sparkCol[j] = _sparkCol[j + 1] = _sparkCol[j + 2] = 0; continue; }
any = true;
_sparkVel[j + 1] -= grav * dt;
_sparkPos[j] += _sparkVel[j] * dt; _sparkPos[j + 1] += _sparkVel[j + 1] * dt; _sparkPos[j + 2] += _sparkVel[j + 2] * dt;
const fade = 1 - Math.min(1, dt * 3.2);
_sparkCol[j] *= fade; _sparkCol[j + 1] *= fade; _sparkCol[j + 2] *= fade;
}
_sparkPts.geometry.attributes.position.needsUpdate = true;
_sparkPts.geometry.attributes.color.needsUpdate = true;
_sparkPts.visible = any;
}
// #4 Bloom: lazy-load the vendored postprocessing addons and build an
// EffectComposer (RenderPass -> UnrealBloomPass -> OutputPass/ACES). Returns
// the composer once ready, or null (caller falls back to a direct render).
function _bloomEnsure() {
if (_composer) return _composer;
if (_bloomLoad || !ren || !scene || !cam) return null;
const A = '/static/vendor/three/addons/';
_bloomLoad = Promise.all([
import(A + 'postprocessing/EffectComposer.js'),
import(A + 'postprocessing/RenderPass.js'),
import(A + 'postprocessing/UnrealBloomPass.js'),
import(A + 'postprocessing/OutputPass.js'),
]).then(([EC, RP, UB, OP]) => {
try {
const sz = canvasSize(highwayCanvas) || { w: 1280, h: 720 };
const w = Math.max(2, sz.w | 0), h = Math.max(2, sz.h | 0);
// Multisampled (WebGL2 MSAA) HalfFloat target so anti-aliasing
// survives the bloom path — EffectComposer's default target has no
// `samples`, which is why bloom-on looked jagged (worst on non-Retina
// DPR1 displays that have no supersampling cushion).
const _bloomRT = new T.WebGLRenderTarget(w, h, { type: T.HalfFloatType, samples: 4 });
const comp = new EC.EffectComposer(ren, _bloomRT);
comp.addPass(new RP.RenderPass(scene, cam));
_bloomPass = new UB.UnrealBloomPass(new T.Vector2(w, h), 0.65, 0.5, 0.82); // strength, radius, threshold (high → only emissive blooms)
comp.addPass(_bloomPass);
comp.addPass(new OP.OutputPass());
comp.setSize(w, h);
_bloomW = w; _bloomH = h; _composer = comp;
} catch (e) { console.warn('[3D-Hwy] bloom init failed', e); _composer = null; }
}).catch((e) => console.warn('[3D-Hwy] bloom modules failed', e));
return null;
}
function buildBoard() { function buildBoard() {
// Dispose before clearing (traverse: nut/headstock may live in a Group). // Dispose before clearing (traverse: nut/headstock may live in a Group).
while (fretG.children.length) { while (fretG.children.length) {
@@ -12585,6 +12712,7 @@
// blocks, so _showHit can be a const and _ndGood is available for the // blocks, so _showHit can be a const and _ndGood is available for the
// sustain trail (which renders even when skipBody=true for slide targets). // sustain trail (which renders even when skipBody=true for slide targets).
let _ndGood = false; // true when provider confirms hit/active let _ndGood = false; // true when provider confirms hit/active
let _hitPunch = 1; // #3 per-gem scale-punch on a fresh hit
let _ndState = null; // 'hit'|'active'|'miss'|null; null → fall back to proximity heuristic let _ndState = null; // 'hit'|'active'|'miss'|null; null → fall back to proximity heuristic
let _ndCs = null; // raw provider response — truthy when provider returned a verdict let _ndCs = null; // raw provider response — truthy when provider returned a verdict
let _ndCsIsObj = false; // typeof _ndCs === 'object' let _ndCsIsObj = false; // typeof _ndCs === 'object'
@@ -12760,12 +12888,27 @@
// hit/active → green outline (mHitBright[s]) + green lateral faces; // hit/active → green outline (mHitBright[s]) + green lateral faces;
// miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent. // miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent.
if (_ndCs) { if (_ndCs) {
const _vAlpha = (_ndCsIsObj && typeof _ndCs.alpha === 'number') ? _ndCs.alpha : 1;
if (_ndState === 'miss') { if (_ndState === 'miss') {
_ndOutline = mMissOutline; _ndOutline = mMissOutline;
_ndFaceMat = mMissEdgeArrays; _ndFaceMat = mMissEdgeArrays;
_streakHits = 0; // #7 break the streak (heat eases down)
if (_verdictMarks) _ndLabels.push({ x, y: y + NH * 1.7, z: noteZ + 0.02, labels: [{ text: '✗', color: '#ff5a7a' }] }); // #6
} else if (_ndGood) { } else if (_ndGood) {
_ndOutline = mHitBright[s] ?? mGlow[s]; _ndOutline = mHitBright[s] ?? mGlow[s];
_ndFaceMat = mHitBrightArrays[s] ?? null; _ndFaceMat = mHitBrightArrays[s] ?? null;
_hitPunch = 1 + 0.22 * _hitFx * _vAlpha; // #3 scale-punch (biggest at strike, eases)
if (_verdictMarks) { const _tc = _timingHex(_ndMatchedMark && _ndMatchedMark.timingState); _ndLabels.push({ x, y: y + NH * 1.7, z: noteZ + 0.02, labels: [{ text: '✓', color: '#' + _tc.toString(16).padStart(6, '0') }] }); } // #6 + #5
if (_sparks && _hitFx > 0 && _vAlpha > 0.5) {
const _spk = s + '|' + n.f + '|' + n.t.toFixed(2);
if (!(_sparkSeen.get(_spk) > now)) {
_sparkSeen.set(_spk, now + 1.0);
if (_sparkSeen.size > 600) _sparkSeen.clear();
_streakHits++;
const _heatMul = _streakFx ? (1 + 0.85 * _streakHeat) : 1; // #7 escalate
_sparkBurst(x, y, noteZ, _timingHex(_ndMatchedMark && _ndMatchedMark.timingState), Math.round((4 + 7 * _hitFx) * _heatMul));
}
}
} }
} }
@@ -12878,6 +13021,7 @@
} else { } else {
core.scale.set(rimXY, rimXY, 2.5 * rimZ); core.scale.set(rimXY, rimXY, 2.5 * rimZ);
} }
if (_hitPunch !== 1) core.scale.multiplyScalar(_hitPunch); // #3 hit scale-punch
// Fret digits on fretted (n.f > 0) flying notes deliberately // Fret digits on fretted (n.f > 0) flying notes deliberately
// omitted: the showFretOnNote setting and its UI helper text // omitted: the showFretOnNote setting and its UI helper text
// promise digits on the fretboard ghost only, never on the // promise digits on the fretboard ghost only, never on the
@@ -14012,6 +14156,8 @@
for (const g of _ownedSharedGeos) g?.dispose?.(); for (const g of _ownedSharedGeos) g?.dispose?.();
_ownedSharedGeos.length = 0; _ownedSharedGeos.length = 0;
txtCache = {}; txtCache = {};
if (_sparkPts) { try { _sparkPts.geometry.dispose(); _sparkPts.material.dispose(); } catch (e) {} _sparkPts = null; }
if (_composer) { try { _composer.dispose(); if (_bloomPass && _bloomPass.dispose) _bloomPass.dispose(); } catch (e) {} _composer = null; _bloomPass = null; }
if (ren) { ren.dispose(); ren = null; } if (ren) { ren.dispose(); ren = null; }
scene = cam = noteG = beatG = lblG = fretG = tuningLblG = null; scene = cam = noteG = beatG = lblG = fretG = tuningLblG = null;
ambLight = dirLight = null; ambLight = dirLight = null;
@@ -14337,7 +14483,27 @@
} }
bcCtrl.render(); bcCtrl.render();
} }
pbBeg(6); ren.render(scene, cam); pbEnd(6); {
const _jNow = performance.now();
const _jdt = _juiceLastT === 0 ? 1 / 60 : Math.min(0.05, (_jNow - _juiceLastT) / 1000);
_juiceLastT = _jNow;
_sparkUpdate(_jdt);
_streakHeat += (Math.min(1, _streakHits / 16) - _streakHeat) * 0.08; // #7 ease heat
}
{
const comp = (_bloom && !_ssActive()) ? _bloomEnsure() : null;
if (comp) {
const bsz = canvasSize(highwayCanvas);
if (bsz && bsz.w > 0 && bsz.h > 0 && (bsz.w !== _bloomW || bsz.h !== _bloomH)) {
comp.setSize(bsz.w | 0, bsz.h | 0); _bloomW = bsz.w | 0; _bloomH = bsz.h | 0;
}
if (ren.toneMapping !== T.ACESFilmicToneMapping) ren.toneMapping = T.ACESFilmicToneMapping;
pbBeg(6); comp.render(); pbEnd(6);
} else {
if (ren.toneMapping !== T.NoToneMapping) ren.toneMapping = T.NoToneMapping;
pbBeg(6); ren.render(scene, cam); pbEnd(6);
}
}
if (lyricsCtx && lyricsCanvas) { if (lyricsCtx && lyricsCanvas) {
lyricsCtx.clearRect(0, 0, lyricsCanvas.width, lyricsCanvas.height); lyricsCtx.clearRect(0, 0, lyricsCanvas.width, lyricsCanvas.height);
// Capture the actual lyrics-banner bottom so overlay cards // Capture the actual lyrics-banner bottom so overlay cards
+74
View File
@@ -577,6 +577,80 @@
<span>Glowy</span> <span>Glowy</span>
</div> </div>
</div> </div>
<div class="mt-4">
<label for="h3d-hitfx" class="text-xs font-medium text-gray-400 mb-1 block">
Hit feedback intensity: <span id="h3d-hitfx-label">0.70</span>
</label>
<input type="range" id="h3d-hitfx" min="0" max="1" step="0.05" value="0.70"
oninput="document.getElementById('h3d-hitfx-label').textContent = parseFloat(this.value).toFixed(2); window.h3dBgSetHitFx && window.h3dBgSetHitFx(this.value)"
onchange="window.h3dBgSetHitFx && window.h3dBgSetHitFx(this.value)"
class="w-full accent-accent">
<p class="text-[10px] text-gray-500 mt-1">
How much "juice" a nailed note gets — the strike-line flash and the
spark burst at the hit line. <em>0</em> = colour verdict only (no sparks).
</p>
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-sparks" class="text-xs font-medium text-gray-400">
Hit sparks
<span class="block text-[10px] text-gray-500 font-normal">The particle burst that pops off a note the instant it's detected as a hit. Turn off for a calmer highway — the strike-line flash and colour verdict stay.</span>
</label>
<input type="checkbox" id="h3d-sparks" checked
onchange="window.h3dBgSetSparks && window.h3dBgSetSparks(this.checked)"
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-cinematic" class="text-xs font-medium text-gray-400">
Cinematic lighting
<span class="block text-[10px] text-gray-500 font-normal">Darker stage so the glowing notes pop against it. Turn off for the brighter, flatter look.</span>
</label>
<input type="checkbox" id="h3d-cinematic" checked
onchange="window.h3dBgSetCinematic && window.h3dBgSetCinematic(this.checked)"
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-streakfx" class="text-xs font-medium text-gray-400">
Streak feedback
<span class="block text-[10px] text-gray-500 font-normal">A clean run quietly "heats up" — bigger sparks the longer you stay accurate. Eases back on a miss.</span>
</label>
<input type="checkbox" id="h3d-streakfx" checked
onchange="window.h3dBgSetStreakFx && window.h3dBgSetStreakFx(this.checked)"
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-verdictmarks" class="text-xs font-medium text-gray-400">
Accessible verdict marks (✓ / ✗)
<span class="block text-[10px] text-gray-500 font-normal">Adds a shape mark to each hit/miss so the result doesn't rely on the green/red colour pair alone.</span>
</label>
<input type="checkbox" id="h3d-verdictmarks" checked
onchange="window.h3dBgSetVerdictMarks && window.h3dBgSetVerdictMarks(this.checked)"
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-bloom" class="text-xs font-medium text-gray-400">
Glow bloom
<span class="block text-[10px] text-gray-500 font-normal">Real light-bleed around the glowing notes and hit flash (higher fidelity). Turns itself off in split-screen. If your machine struggles, turn this off first.</span>
</label>
<input type="checkbox" id="h3d-bloom" checked
onchange="window.h3dBgSetBloom && window.h3dBgSetBloom(this.checked)"
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-timingfx" class="text-xs font-medium text-gray-400">
Timing feedback
<span class="block text-[10px] text-gray-500 font-normal">Colours a hit by your timing — on-time green, a touch <span style="color:#35d6ff">early (cyan)</span> or <span style="color:#ffb84d">late (amber)</span> — so you can feel where you sit in the beat.</span>
</label>
<input type="checkbox" id="h3d-timingfx" checked
onchange="window.h3dBgSetTimingFx && window.h3dBgSetTimingFx(this.checked)"
class="accent-accent mt-0.5">
</div>
</div> </div>
</details> </details>
</div> </div>
+21 -2
View File
@@ -248,6 +248,7 @@ function createHighway() {
let _frameMsEMA = 0; // smoothed frame interval (for the HUD) let _frameMsEMA = 0; // smoothed frame interval (for the HUD)
let _lastFramePerf = 0; let _lastFramePerf = 0;
let _lastAutoAdjustAt = 0; let _lastAutoAdjustAt = 0;
let _lastUpscaleAt = 0; // separate, longer clock for UPscaling (lazy)
let _perfHud = null; let _perfHud = null;
let _hudOn = false; // cached highwayPerfHud flag (re-read ~2x/sec, not per-frame) let _hudOn = false; // cached highwayPerfHud flag (re-read ~2x/sec, not per-frame)
let _hudFlagAt = 0; let _hudFlagAt = 0;
@@ -266,6 +267,11 @@ function createHighway() {
return Number.isFinite(v) ? Math.max(_AUTO_SCALE_MIN, Math.min(1, v)) : _AUTO_SCALE_MIN; return Number.isFinite(v) ? Math.max(_AUTO_SCALE_MIN, Math.min(1, v)) : _AUTO_SCALE_MIN;
})(); })();
const _AUTO_ADJUST_COOLDOWN_MS = 600; const _AUTO_ADJUST_COOLDOWN_MS = 600;
// Upscaling is deliberately LAZY (longer cooldown than the downscale path) so
// the resolution doesn't visibly hunt up/down on passages that hover near the
// budget — testers saw "quality going up and down" as parts got busier (#618
// charrette). Downscale stays prompt to protect the frame rate.
const _AUTO_UPSCALE_COOLDOWN_MS = 2500;
let _inverted = localStorage.getItem('invertHighway') === 'true'; let _inverted = localStorage.getItem('invertHighway') === 'true';
let _lefty = localStorage.getItem('lefty') === '1'; let _lefty = localStorage.getItem('lefty') === '1';
let _lastChordOnFretLine = null; // chord object currently shown on fret line let _lastChordOnFretLine = null; // chord object currently shown on fret line
@@ -1291,9 +1297,22 @@ function createHighway() {
const eff = _effectiveRenderScale(); const eff = _effectiveRenderScale();
let next = _autoScale; let next = _autoScale;
if (_drawMsEMA > _DRAW_BUDGET_HI_MS && eff > _autoScaleMin) { if (_drawMsEMA > _DRAW_BUDGET_HI_MS && eff > _autoScaleMin) {
// Over budget — downscale promptly to protect the frame rate, and reset
// the upscale clock so we don't immediately bounce back up.
next = _autoScale * 0.85; next = _autoScale * 0.85;
} else if (_drawMsEMA < _DRAW_BUDGET_LO_MS && eff < 1) { _lastUpscaleAt = nowP;
next = _autoScale * 1.1; } else if (_drawMsEMA < _DRAW_BUDGET_LO_MS && eff < 1
&& nowP - _lastUpscaleAt >= _AUTO_UPSCALE_COOLDOWN_MS) {
// Headroom — upscale LAZILY: a smaller step on a longer cooldown, and
// only when the projected cost AFTER the step (cost scales ~with the
// pixel count, i.e. step²) still clears the high budget. That predictive
// guard is what stops the up→over-budget→down ping-pong testers saw: the
// scale settles just inside the deadband instead of oscillating across it.
const step = 1.06;
if (_drawMsEMA * step * step < _DRAW_BUDGET_HI_MS) {
next = _autoScale * step;
_lastUpscaleAt = nowP;
}
} }
// Clamp so _renderScale * _autoScale stays within [_autoScaleMin, 1]. // Clamp so _renderScale * _autoScale stays within [_autoScaleMin, 1].
// Cap `lo` at 1: when the floor exceeds the manual ceiling (e.g. quality // Cap `lo` at 1: when the floor exceeds the manual ceiling (e.g. quality
@@ -0,0 +1,231 @@
import {
Clock,
HalfFloatType,
NoBlending,
Vector2,
WebGLRenderTarget
} from '../../three.module.min.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { ShaderPass } from './ShaderPass.js';
import { MaskPass } from './MaskPass.js';
import { ClearMaskPass } from './MaskPass.js';
class EffectComposer {
constructor( renderer, renderTarget ) {
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';
this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
this.renderToScreen = true;
this.passes = [];
this.copyPass = new ShaderPass( CopyShader );
this.copyPass.material.blending = NoBlending;
this.clock = new Clock();
}
swapBuffers() {
const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}
addPass( pass ) {
this.passes.push( pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
insertPass( pass, index ) {
this.passes.splice( index, 0, pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
removePass( pass ) {
const index = this.passes.indexOf( pass );
if ( index !== - 1 ) {
this.passes.splice( index, 1 );
}
}
isLastEnabledPass( passIndex ) {
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
if ( this.passes[ i ].enabled ) {
return false;
}
}
return true;
}
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 );
}
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;
}
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 );
}
}
setPixelRatio( pixelRatio ) {
this._pixelRatio = pixelRatio;
this.setSize( this._width, this._height );
}
dispose() {
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.copyPass.dispose();
}
}
export { EffectComposer };
+104
View File
@@ -0,0 +1,104 @@
import { Pass } from './Pass.js';
class MaskPass extends Pass {
constructor( scene, camera ) {
super();
this.scene = scene;
this.camera = camera;
this.clear = true;
this.needsSwap = false;
this.inverse = false;
}
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 );
}
}
class ClearMaskPass extends Pass {
constructor() {
super();
this.needsSwap = false;
}
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
renderer.state.buffers.stencil.setLocked( false );
renderer.state.buffers.stencil.setTest( false );
}
}
export { MaskPass, ClearMaskPass };
+97
View File
@@ -0,0 +1,97 @@
import {
ColorManagement,
RawShaderMaterial,
UniformsUtils,
LinearToneMapping,
ReinhardToneMapping,
CineonToneMapping,
AgXToneMapping,
ACESFilmicToneMapping,
NeutralToneMapping,
SRGBTransfer
} from '../../three.module.min.js';
import { Pass, FullScreenQuad } from './Pass.js';
import { OutputShader } from '../shaders/OutputShader.js';
class OutputPass extends Pass {
constructor() {
super();
//
const shader = OutputShader;
this.uniforms = UniformsUtils.clone( shader.uniforms );
this.material = new RawShaderMaterial( {
name: shader.name,
uniforms: this.uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
} );
this.fsQuad = new FullScreenQuad( this.material );
// internal cache
this._outputColorSpace = null;
this._toneMapping = null;
}
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 = '';
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 );
}
}
dispose() {
this.material.dispose();
this.fsQuad.dispose();
}
}
export { OutputPass };
+95
View File
@@ -0,0 +1,95 @@
import {
BufferGeometry,
Float32BufferAttribute,
OrthographicCamera,
Mesh
} from '../../three.module.min.js';
class Pass {
constructor() {
this.isPass = true;
// if set to true, the pass is processed by the composer
this.enabled = true;
// if set to true, the pass indicates to swap read and write buffer after rendering
this.needsSwap = true;
// if set to true, the pass clears its buffer before rendering
this.clear = false;
// if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
this.renderToScreen = false;
}
setSize( /* width, height */ ) {}
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
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();
class FullScreenQuad {
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
dispose() {
this._mesh.geometry.dispose();
}
render( renderer ) {
renderer.render( this._mesh, _camera );
}
get material() {
return this._mesh.material;
}
set material( value ) {
this._mesh.material = value;
}
}
export { Pass, FullScreenQuad };
+99
View File
@@ -0,0 +1,99 @@
import {
Color
} from '../../three.module.min.js';
import { Pass } from './Pass.js';
class RenderPass extends Pass {
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
super();
this.scene = scene;
this.camera = camera;
this.overrideMaterial = overrideMaterial;
this.clearColor = clearColor;
this.clearAlpha = clearAlpha;
this.clear = true;
this.clearDepth = false;
this.needsSwap = false;
this._oldClearColor = new Color();
}
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 };
+77
View File
@@ -0,0 +1,77 @@
import {
ShaderMaterial,
UniformsUtils
} from '../../three.module.min.js';
import { Pass, FullScreenQuad } from './Pass.js';
class ShaderPass extends Pass {
constructor( shader, textureID ) {
super();
this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
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
} );
}
this.fsQuad = new FullScreenQuad( this.material );
}
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 );
}
}
dispose() {
this.material.dispose();
this.fsQuad.dispose();
}
}
export { ShaderPass };
@@ -0,0 +1,415 @@
import {
AdditiveBlending,
Color,
HalfFloatType,
MeshBasicMaterial,
ShaderMaterial,
UniformsUtils,
Vector2,
Vector3,
WebGLRenderTarget
} from '../../three.module.min.js';
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
/**
* UnrealBloomPass 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.
*
* Reference:
* - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/
*/
class UnrealBloomPass extends Pass {
constructor( resolution, strength, radius, threshold ) {
super();
this.strength = ( strength !== undefined ) ? strength : 1;
this.radius = radius;
this.threshold = threshold;
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
// create color only once here, reuse it later inside the render function
this.clearColor = new Color( 0, 0, 0 );
// 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.getSeperableBlurMaterial( 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
const copyShader = CopyShader;
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.enabled = true;
this.needsSwap = false;
this._oldClearColor = new Color();
this.oldClearAlpha = 1;
this.basic = new MeshBasicMaterial();
this.fsQuad = new FullScreenQuad( null );
}
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();
}
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 );
}
}
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;
}
getSeperableBlurMaterial( 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 };
+45
View File
@@ -0,0 +1,45 @@
/**
* Full-screen textured quad 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 };
@@ -0,0 +1,64 @@
import {
Color
} from '../../three.module.min.js';
/**
* Luminosity
* http://en.wikipedia.org/wiki/Luminosity
*/
const LuminosityHighPassShader = {
name: 'LuminosityHighPassShader',
shaderID: 'luminosityHighPass',
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 };
+85
View File
@@ -0,0 +1,85 @@
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 );
#endif
// color space
#ifdef SRGB_TRANSFER
gl_FragColor = sRGBTransferOETF( gl_FragColor );
#endif
}`
};
export { OutputShader };