mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 19:59:35 +00:00
perf(highway_3d): forceSinglePass on transparent DoubleSide quads
The retrace after the label-swap fix showed getParameters unchanged (~2.5s / ~4% throttled main thread) — the real driver is Three r158+'s transparent-DoubleSide two-pass path: renderBufferDirect renders such objects back side then front side, setting material.needsUpdate BOTH times, i.e. a full getParameters/program-cache lookup twice per object per frame, plus double draw calls. (Found by reading the two-pass branch in the vendored three.module.min.js right next to the getParameters call site.) All 18 transparent DoubleSide materials in this renderer are flat unlit quads — technique markers, sustain rails, chord frames, lane planes, halo bars — where the two-pass self-occlusion ordering buys nothing. Declare forceSinglePass: true on all of them. Also corrects the _setLabelMap comment's churn attribution (that fix removes the label-swap contribution; this one removes the dominant source). Plugin 3.31.1 -> 3.31.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
59aa70ce5a
commit
95cb51b2ad
+1
-1
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s).
|
- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). The dominant residual — steady `getParameters` shader-program re-resolution (~4% of throttled main thread) — turned out to be Three r158+'s transparent-DoubleSide two-pass rendering, which sets `material.needsUpdate` twice per object per frame; all 18 of the 3D highway's transparent DoubleSide materials are flat unlit quads (labels, rails, chord frames, lanes), so they now declare `forceSinglePass: true`, eliminating the recompile churn and halving those objects' draw calls.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **The tuner now tracks what tuning your instrument is *actually* in, so it prompts you to retune in BOTH directions — down to a song's tuning, and back up when the next song needs it.** The coverage check used to compare each song against your fixed instrument-profile tuning, so it only ever prompted you *away* from "home" (e.g. E → Drop C#) and stayed silent coming back (Drop C# → E), even though you'd physically retuned. It now reads the host's live **per-instrument working tuning** (`window.feedBack.workingTuning`) — what your selected instrument is currently in — so coverage is measured against your *actual* tuning and fires both ways. When you clear an auto-opened tuner, the tuner publishes that song's tuning as your instrument's live working tuning (`assumed` — an explicit "I tuned / Skip" refines it in a later PR), so the next song is judged against where you now are. **Per-instrument** — your guitar's and bass's tunings are tracked separately (keyed like the selector), so switching instruments uses the right one. Feature-detected: on a host without the working-tuning capability it falls back to the static `/api/settings` tuning (today's behavior). `plugins/tuner/screen.js` (`_playerTuning` reads `workingTuning` keyed by the selected instrument; `_publishWorkingTuning` writes on clear). Builds on the host `workingTuning` foundation (PR 1 of the series) + the instrument→chart routing (PR 2). Tests: `tests/js/tuner_auto_open.test.js` (both-directions coverage via a live Drop-D working tuning; publish-on-clear targets the right instrument slot) — 29 pass.
|
- **The tuner now tracks what tuning your instrument is *actually* in, so it prompts you to retune in BOTH directions — down to a song's tuning, and back up when the next song needs it.** The coverage check used to compare each song against your fixed instrument-profile tuning, so it only ever prompted you *away* from "home" (e.g. E → Drop C#) and stayed silent coming back (Drop C# → E), even though you'd physically retuned. It now reads the host's live **per-instrument working tuning** (`window.feedBack.workingTuning`) — what your selected instrument is currently in — so coverage is measured against your *actual* tuning and fires both ways. When you clear an auto-opened tuner, the tuner publishes that song's tuning as your instrument's live working tuning (`assumed` — an explicit "I tuned / Skip" refines it in a later PR), so the next song is judged against where you now are. **Per-instrument** — your guitar's and bass's tunings are tracked separately (keyed like the selector), so switching instruments uses the right one. Feature-detected: on a host without the working-tuning capability it falls back to the static `/api/settings` tuning (today's behavior). `plugins/tuner/screen.js` (`_playerTuning` reads `workingTuning` keyed by the selected instrument; `_publishWorkingTuning` writes on clear). Builds on the host `workingTuning` foundation (PR 1 of the series) + the instrument→chart routing (PR 2). Tests: `tests/js/tuner_auto_open.test.js` (both-directions coverage via a live Drop-D working tuning; publish-on-clear targets the right instrument slot) — 29 pass.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "highway_3d",
|
"id": "highway_3d",
|
||||||
"name": "3D Highway",
|
"name": "3D Highway",
|
||||||
"version": "3.31.1",
|
"version": "3.31.2",
|
||||||
"type": "visualization",
|
"type": "visualization",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"script": "screen.js",
|
"script": "screen.js",
|
||||||
|
|||||||
@@ -5278,7 +5278,17 @@
|
|||||||
// appearing on top without depthTest.
|
// appearing on top without depthTest.
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
side: T.DoubleSide,
|
// forceSinglePass accompanies EVERY transparent DoubleSide
|
||||||
|
// material in this file: without it, Three r158+ renders
|
||||||
|
// each such object in TWO passes (back side then front),
|
||||||
|
// setting material.needsUpdate on both — which forces a
|
||||||
|
// full getParameters/program-cache lookup per object per
|
||||||
|
// frame (profiled at ~4% of throttled main-thread time)
|
||||||
|
// and doubles the draw calls. The two-pass path exists to
|
||||||
|
// fix self-occlusion sorting on closed transparent meshes;
|
||||||
|
// all our DoubleSide materials are flat unlit quads
|
||||||
|
// (labels, rails, frames, lanes) where it buys nothing.
|
||||||
|
side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
sm.userData.h3dTechMeshMat = base;
|
sm.userData.h3dTechMeshMat = base;
|
||||||
}
|
}
|
||||||
@@ -6660,7 +6670,7 @@
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: true,
|
depthTest: true,
|
||||||
blending: T.AdditiveBlending,
|
blending: T.AdditiveBlending,
|
||||||
side: T.DoubleSide,
|
side: T.DoubleSide, forceSinglePass: true,
|
||||||
fog: true,
|
fog: true,
|
||||||
}));
|
}));
|
||||||
mAccentHaloNear = mkAccentHaloMats(ACCENT_HALO_OP_NEAR);
|
mAccentHaloNear = mkAccentHaloMats(ACCENT_HALO_OP_NEAR);
|
||||||
@@ -6720,7 +6730,7 @@
|
|||||||
new T.MeshBasicMaterial({
|
new T.MeshBasicMaterial({
|
||||||
vertexColors: true,
|
vertexColors: true,
|
||||||
transparent: true, opacity: 1.0, depthWrite: false,
|
transparent: true, opacity: 1.0, depthWrite: false,
|
||||||
blending: T.AdditiveBlending, side: T.DoubleSide, fog: false,
|
blending: T.AdditiveBlending, side: T.DoubleSide, forceSinglePass: true, fog: false,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
// Notedetect feedback outline (issue #9): hot magenta-red (0xff0066, hue
|
// Notedetect feedback outline (issue #9): hot magenta-red (0xff0066, hue
|
||||||
@@ -6860,7 +6870,7 @@
|
|||||||
emissiveIntensity: 0.9,
|
emissiveIntensity: 0.9,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
side: T.DoubleSide,
|
side: T.DoubleSide, forceSinglePass: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
});
|
});
|
||||||
@@ -6887,7 +6897,7 @@
|
|||||||
color: CHORD_BOX_TEAL_HEX,
|
color: CHORD_BOX_TEAL_HEX,
|
||||||
transparent: true, opacity: 0.85,
|
transparent: true, opacity: 0.85,
|
||||||
depthTest: false, depthWrite: false,
|
depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
pSusRail = pool(noteG, () => {
|
pSusRail = pool(noteG, () => {
|
||||||
const m = new T.Mesh(gSusRail, mSusRailBase.clone());
|
const m = new T.Mesh(gSusRail, mSusRailBase.clone());
|
||||||
@@ -6908,7 +6918,7 @@
|
|||||||
transparent: true, opacity: 0.55,
|
transparent: true, opacity: 0.55,
|
||||||
blending: T.AdditiveBlending,
|
blending: T.AdditiveBlending,
|
||||||
depthTest: false, depthWrite: false,
|
depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
pSusRailBloom = pool(noteG, () => {
|
pSusRailBloom = pool(noteG, () => {
|
||||||
const m = new T.Mesh(gSusRailBloom, mSusRailBloomBase.clone());
|
const m = new T.Mesh(gSusRailBloom, mSusRailBloomBase.clone());
|
||||||
@@ -6922,7 +6932,7 @@
|
|||||||
gTechPlane = new T.PlaneGeometry(1, 1);
|
gTechPlane = new T.PlaneGeometry(1, 1);
|
||||||
pTechPlane = pool(noteG, () => {
|
pTechPlane = pool(noteG, () => {
|
||||||
const m = new T.Mesh(gTechPlane, new T.MeshBasicMaterial({
|
const m = new T.Mesh(gTechPlane, new T.MeshBasicMaterial({
|
||||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide,
|
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
}));
|
}));
|
||||||
m.renderOrder = 1000;
|
m.renderOrder = 1000;
|
||||||
return m;
|
return m;
|
||||||
@@ -6976,7 +6986,7 @@
|
|||||||
uniforms: { map: { value: spriteMat.map } },
|
uniforms: { map: { value: spriteMat.map } },
|
||||||
vertexShader: _imTechVert,
|
vertexShader: _imTechVert,
|
||||||
fragmentShader: _imTechFrag,
|
fragmentShader: _imTechFrag,
|
||||||
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide,
|
transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
const im = new T.InstancedMesh(geo, mat, IM_TECH_CAP);
|
const im = new T.InstancedMesh(geo, mat, IM_TECH_CAP);
|
||||||
im.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
im.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||||
@@ -7089,7 +7099,7 @@
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
fog: false,
|
fog: false,
|
||||||
side: T.DoubleSide,
|
side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
pChordBox = pool(noteG, () => new T.Mesh(
|
pChordBox = pool(noteG, () => new T.Mesh(
|
||||||
@@ -7101,7 +7111,7 @@
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
fog: false,
|
fog: false,
|
||||||
side: T.DoubleSide,
|
side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -7184,7 +7194,7 @@
|
|||||||
_imPMXFillMat = new T.ShaderMaterial({
|
_imPMXFillMat = new T.ShaderMaterial({
|
||||||
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
||||||
transparent: true, depthTest: false, depthWrite: false,
|
transparent: true, depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
imPMXFill = new T.InstancedMesh(gPMXFill, _imPMXFillMat, IM_STRUM_CAP);
|
imPMXFill = new T.InstancedMesh(gPMXFill, _imPMXFillMat, IM_STRUM_CAP);
|
||||||
imPMXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
imPMXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||||
@@ -7264,7 +7274,7 @@
|
|||||||
_imFHXFillMat = new T.ShaderMaterial({
|
_imFHXFillMat = new T.ShaderMaterial({
|
||||||
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
vertexShader: _imFillVert, fragmentShader: _imFillFrag,
|
||||||
transparent: true, depthTest: false, depthWrite: false,
|
transparent: true, depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
imFHXFill = new T.InstancedMesh(gFHXFill, _imFHXFillMat, IM_STRUM_CAP);
|
imFHXFill = new T.InstancedMesh(gFHXFill, _imFHXFillMat, IM_STRUM_CAP);
|
||||||
imFHXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
imFHXFill.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||||
@@ -7345,7 +7355,7 @@
|
|||||||
_imPMXLinesMat = new T.ShaderMaterial({
|
_imPMXLinesMat = new T.ShaderMaterial({
|
||||||
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
||||||
transparent: true, depthTest: false, depthWrite: false,
|
transparent: true, depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
imPMXLines = new T.InstancedMesh(gPMXLines, _imPMXLinesMat, IM_STRUM_CAP);
|
imPMXLines = new T.InstancedMesh(gPMXLines, _imPMXLinesMat, IM_STRUM_CAP);
|
||||||
imPMXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
imPMXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||||
@@ -7427,7 +7437,7 @@
|
|||||||
_imFHXLinesMat = new T.ShaderMaterial({
|
_imFHXLinesMat = new T.ShaderMaterial({
|
||||||
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
vertexShader: _imLinesVert, fragmentShader: _imLinesFrag,
|
||||||
transparent: true, depthTest: false, depthWrite: false,
|
transparent: true, depthTest: false, depthWrite: false,
|
||||||
fog: false, side: T.DoubleSide,
|
fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
});
|
});
|
||||||
imFHXLines = new T.InstancedMesh(gFHXLines, _imFHXLinesMat, IM_STRUM_CAP);
|
imFHXLines = new T.InstancedMesh(gFHXLines, _imFHXLinesMat, IM_STRUM_CAP);
|
||||||
imFHXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
imFHXLines.instanceMatrix.setUsage(T.DynamicDrawUsage);
|
||||||
@@ -7449,28 +7459,28 @@
|
|||||||
gPMXFill,
|
gPMXFill,
|
||||||
new T.MeshBasicMaterial({
|
new T.MeshBasicMaterial({
|
||||||
color: 0x000000, transparent: true, opacity: 1,
|
color: 0x000000, transparent: true, opacity: 1,
|
||||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
pFHXFill = pool(noteG, () => new T.Mesh(
|
pFHXFill = pool(noteG, () => new T.Mesh(
|
||||||
gFHXFill,
|
gFHXFill,
|
||||||
new T.MeshBasicMaterial({
|
new T.MeshBasicMaterial({
|
||||||
color: 0x000000, transparent: true, opacity: 1,
|
color: 0x000000, transparent: true, opacity: 1,
|
||||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
pMuteXLines = pool(noteG, () => new T.Mesh(
|
pMuteXLines = pool(noteG, () => new T.Mesh(
|
||||||
gPMXLines,
|
gPMXLines,
|
||||||
new T.MeshBasicMaterial({
|
new T.MeshBasicMaterial({
|
||||||
color: 0xffffff, transparent: true, opacity: 1,
|
color: 0xffffff, transparent: true, opacity: 1,
|
||||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
pFHXLines = pool(noteG, () => new T.Mesh(
|
pFHXLines = pool(noteG, () => new T.Mesh(
|
||||||
gFHXLines,
|
gFHXLines,
|
||||||
new T.MeshBasicMaterial({
|
new T.MeshBasicMaterial({
|
||||||
color: 0xffffff, transparent: true, opacity: 1,
|
color: 0xffffff, transparent: true, opacity: 1,
|
||||||
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide,
|
depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -9935,12 +9945,14 @@
|
|||||||
// Swap a pooled label sprite's cached texture WITHOUT recompiling.
|
// Swap a pooled label sprite's cached texture WITHOUT recompiling.
|
||||||
// Setting material.needsUpdate bumps material.version, which forces
|
// Setting material.needsUpdate bumps material.version, which forces
|
||||||
// Three.js through getParameters/getProgramCacheKey on the next
|
// Three.js through getParameters/getProgramCacheKey on the next
|
||||||
// render — profiled at ~4% of throttled main-thread time from the
|
// render. Swapping one non-null texture for another does NOT change
|
||||||
// per-frame label map swaps in dense charts. Swapping one non-null
|
// the compiled program (the USE_MAP define is unchanged); only a
|
||||||
// texture for another does NOT change the compiled program (the
|
// null <-> non-null transition does, and pooled label sprites are
|
||||||
// USE_MAP define is unchanged); only a null <-> non-null transition
|
// constructed with a non-null map, so in practice this never
|
||||||
// does, and pooled label sprites are constructed with a non-null
|
// recompiles. (Note: the DOMINANT getParameters churn turned out to
|
||||||
// map, so in practice this never recompiles.
|
// be Three's transparent-DoubleSide two-pass path — see the
|
||||||
|
// forceSinglePass comment in _spriteMat2MeshMat — this helper
|
||||||
|
// removes the label-swap contribution on top of that.)
|
||||||
function _setLabelMap(sprite, srcMat) {
|
function _setLabelMap(sprite, srcMat) {
|
||||||
const m = sprite.material;
|
const m = sprite.material;
|
||||||
if (m.map === srcMat.map) return;
|
if (m.map === srcMat.map) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user