From 4214ef365ef37b97d3d35c09a5acf17618675751 Mon Sep 17 00:00:00 2001 From: Byron Gamatos Date: Thu, 2 Jul 2026 08:54:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(drum=5Fhighway=5F3d):=20materials=20&=20th?= =?UTF-8?q?emes=20=E2=80=94=20studio=20env=20map,=20metal=20cymbals,=20BG?= =?UTF-8?q?=5FTHEMES,=20cinematic,=20glow/vibrancy=20(D3)=20(#712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _makeStudioEnv: procedural PMREM environment (no RoomEnvironment addon vendored) -> scene.environment; rebuilt on kit-change, disposed both teardown paths - Cymbals roughness 0.2 / metalness 0.85 / envMapIntensity 1.2 (metal finally reads); discs satin 0.45 + envInt 0.5; floor 0.7 + envInt 0.35; hit bar envInt 1.0 - BG_THEMES port (guitar ids/values; drum 'default' = original palette byte-for-byte; single pick drives clear/fog + board + lane stripes); drumH3dSetTheme + drum_h3d_bg_theme; live _applyTheme - Cinematic lighting toggle (0.3/1.2 on, 0.4/1.0 off = stock) - Glow slider (base * glow*2; 0.5 default = stock) across notes/hit bar/snare stripe; Lane vibrancy slider (stripe base 0.12+0.24v + halo/ghost opacities), stacking under the D2 approach highlight - Fixed pre-existing floor/hit-bar geometry+material leak on kit change - Tests: theme table parity + default preservation + fallbacks (13) Co-authored-by: Claude Fable 5 --- CHANGELOG.md | 1 + plugins/drum_highway_3d/README.md | 4 + plugins/drum_highway_3d/screen.js | 293 ++++++++++++++++-- plugins/drum_highway_3d/settings.html | 78 ++++- .../drum_highway_3d/tests/data_layer.test.js | 33 ++ 5 files changed, 374 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8258e7c..7b1e57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **3D Keys Highway: the anti-plastic pass — lacquered note gems, glossy piano-black keys, a studio environment, scene themes and a gradient sky (parity slice 3).** The note gems move to `MeshPhysicalMaterial` with a full **clearcoat** (roughness 0.32, clearcoat 1.0/0.18, envMapIntensity 0.9): a sharp lacquer highlight over the colored body instead of the old dead matte surface — glass, not plastic. What sells it is **image-based lighting**: the same procedural PMREM "studio" environment as the drum highway (dark room + cool overhead / warm+cool side light strips, no addon dependency) feeds `scene.environment`, so the **black keys finally read as glossy piano black** (roughness 0.55 → 0.22, envMapIntensity 1.3) with visible strip reflections, whites keep an ivory sheen (0.42/0.55), and the highway floor gets a stage sheen (roughness 0.9 → 0.55, metalness 0.15). The flat background becomes a **vertical gradient** (lighter above the horizon → theme color → darker toward the keyboard), and the guitar highway's **11 scene themes** arrive (same names/values — your look carries across instruments; `default` preserves the original keys palette; pitch-class note/key colors are never themed — themes own the scene, Synthesia colors own the notes). Plus **Cinematic lighting** (ambient 0.55/key 1.3, on by default) and a **Glow strength** slider multiplying the note glow, key approach-glow and the sustain consume-flash (0.5 = stock). All live-applying from the Graphics settings (`keys3d_bg_theme` + `keys3d_bg_*`); the PMREM target and gradient texture are disposed with the scene. Tests: theme-table id parity + default-look preservation + fallbacks (28 total). +- **3D Drum Highway: real materials + scene themes (parity slice 3).** The scene gets **image-based lighting**: a procedural PMREM "studio" environment (dark room + three emissive light strips — cool overhead key, warm/cool side fills; no vendored-addon dependency) feeds `scene.environment`, so the cymbals' metalness **finally reads as metal** (retuned to roughness 0.2 / metalness 0.85 / envMapIntensity 1.2 — the old 0.7-metalness look was matte because there was nothing to reflect), drumheads get a satin sheen, and the floor (roughness 0.95 → 0.7) catches the strips without turning into a mirror. **Scene themes arrive** — the same 11 theme names as the guitar highway (Midnight, Charcoal, Deep Purple, Forest, Warm Slate, Deep Focus, Deep Sea, Cathode, Cathode Green, Hearth) retint the background/fog, floor and lane stripes so your look carries across instruments; `default` preserves the original drum palette byte-for-byte, and piece colours stay with the existing Palette picker (themes own the scene, palettes own the kit). Plus **Cinematic lighting** (dimmer ambient / stronger key, on by default), a **Glow strength** slider (0–1, 0.5 = stock) multiplying every emissive base — notes, hit line, snare wires — and a **Lane vibrancy** slider driving stripe/halo/ghost-ring strength (the hit-FX approach highlight stacks on top). Everything applies live from the plugin settings (`drum_h3d_bg_theme` + `drum_h3d_bg_*` keys); the PMREM render target is rebuilt across kit-change renderer recreation and disposed in both teardown paths — and the floor/hit-bar geometry+materials that previously leaked on every kit change are now tracked and disposed too. Tests: theme-table parity with the guitar ids + default-look preservation + fallbacks (13 total). - **3D Drum Highway: hit FX — sparks, timing-colored lane flashes, kick camera pulse, approach glow, and open hi-hat notation (parity slice 2).** Striking a pad now *feels* struck: a pooled additive **spark burst** fires at the lane (ported from the guitar highway's Points-cloud system, pool 160), colored by **timing** — on-time green, early cyan, late amber (same `_timingHex` vocabulary as `highway_3d`, classified against the ±50 ms hit window with the inner 40% reading as on-time); with **Streak feedback** on, bursts grow with your combo. The **lane flash** feedback that was removed when note-recoloring landed is resurrected properly: pooled additive quads with a soft gaussian falloff light up the struck lane at the hit line (timing-colored; red for wrong-pad hits), and a **kick** hit fires triple amber bursts across the bar plus a subtle **camera dip + amber floor wash** that decays exponentially. Lanes also glow ahead of time: each stripe brightens as its next note approaches the hit line, so the eye is led to where the next hit lands. **Open hi-hat finally renders distinctly** — `hh_open` chart hits get a thin warm ring around the cymbal gem (standard notation's "o"), closing the long-standing TODO; the flag is orthogonal to accents/ghosts/flams so combined cues stack. All of it is settings-gated (Graphics → Hit sparks / Timing colours / Streak feedback / a 0–1 **Hit feedback intensity** slider driving flashes, approach glow and the kick pulse; everything on by default, `drum_h3d_bg_*` keys, live-applying) and GPU-frugal: every new visual is pooled or shares geometry/materials — zero per-note allocation on top of the per-frame notes rebuild, all registered in both dispose paths (kit-change renderer recreation included). Tests: timing-classifier boundaries + FX defaults added to `plugins/drum_highway_3d/tests/data_layer.test.js` (10 total). - **3D Keys Highway: hit FX — vibrant note gems, timing-colored sparks, and a hit-line that reacts to your playing (parity slice 2).** The washed-out note look is gone: gem opacity is now driven by a **Note vibrancy** slider (default 0.85 → opacity 0.92, up from a fixed 0.8; lane guides scale with it too, live-applying without a chart rebuild) and the resting emissive glow rises 0.08 → 0.22, so the falling notes finally read saturated against the dark floor. Scored key presses fire a pooled additive **spark burst** at the struck key (guitar-highway port, pool 96) **colored by timing** — on-time green, early cyan, late amber, classified against the ±100 ms window with the inner 40% reading as on-time (the timing delta is recovered from the matched note's key, so `judgeHit`'s tested contract is untouched); the per-pitch-class flame sprite keeps its identity color so pitch and timing stay separate signals. With **Streak feedback** on, bursts grow with the combo. The **hit line kicks brighter** for a beat on every scored press (exponential decay, scaled by a 0–1 **Hit feedback intensity** slider). All new controls live in the plugin's Graphics settings (on by default, `keys3d_bg_*` keys, live-applying), and the spark pool is disposed with the scene like every other GPU resource. Tests: timing-classifier boundaries, the noteKey time round-trip that the delta recovery relies on, and the new FX defaults (26 total). - **3D Drum Highway: bloom glow + adaptive-resolution support — the first slice of visual parity with the guitar highway.** The drum highway now renders through the same post-processing path as `highway_3d`: an `UnrealBloomPass` (strength 0.65, radius 0.5, threshold 0.82 — high, so only emissive/bright surfaces bleed) on a multisampled HalfFloat target with ACES filmic tone mapping, so the white hit-line bar and proximity-lit notes get a real glow instead of a flat emissive tint. **On by default**, with a new **Graphics → "Glow (bloom)"** toggle in the plugin settings (`drum_h3d_bg_bloom`, applies live, no reload); if the vendored postprocessing addons can't load (older self-hosted core), the plugin silently falls back to the direct render path. The plugin also now honors the host's **adaptive render scale** (`bundle.renderScale` — the Quality/"Min res" controls that the guitar highway already respected), multiplying it into the device pixel ratio, and caps DPR at 1.25 when more than one viz instance is live (splitscreen) so two panels don't double the GPU fill cost. Groundwork for the rest of the parity series: an FX-settings scaffold (`FX_DEFAULTS`/`readFxSettings`/`window.drumH3dSetFx`, `drum_h3d_bg_*` localStorage keys) that the sparks/themes/backgrounds PRs extend, plus a first node test suite for the plugin (`plugins/drum_highway_3d/tests/data_layer.test.js` — vm-loaded like the keys plugin's, covering the hit-variant precedence, the Auto-mode steal-guard predicate, and FX defaults; 8 tests, runs in CI via the `plugins/*/tests/*.test.js` glob). diff --git a/plugins/drum_highway_3d/README.md b/plugins/drum_highway_3d/README.md index 5cc96aa..82fb585 100644 --- a/plugins/drum_highway_3d/README.md +++ b/plugins/drum_highway_3d/README.md @@ -78,6 +78,10 @@ tunes one of these, mirror the change here (and in `keys_highway_3d`): - `_timingHex()` — early/late/on-time feedback colors (green/cyan/amber) - `_ssActive()` — host splitscreen probe (minus the guitar's focus-API checks, which it needs for input routing and we don't) +- `BG_THEMES` / `_bgThemeColors()` — the scene theme table (same ids/values + as the guitar's, except `default` which is this plugin's original + palette); one pick drives both of the guitar's background/highway axes +- `_applyCinematic()` — ambient/key rebalance (values tuned per plugin) ## Why a separate plugin (vs. drum mode inside highway_3d)? diff --git a/plugins/drum_highway_3d/screen.js b/plugins/drum_highway_3d/screen.js index fe8146b..d9b286b 100644 --- a/plugins/drum_highway_3d/screen.js +++ b/plugins/drum_highway_3d/screen.js @@ -1115,8 +1115,57 @@ hitFx: 0.7, // 0–1 master intensity for lane flashes / kick pulse timingFx: true, // early/late/on-time coloring of hit feedback streakFx: true, // consecutive-hit escalation (bigger spark bursts) + cinematic: true, // rebalanced lighting (dimmer ambient, stronger key) + glow: 0.5, // 0–1 emissive multiplier (0.5 = the stock look) + vibrancy: 0.85, // lane stripe / halo / ghost opacity strength }; const FX_LS_PREFIX = 'drum_h3d_bg_'; + // Theme id lives OUTSIDE FX_DEFAULTS (string, not bool/number) — its own + // localStorage key + validation against BG_THEMES. + const FX_LS_THEME = 'drum_h3d_bg_theme'; + + // Scene color themes — PORTED FROM highway_3d BG_THEMES (keep the ids, + // names and values in sync so a user's look carries across instruments). + // The guitar splits background/highway into two axes; here one pick + // drives both (clear/fog + board/lane) — the drum scene is simpler and + // a single dropdown reads better in its settings panel. + // clear/fog — WebGL clear color + distance fog (kept equal so the + // horizon dissolves cleanly) + // board — the floor plane under the lanes + // lane/laneDim — the alternating lane stripes; themes without them + // fall back to the stock teal/blue pair. + const BG_THEMES = { + // 'default' is THIS plugin's original palette (fog 0x1a1a2e, floor + // 0x0a0e1a, stock teal/blue stripes) — byte-identical for anyone who + // never touches the setting. The guitar's default differs; every + // OTHER id below matches the guitar's table exactly. + default: { clear: 0x1a1a2e, fog: 0x1a1a2e, board: 0x0a0e1a }, + midnight: { clear: 0x0a0e1a, fog: 0x0a0e1a, board: 0x080d1c, lane: 0x244fae, laneDim: 0x122a5e }, + charcoal: { clear: 0x16181c, fog: 0x16181c, board: 0x141417, lane: 0x525a66, laneDim: 0x282d34 }, + deeppurple: { clear: 0x140a1e, fog: 0x140a1e, board: 0x0b0610, lane: 0x3a1f6e, laneDim: 0x1f1040 }, + forest: { clear: 0x0a1614, fog: 0x0a1614, board: 0x06100c, lane: 0x15602a, laneDim: 0x0a3318 }, + warmslate: { clear: 0x1c130b, fog: 0x1c130b, board: 0x0e0805, lane: 0x5e3a12, laneDim: 0x341f0a }, + deepfocus: { clear: 0x0c0c0d, fog: 0x0c0c0d, board: 0x060606, lane: 0x2f7fa0, laneDim: 0x163c4e }, + deepsea: { clear: 0x06222b, fog: 0x06222b, board: 0x03141a, lane: 0x0e5a63, laneDim: 0x063338 }, + cathode: { clear: 0x140b03, fog: 0x140b03, board: 0x0c0702, lane: 0x6e4a0e, laneDim: 0x3a2806 }, + cathodegreen: { clear: 0x07301a, fog: 0x07301a, board: 0x031a0c, lane: 0x0e6e2a, laneDim: 0x073a18 }, + hearth: { clear: 0x280806, fog: 0x280806, board: 0x1a0606, lane: 0x7a2410, laneDim: 0x3f1409 }, + }; + function _bgThemeColors(id) { return BG_THEMES[id] || BG_THEMES.default; } + function readThemeSetting() { + try { + const id = localStorage.getItem(FX_LS_THEME); + if (id && BG_THEMES[id]) return id; + } catch (_) {} + return 'default'; + } + window.drumH3dSetTheme = function (id) { + if (!BG_THEMES[id]) return; + try { localStorage.setItem(FX_LS_THEME, id); } catch (_) {} + try { + window.dispatchEvent(new CustomEvent('drum_h3d:settings', { detail: { theme: id } })); + } catch (_) { /* dispatch unavailable — persisted value applies next init */ } + }; function readFxSettings() { const fx = Object.assign({}, FX_DEFAULTS); @@ -1181,6 +1230,14 @@ let settings = readSettings(); let activePalette = PALETTES[settings.palette]; let fx = readFxSettings(); + let _theme = readThemeSetting(); + + // Theme/material handles (initScene builds them; _applyTheme / + // _applyCinematic / the glow slider retune them live). + let ambLight = null, dirLight = null; + let mFloor = null, _gFloor = null; + let _hitBarMat = null, _gHitBar = null; + let _envRT = null; // PMREM render target backing scene.environment // Host adaptive-quality scale (bundle.renderScale, 0.25–1) — // multiplied into the device pixel ratio like highway_3d does. @@ -1638,7 +1695,31 @@ for (const k of Object.keys(detail.fx)) { if (k in FX_DEFAULTS) fx[k] = detail.fx[k]; } + if ('cinematic' in detail.fx) _applyCinematic(); + if ('glow' in detail.fx) { + // Static emissive bases (hit bar, snare stripe, hit/miss + // tints) are baked at build time — rebuild them; the + // per-frame placeNote pulse picks the new value up on + // its own. + rebuildPaletteMaterials(); + if (_hitBarMat) _hitBarMat.emissiveIntensity = 0.9 * _glowMul(); + if (mSnareStripe) mSnareStripe.emissiveIntensity = 0.5 * _glowMul(); + } + if ('vibrancy' in detail.fx) _applyVibrancy(); } + if (detail.theme && BG_THEMES[detail.theme]) { + _theme = detail.theme; + _applyTheme(); + } + } + + // Vibrancy slider: stripes get their new base next frame (draw() + // recomputes them); the halo/ghost ring statics update here. + function _applyVibrancy() { + const v = Math.min(1, Math.max(0, fx.vibrancy)); + if (mAccentRing) mAccentRing.opacity = 0.6 + 0.3 * v; + // Ghost rings get their vibrancy at per-note material creation in + // buildNoteMesh (they never render with the shared mGhostRing). } function rebuildPaletteMaterials() { @@ -1661,9 +1742,12 @@ mDrumByLane[i] = new T.MeshStandardMaterial({ color, emissive: color, - emissiveIntensity: 0.45, - roughness: 0.55, + emissiveIntensity: 0.45 * _glowMul(), + // Drumhead: satin, a little env pickup so the studio + // strips give the discs some dimension. + roughness: 0.45, metalness: 0.1, + envMapIntensity: 0.5, }); // Pre-cloned tint materials for hit (green) and miss (red), // keyed per lane so proximity emissive adjustments on the base @@ -1678,9 +1762,13 @@ mCymbalByLane[i] = new T.MeshStandardMaterial({ color, emissive: color, - emissiveIntensity: 0.55, - roughness: 0.25, - metalness: 0.7, + emissiveIntensity: 0.55 * _glowMul(), + // With scene.environment set (studio env map) the + // metalness finally reads as metal — the old 0.7 + // metalness had no envMap and looked matte. + roughness: 0.2, + metalness: 0.85, + envMapIntensity: 1.2, transparent: true, opacity: 0.92, }); @@ -1698,9 +1786,10 @@ mKick = new T.MeshStandardMaterial({ color: KICK_COLOR, emissive: KICK_COLOR, - emissiveIntensity: 0.6, + emissiveIntensity: 0.6 * _glowMul(), roughness: 0.4, metalness: 0.2, + envMapIntensity: 0.6, }); mKickHit = mKick.clone(); mKickHit.color.setHex(0x22c55e); @@ -1717,32 +1806,141 @@ /* -- one-time scene setup --------------------------------------- */ + // Emissive multiplier from the Glow slider — 0.5 is neutral (the + // stock look), so `base * _glowMul()` leaves defaults byte-identical. + function _glowMul() { + return Math.min(1, Math.max(0, fx.glow)) * 2; + } + + // Cinematic lighting: dimmer ambient + stronger key light, same + // rebalance idea as highway_3d's _applyCinematic. Off = the plugin's + // pre-parity values. + function _applyCinematic() { + if (!ambLight || !dirLight) return; + ambLight.intensity = fx.cinematic ? 0.3 : 0.4; + dirLight.intensity = fx.cinematic ? 1.2 : 1.0; + } + + // Procedural "studio" environment for image-based lighting: a dark + // room shell with three emissive light strips (cool overhead key, + // warm left fill, cool right accent), baked through PMREMGenerator. + // No addon needed (PMREMGenerator ships in the core three build) — + // RoomEnvironment is NOT vendored, hence this hand-rolled stand-in. + // Returns the PMREM render target ({texture}) or null; caller owns + // disposing the RT. + function _makeStudioEnv(ThreeLib, renderer) { + if (!renderer) return null; + try { + const envScene = new ThreeLib.Scene(); + const own = []; // geometries/materials to free after baking + const shellGeo = new ThreeLib.BoxGeometry(100, 100, 100); + const shellMat = new ThreeLib.MeshBasicMaterial({ color: 0x11131c, side: ThreeLib.BackSide }); + envScene.add(new ThreeLib.Mesh(shellGeo, shellMat)); + own.push(shellGeo, shellMat); + const strip = (w, h, hex, intensity, x, y, z, rx, ry) => { + const g = new ThreeLib.PlaneGeometry(w, h); + // DoubleSide: orientation-proof — the bake only runs once, + // so the extra faces are free insurance. + const m = new ThreeLib.MeshBasicMaterial({ color: hex, side: ThreeLib.DoubleSide }); + m.color.multiplyScalar(intensity); + const mesh = new ThreeLib.Mesh(g, m); + mesh.position.set(x, y, z); + mesh.rotation.set(rx, ry, 0); + envScene.add(mesh); + own.push(g, m); + }; + strip(60, 8, 0xdfe8ff, 18, 0, 45, 0, Math.PI / 2, 0); // overhead key + strip(30, 50, 0xffd9a8, 6, -48, 5, 0, 0, Math.PI / 2); // warm left wall + strip(30, 50, 0x9fd8ff, 6, 48, 5, 0, 0, -Math.PI / 2); // cool right wall + const pmrem = new ThreeLib.PMREMGenerator(renderer); + const rt = pmrem.fromScene(envScene, 0.04); + pmrem.dispose(); + for (const r of own) r.dispose(); + return rt; + } catch (e) { + console.warn('[Drum-Hwy3D] studio env failed (continuing without IBL)', e); + return null; + } + } + + function _disposeEnv() { + if (_envRT) { + try { _envRT.dispose(); } catch (_) {} + _envRT = null; + } + } + + // Apply the active theme to the live scene (background/fog/clear + + // floor + lane stripes). Notes keep their piece palette — themes own + // the SCENE, palettes own the KIT. + function _applyTheme() { + const c = _bgThemeColors(_theme); + if (scene) { + if (scene.background && scene.background.isColor) scene.background.setHex(c.clear); + if (scene.fog) scene.fog.color.setHex(c.fog); + } + if (ren) ren.setClearColor(c.clear, 1); + if (mFloor) mFloor.color.setHex(c.board); + _applyStripeColors(); + } + + // Lane stripe colors + base opacity (theme lane pair or the stock + // teal/blue; vibrancy drives how strongly they read). draw()'s + // approach highlight adds on top of the vibrancy base each frame. + function _stripeBaseOpacity() { + return 0.12 + 0.24 * Math.min(1, Math.max(0, fx.vibrancy)); + } + function _applyStripeColors() { + const c = _bgThemeColors(_theme); + const pair = (c.lane != null && c.laneDim != null) + ? [c.laneDim, c.lane] + : [0x2d5476, 0x42759d]; + for (let i = 0; i < laneStripeMats.length; i++) { + laneStripeMats[i].color.setHex(pair[i % 2]); + } + } + function initScene() { scene = new T.Scene(); - scene.background = new T.Color(FOG_COLOR); - scene.fog = new T.Fog(FOG_COLOR, FOG_START, FOG_END); + const themeCols = _bgThemeColors(_theme); + scene.background = new T.Color(themeCols.clear); + scene.fog = new T.Fog(themeCols.fog, FOG_START, FOG_END); + if (ren) ren.setClearColor(themeCols.clear, 1); + + // Studio environment map: emissive-strip room baked through + // PMREM so MeshStandardMaterial metalness/roughness finally get + // specular payoff (the cymbals' 0.85 metalness reads as metal). + // Rebuilt here so the kit-change renderer recreation gets a + // fresh one; the old RT is disposed by _disposeScene/teardown. + _envRT = _makeStudioEnv(T, ren); + if (_envRT) scene.environment = _envRT.texture; cam = new T.PerspectiveCamera(60, 16 / 9, 0.1, 1000 * K); positionCamera(); lights = new T.Group(); - const ambient = new T.AmbientLight(0xffffff, 0.4); - const dir = new T.DirectionalLight(0xffffff, 1.0); - dir.position.set(-50 * K, 200 * K, 200 * K); - lights.add(ambient); - lights.add(dir); + ambLight = new T.AmbientLight(0xffffff, 0.4); + dirLight = new T.DirectionalLight(0xffffff, 1.0); + dirLight.position.set(-50 * K, 200 * K, 200 * K); + lights.add(ambLight); + lights.add(dirLight); scene.add(lights); + _applyCinematic(); // Floor plane — wide darkened quad under the lanes. // Use Math.max(1, LANE_COUNT) so a kick-only kit (LANE_COUNT=0) // still produces a floor at least as wide as the kick bar (KICK_W). const floorW = Math.max(1, LANE_COUNT) * LANE_GAP + 8 * K; const floorD = (AHEAD + BEHIND + 0.5) * TS + 60 * K; - const gFloor = new T.PlaneGeometry(floorW, floorD); - const mFloor = new T.MeshStandardMaterial({ - color: 0x0a0e1a, - roughness: 0.95, + _gFloor = new T.PlaneGeometry(floorW, floorD); + const gFloor = _gFloor; + mFloor = new T.MeshStandardMaterial({ + color: themeCols.board, + // 0.7 (was 0.95): just enough sheen to catch the env strips + // without turning the floor into a mirror. + roughness: 0.7, metalness: 0.0, + envMapIntensity: 0.35, }); const floor = new T.Mesh(gFloor, mFloor); floor.rotation.x = -Math.PI / 2; @@ -1758,14 +1956,17 @@ rebuildKitMap(); // Hit-line bar in front of the camera, perpendicular to scroll. - const gHit = new T.BoxGeometry(floorW, 0.6 * K, 0.6 * K); + _gHitBar = new T.BoxGeometry(floorW, 0.6 * K, 0.6 * K); + const gHit = _gHitBar; const mHit = new T.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, - emissiveIntensity: 0.9, + emissiveIntensity: 0.9 * _glowMul(), roughness: 0.3, metalness: 0.6, + envMapIntensity: 1.0, }); + _hitBarMat = mHit; const hitBar = new T.Mesh(gHit, mHit); hitBar.position.set(0, 0.3 * K, 0); scene.add(hitBar); @@ -1878,7 +2079,7 @@ mSnareStripe = new T.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, - emissiveIntensity: 0.5, + emissiveIntensity: 0.5 * _glowMul(), roughness: 0.3, }); mBellDot = new T.MeshBasicMaterial({ @@ -1889,6 +2090,7 @@ }); rebuildPaletteMaterials(); + _applyVibrancy(); // halo/ghost ring opacities from the slider } function positionCamera() { @@ -1909,16 +2111,21 @@ function buildLanes(_floorW, floorD) { laneGroup = new T.Group(); laneStripeMats = []; - // Alternating lane stripes for the 7 hand lanes (same teal/blue - // as highway_3d, but a notch darker so the brighter discs pop). - const colors = [0x2d5476, 0x42759d]; + // Alternating lane stripes for the 7 hand lanes — theme lane + // pair when the active theme defines one, else the stock + // teal/blue (a notch darker than highway_3d so discs pop). + // _applyStripeColors() retints these on a live theme switch. + const tc = _bgThemeColors(_theme); + const colors = (tc.lane != null && tc.laneDim != null) + ? [tc.laneDim, tc.lane] + : [0x2d5476, 0x42759d]; for (let i = 0; i < LANE_COUNT; i++) { const x = LANE_X0 + i * LANE_GAP; const g = new T.PlaneGeometry(LANE_GAP * 0.96, floorD); const m = new T.MeshBasicMaterial({ color: colors[i % 2], transparent: true, - opacity: 0.32, + opacity: _stripeBaseOpacity(), }); const stripe = new T.Mesh(g, m); stripe.rotation.x = -Math.PI / 2; @@ -2035,7 +2242,11 @@ const ghostMat = new T.MeshBasicMaterial({ color: activePalette[laneCfg.paletteIdx], transparent: true, - opacity: 0.75, + // Vibrancy-driven, evaluated per build — notesGroup is + // rebuilt every frame, so slider changes apply live. + // (The shared mGhostRing is only the hit/miss-tint + // template, not what pending ghosts render with.) + opacity: 0.5 + 0.29 * Math.min(1, Math.max(0, fx.vibrancy)), side: T.DoubleSide, }); ghostMat.userData.transient = true; @@ -2254,13 +2465,14 @@ if (note.lane < LANE_COUNT && proximity > (_laneProx[note.lane] || 0)) { _laneProx[note.lane] = proximity; } + const g = _glowMul(); // Glow slider (0.5 default = 1.0x, stock) if (laneCfg.kind === 'drum' && note.variant !== 'ghost' && mDrumByLane[note.lane]) { // Subtle pulse via emissiveIntensity — palette-driven base + pulse. - mDrumByLane[note.lane].emissiveIntensity = 0.45 + proximity * 0.35; + mDrumByLane[note.lane].emissiveIntensity = (0.45 + proximity * 0.35) * g; } else if (laneCfg.kind === 'cymbal' && mCymbalByLane[note.lane]) { - mCymbalByLane[note.lane].emissiveIntensity = 0.55 + proximity * 0.35; + mCymbalByLane[note.lane].emissiveIntensity = (0.55 + proximity * 0.35) * g; } else if (laneCfg.kind === 'kick' && mKick) { - mKick.emissiveIntensity = 0.6 + proximity * 0.5; + mKick.emissiveIntensity = (0.6 + proximity * 0.5) * g; } // Slight scale-up as notes near the hit line gives the eye a @@ -2374,6 +2586,7 @@ function _disposeScene() { _bloomDispose(); // composer is bound to the renderer we're about to replace _disposeFxObjects(); + _disposeEnv(); // PMREM RT is renderer-bound too; initScene rebuilds it if (notesGroup) { while (notesGroup.children.length) { disposeMeshTree(notesGroup.children.pop()); @@ -2419,7 +2632,14 @@ if (gOpenRing) gOpenRing.dispose(); if (mOpenRing) mOpenRing.dispose(); if (ren) ren.dispose(); + // Floor + hit-bar resources (tracked since the themes PR — they + // previously leaked across kit-change scene rebuilds). + if (mFloor) mFloor.dispose(); + if (_gFloor) _gFloor.dispose(); + if (_hitBarMat) _hitBarMat.dispose(); + if (_gHitBar) _gHitBar.dispose(); scene = cam = ren = lights = laneGroup = kitMapGroup = notesGroup = null; + ambLight = dirLight = mFloor = _gFloor = _hitBarMat = _gHitBar = null; mDrumByLane = mCymbalByLane = mKick = mKickHit = mKickMiss = null; mDrumHitByLane = mDrumMissByLane = mCymbalHitByLane = mCymbalMissByLane = null; mAccentRing = mGhostRing = mSnareStripe = mBellDot = null; @@ -2446,6 +2666,7 @@ function teardown() { _bloomDispose(); _disposeFxObjects(); + _disposeEnv(); // HUD cleanup lives here (not only destroy): init() re-runs // teardown() for renderer re-initialization, possibly against a // different canvas — a stale _hudEl would both linger in the old @@ -2495,7 +2716,14 @@ if (gOpenRing) gOpenRing.dispose(); if (mOpenRing) mOpenRing.dispose(); if (ren) ren.dispose(); + // Floor + hit-bar resources (tracked since the themes PR — they + // previously leaked across kit-change scene rebuilds). + if (mFloor) mFloor.dispose(); + if (_gFloor) _gFloor.dispose(); + if (_hitBarMat) _hitBarMat.dispose(); + if (_gHitBar) _gHitBar.dispose(); scene = cam = ren = lights = laneGroup = kitMapGroup = notesGroup = null; + ambLight = dirLight = mFloor = _gFloor = _hitBarMat = _gHitBar = null; mDrumByLane = mCymbalByLane = mKick = mKickHit = mKickMiss = null; mDrumHitByLane = mDrumMissByLane = mCymbalHitByLane = mCymbalMissByLane = null; mAccentRing = mGhostRing = mSnareStripe = mBellDot = null; @@ -2516,6 +2744,10 @@ settings = readSettings(); activePalette = PALETTES[settings.palette]; fx = readFxSettings(); + // Persisted string settings refresh here too — a theme saved + // while no instance was listening must not come up stale on a + // later init(). + _theme = readThemeSetting(); loadThree().then(() => { if (!highwayCanvas) return; // destroyed before load resolved @@ -2623,7 +2855,7 @@ // Approach highlight: raise each lane stripe toward its next // note (accumulated by the rebuildNotes walk above). for (let i = 0; i < laneStripeMats.length; i++) { - laneStripeMats[i].opacity = 0.32 + 0.28 * (_laneProx[i] || 0) * fx.hitFx; + laneStripeMats[i].opacity = _stripeBaseOpacity() + 0.28 * (_laneProx[i] || 0) * fx.hitFx; } _applyLaneFlashes(); _refreshHud(); @@ -2732,6 +2964,9 @@ _variantForHit, _classifyTiming, readFxSettings, + readThemeSetting, + _bgThemeColors, + BG_THEMES, FX_DEFAULTS, MIDI_TO_PIECE, HIT_TOLERANCE_S, diff --git a/plugins/drum_highway_3d/settings.html b/plugins/drum_highway_3d/settings.html index c2821a0..440922a 100644 --- a/plugins/drum_highway_3d/settings.html +++ b/plugins/drum_highway_3d/settings.html @@ -45,7 +45,63 @@

Graphics

-