diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index 0ddd9f6..f2005bc 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.27.0", + "version": "3.30.0", "type": "visualization", "bundled": true, "script": "screen.js", diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 766abff..b0a8101 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -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 }; + 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 // 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']); + 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) { if (val === 'true' || val === '1') return true; if (val === 'false' || val === '0') return false; @@ -2125,7 +2125,7 @@ // hysteresis; zoomSmoothing the zoom dead zone; tiltSmoothing the // vertical-tilt deadband + correction strength. All three slider- // 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) { if (_BG_FLOAT_KEYS.has(key)) { const n = parseFloat(val); @@ -2259,6 +2259,13 @@ window.h3dBgSetTextSize = (v) => _bgWriteGlobal('textSize', v); window.h3dBgSetVibrancy = (v) => _bgWriteGlobal('vibrancy', 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.h3dBgSetToneHudPosition = (v) => _bgWriteGlobal('toneHudPosition', v); window.h3dBgSetToneHudSize = (v) => _bgWriteGlobal('toneHudSize', v); @@ -3552,6 +3559,19 @@ // linear blend every frame. let vibrancy = BG_DEFAULTS.vibrancy; 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 fretDividersVisible = BG_DEFAULTS.fretDividersVisible; let chordDiagramVisible = BG_DEFAULTS.chordDiagramVisible; @@ -5912,10 +5932,23 @@ dirLight = new T.DirectionalLight(0xffffff, 0.8); dirLight.position.set(40 * K, 120 * K, 80 * K); scene.add(dirLight); + _applyCinematic(); fretG = new T.Group(); scene.add(fretG); tuningLblG = new T.Group(); scene.add(tuningLblG); 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); lblG = new T.Group(); scene.add(lblG); @@ -6134,6 +6167,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 @@ -7191,7 +7231,7 @@ 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 mark = _ndNormalizeMark(d); @@ -7347,6 +7387,14 @@ textSize = _bgReadSetting(panelKey, 'textSize'); vibrancy = _bgReadSetting(panelKey, 'vibrancy'); 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'); fretDividersVisible = _bgReadSetting(panelKey, 'fretDividersVisible'); chordDiagramVisible = _bgReadSetting(panelKey, 'chordDiagramVisible'); @@ -7785,6 +7833,85 @@ : d; 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() { // Dispose before clearing (traverse: nut/headstock may live in a Group). while (fretG.children.length) { @@ -12585,6 +12712,7 @@ // blocks, so _showHit can be a const and _ndGood is available for the // sustain trail (which renders even when skipBody=true for slide targets). 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 _ndCs = null; // raw provider response — truthy when provider returned a verdict let _ndCsIsObj = false; // typeof _ndCs === 'object' @@ -12760,12 +12888,27 @@ // hit/active → green outline (mHitBright[s]) + green lateral faces; // miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent. if (_ndCs) { + const _vAlpha = (_ndCsIsObj && typeof _ndCs.alpha === 'number') ? _ndCs.alpha : 1; if (_ndState === 'miss') { _ndOutline = mMissOutline; _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) { _ndOutline = mHitBright[s] ?? mGlow[s]; _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 { 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 // omitted: the showFretOnNote setting and its UI helper text // promise digits on the fretboard ghost only, never on the @@ -14012,6 +14156,8 @@ for (const g of _ownedSharedGeos) g?.dispose?.(); _ownedSharedGeos.length = 0; 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; } scene = cam = noteG = beatG = lblG = fretG = tuningLblG = null; ambLight = dirLight = null; @@ -14337,7 +14483,27 @@ } 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) { lyricsCtx.clearRect(0, 0, lyricsCanvas.width, lyricsCanvas.height); // Capture the actual lyrics-banner bottom so overlay cards diff --git a/plugins/highway_3d/settings.html b/plugins/highway_3d/settings.html index e24c3c2..cf6a57d 100644 --- a/plugins/highway_3d/settings.html +++ b/plugins/highway_3d/settings.html @@ -577,6 +577,80 @@ Glowy + +
+ + +

+ How much "juice" a nailed note gets — the strike-line flash and the + spark burst at the hit line. 0 = colour verdict only (no sparks). +

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
@@ -1299,6 +1373,35 @@ if (thsi) thsi.value = String(toneHudSize); if (thslbl) thslbl.textContent = toneHudSize.toFixed(2); + // Hit-feedback "juice" controls — hydrate from saved state so the + // panel reflects persistence on reopen (the renderer already reads + // these via _bgReadSetting; without this the controls always showed + // their default markup, misrepresenting a saved non-default). Reads + // h3d_bg_* directly; defaults mirror BG_DEFAULTS (all bools on, + // hitFx 0.70) and the _bgCoerceBool 'true'/'1' vs 'false'/'0' rules. + try { + const _bgBool = (k, def) => { + const v = localStorage.getItem('h3d_bg_' + k); + return v == null ? def : !(v === 'false' || v === '0'); + }; + const _setChk = (id, on) => { const el = document.getElementById(id); if (el) el.checked = on; }; + _setChk('h3d-sparks', _bgBool('sparks', true)); + _setChk('h3d-cinematic', _bgBool('cinematic', true)); + _setChk('h3d-streakfx', _bgBool('streakFx', true)); + _setChk('h3d-verdictmarks', _bgBool('verdictMarks', true)); + _setChk('h3d-bloom', _bgBool('bloom', true)); + _setChk('h3d-timingfx', _bgBool('timingFx', true)); + const _hf = document.getElementById('h3d-hitfx'); + if (_hf) { + let v = parseFloat(localStorage.getItem('h3d_bg_hitFx')); + if (!isFinite(v)) v = 0.70; + v = Math.max(0, Math.min(1, v)); + _hf.value = String(v); + const _hfl = document.getElementById('h3d-hitfx-label'); + if (_hfl) _hfl.textContent = v.toFixed(2); + } + } catch (_) { /* storage blocked — controls keep their default markup */ } + // (3D Highway palette picker removed — string colors are now set // via the core "Highway String Colors" UI above, which drives both // the 2D and 3D highways. The bg-settings 'palette' key still exists diff --git a/static/vendor/three/addons/postprocessing/EffectComposer.js b/static/vendor/three/addons/postprocessing/EffectComposer.js new file mode 100644 index 0000000..8953c56 --- /dev/null +++ b/static/vendor/three/addons/postprocessing/EffectComposer.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/MaskPass.js b/static/vendor/three/addons/postprocessing/MaskPass.js new file mode 100644 index 0000000..b30811c --- /dev/null +++ b/static/vendor/three/addons/postprocessing/MaskPass.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/OutputPass.js b/static/vendor/three/addons/postprocessing/OutputPass.js new file mode 100644 index 0000000..313d745 --- /dev/null +++ b/static/vendor/three/addons/postprocessing/OutputPass.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/Pass.js b/static/vendor/three/addons/postprocessing/Pass.js new file mode 100644 index 0000000..fc1db1f --- /dev/null +++ b/static/vendor/three/addons/postprocessing/Pass.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/RenderPass.js b/static/vendor/three/addons/postprocessing/RenderPass.js new file mode 100644 index 0000000..29ec553 --- /dev/null +++ b/static/vendor/three/addons/postprocessing/RenderPass.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/ShaderPass.js b/static/vendor/three/addons/postprocessing/ShaderPass.js new file mode 100644 index 0000000..8c49b7e --- /dev/null +++ b/static/vendor/three/addons/postprocessing/ShaderPass.js @@ -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 }; diff --git a/static/vendor/three/addons/postprocessing/UnrealBloomPass.js b/static/vendor/three/addons/postprocessing/UnrealBloomPass.js new file mode 100644 index 0000000..111c661 --- /dev/null +++ b/static/vendor/three/addons/postprocessing/UnrealBloomPass.js @@ -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 + 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 }; diff --git a/static/vendor/three/addons/shaders/CopyShader.js b/static/vendor/three/addons/shaders/CopyShader.js new file mode 100644 index 0000000..8c3f2cd --- /dev/null +++ b/static/vendor/three/addons/shaders/CopyShader.js @@ -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 }; diff --git a/static/vendor/three/addons/shaders/LuminosityHighPassShader.js b/static/vendor/three/addons/shaders/LuminosityHighPassShader.js new file mode 100644 index 0000000..3e542e0 --- /dev/null +++ b/static/vendor/three/addons/shaders/LuminosityHighPassShader.js @@ -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 }; diff --git a/static/vendor/three/addons/shaders/OutputShader.js b/static/vendor/three/addons/shaders/OutputShader.js new file mode 100644 index 0000000..289ac10 --- /dev/null +++ b/static/vendor/three/addons/shaders/OutputShader.js @@ -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 + #include + + 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 };