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>
This commit is contained in:
ChrisBeWithYou 2026-06-27 22:03:01 -05:00
parent b04d7fa00d
commit 5111ef9440
3 changed files with 48 additions and 14 deletions

View File

@ -1,7 +1,7 @@
{
"id": "highway_3d",
"name": "3D Highway",
"version": "3.28.0",
"version": "3.29.0",
"type": "visualization",
"bundled": true,
"script": "screen.js",

View File

@ -1768,7 +1768,7 @@
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, hitFx: 0.7, sparks: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: 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, strikeLine: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true };
// User-selectable, persistable bg styles — must mirror settings.html's
// VALID_STYLES. 'venue' is deliberately NOT here: it is an internal effective
// 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
// false). Add new boolean keys to BG_DEFAULTS and they pick this
// 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', 'sparks', 'cinematic', 'verdictMarks', 'timingFx', 'streakFx', 'bloom']);
const _BG_BOOL_KEYS = new Set(['reactive', 'showFretOnNote', 'cameraLockLow', 'inlayLabelsVisible', 'sectionLabelsOnHighway', 'sectionHudVisible', 'nutHeadstockVisible', 'tuningLabelsVisible', 'projectionVisible', 'chordDiagramVisible', 'fpsVisible', 'toneHudVisible', 'fretDividersVisible', 'slideArrowApproachVisible', 'slideArrowNeckVisible', 'slideArrowChainPreviewVisible', 'sparks', 'strikeLine', 'cinematic', 'verdictMarks', 'timingFx', 'streakFx', 'bloom']);
function _bgCoerceBool(val, fallback) {
if (val === 'true' || val === '1') return true;
if (val === 'false' || val === '0') return false;
@ -2261,6 +2261,7 @@
window.h3dBgSetGlow = (v) => _bgWriteGlobal('glow', v);
window.h3dBgSetHitFx = (v) => _bgWriteGlobal('hitFx', v);
window.h3dBgSetSparks = (v) => _bgWriteGlobal('sparks', !!v);
window.h3dBgSetStrikeLine = (v) => _bgWriteGlobal('strikeLine', !!v);
window.h3dBgSetCinematic = (v) => _bgWriteGlobal('cinematic', !!v);
window.h3dBgSetVerdictMarks = (v) => _bgWriteGlobal('verdictMarks', !!v);
window.h3dBgSetTimingFx = (v) => _bgWriteGlobal('timingFx', !!v);
@ -3561,6 +3562,7 @@
let glowMul = BG_DEFAULTS.glow;
let _hitFx = BG_DEFAULTS.hitFx;
let _sparks = BG_DEFAULTS.sparks;
let _strikeFx = BG_DEFAULTS.strikeLine;
let _cinematic = BG_DEFAULTS.cinematic;
let _verdictMarks = BG_DEFAULTS.verdictMarks;
let _timingFx = BG_DEFAULTS.timingFx;
@ -6170,6 +6172,13 @@
transparent: true, opacity: 1.0, depthWrite: false,
}));
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.
// The body is rendered on top with opacity:1 on hit/miss, which
// fully covers the outline center — only the fringe that extends
@ -7385,6 +7394,7 @@
glowMul = _bgReadSetting(panelKey, 'glow');
_hitFx = _bgReadSetting(panelKey, 'hitFx');
_sparks = _bgReadSetting(panelKey, 'sparks');
_strikeFx = _bgReadSetting(panelKey, 'strikeLine');
_cinematic = _bgReadSetting(panelKey, 'cinematic');
_verdictMarks = _bgReadSetting(panelKey, 'verdictMarks');
_timingFx = _bgReadSetting(panelKey, 'timingFx');
@ -7834,7 +7844,7 @@
// Toggle via the 'cinematic' setting so it's directly comparable.
function _applyCinematic() {
if (!ambLight || !dirLight) return;
ambLight.intensity = _cinematic ? 0.35 : 0.85;
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,
@ -7892,7 +7902,12 @@
try {
const sz = canvasSize(highwayCanvas) || { w: 1280, h: 720 };
const w = Math.max(2, sz.w | 0), h = Math.max(2, sz.h | 0);
const comp = new EC.EffectComposer(ren);
// 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);
@ -7949,13 +7964,17 @@
p.position.set(board.center, S_BASE - NH / 2 - 2 * K, -blAhead / 2);
fretG.add(p);
// Strike line (#1): a thin glowing bar at the hit line (Z=0). Idle dim;
// flashes green on a hit / red on a miss (driven each frame in the render loop).
// Strike line (#1): a thin glowing "now" bar at the hit line (Z=0). Idle
// faint; flashes green on a HIT only (driven each frame in the render loop).
// Misses are shown at the gem (red wash + ✗), NOT here — a single missed
// string never reds-out the whole bar (charrette panel rec 1a).
// Placed at the vertical CENTRE of the string field (not the bottom edge,
// which read as the fretboard's lower border and fused with open-string gems).
{
const slGeo = new T.BoxGeometry(bw, NH * 0.42, 0.7 * K);
const slMat = new T.MeshBasicMaterial({ color: 0x2a3a52, transparent: true, opacity: 0.0, depthWrite: false, blending: T.AdditiveBlending });
_strikeLine = new T.Mesh(slGeo, slMat);
_strikeLine.position.set(board.center, S_BASE - NH / 2, 0);
_strikeLine.position.set(board.center, (sY(0) + sY(nStr - 1)) / 2, 0);
_strikeLine.renderOrder = 6;
fretG.add(_strikeLine);
}
@ -14494,12 +14513,17 @@
_sparkUpdate(_jdt);
_streakHeat += (Math.min(1, _streakHits / 16) - _streakHeat) * 0.08; // #7 ease heat
if (_strikeLine) {
const hf = _ndHitFlash * _hitFx, mf = _ndMissFlash * _hitFx;
const lit = Math.max(hf, mf);
const m = _strikeLine.material;
m.opacity = 0.10 + (_streakFx ? 0.12 * _streakHeat : 0) + 0.85 * Math.min(1, lit);
if (lit > 0.002) m.color.setHex(hf >= mf ? 0x22ff88 : 0xff0066);
else m.color.setHex(0x2a3a52);
if (_strikeFx) {
_strikeLine.visible = true;
// Hit-only celebration: faint idle, flashes green on a confirmed
// hit. No red/miss branch — misses live at the gem (charrette 1a).
const hf = _ndHitFlash * _hitFx;
const m = _strikeLine.material;
m.opacity = 0.06 + (_streakFx ? 0.10 * _streakHeat : 0) + 0.85 * Math.min(1, hf);
m.color.setHex(hf > 0.002 ? 0x22ff88 : 0x2a3a52);
} else {
_strikeLine.visible = false;
}
_ndHitFlash *= 0.82; _ndMissFlash *= 0.82;
}
}

View File

@ -602,6 +602,16 @@
class="accent-accent mt-0.5">
</div>
<div class="mt-3 flex items-start justify-between gap-3">
<label for="h3d-strikeline" class="text-xs font-medium text-gray-400">
Strike line
<span class="block text-[10px] text-gray-500 font-normal">A faint line across the hit point that flashes green when you nail a note. Hit-only — misses show on the note itself.</span>
</label>
<input type="checkbox" id="h3d-strikeline" checked
onchange="window.h3dBgSetStrikeLine && window.h3dBgSetStrikeLine(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