@codexo/exojs 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/esm/core/dev.d.ts +21 -0
- package/dist/esm/core/dev.js +18 -0
- package/dist/esm/core/dev.js.map +1 -0
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +2 -2
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +5 -1
- package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -1
- package/dist/esm/rendering/TransformBuffer.d.ts +44 -0
- package/dist/esm/rendering/TransformBuffer.js +64 -0
- package/dist/esm/rendering/TransformBuffer.js.map +1 -1
- package/dist/esm/rendering/gradient/Gradient.d.ts +35 -2
- package/dist/esm/rendering/gradient/Gradient.js +51 -5
- package/dist/esm/rendering/gradient/Gradient.js.map +1 -1
- package/dist/esm/rendering/gradient/LinearGradient.d.ts +11 -3
- package/dist/esm/rendering/gradient/LinearGradient.js +23 -0
- package/dist/esm/rendering/gradient/LinearGradient.js.map +1 -1
- package/dist/esm/rendering/gradient/RadialGradient.d.ts +11 -3
- package/dist/esm/rendering/gradient/RadialGradient.js +19 -0
- package/dist/esm/rendering/gradient/RadialGradient.js.map +1 -1
- package/dist/esm/rendering/index.d.ts +1 -1
- package/dist/esm/rendering/pass/RenderPassCoordinator.d.ts +2 -2
- package/dist/esm/rendering/pass/RenderPassDescriptor.d.ts +2 -2
- package/dist/esm/rendering/pass/RenderPassDescriptor.js +1 -1
- package/dist/esm/rendering/plan/RenderCommand.d.ts +21 -2
- package/dist/esm/rendering/plan/RenderCommand.js +34 -1
- package/dist/esm/rendering/plan/RenderCommand.js.map +1 -1
- package/dist/esm/rendering/plan/RenderInstruction.d.ts +51 -0
- package/dist/esm/rendering/plan/RenderInstruction.js +45 -0
- package/dist/esm/rendering/plan/RenderInstruction.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.d.ts +4 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.js +58 -7
- package/dist/esm/rendering/plan/RenderPlanPlayer.js.map +1 -1
- package/dist/esm/rendering/primitives/Graphics.d.ts +70 -5
- package/dist/esm/rendering/primitives/Graphics.js +172 -14
- package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
- package/dist/esm/rendering/sprite/spriteMaterialSources.d.ts +13 -8
- package/dist/esm/rendering/sprite/spriteMaterialSources.js +35 -14
- package/dist/esm/rendering/sprite/spriteMaterialSources.js.map +1 -1
- package/dist/esm/rendering/text/BitmapText.d.ts +2 -0
- package/dist/esm/rendering/text/BitmapText.js +8 -1
- package/dist/esm/rendering/text/BitmapText.js.map +1 -1
- package/dist/esm/rendering/text/BmFont.js +3 -0
- package/dist/esm/rendering/text/BmFont.js.map +1 -1
- package/dist/esm/rendering/text/GlyphSdf.d.ts +14 -0
- package/dist/esm/rendering/text/GlyphSdf.js +41 -11
- package/dist/esm/rendering/text/GlyphSdf.js.map +1 -1
- package/dist/esm/rendering/text/TextStyle.d.ts +5 -0
- package/dist/esm/rendering/text/TextStyle.js +1 -1
- package/dist/esm/rendering/text/TextStyle.js.map +1 -1
- package/dist/esm/rendering/texture/RenderTexture.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +23 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +50 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +3 -3
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +8 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +8 -0
- package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +2 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +62 -39
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js +7 -0
- package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js.map +1 -1
- package/dist/esm/rendering/webgl2/glsl/sprite.vert.js +1 -1
- package/dist/esm/rendering/webgl2/glsl/text-color.frag.js +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +16 -3
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +49 -4
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +53 -41
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +17 -11
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.d.ts +2 -2
- package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js +2 -2
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +9 -1
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +122 -77
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.d.ts +7 -0
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js +22 -13
- package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +32 -0
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +58 -12
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js.map +1 -1
- package/dist/esm/resources/Loader.js +1 -1
- package/dist/esm/resources/Loader.js.map +1 -1
- package/dist/exo.esm.js +1022 -265
- package/dist/exo.esm.js.map +1 -1
- package/package.json +9 -5
package/dist/exo.esm.js
CHANGED
|
@@ -11828,6 +11828,39 @@ const makeMaterialKey = (drawable, backend) => {
|
|
|
11828
11828
|
bindKey,
|
|
11829
11829
|
};
|
|
11830
11830
|
};
|
|
11831
|
+
/**
|
|
11832
|
+
* Whether a draw command's renderer reads the shared {@link TransformBuffer} /
|
|
11833
|
+
* transform storage. The render-group upload boundary packs each command's
|
|
11834
|
+
* world transform (+ tint) keyed by its `nodeIndex`; only renderers that fetch
|
|
11835
|
+
* those rows back from the buffer need a record written.
|
|
11836
|
+
*
|
|
11837
|
+
* Sprite and Mesh (and their subclasses — {@link AnimatedSprite}, Video,
|
|
11838
|
+
* Graphics' meshes) fetch the transform via `nodeIndex` and therefore consume
|
|
11839
|
+
* it. Text/BitmapText and particle renderers pack their own per-node data into
|
|
11840
|
+
* a private data texture / uniforms and never touch the shared buffer, so they
|
|
11841
|
+
* opt out via `_consumesSharedTransform === false` and their writes are skipped.
|
|
11842
|
+
*
|
|
11843
|
+
* Anything else — a custom renderer, or a drawable with no registered renderer
|
|
11844
|
+
* (resolve throws) — defaults to writing, so behaviour is unchanged for any
|
|
11845
|
+
* path that might still rely on the shared transform.
|
|
11846
|
+
*
|
|
11847
|
+
* @internal
|
|
11848
|
+
*/
|
|
11849
|
+
const drawCommandUsesSharedTransform = (command, backend) => {
|
|
11850
|
+
const registry = backend.rendererRegistry;
|
|
11851
|
+
if (!registry || typeof registry.resolve !== 'function') {
|
|
11852
|
+
return true;
|
|
11853
|
+
}
|
|
11854
|
+
try {
|
|
11855
|
+
const renderer = registry.resolve(command.drawable);
|
|
11856
|
+
return renderer._consumesSharedTransform !== false;
|
|
11857
|
+
}
|
|
11858
|
+
catch {
|
|
11859
|
+
// No renderer registered for a custom drawable: keep the conservative
|
|
11860
|
+
// write so any consumer of the shared transform keeps working.
|
|
11861
|
+
return true;
|
|
11862
|
+
}
|
|
11863
|
+
};
|
|
11831
11864
|
|
|
11832
11865
|
/** @internal */
|
|
11833
11866
|
class MutableRenderPlan {
|
|
@@ -12264,6 +12297,22 @@ class RenderPlanOptimizer {
|
|
|
12264
12297
|
}
|
|
12265
12298
|
}
|
|
12266
12299
|
|
|
12300
|
+
// Internal dev/diagnostic utilities. Guards are stripped in production builds
|
|
12301
|
+
// when false is replaced with `false` by the rollup replace plugin.
|
|
12302
|
+
/**
|
|
12303
|
+
* Assert `condition` at dev/test time. Throws with `[ExoJS] message` when the
|
|
12304
|
+
* condition is falsy and `false` is true. No-op in production builds.
|
|
12305
|
+
*/
|
|
12306
|
+
function assert(condition, message) {
|
|
12307
|
+
}
|
|
12308
|
+
/**
|
|
12309
|
+
* Print a `console.warn` at most once per unique `key`.
|
|
12310
|
+
* Subsequent calls with the same key are silenced to avoid per-frame spam.
|
|
12311
|
+
* No-op in production builds.
|
|
12312
|
+
*/
|
|
12313
|
+
function warnOnce(key, message) {
|
|
12314
|
+
}
|
|
12315
|
+
|
|
12267
12316
|
/**
|
|
12268
12317
|
* A {@link Size} subclass that fires a callback whenever `width` or `height`
|
|
12269
12318
|
* changes. Used internally by layout-aware types to invalidate cached geometry
|
|
@@ -13547,6 +13596,47 @@ class RenderEffectExecutor {
|
|
|
13547
13596
|
}
|
|
13548
13597
|
}
|
|
13549
13598
|
|
|
13599
|
+
/**
|
|
13600
|
+
* Materialize the {@link RenderGroup} batch units contained directly in
|
|
13601
|
+
* `scope`. Consecutive draw instructions that share a defined `groupIndex`
|
|
13602
|
+
* coalesce into one group; any non-draw entry (a nested group or barrier)
|
|
13603
|
+
* breaks the run, and a draw whose `groupIndex` is still `undefined` (i.e.
|
|
13604
|
+
* the plan has not been optimized) forms its own singleton group — mirroring
|
|
13605
|
+
* the adjacency semantics the mesh renderers already rely on.
|
|
13606
|
+
*
|
|
13607
|
+
* This is a read-only view over an (optimized) scope; it does not mutate the
|
|
13608
|
+
* plan or affect playback order.
|
|
13609
|
+
*
|
|
13610
|
+
* @internal
|
|
13611
|
+
*/
|
|
13612
|
+
function collectRenderGroups(scope) {
|
|
13613
|
+
const groups = [];
|
|
13614
|
+
let current = null;
|
|
13615
|
+
for (const entry of scope.entries) {
|
|
13616
|
+
if (entry.kind !== RenderEntryKind.Draw) {
|
|
13617
|
+
current = null;
|
|
13618
|
+
continue;
|
|
13619
|
+
}
|
|
13620
|
+
const command = entry.command;
|
|
13621
|
+
const groupIndex = command.groupIndex;
|
|
13622
|
+
if (current !== null && groupIndex !== undefined && groupIndex === current.groupIndex) {
|
|
13623
|
+
current.instructions.push(command);
|
|
13624
|
+
continue;
|
|
13625
|
+
}
|
|
13626
|
+
current = {
|
|
13627
|
+
groupIndex: groupIndex ?? 0,
|
|
13628
|
+
material: command.material,
|
|
13629
|
+
instructions: [command],
|
|
13630
|
+
};
|
|
13631
|
+
groups.push(current);
|
|
13632
|
+
if (groupIndex === undefined) {
|
|
13633
|
+
// Unoptimized / non-batchable draw: never coalesce with the next one.
|
|
13634
|
+
current = null;
|
|
13635
|
+
}
|
|
13636
|
+
}
|
|
13637
|
+
return groups;
|
|
13638
|
+
}
|
|
13639
|
+
|
|
13550
13640
|
/** @internal */
|
|
13551
13641
|
class RenderPlanPlayer {
|
|
13552
13642
|
static play(plan, backend) {
|
|
@@ -13563,7 +13653,7 @@ class RenderPlanPlayer {
|
|
|
13563
13653
|
if (pass.clearColor !== null) {
|
|
13564
13654
|
backend.clear(pass.clearColor);
|
|
13565
13655
|
}
|
|
13566
|
-
this.
|
|
13656
|
+
this._playScope(pass.root, backend, hooks, this._createPlaybackContext());
|
|
13567
13657
|
}
|
|
13568
13658
|
}
|
|
13569
13659
|
finally {
|
|
@@ -13571,31 +13661,81 @@ class RenderPlanPlayer {
|
|
|
13571
13661
|
}
|
|
13572
13662
|
}
|
|
13573
13663
|
static playScope(scope, backend) {
|
|
13664
|
+
const hooks = backend;
|
|
13665
|
+
this._playScope(scope, backend, hooks, this._createPlaybackContext());
|
|
13666
|
+
}
|
|
13667
|
+
static _playScope(scope, backend, hooks, context) {
|
|
13574
13668
|
if (scope.kind === RenderEntryKind.Barrier) {
|
|
13575
13669
|
RenderEffectExecutor.play(scope, backend, childScope => {
|
|
13576
|
-
this.
|
|
13670
|
+
this._playScope(childScope, backend, hooks, context);
|
|
13577
13671
|
});
|
|
13578
13672
|
return;
|
|
13579
13673
|
}
|
|
13580
|
-
this._playGroup(scope, backend);
|
|
13674
|
+
this._playGroup(scope, backend, hooks, context);
|
|
13581
13675
|
}
|
|
13582
|
-
static _playGroup(scope, backend) {
|
|
13583
|
-
const
|
|
13676
|
+
static _playGroup(scope, backend, hooks, context) {
|
|
13677
|
+
const groups = collectRenderGroups(scope);
|
|
13678
|
+
let groupCursor = 0;
|
|
13679
|
+
let currentGroup = null;
|
|
13680
|
+
let currentInstructionIndex = 0;
|
|
13584
13681
|
for (const entry of scope.entries) {
|
|
13585
13682
|
if (entry.kind === RenderEntryKind.Draw) {
|
|
13683
|
+
if (currentGroup === null) {
|
|
13684
|
+
currentGroup = groups[groupCursor];
|
|
13685
|
+
currentInstructionIndex = 0;
|
|
13686
|
+
hooks._beginRenderGroup?.(currentGroup);
|
|
13687
|
+
hooks._prepareRenderGroupUpload?.(currentGroup, this._createRenderGroupPlaybackContext(currentGroup.instructions.length, context.passInstructionIndex, context.passGroupIndex));
|
|
13688
|
+
context.passGroupIndex++;
|
|
13689
|
+
}
|
|
13690
|
+
// Allocate the per-draw instruction slot only when a backend consumes
|
|
13691
|
+
// it. No shipped backend implements `_prepareRenderInstructionSlot`, so
|
|
13692
|
+
// skipping the `Object.freeze` slot allocation removes per-draw garbage
|
|
13693
|
+
// from the playback hot path while preserving the extension point.
|
|
13694
|
+
if (hooks._prepareRenderInstructionSlot !== undefined) {
|
|
13695
|
+
const slot = this._createRenderInstructionSlot(currentInstructionIndex, context.passInstructionIndex);
|
|
13696
|
+
hooks._prepareRenderInstructionSlot(entry.command, slot);
|
|
13697
|
+
}
|
|
13586
13698
|
hooks._prepareDrawCommand?.(entry.command);
|
|
13587
13699
|
backend.draw(entry.command.drawable);
|
|
13700
|
+
currentInstructionIndex++;
|
|
13701
|
+
context.passInstructionIndex++;
|
|
13702
|
+
if (currentGroup !== null && currentInstructionIndex === currentGroup.instructions.length) {
|
|
13703
|
+
hooks._endRenderGroup?.(currentGroup);
|
|
13704
|
+
currentGroup = null;
|
|
13705
|
+
currentInstructionIndex = 0;
|
|
13706
|
+
groupCursor++;
|
|
13707
|
+
}
|
|
13588
13708
|
}
|
|
13589
13709
|
else if (entry.kind === RenderEntryKind.Group) {
|
|
13590
|
-
this._playGroup(entry.scope, backend);
|
|
13710
|
+
this._playGroup(entry.scope, backend, hooks, context);
|
|
13591
13711
|
}
|
|
13592
13712
|
else {
|
|
13593
13713
|
RenderEffectExecutor.play(entry.scope, backend, childScope => {
|
|
13594
|
-
this.
|
|
13714
|
+
this._playScope(childScope, backend, hooks, context);
|
|
13595
13715
|
});
|
|
13596
13716
|
}
|
|
13597
13717
|
}
|
|
13598
13718
|
}
|
|
13719
|
+
static _createPlaybackContext() {
|
|
13720
|
+
return {
|
|
13721
|
+
passInstructionIndex: 0,
|
|
13722
|
+
passGroupIndex: 0,
|
|
13723
|
+
};
|
|
13724
|
+
}
|
|
13725
|
+
static _createRenderGroupPlaybackContext(groupInstructionCount, firstPassInstructionIndex, passGroupIndex) {
|
|
13726
|
+
return Object.freeze({
|
|
13727
|
+
groupInstructionCount,
|
|
13728
|
+
firstPassInstructionIndex,
|
|
13729
|
+
lastPassInstructionIndex: firstPassInstructionIndex + groupInstructionCount - 1,
|
|
13730
|
+
passGroupIndex,
|
|
13731
|
+
});
|
|
13732
|
+
}
|
|
13733
|
+
static _createRenderInstructionSlot(groupInstructionIndex, passInstructionIndex) {
|
|
13734
|
+
return Object.freeze({
|
|
13735
|
+
groupInstructionIndex,
|
|
13736
|
+
passInstructionIndex,
|
|
13737
|
+
});
|
|
13738
|
+
}
|
|
13599
13739
|
}
|
|
13600
13740
|
|
|
13601
13741
|
/** @internal — single source of truth for plan build→optimize→play. */
|
|
@@ -13615,7 +13755,7 @@ function playRenderTree(node, backend) {
|
|
|
13615
13755
|
* Whether a render pass carries a stencil attachment.
|
|
13616
13756
|
*
|
|
13617
13757
|
* WebGL2 stencil is ambient per-target GL state, so this flag is largely
|
|
13618
|
-
* informational there; the WebGPU backend
|
|
13758
|
+
* informational there; the WebGPU backend uses it to
|
|
13619
13759
|
* decide whether a pass descriptor needs a `depthStencilAttachment`.
|
|
13620
13760
|
* @internal
|
|
13621
13761
|
*/
|
|
@@ -16654,6 +16794,7 @@ class BmFont {
|
|
|
16654
16794
|
fontData;
|
|
16655
16795
|
textures;
|
|
16656
16796
|
constructor(fontData, textures) {
|
|
16797
|
+
assert(textures.length === fontData.pages.length, `BmFont: texture count (${textures.length}) must match page count (${fontData.pages.length})`);
|
|
16657
16798
|
this.fontData = fontData;
|
|
16658
16799
|
this.textures = textures;
|
|
16659
16800
|
}
|
|
@@ -16975,7 +17116,7 @@ class TextStyle {
|
|
|
16975
17116
|
constructor(options = {}) {
|
|
16976
17117
|
const explicitFace = typeof FontFace !== 'undefined' && options.font instanceof FontFace ? options.font : null;
|
|
16977
17118
|
this._fontFamily = explicitFace ? explicitFace.family : (options.fontFamily ?? 'Arial');
|
|
16978
|
-
this._fontWeight = options.fontWeight ?? '
|
|
17119
|
+
this._fontWeight = options.fontWeight ?? 'normal';
|
|
16979
17120
|
this._fontStyle = options.fontStyle ?? 'normal';
|
|
16980
17121
|
this._fontSize = options.fontSize ?? 20;
|
|
16981
17122
|
this._fillColor = options.fillColor ? options.fillColor.clone() : Color.white.clone();
|
|
@@ -17256,11 +17397,14 @@ class BmFontAdapter {
|
|
|
17256
17397
|
_scale;
|
|
17257
17398
|
/** Fallback advance for characters not present in the font (≈ ½ line height). */
|
|
17258
17399
|
_fallbackAdvance;
|
|
17400
|
+
/** Identifier used as part of the warnOnce key — derived from the first page filename. */
|
|
17401
|
+
_fontId;
|
|
17259
17402
|
constructor(fontData, textures, scale) {
|
|
17260
17403
|
this._fontData = fontData;
|
|
17261
17404
|
this._textures = textures;
|
|
17262
17405
|
this._scale = scale;
|
|
17263
17406
|
this._fallbackAdvance = fontData.lineHeight * scale * 0.5;
|
|
17407
|
+
this._fontId = fontData.pages[0] ?? 'unknown';
|
|
17264
17408
|
}
|
|
17265
17409
|
getGlyph(char, _fontSize) {
|
|
17266
17410
|
const cp = char.codePointAt(0) ?? 0;
|
|
@@ -17269,7 +17413,9 @@ class BmFontAdapter {
|
|
|
17269
17413
|
const lh = this._fontData.lineHeight;
|
|
17270
17414
|
const base = this._fontData.base;
|
|
17271
17415
|
if (g === undefined) {
|
|
17272
|
-
// Unknown glyph —
|
|
17416
|
+
// Unknown glyph — warn once per font + codepoint, then return an invisible
|
|
17417
|
+
// placeholder with a cursor advance so layout still makes progress.
|
|
17418
|
+
warnOnce(`bitmaptext:${this._fontId}:${cp}`, `BitmapText: missing glyph U+${cp.toString(16).toUpperCase().padStart(4, '0')} ('${char}') in "${this._fontId}"`);
|
|
17273
17419
|
return {
|
|
17274
17420
|
x: 0,
|
|
17275
17421
|
y: 0,
|
|
@@ -17284,6 +17430,7 @@ class BmFontAdapter {
|
|
|
17284
17430
|
uvBottom: 0,
|
|
17285
17431
|
};
|
|
17286
17432
|
}
|
|
17433
|
+
assert(g.page < this._textures.length, `BitmapText: glyph page index ${g.page} is out of range — font "${this._fontId}" has ${this._textures.length} page(s)`);
|
|
17287
17434
|
const texW = this._textures[g.page]?.width ?? 1;
|
|
17288
17435
|
const texH = this._textures[g.page]?.height ?? 1;
|
|
17289
17436
|
return {
|
|
@@ -17655,6 +17802,14 @@ class GlyphSdf {
|
|
|
17655
17802
|
_radius;
|
|
17656
17803
|
_cutoff;
|
|
17657
17804
|
_font;
|
|
17805
|
+
_fontSize;
|
|
17806
|
+
// Font-level ascent/descent — measured once from a reference string so that
|
|
17807
|
+
// all glyph tiles share the same height and baseline position. This avoids
|
|
17808
|
+
// the per-glyph actualBoundingBoxAscent variation that causes baseline jumps
|
|
17809
|
+
// when fontBoundingBoxAscent is unavailable (common for loaded web fonts).
|
|
17810
|
+
_fontAscent = 0;
|
|
17811
|
+
_fontDescent = 0;
|
|
17812
|
+
_metricsReady = false;
|
|
17658
17813
|
_canvasW = 0;
|
|
17659
17814
|
_canvasH = 0;
|
|
17660
17815
|
_canvas;
|
|
@@ -17671,18 +17826,19 @@ class GlyphSdf {
|
|
|
17671
17826
|
this._buffer = options.buffer ?? 8;
|
|
17672
17827
|
this._radius = options.radius ?? this._buffer;
|
|
17673
17828
|
this._cutoff = options.cutoff ?? 0.5;
|
|
17829
|
+
this._fontSize = options.fontSize;
|
|
17674
17830
|
const stylePart = options.fontStyle && options.fontStyle !== 'normal' ? `${options.fontStyle} ` : '';
|
|
17675
17831
|
const weight = options.fontWeight ?? 'normal';
|
|
17676
17832
|
this._font = `${stylePart}${weight} ${options.fontSize}px ${options.fontFamily}`;
|
|
17677
17833
|
if (typeof OffscreenCanvas !== 'undefined') {
|
|
17678
17834
|
const c = new OffscreenCanvas(1, 1);
|
|
17679
17835
|
this._canvas = c;
|
|
17680
|
-
this._ctx = c.getContext('2d');
|
|
17836
|
+
this._ctx = c.getContext('2d', { willReadFrequently: true });
|
|
17681
17837
|
}
|
|
17682
17838
|
else {
|
|
17683
17839
|
const c = document.createElement('canvas');
|
|
17684
17840
|
this._canvas = c;
|
|
17685
|
-
this._ctx = c.getContext('2d');
|
|
17841
|
+
this._ctx = c.getContext('2d', { willReadFrequently: true });
|
|
17686
17842
|
}
|
|
17687
17843
|
}
|
|
17688
17844
|
/**
|
|
@@ -17692,7 +17848,28 @@ class GlyphSdf {
|
|
|
17692
17848
|
* fresh `Uint8ClampedArray`; the caller may hold a reference to it safely
|
|
17693
17849
|
* across subsequent `draw()` calls.
|
|
17694
17850
|
*/
|
|
17851
|
+
/**
|
|
17852
|
+
* Ensure font-level ascent/descent are measured. Called once per GlyphSdf
|
|
17853
|
+
* instance — deferred to the first draw() so the canvas context is ready.
|
|
17854
|
+
* Uses `fontBoundingBoxAscent/Descent` when available; falls back to
|
|
17855
|
+
* `actualBoundingBoxAscent/Descent` measured from a reference string that
|
|
17856
|
+
* covers ascenders ('H'), descenders ('g'), and diacritics ('É').
|
|
17857
|
+
* Either way the same values are used for EVERY glyph so all tiles share a
|
|
17858
|
+
* consistent height and a consistent baseline position within the tile.
|
|
17859
|
+
*/
|
|
17860
|
+
_ensureFontMetrics() {
|
|
17861
|
+
if (this._metricsReady)
|
|
17862
|
+
return;
|
|
17863
|
+
const ctx = this._ctx;
|
|
17864
|
+
ctx.font = this._font;
|
|
17865
|
+
ctx.textBaseline = 'alphabetic';
|
|
17866
|
+
const m = ctx.measureText('HgjpqyÉÅ');
|
|
17867
|
+
this._fontAscent = Math.max(1, Math.ceil(m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent ?? this._fontSize * 0.8));
|
|
17868
|
+
this._fontDescent = Math.max(1, Math.ceil(m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent ?? this._fontSize * 0.2));
|
|
17869
|
+
this._metricsReady = true;
|
|
17870
|
+
}
|
|
17695
17871
|
draw(char) {
|
|
17872
|
+
this._ensureFontMetrics();
|
|
17696
17873
|
const ctx = this._ctx;
|
|
17697
17874
|
const buf = this._buffer;
|
|
17698
17875
|
// ── Measure ──────────────────────────────────────────────────────────
|
|
@@ -17700,20 +17877,20 @@ class GlyphSdf {
|
|
|
17700
17877
|
ctx.textBaseline = 'alphabetic';
|
|
17701
17878
|
const m = ctx.measureText(char);
|
|
17702
17879
|
const advance = m.width;
|
|
17880
|
+
// Width uses per-glyph actual bounds — each glyph has a different width.
|
|
17703
17881
|
// Canvas 2D `actualBoundingBoxLeft` is the distance from the text's
|
|
17704
17882
|
// left-alignment point going LEFT to the left edge of the glyph bounding
|
|
17705
17883
|
// box. For most LTR characters this is 0; italic fonts may have a small
|
|
17706
17884
|
// positive value (left overhang).
|
|
17707
17885
|
const bbLeft = Math.max(0, Math.ceil(m.actualBoundingBoxLeft ?? 0));
|
|
17708
17886
|
const bbRight = Math.max(0, Math.ceil(m.actualBoundingBoxRight ?? advance));
|
|
17709
|
-
const bbAscent = Math.max(0, Math.ceil(m.fontBoundingBoxAscent ??
|
|
17710
|
-
m.actualBoundingBoxAscent ??
|
|
17711
|
-
0));
|
|
17712
|
-
const bbDescent = Math.max(0, Math.ceil(m.fontBoundingBoxDescent ??
|
|
17713
|
-
m.actualBoundingBoxDescent ??
|
|
17714
|
-
0));
|
|
17715
17887
|
const glyphWidth = Math.max(1, bbLeft + bbRight);
|
|
17716
|
-
|
|
17888
|
+
// Height uses font-level metrics — same for every glyph of this font/size
|
|
17889
|
+
// so that all tiles are the same height and the baseline is always at
|
|
17890
|
+
// buf + _fontAscent from the tile top. If per-glyph actualBoundingBoxAscent
|
|
17891
|
+
// were used here instead, different glyphs would have their baselines at
|
|
17892
|
+
// different positions in world space (the visual baseline-jump bug).
|
|
17893
|
+
const glyphHeight = this._fontAscent + this._fontDescent;
|
|
17717
17894
|
const tileW = glyphWidth + 2 * buf;
|
|
17718
17895
|
const tileH = glyphHeight + 2 * buf;
|
|
17719
17896
|
// ── Resize canvas if the tile dimensions changed ───────────────────────
|
|
@@ -17726,8 +17903,8 @@ class GlyphSdf {
|
|
|
17726
17903
|
// ── Rasterize white glyph on transparent background ───────────────────
|
|
17727
17904
|
ctx.clearRect(0, 0, tileW, tileH);
|
|
17728
17905
|
ctx.fillStyle = '#ffffff';
|
|
17729
|
-
//
|
|
17730
|
-
ctx.fillText(char, buf + bbLeft, buf +
|
|
17906
|
+
// Baseline at buf + _fontAscent — consistent across all glyphs.
|
|
17907
|
+
ctx.fillText(char, buf + bbLeft, buf + this._fontAscent);
|
|
17731
17908
|
const rgba = ctx.getImageData(0, 0, tileW, tileH).data;
|
|
17732
17909
|
const n = tileW * tileH;
|
|
17733
17910
|
// ── Grow working arrays lazily ────────────────────────────────────────
|
|
@@ -18814,9 +18991,48 @@ class TransformBuffer {
|
|
|
18814
18991
|
_frameHash = hashOffset >>> 0;
|
|
18815
18992
|
_lastCommittedHash = 0;
|
|
18816
18993
|
_lastCommittedCount = -1;
|
|
18994
|
+
_writeCount = 0;
|
|
18995
|
+
_skippedWriteCount = 0;
|
|
18996
|
+
_uploadCount = 0;
|
|
18997
|
+
_uploadedRecordCount = 0;
|
|
18817
18998
|
get count() {
|
|
18818
18999
|
return this._count;
|
|
18819
19000
|
}
|
|
19001
|
+
/**
|
|
19002
|
+
* Number of transform rows written into the buffer (via {@link write} /
|
|
19003
|
+
* {@link push}) since the last {@link begin}. Internal stat for profiling and
|
|
19004
|
+
* regression guards; does not affect packing.
|
|
19005
|
+
* @internal
|
|
19006
|
+
*/
|
|
19007
|
+
get writeCount() {
|
|
19008
|
+
return this._writeCount;
|
|
19009
|
+
}
|
|
19010
|
+
/**
|
|
19011
|
+
* Number of draw commands whose transform write was skipped since the last
|
|
19012
|
+
* {@link begin} — recorded by the backend for renderers that opt out of the
|
|
19013
|
+
* shared transform storage (`_consumesSharedTransform === false`).
|
|
19014
|
+
* @internal
|
|
19015
|
+
*/
|
|
19016
|
+
get skippedWriteCount() {
|
|
19017
|
+
return this._skippedWriteCount;
|
|
19018
|
+
}
|
|
19019
|
+
/**
|
|
19020
|
+
* Number of GPU uploads (texture / storage writes) issued for this buffer
|
|
19021
|
+
* since the last {@link begin}. Recorded by the backend at its upload
|
|
19022
|
+
* boundary; an unchanged frame uploads zero times.
|
|
19023
|
+
* @internal
|
|
19024
|
+
*/
|
|
19025
|
+
get uploadCount() {
|
|
19026
|
+
return this._uploadCount;
|
|
19027
|
+
}
|
|
19028
|
+
/**
|
|
19029
|
+
* Total transform rows pushed to the GPU across all uploads since the last
|
|
19030
|
+
* {@link begin}.
|
|
19031
|
+
* @internal
|
|
19032
|
+
*/
|
|
19033
|
+
get uploadedRecordCount() {
|
|
19034
|
+
return this._uploadedRecordCount;
|
|
19035
|
+
}
|
|
18820
19036
|
get capacity() {
|
|
18821
19037
|
return this._data.length / floatsPerSlot;
|
|
18822
19038
|
}
|
|
@@ -18832,6 +19048,10 @@ class TransformBuffer {
|
|
|
18832
19048
|
}
|
|
18833
19049
|
this._count = 0;
|
|
18834
19050
|
this._frameHash = hashOffset >>> 0;
|
|
19051
|
+
this._writeCount = 0;
|
|
19052
|
+
this._skippedWriteCount = 0;
|
|
19053
|
+
this._uploadCount = 0;
|
|
19054
|
+
this._uploadedRecordCount = 0;
|
|
18835
19055
|
return this;
|
|
18836
19056
|
}
|
|
18837
19057
|
push(transform, tint) {
|
|
@@ -18865,6 +19085,27 @@ class TransformBuffer {
|
|
|
18865
19085
|
for (let i = 0; i < floatsPerSlot; i++) {
|
|
18866
19086
|
this._frameHash = this._mix(this._frameHash, this._hashFloat(data[offset + i]));
|
|
18867
19087
|
}
|
|
19088
|
+
this._writeCount++;
|
|
19089
|
+
return this;
|
|
19090
|
+
}
|
|
19091
|
+
/**
|
|
19092
|
+
* Record that a draw command's transform write was intentionally skipped
|
|
19093
|
+
* because its renderer opts out of the shared transform storage. Counts
|
|
19094
|
+
* toward {@link skippedWriteCount} only — buffer contents are untouched.
|
|
19095
|
+
* @internal
|
|
19096
|
+
*/
|
|
19097
|
+
recordSkippedWrite() {
|
|
19098
|
+
this._skippedWriteCount++;
|
|
19099
|
+
return this;
|
|
19100
|
+
}
|
|
19101
|
+
/**
|
|
19102
|
+
* Record a GPU upload of `recordCount` transform rows. Called by the backend
|
|
19103
|
+
* at its upload boundary after committing a snapshot; affects stats only.
|
|
19104
|
+
* @internal
|
|
19105
|
+
*/
|
|
19106
|
+
recordUpload(recordCount) {
|
|
19107
|
+
this._uploadCount++;
|
|
19108
|
+
this._uploadedRecordCount += recordCount;
|
|
18868
19109
|
return this;
|
|
18869
19110
|
}
|
|
18870
19111
|
commitSnapshot(minCount = 0) {
|
|
@@ -19809,16 +20050,16 @@ const initialIndexCapacity$2 = 192;
|
|
|
19809
20050
|
const initialNodeIndexCapacity = 64;
|
|
19810
20051
|
const defaultVertexColor = 0xffffffff; // white, full alpha
|
|
19811
20052
|
const maxCustomTextureSlots$2 = 8;
|
|
19812
|
-
const transformTextureUnit = 8;
|
|
20053
|
+
const transformTextureUnit$1 = 8;
|
|
19813
20054
|
class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
19814
20055
|
_defaultShader = new Shader(vertexSource$3, fragmentSource$3);
|
|
19815
20056
|
_customShaders = new Map();
|
|
19816
20057
|
_compatibilityCache = new Map();
|
|
19817
20058
|
_textureUnitScratch = new Int32Array([0]);
|
|
19818
|
-
_transformUnitScratch = new Int32Array([transformTextureUnit]);
|
|
20059
|
+
_transformUnitScratch = new Int32Array([transformTextureUnit$1]);
|
|
19819
20060
|
// Pre-built texture-unit indices used for custom-shader sampler bindings;
|
|
19820
20061
|
// pre-allocated so the per-frame uniform path stays allocation-free.
|
|
19821
|
-
_slotScratches = Array.from({ length: Math.max(transformTextureUnit + 1, maxCustomTextureSlots$2 + 1) }, (_, i) => new Int32Array([i]));
|
|
20062
|
+
_slotScratches = Array.from({ length: Math.max(transformTextureUnit$1 + 1, maxCustomTextureSlots$2 + 1) }, (_, i) => new Int32Array([i]));
|
|
19822
20063
|
_vertexCapacity = initialVertexCapacity$2;
|
|
19823
20064
|
_indexCapacity = initialIndexCapacity$2;
|
|
19824
20065
|
_nodeIndexCapacity = initialNodeIndexCapacity;
|
|
@@ -19972,9 +20213,9 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
|
19972
20213
|
this._packIndices(draw.mesh, 0);
|
|
19973
20214
|
const nodeIndex = draw.command?.nodeIndex ?? 0;
|
|
19974
20215
|
if (draw.command === null) {
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
20216
|
+
// The synthetic, non-plan path does not arrive through a render-group
|
|
20217
|
+
// upload boundary, so write its transform into the shared buffer directly.
|
|
20218
|
+
backend._writeTransformCommand(this._createSyntheticCommand(draw.mesh, nodeIndex));
|
|
19978
20219
|
}
|
|
19979
20220
|
this._nodeIndexData[0] = nodeIndex >>> 0;
|
|
19980
20221
|
backend.bindVertexArrayObject(connection.dynamicVao);
|
|
@@ -20045,7 +20286,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
|
20045
20286
|
shader.getUniform('u_projection').setValue(backend.view.getTransform().toArray(false));
|
|
20046
20287
|
}
|
|
20047
20288
|
if (shader.uniforms.has('u_transforms')) {
|
|
20048
|
-
backend.bindTransformBufferTexture(transformTextureUnit, maxNodeIndex + 1);
|
|
20289
|
+
backend.bindTransformBufferTexture(transformTextureUnit$1, maxNodeIndex + 1);
|
|
20049
20290
|
shader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
20050
20291
|
}
|
|
20051
20292
|
if (shader.uniforms.has('u_texture')) {
|
|
@@ -20429,6 +20670,14 @@ const wordsPerInstance$2 = instanceStrideBytes$3 / Uint32Array.BYTES_PER_ELEMENT
|
|
|
20429
20670
|
const indicesPerQuad = 6;
|
|
20430
20671
|
const quadIndices$1 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
20431
20672
|
class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
20673
|
+
/**
|
|
20674
|
+
* The particle system's transform is bound as a `u_systemTransform` uniform and
|
|
20675
|
+
* each particle is positioned system-locally, so this renderer never reads the
|
|
20676
|
+
* shared {@link TransformBuffer}; the render-group upload boundary skips writing
|
|
20677
|
+
* transform records for particle draws.
|
|
20678
|
+
* @internal
|
|
20679
|
+
*/
|
|
20680
|
+
_consumesSharedTransform = false;
|
|
20432
20681
|
_shader;
|
|
20433
20682
|
_batchSize;
|
|
20434
20683
|
_instanceData;
|
|
@@ -20796,10 +21045,15 @@ class WebGl2PassCoordinator {
|
|
|
20796
21045
|
* `WebGl2SpriteRenderer` pairs {@link spriteVertexGlsl} with the material's
|
|
20797
21046
|
* fragment (ignoring any author-supplied `glsl.vertex`), and
|
|
20798
21047
|
* `WebGpuSpriteRenderer` prepends {@link spriteVertexWgsl} to the material's
|
|
20799
|
-
* fragment WGSL. Both pin the per-instance attribute locations
|
|
21048
|
+
* fragment WGSL. Both pin the per-instance attribute locations and the
|
|
20800
21049
|
* projection binding exactly as the default sprite path does, so a custom
|
|
20801
21050
|
* material keeps the single `drawArraysInstanced` / `drawIndexed` batch.
|
|
20802
21051
|
*
|
|
21052
|
+
* Both paths (locations 0, 3, 4, 5, 6) fetch each instance's world transform
|
|
21053
|
+
* from the shared transform storage keyed by `a_nodeIndex` / `nodeIndex`: the
|
|
21054
|
+
* WebGL2 path samples the `u_transforms` texture, the WGSL path reads the
|
|
21055
|
+
* `transforms` storage buffer (group(0) binding(1)).
|
|
21056
|
+
*
|
|
20803
21057
|
* A custom fragment receives the interpolated `v_texcoord` and premultiplied
|
|
20804
21058
|
* `v_color`, plus the per-batch base texture bound as `u_texture` (WebGL2 unit 0
|
|
20805
21059
|
* / WGSL group(1) binding 0). Material uniforms and additional textures bind on
|
|
@@ -20818,13 +21072,13 @@ precision highp int;
|
|
|
20818
21072
|
// to the per-instance buffer; gl_VertexID 0..3 selects which corner of the
|
|
20819
21073
|
// quad this invocation is computing.
|
|
20820
21074
|
layout(location = 0) in vec4 a_localBounds; // left, top, right, bottom (local space)
|
|
20821
|
-
layout(location = 1) in vec3 a_transformAB; // a, b, x — first row of 2D affine
|
|
20822
|
-
layout(location = 2) in vec3 a_transformCD; // c, d, y — second row
|
|
20823
21075
|
layout(location = 3) in vec4 a_uvBounds; // uMin, vMin, uMax, vMax (normalised, already flipY-swapped)
|
|
20824
21076
|
layout(location = 4) in vec4 a_color; // RGBA tint
|
|
20825
21077
|
layout(location = 5) in uint a_textureSlot;
|
|
21078
|
+
layout(location = 6) in uint a_nodeIndex; // row into the shared transform buffer
|
|
20826
21079
|
|
|
20827
21080
|
uniform mat3 u_projection;
|
|
21081
|
+
uniform sampler2D u_transforms; // shared per-frame transform buffer (3 texels/row)
|
|
20828
21082
|
|
|
20829
21083
|
out vec2 v_texcoord;
|
|
20830
21084
|
out vec4 v_color;
|
|
@@ -20839,8 +21093,14 @@ void main(void) {
|
|
|
20839
21093
|
float localX = (cornerX == 0) ? a_localBounds.x : a_localBounds.z;
|
|
20840
21094
|
float localY = (cornerY == 0) ? a_localBounds.y : a_localBounds.w;
|
|
20841
21095
|
|
|
20842
|
-
|
|
20843
|
-
|
|
21096
|
+
// Fetch the per-instance world transform from the shared buffer (row =
|
|
21097
|
+
// a_nodeIndex): texel 0 = (a, b, c, d), texel 1 = (tx, ty, 0, 0).
|
|
21098
|
+
int row = int(a_nodeIndex);
|
|
21099
|
+
vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0);
|
|
21100
|
+
vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0);
|
|
21101
|
+
|
|
21102
|
+
float worldX = (m0.x * localX) + (m0.y * localY) + m1.x;
|
|
21103
|
+
float worldY = (m0.z * localX) + (m0.w * localY) + m1.y;
|
|
20844
21104
|
|
|
20845
21105
|
gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);
|
|
20846
21106
|
|
|
@@ -20855,11 +21115,11 @@ void main(void) {
|
|
|
20855
21115
|
/**
|
|
20856
21116
|
* WGSL vertex stage + shared bindings for the custom sprite-material path.
|
|
20857
21117
|
* Prepend to a material's fragment WGSL: it declares the per-instance
|
|
20858
|
-
* `VertexInput` (locations 0
|
|
20859
|
-
* consumes, the group(0) projection uniform
|
|
20860
|
-
*
|
|
20861
|
-
*
|
|
20862
|
-
* `VertexOutput`.
|
|
21118
|
+
* `VertexInput` (locations 0, 3, 4, 5, 6), the `VertexOutput` a custom
|
|
21119
|
+
* `@fragment` consumes, the group(0) projection uniform + shared transform
|
|
21120
|
+
* storage buffer, the group(1) base texture and sampler (`u_texture` /
|
|
21121
|
+
* `u_sampler`), and the `vertexMain` entry point. The fragment author adds their
|
|
21122
|
+
* group(2) bindings and a `fragmentMain` reading `VertexOutput`.
|
|
20863
21123
|
* @internal
|
|
20864
21124
|
*/
|
|
20865
21125
|
const spriteVertexWgsl = `
|
|
@@ -20867,18 +21127,24 @@ struct ProjectionUniforms {
|
|
|
20867
21127
|
matrix: mat4x4<f32>,
|
|
20868
21128
|
};
|
|
20869
21129
|
|
|
21130
|
+
struct TransformSlot {
|
|
21131
|
+
m0: vec4<f32>,
|
|
21132
|
+
m1: vec4<f32>,
|
|
21133
|
+
m2: vec4<f32>,
|
|
21134
|
+
};
|
|
21135
|
+
|
|
20870
21136
|
@group(0) @binding(0) var<uniform> projection: ProjectionUniforms;
|
|
21137
|
+
@group(0) @binding(1) var<storage, read> transforms: array<TransformSlot>;
|
|
20871
21138
|
|
|
20872
21139
|
@group(1) @binding(0) var u_texture: texture_2d<f32>;
|
|
20873
21140
|
@group(1) @binding(1) var u_sampler: sampler;
|
|
20874
21141
|
|
|
20875
21142
|
struct VertexInput {
|
|
20876
21143
|
@location(0) localBounds: vec4<f32>,
|
|
20877
|
-
@location(1) transformAB: vec3<f32>,
|
|
20878
|
-
@location(2) transformCD: vec3<f32>,
|
|
20879
21144
|
@location(3) uvBounds: vec4<f32>,
|
|
20880
21145
|
@location(4) color: vec4<f32>,
|
|
20881
21146
|
@location(5) textureSlot: u32,
|
|
21147
|
+
@location(6) nodeIndex: u32,
|
|
20882
21148
|
};
|
|
20883
21149
|
|
|
20884
21150
|
struct VertexOutput {
|
|
@@ -20897,8 +21163,12 @@ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutp
|
|
|
20897
21163
|
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
20898
21164
|
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
20899
21165
|
|
|
20900
|
-
|
|
20901
|
-
|
|
21166
|
+
// Fetch this instance's world transform from the shared storage buffer,
|
|
21167
|
+
// keyed by nodeIndex: m0 = (a, b, c, d), m1 = (tx, ty, 0, 0). (m2 carries the
|
|
21168
|
+
// node tint, unused here — the sprite keeps its own per-instance color.)
|
|
21169
|
+
let slot = transforms[input.nodeIndex];
|
|
21170
|
+
let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
|
|
21171
|
+
let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
|
|
20902
21172
|
|
|
20903
21173
|
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
20904
21174
|
|
|
@@ -20914,7 +21184,7 @@ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutp
|
|
|
20914
21184
|
|
|
20915
21185
|
var fragmentSource$1 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Multi-texture sprite batching: up to 8 textures bound per draw call,\n// each fragment selects its source via a flat-interpolated slot index.\n//\n// GLSL ES 3.0 forbids non-constant array-of-sampler indexing unless the\n// expression is dynamically uniform — which a per-vertex slot is not\n// once different triangles in the same batch carry different slots. The\n// if/else chain below dispatches statically and dodges that constraint.\n\nuniform sampler2D u_texture0;\nuniform sampler2D u_texture1;\nuniform sampler2D u_texture2;\nuniform sampler2D u_texture3;\nuniform sampler2D u_texture4;\nuniform sampler2D u_texture5;\nuniform sampler2D u_texture6;\nuniform sampler2D u_texture7;\n\nin vec2 v_texcoord;\nin vec4 v_color;\nflat in uint v_textureSlot;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 sampleColor;\n\n if (v_textureSlot == 0u) {\n sampleColor = texture(u_texture0, v_texcoord);\n } else if (v_textureSlot == 1u) {\n sampleColor = texture(u_texture1, v_texcoord);\n } else if (v_textureSlot == 2u) {\n sampleColor = texture(u_texture2, v_texcoord);\n } else if (v_textureSlot == 3u) {\n sampleColor = texture(u_texture3, v_texcoord);\n } else if (v_textureSlot == 4u) {\n sampleColor = texture(u_texture4, v_texcoord);\n } else if (v_textureSlot == 5u) {\n sampleColor = texture(u_texture5, v_texcoord);\n } else if (v_textureSlot == 6u) {\n sampleColor = texture(u_texture6, v_texcoord);\n } else {\n sampleColor = texture(u_texture7, v_texcoord);\n }\n\n fragColor = sampleColor * v_color;\n}\n";
|
|
20916
21186
|
|
|
20917
|
-
var vertexSource$1 = "#version 300 es\nprecision lowp float;\nprecision
|
|
21187
|
+
var vertexSource$1 = "#version 300 es\nprecision lowp float;\nprecision highp int;\n\n// Per-instance attributes (divisor = 1). Each Sprite contributes one entry\n// to the per-instance buffer; gl_VertexID 0..3 selects which corner of the\n// quad this invocation is computing.\nlayout(location = 0) in vec4 a_localBounds; // left, top, right, bottom (local space)\nlayout(location = 3) in vec4 a_uvBounds; // uMin, vMin, uMax, vMax (normalised, already flipY-swapped)\nlayout(location = 4) in vec4 a_color; // RGBA tint\nlayout(location = 5) in uint a_textureSlot;\nlayout(location = 6) in uint a_nodeIndex; // row into the shared transform buffer\n\nuniform mat3 u_projection;\nuniform sampler2D u_transforms; // shared per-frame transform buffer (3 texels/row)\n\nout vec2 v_texcoord;\nout vec4 v_color;\nflat out uint v_textureSlot;\n\nvoid main(void) {\n // gl_VertexID 0..3 → corner: 0=TL, 1=TR, 2=BL, 3=BR (TRIANGLE_STRIP order)\n int vid = gl_VertexID;\n int cornerX = vid & 1;\n int cornerY = (vid >> 1) & 1;\n\n // Local-space corner: pick from the bounds rectangle.\n float localX = (cornerX == 0) ? a_localBounds.x : a_localBounds.z;\n float localY = (cornerY == 0) ? a_localBounds.y : a_localBounds.w;\n\n // Fetch the world transform for this instance from the shared buffer,\n // keyed by a_nodeIndex. Row layout: texel 0 = (a, b, c, d),\n // texel 1 = (tx, ty, 0, 0). (texel 2 carries tint, unused here — the\n // sprite keeps its own per-instance a_color.)\n int row = int(a_nodeIndex);\n vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0); // a, b, c, d\n vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0); // tx, ty, 0, 0\n\n // world = M * (localX, localY, 1)\n float worldX = (m0.x * localX) + (m0.y * localY) + m1.x;\n float worldY = (m0.z * localX) + (m0.w * localY) + m1.y;\n\n gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);\n\n // UV: pick from the bounds rectangle. The CPU pre-swaps Y bounds when\n // the texture is flipY, so the shader doesn't have to know.\n float u = (cornerX == 0) ? a_uvBounds.x : a_uvBounds.z;\n float v = (cornerY == 0) ? a_uvBounds.y : a_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n v_textureSlot = a_textureSlot;\n}\n";
|
|
20918
21188
|
|
|
20919
21189
|
/**
|
|
20920
21190
|
* Instanced sprite renderer for WebGL2.
|
|
@@ -20924,21 +21194,23 @@ var vertexSource$1 = "#version 300 es\nprecision lowp float;\nprecision lowp int
|
|
|
20924
21194
|
* the quad each invocation is computing. All per-sprite data lives in a
|
|
20925
21195
|
* single per-instance buffer (divisor = 1).
|
|
20926
21196
|
*
|
|
20927
|
-
* Per-instance layout (
|
|
21197
|
+
* Per-instance layout (36 bytes per sprite, 5 attributes):
|
|
20928
21198
|
* ```
|
|
20929
21199
|
* localBounds f32x4 (offset 0, 16 bytes) — left, top, right, bottom
|
|
20930
|
-
*
|
|
20931
|
-
*
|
|
20932
|
-
*
|
|
20933
|
-
*
|
|
20934
|
-
* textureSlot u32 (offset 52, 4 bytes) — multi-texture slot
|
|
21200
|
+
* uvBounds u16x4 norm (offset 16, 8 bytes) — uMin, vMin, uMax, vMax
|
|
21201
|
+
* color u8x4 norm (offset 24, 4 bytes) — RGBA tint
|
|
21202
|
+
* textureSlot u32 (offset 28, 4 bytes) — multi-texture slot
|
|
21203
|
+
* nodeIndex u32 (offset 32, 4 bytes) — row into the shared TransformBuffer
|
|
20935
21204
|
* ```
|
|
20936
21205
|
*
|
|
20937
|
-
*
|
|
20938
|
-
*
|
|
20939
|
-
*
|
|
20940
|
-
*
|
|
20941
|
-
*
|
|
21206
|
+
* The per-instance world transform no longer lives in this buffer: it is
|
|
21207
|
+
* fetched in the vertex shader from the shared {@link TransformBuffer}
|
|
21208
|
+
* texture (`u_transforms`) keyed by `a_nodeIndex`, exactly like the mesh
|
|
21209
|
+
* renderer. The render-group upload boundary (PR #44) already packs every
|
|
21210
|
+
* draw command's transform into that buffer at its stable `nodeIndex`, so the
|
|
21211
|
+
* sprite just reads it back instead of re-packing 24 bytes of affine rows per
|
|
21212
|
+
* instance. vs. the previous per-vertex layout (80 bytes per quad), the
|
|
21213
|
+
* vertex shader still expands one instance into four corners on the GPU.
|
|
20942
21214
|
*
|
|
20943
21215
|
* # Default vs custom-material path
|
|
20944
21216
|
*
|
|
@@ -20952,7 +21224,10 @@ var vertexSource$1 = "#version 300 es\nprecision lowp float;\nprecision lowp int
|
|
|
20952
21224
|
* breaks on material instance, base texture, blend mode, or buffer capacity.
|
|
20953
21225
|
*/
|
|
20954
21226
|
const maxBatchTextures$1 = 8;
|
|
20955
|
-
|
|
21227
|
+
// Sprite base textures occupy units 0..7; the shared transform buffer texture
|
|
21228
|
+
// binds on unit 8, matching the mesh renderer's convention.
|
|
21229
|
+
const transformTextureUnit = 8;
|
|
21230
|
+
const instanceStrideBytes$2 = 36;
|
|
20956
21231
|
const wordsPerInstance$1 = instanceStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
|
|
20957
21232
|
class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
20958
21233
|
_shader;
|
|
@@ -20969,9 +21244,14 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
20969
21244
|
// Texture-unit index scratches reused for sampler-uniform binds so the
|
|
20970
21245
|
// per-batch path stays allocation-free.
|
|
20971
21246
|
_slotScratches = Array.from({ length: maxBatchTextures$1 }, (_, i) => new Int32Array([i]));
|
|
21247
|
+
// Pinned unit index for the shared transform buffer sampler.
|
|
21248
|
+
_transformUnitScratch = new Int32Array([transformTextureUnit]);
|
|
20972
21249
|
_currentMaterial = null;
|
|
20973
21250
|
_currentBaseTexture = null;
|
|
20974
21251
|
_instanceCount = 0;
|
|
21252
|
+
// Highest transform-buffer row referenced by the pending batch; drives the
|
|
21253
|
+
// minimum row count uploaded for the transform texture at flush time.
|
|
21254
|
+
_maxNodeIndex = 0;
|
|
20975
21255
|
_currentBlendMode = null;
|
|
20976
21256
|
_currentView = null;
|
|
20977
21257
|
_currentViewId = -1;
|
|
@@ -20993,11 +21273,17 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
20993
21273
|
}
|
|
20994
21274
|
const backend = this.getBackend();
|
|
20995
21275
|
const material = sprite.material;
|
|
21276
|
+
// The transform lives in the shared buffer, keyed by the draw command's
|
|
21277
|
+
// stable nodeIndex (already packed at the render-group upload boundary).
|
|
21278
|
+
// A direct, non-plan `backend.draw(sprite)` has no command — push the
|
|
21279
|
+
// sprite's transform into the buffer and use the freshly-allocated slot.
|
|
21280
|
+
const command = backend.activeDrawCommand;
|
|
21281
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
20996
21282
|
if (material === null) {
|
|
20997
|
-
this._renderDefault(sprite, texture, backend);
|
|
21283
|
+
this._renderDefault(sprite, texture, backend, nodeIndex);
|
|
20998
21284
|
}
|
|
20999
21285
|
else {
|
|
21000
|
-
this._renderCustom(sprite, texture, material, backend);
|
|
21286
|
+
this._renderCustom(sprite, texture, material, backend, nodeIndex);
|
|
21001
21287
|
}
|
|
21002
21288
|
return this;
|
|
21003
21289
|
}
|
|
@@ -21007,6 +21293,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21007
21293
|
const vao = this._vao;
|
|
21008
21294
|
const connection = this._connection;
|
|
21009
21295
|
if (this._instanceCount === 0 || backend === null || instanceBuffer === null || vao === null || connection === null) {
|
|
21296
|
+
this._maxNodeIndex = 0;
|
|
21010
21297
|
this._resetSlots();
|
|
21011
21298
|
return;
|
|
21012
21299
|
}
|
|
@@ -21034,6 +21321,13 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21034
21321
|
}
|
|
21035
21322
|
this._bindCustomUniforms(shader, material, backend);
|
|
21036
21323
|
}
|
|
21324
|
+
// Bind the shared transform buffer texture (one row per nodeIndex) on the
|
|
21325
|
+
// dedicated unit and point the sampler at it. Done for both the default and
|
|
21326
|
+
// custom programs — both fetch the world transform via a_nodeIndex.
|
|
21327
|
+
backend.bindTransformBufferTexture(transformTextureUnit, this._maxNodeIndex + 1);
|
|
21328
|
+
if (shader.uniforms.has('u_transforms')) {
|
|
21329
|
+
shader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
21330
|
+
}
|
|
21037
21331
|
shader.sync();
|
|
21038
21332
|
backend.bindVertexArrayObject(vao);
|
|
21039
21333
|
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._instanceCount * wordsPerInstance$1));
|
|
@@ -21041,6 +21335,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21041
21335
|
backend.stats.batches++;
|
|
21042
21336
|
backend.stats.drawCalls++;
|
|
21043
21337
|
this._instanceCount = 0;
|
|
21338
|
+
this._maxNodeIndex = 0;
|
|
21044
21339
|
this._resetSlots();
|
|
21045
21340
|
}
|
|
21046
21341
|
onConnect(backend) {
|
|
@@ -21051,11 +21346,10 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21051
21346
|
this._shader.sync();
|
|
21052
21347
|
this._vao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
|
|
21053
21348
|
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_localBounds'), gl.FLOAT, false, instanceStrideBytes$2, 0, false, 1)
|
|
21054
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('
|
|
21055
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('
|
|
21056
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('
|
|
21057
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('
|
|
21058
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_textureSlot'), gl.UNSIGNED_INT, false, instanceStrideBytes$2, 52, true, 1)
|
|
21349
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, instanceStrideBytes$2, 16, false, 1)
|
|
21350
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$2, 24, false, 1)
|
|
21351
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_textureSlot'), gl.UNSIGNED_INT, false, instanceStrideBytes$2, 28, true, 1)
|
|
21352
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, instanceStrideBytes$2, 32, true, 1)
|
|
21059
21353
|
.connect(this._createVaoRuntime(this._connection));
|
|
21060
21354
|
// Pin the per-slot sampler uniforms to texture units 0..N-1.
|
|
21061
21355
|
const samplerUnit = new Int32Array(1);
|
|
@@ -21081,13 +21375,14 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21081
21375
|
this._currentView = null;
|
|
21082
21376
|
this._currentViewId = -1;
|
|
21083
21377
|
this._instanceCount = 0;
|
|
21378
|
+
this._maxNodeIndex = 0;
|
|
21084
21379
|
}
|
|
21085
21380
|
destroy() {
|
|
21086
21381
|
this.disconnect();
|
|
21087
21382
|
this._shader.destroy();
|
|
21088
21383
|
}
|
|
21089
21384
|
/** Default multi-texture path: rotate the base texture through 8 slots. */
|
|
21090
|
-
_renderDefault(sprite, texture, backend) {
|
|
21385
|
+
_renderDefault(sprite, texture, backend, nodeIndex) {
|
|
21091
21386
|
const blendMode = sprite.blendMode;
|
|
21092
21387
|
const batchFull = this._instanceCount >= this._batchSize;
|
|
21093
21388
|
const blendModeChanged = blendMode !== this._currentBlendMode;
|
|
@@ -21109,11 +21404,11 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21109
21404
|
this._activeTextures[slot] = texture;
|
|
21110
21405
|
backend.bindTexture(texture, slot);
|
|
21111
21406
|
}
|
|
21112
|
-
this._packInstance(sprite, texture, slot);
|
|
21407
|
+
this._packInstance(sprite, texture, slot, nodeIndex);
|
|
21113
21408
|
this._instanceCount++;
|
|
21114
21409
|
}
|
|
21115
21410
|
/** Custom-material path: single base texture on unit 0, instanced. */
|
|
21116
|
-
_renderCustom(sprite, texture, material, backend) {
|
|
21411
|
+
_renderCustom(sprite, texture, material, backend, nodeIndex) {
|
|
21117
21412
|
// The material owns its blend mode; the sprite's own blendMode overrides it
|
|
21118
21413
|
// when set away from the default (Normal).
|
|
21119
21414
|
const blendMode = sprite.blendMode === BlendModes.Normal ? material.blendMode : sprite.blendMode;
|
|
@@ -21131,10 +21426,10 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21131
21426
|
this._currentMaterial = material;
|
|
21132
21427
|
this._currentBaseTexture = texture;
|
|
21133
21428
|
// textureSlot word is unused by custom fragments (base binds to unit 0).
|
|
21134
|
-
this._packInstance(sprite, texture, 0);
|
|
21429
|
+
this._packInstance(sprite, texture, 0, nodeIndex);
|
|
21135
21430
|
this._instanceCount++;
|
|
21136
21431
|
}
|
|
21137
|
-
_packInstance(sprite, texture, slot) {
|
|
21432
|
+
_packInstance(sprite, texture, slot, nodeIndex) {
|
|
21138
21433
|
const offset = this._instanceCount * wordsPerInstance$1;
|
|
21139
21434
|
const f32 = this._instanceFloat32;
|
|
21140
21435
|
const u32 = this._instanceUint32;
|
|
@@ -21144,15 +21439,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21144
21439
|
f32[offset + 1] = bounds.top;
|
|
21145
21440
|
f32[offset + 2] = bounds.right;
|
|
21146
21441
|
f32[offset + 3] = bounds.bottom;
|
|
21147
|
-
//
|
|
21148
|
-
const transform = sprite.getGlobalTransform();
|
|
21149
|
-
f32[offset + 4] = transform.a;
|
|
21150
|
-
f32[offset + 5] = transform.b;
|
|
21151
|
-
f32[offset + 6] = transform.x;
|
|
21152
|
-
f32[offset + 7] = transform.c;
|
|
21153
|
-
f32[offset + 8] = transform.d;
|
|
21154
|
-
f32[offset + 9] = transform.y;
|
|
21155
|
-
// uvBounds at offset 10 — 8 bytes = 2 u32 slots, normalised u16x4.
|
|
21442
|
+
// uvBounds at offset 4 — 8 bytes = 2 u32 slots, normalised u16x4.
|
|
21156
21443
|
// Pack (uMin, vMin, uMax, vMax) into two uint32s, with flipY swap
|
|
21157
21444
|
// applied at pack time so the shader can stay flip-agnostic.
|
|
21158
21445
|
const frame = sprite.textureFrame;
|
|
@@ -21165,12 +21452,18 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
21165
21452
|
const vMaxRaw = ((frame.bottom / texHeight) * 0xffff) & 0xffff;
|
|
21166
21453
|
const vMin = texture.flipY ? vMaxRaw : vMinRaw;
|
|
21167
21454
|
const vMax = texture.flipY ? vMinRaw : vMaxRaw;
|
|
21168
|
-
u32[offset +
|
|
21169
|
-
u32[offset +
|
|
21170
|
-
// color (u8x4 packed) at word
|
|
21171
|
-
u32[offset +
|
|
21172
|
-
// textureSlot (u32) at word
|
|
21173
|
-
u32[offset +
|
|
21455
|
+
u32[offset + 4] = uMin | (vMin << 16);
|
|
21456
|
+
u32[offset + 5] = uMax | (vMax << 16);
|
|
21457
|
+
// color (u8x4 packed) at word 6
|
|
21458
|
+
u32[offset + 6] = sprite.tint.toRgba();
|
|
21459
|
+
// textureSlot (u32) at word 7
|
|
21460
|
+
u32[offset + 7] = slot;
|
|
21461
|
+
// nodeIndex (u32) at word 8 — row into the shared transform buffer.
|
|
21462
|
+
const node = nodeIndex >>> 0;
|
|
21463
|
+
u32[offset + 8] = node;
|
|
21464
|
+
if (node > this._maxNodeIndex) {
|
|
21465
|
+
this._maxNodeIndex = node;
|
|
21466
|
+
}
|
|
21174
21467
|
}
|
|
21175
21468
|
_getOrCreateCustomShader(material, gl) {
|
|
21176
21469
|
const cached = this._customShaders.get(material);
|
|
@@ -21499,7 +21792,7 @@ class WebGl2StencilClipper {
|
|
|
21499
21792
|
|
|
21500
21793
|
var textVertSource = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\nlayout(location = 2) in float a_nodeIndex;\n\nuniform mat3 u_projection;\nuniform sampler2D u_nodeData;\n\nflat out int v_nodeIndex;\n out vec2 v_texcoord;\n out vec2 v_gradUV;\n\nvoid main(void) {\n int ni = int(a_nodeIndex);\n\n // texel 0: (a, c, 0, tx) texel 1: (b, d, 0, ty)\n vec4 t0 = texelFetch(u_nodeData, ivec2(0, ni), 0);\n vec4 t1 = texelFetch(u_nodeData, ivec2(1, ni), 0);\n\n // Reconstruct column-major mat3 from stored components\n mat3 xf = mat3(\n t0.x, t0.y, 0.0, // col 0: (a, c, 0)\n t1.x, t1.y, 0.0, // col 1: (b, d, 0)\n t0.w, t1.w, 1.0 // col 2: (tx, ty, 1)\n );\n\n gl_Position = vec4((u_projection * xf * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n v_nodeIndex = ni;\n\n // texel 9: (minX, minY, width, height) — text block bounds in local space.\n // Normalise a_position into [0,1] so fragment shaders can interpolate\n // gradients across the whole block rather than individual atlas UVs.\n vec4 tBounds = texelFetch(u_nodeData, ivec2(9, ni), 0);\n vec2 bSize = tBounds.zw;\n v_gradUV = (bSize.x > 0.0 && bSize.y > 0.0)\n ? clamp((a_position - tBounds.xy) / bSize, 0.0, 1.0)\n : vec2(0.0);\n}\n";
|
|
21501
21794
|
|
|
21502
|
-
var textColorFragSource = "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_texture; // RGBA colour-font / emoji atlas\nuniform sampler2D u_nodeData; // RGBA32F per-node data\n\nflat in int v_nodeIndex;\n in vec2 v_texcoord;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n // texel 2: fillColor (tint multiplier; (1,1,1,1) = no tint)\n vec4 tint = texelFetch(u_nodeData, ivec2(2, v_nodeIndex), 0);\n vec4
|
|
21795
|
+
var textColorFragSource = "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_texture; // RGBA colour-font / emoji atlas\nuniform sampler2D u_nodeData; // RGBA32F per-node data\n\nflat in int v_nodeIndex;\n in vec2 v_texcoord;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n // texel 2: fillColor (tint multiplier; (1,1,1,1) = no tint)\n vec4 tint = texelFetch(u_nodeData, ivec2(2, v_nodeIndex), 0);\n vec4 texel = texture(u_texture, v_texcoord);\n fragColor = texel * tint;\n}\n";
|
|
21503
21796
|
|
|
21504
21797
|
var textMsdfFragSource = "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_texture; // RGB MSDF atlas\nuniform sampler2D u_nodeData; // RGBA32F per-node data (see WebGl2TextRenderer)\nuniform float u_pageSize; // atlas page size in px (for shadow UV conversion)\n\nflat in int v_nodeIndex;\n in vec2 v_texcoord;\n in vec2 v_gradUV;\n\nlayout(location = 0) out vec4 fragColor;\n\nfloat median(float r, float g, float b) {\n return max(min(r, g), min(max(r, g), b));\n}\n\nvoid main(void) {\n int ni = v_nodeIndex;\n\n // Same node data layout as text-sdf.frag\n vec4 tFill = texelFetch(u_nodeData, ivec2(2, ni), 0);\n vec4 tOutline = texelFetch(u_nodeData, ivec2(3, ni), 0);\n vec4 tParams = texelFetch(u_nodeData, ivec2(4, ni), 0); // (outlineMin, shadowAlpha, softness, gradientEnabled)\n vec4 tShadow = texelFetch(u_nodeData, ivec2(5, ni), 0);\n vec4 tShadow2 = texelFetch(u_nodeData, ivec2(6, ni), 0); // (shadowOffX_px, shadowOffY_px, gradientVertical, 0)\n vec4 tGradTop = texelFetch(u_nodeData, ivec2(7, ni), 0);\n vec4 tGradBot = texelFetch(u_nodeData, ivec2(8, ni), 0);\n\n float outlineMin = tParams.x;\n float shadowAlpha = tParams.y;\n float soft = max(tParams.z, 0.001);\n float gradEnabled = tParams.w;\n vec2 shadowOffset = tShadow2.xy / u_pageSize;\n float gradVertical = tShadow2.z;\n\n vec3 msd = texture(u_texture, v_texcoord).rgb;\n float sd = median(msd.r, msd.g, msd.b);\n float fill = smoothstep(0.5 - soft, 0.5 + soft, sd);\n\n float outline = outlineMin < 0.5\n ? smoothstep(outlineMin - soft, outlineMin + soft, sd) * (1.0 - fill)\n : 0.0;\n\n vec3 shadowMsd = texture(u_texture, v_texcoord - shadowOffset).rgb;\n float shadowSd = median(shadowMsd.r, shadowMsd.g, shadowMsd.b);\n float shadow = smoothstep(0.5 - soft, 0.5 + soft, shadowSd)\n * shadowAlpha * (1.0 - fill) * (1.0 - outline);\n\n vec4 fillColor;\n if (gradEnabled > 0.5) {\n float t = gradVertical > 0.5 ? v_gradUV.y : v_gradUV.x;\n fillColor = mix(tGradBot, tGradTop, t);\n } else {\n fillColor = tFill;\n }\n\n fragColor = fillColor * fill\n + tOutline * outline\n + tShadow * shadow;\n}\n";
|
|
21505
21798
|
|
|
@@ -21549,6 +21842,13 @@ const initialNodeCapacity$1 = 32;
|
|
|
21549
21842
|
* same shader type and atlas page are drawn in a single `drawElements` call.
|
|
21550
21843
|
*/
|
|
21551
21844
|
class WebGl2TextRenderer extends AbstractWebGl2Renderer {
|
|
21845
|
+
/**
|
|
21846
|
+
* Text packs its world transform into its own per-node data texture and never
|
|
21847
|
+
* reads the shared {@link TransformBuffer}, so the render-group upload boundary
|
|
21848
|
+
* skips writing transform records for text draws.
|
|
21849
|
+
* @internal
|
|
21850
|
+
*/
|
|
21851
|
+
_consumesSharedTransform = false;
|
|
21552
21852
|
_sdfShader = new Shader(textVertSource, textSdfFragSource);
|
|
21553
21853
|
_msdfShader = new Shader(textVertSource, textMsdfFragSource);
|
|
21554
21854
|
_colorShader = new Shader(textVertSource, textColorFragSource);
|
|
@@ -22138,11 +22438,59 @@ class WebGl2Backend {
|
|
|
22138
22438
|
this._drawPlanDepth++;
|
|
22139
22439
|
}
|
|
22140
22440
|
/** @internal */
|
|
22441
|
+
_prepareRenderGroupUpload(group) {
|
|
22442
|
+
// Pack the whole render group's world transforms (+ tint) into the shared
|
|
22443
|
+
// transform buffer at the group's upload boundary, keyed by each draw
|
|
22444
|
+
// command's stable nodeIndex. Every draw the player will submit for this
|
|
22445
|
+
// group is covered here, before the group's first draw — so the per-draw
|
|
22446
|
+
// write previously done in `_prepareDrawCommand` is no longer needed and
|
|
22447
|
+
// the buffer is filled one contiguous group slice at a time.
|
|
22448
|
+
//
|
|
22449
|
+
// Renderers that pack their own per-node data (Text, Particle) never read
|
|
22450
|
+
// the shared buffer, so their commands are skipped — no consuming draw ever
|
|
22451
|
+
// references their slots (nodeIndex is unique per command).
|
|
22452
|
+
const instructions = group.instructions;
|
|
22453
|
+
for (let i = 0; i < instructions.length; i++) {
|
|
22454
|
+
const command = instructions[i];
|
|
22455
|
+
if (drawCommandUsesSharedTransform(command, this)) {
|
|
22456
|
+
this._writeTransformCommand(command);
|
|
22457
|
+
}
|
|
22458
|
+
else {
|
|
22459
|
+
this._transformBuffer.recordSkippedWrite();
|
|
22460
|
+
}
|
|
22461
|
+
}
|
|
22462
|
+
}
|
|
22463
|
+
/** @internal */
|
|
22141
22464
|
_prepareDrawCommand(command) {
|
|
22465
|
+
// Transform packing now happens at the render-group upload boundary
|
|
22466
|
+
// (`_prepareRenderGroupUpload`); this hook only tracks the active draw so
|
|
22467
|
+
// the mesh renderer can read the current command's nodeIndex.
|
|
22142
22468
|
this._activeDrawCommand = command;
|
|
22469
|
+
}
|
|
22470
|
+
/**
|
|
22471
|
+
* Write a single draw command's world transform (+ tint) into the shared
|
|
22472
|
+
* transform buffer at its `nodeIndex` slot. Used for draws that do not arrive
|
|
22473
|
+
* through a render-group upload boundary — currently the mesh renderer's
|
|
22474
|
+
* synthetic, non-plan instanced path.
|
|
22475
|
+
* @internal
|
|
22476
|
+
*/
|
|
22477
|
+
_writeTransformCommand(command) {
|
|
22143
22478
|
const drawable = command.drawable;
|
|
22144
22479
|
this._transformBuffer.write(command.nodeIndex, drawable.getGlobalTransform(), drawable.tint);
|
|
22145
22480
|
}
|
|
22481
|
+
/**
|
|
22482
|
+
* Append a drawable's world transform (+ tint) to the shared transform buffer
|
|
22483
|
+
* and return the slot it was written to. Used by instanced renderers for draws
|
|
22484
|
+
* that arrive without a render-group upload boundary — i.e. a direct
|
|
22485
|
+
* `backend.draw(drawable)` outside the plan player (`activeDrawCommand === null`),
|
|
22486
|
+
* where no stable `nodeIndex` was assigned. Unlike {@link _writeTransformCommand}
|
|
22487
|
+
* (fixed slot) this allocates a fresh slot, so a batch of synthetic draws does
|
|
22488
|
+
* not collide on a single row.
|
|
22489
|
+
* @internal
|
|
22490
|
+
*/
|
|
22491
|
+
_pushTransform(drawable) {
|
|
22492
|
+
return this._transformBuffer.push(drawable.getGlobalTransform(), drawable.tint);
|
|
22493
|
+
}
|
|
22146
22494
|
/** @internal */
|
|
22147
22495
|
_endDrawPlan() {
|
|
22148
22496
|
this._activeDrawCommand = null;
|
|
@@ -22393,6 +22741,7 @@ class WebGl2Backend {
|
|
|
22393
22741
|
}
|
|
22394
22742
|
if (snapshot.changed || snapshot.count !== this._transformTextureCount || snapshot.hash !== this._transformTextureHash) {
|
|
22395
22743
|
nextTransformTexture.commitRect(0, 0, 3, snapshot.count);
|
|
22744
|
+
this._transformBuffer.recordUpload(snapshot.count);
|
|
22396
22745
|
this._transformTextureHash = snapshot.hash;
|
|
22397
22746
|
this._transformTextureCount = snapshot.count;
|
|
22398
22747
|
}
|
|
@@ -23804,16 +24153,6 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
23804
24153
|
if (customShader !== null && customShader.shader.wgsl === null) {
|
|
23805
24154
|
throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
23806
24155
|
}
|
|
23807
|
-
if (customShader !== null && backend._passCoordinator.stencilActive) {
|
|
23808
|
-
// The WebGPU geometric stencil MVP supports clipping default-material
|
|
23809
|
-
// Sprites and Mesh/Graphics (which select stencil-enabled pipeline
|
|
23810
|
-
// variants below); a custom MeshMaterial under a Geometry clip would need
|
|
23811
|
-
// its own stencil pipeline variants and is not supported yet. Throw at
|
|
23812
|
-
// collection time (inside the clip scope's try) so the surrounding
|
|
23813
|
-
// push/pop balances cleanly, rather than at flush time where the pop has
|
|
23814
|
-
// not yet run.
|
|
23815
|
-
throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. A custom-material Mesh under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
|
|
23816
|
-
}
|
|
23817
24156
|
const vertexCount = mesh.vertexCount;
|
|
23818
24157
|
if (vertexCount === 0) {
|
|
23819
24158
|
return;
|
|
@@ -23930,13 +24269,17 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
23930
24269
|
this._packedIndexData[start + j] = j;
|
|
23931
24270
|
}
|
|
23932
24271
|
}
|
|
23933
|
-
// Pack tint+flags for default path.
|
|
24272
|
+
// Pack tint+flags for default path. Color RGB channels are 0..255; the
|
|
24273
|
+
// shader multiplies the sampled texel by this tint, so normalize to
|
|
24274
|
+
// 0..1 (matching TransformBuffer and the WebGL2 mesh shader). Leaving
|
|
24275
|
+
// them at 0..255 scales every non-zero texel channel past 1.0, which
|
|
24276
|
+
// clamps intermediate colors (gradients, photos) to full saturation.
|
|
23934
24277
|
if (defaultUniformF32 !== null) {
|
|
23935
24278
|
const offsetWords = (defaultUniformIndex * this._uniformAlignment) / Float32Array.BYTES_PER_ELEMENT;
|
|
23936
24279
|
const tint = dc.mesh.tint;
|
|
23937
|
-
defaultUniformF32[offsetWords + 0] = tint.r;
|
|
23938
|
-
defaultUniformF32[offsetWords + 1] = tint.g;
|
|
23939
|
-
defaultUniformF32[offsetWords + 2] = tint.b;
|
|
24280
|
+
defaultUniformF32[offsetWords + 0] = tint.r / 255;
|
|
24281
|
+
defaultUniformF32[offsetWords + 1] = tint.g / 255;
|
|
24282
|
+
defaultUniformF32[offsetWords + 2] = tint.b / 255;
|
|
23940
24283
|
defaultUniformF32[offsetWords + 3] = tint.a;
|
|
23941
24284
|
defaultUniformF32[offsetWords + 4] = dc.premultiplySample ? 1 : 0;
|
|
23942
24285
|
defaultUniformF32[offsetWords + 5] = 0;
|
|
@@ -23976,7 +24319,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
23976
24319
|
drawCursor++;
|
|
23977
24320
|
}
|
|
23978
24321
|
device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes$2);
|
|
23979
|
-
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
24322
|
+
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
|
|
23980
24323
|
// Build/refresh user uniform UBO from the material (re-built every frame
|
|
23981
24324
|
// so mutations to material.uniforms.X are picked up).
|
|
23982
24325
|
this._uploadUserUniforms(material, resources);
|
|
@@ -23984,7 +24327,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
23984
24327
|
// Phase 4: single writeBuffer per resource for the default path.
|
|
23985
24328
|
if (defaultVertices > 0) {
|
|
23986
24329
|
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, defaultVertices * vertexStrideBytes$2);
|
|
23987
|
-
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, defaultIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
24330
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, (defaultIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
|
|
23988
24331
|
}
|
|
23989
24332
|
if (defaultUniformData !== null) {
|
|
23990
24333
|
device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
|
|
@@ -23997,10 +24340,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
23997
24340
|
const renderTargetFormat = backend.renderTargetFormat;
|
|
23998
24341
|
// A clip scope flushes the active renderer on push/pop, so every draw call
|
|
23999
24342
|
// in this batch shares one stencil state — read it once. While active, the
|
|
24000
|
-
// coordinator's pass carries a depth/stencil attachment, so the default
|
|
24001
|
-
// static-batch pipelines must select their
|
|
24002
|
-
//
|
|
24003
|
-
// time), so its pipelines stay stencil-free.
|
|
24343
|
+
// coordinator's pass carries a depth/stencil attachment, so the default,
|
|
24344
|
+
// static-batch, and custom-material pipelines must all select their
|
|
24345
|
+
// stencil-enabled variants to match it.
|
|
24004
24346
|
const stencil = backend._passCoordinator.stencilActive;
|
|
24005
24347
|
let lastShader = null;
|
|
24006
24348
|
let lastBlendMode = null;
|
|
@@ -24076,7 +24418,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24076
24418
|
// batched render pass.
|
|
24077
24419
|
pass.pushDebugGroup('MeshMaterial (custom)');
|
|
24078
24420
|
if (needsPipeline) {
|
|
24079
|
-
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
|
|
24421
|
+
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat, stencil));
|
|
24080
24422
|
lastShader = dc.customShader;
|
|
24081
24423
|
lastBlendMode = dc.blendMode;
|
|
24082
24424
|
lastFormat = renderTargetFormat;
|
|
@@ -24528,7 +24870,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24528
24870
|
const vertexFloatView = new Float32Array(vertexData);
|
|
24529
24871
|
const vertexUintView = new Uint32Array(vertexData);
|
|
24530
24872
|
this._writeMeshVerticesIntoBuffer(mesh, 0, vertexFloatView, vertexUintView);
|
|
24531
|
-
|
|
24873
|
+
// Allocate one extra element when indexCount is odd so the GPU buffer and
|
|
24874
|
+
// writeBuffer byte count can be rounded up to 4 without a buffer overread.
|
|
24875
|
+
const indexData = new Uint16Array(mesh.indexCount + (mesh.indexCount & 1));
|
|
24532
24876
|
if (mesh.indices !== null) {
|
|
24533
24877
|
indexData.set(mesh.indices, 0);
|
|
24534
24878
|
}
|
|
@@ -24537,16 +24881,18 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24537
24881
|
indexData[i] = i;
|
|
24538
24882
|
}
|
|
24539
24883
|
}
|
|
24884
|
+
const indexByteLen = mesh.indexCount * Uint16Array.BYTES_PER_ELEMENT;
|
|
24885
|
+
const alignedIndexByteLen = (indexByteLen + 3) & -4;
|
|
24540
24886
|
const vertexBuffer = this._device.createBuffer({
|
|
24541
24887
|
size: vertexData.byteLength,
|
|
24542
24888
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
24543
24889
|
});
|
|
24544
24890
|
const indexBuffer = this._device.createBuffer({
|
|
24545
|
-
size:
|
|
24891
|
+
size: alignedIndexByteLen,
|
|
24546
24892
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
24547
24893
|
});
|
|
24548
24894
|
this._device.queue.writeBuffer(vertexBuffer, 0, vertexData, 0, vertexData.byteLength);
|
|
24549
|
-
this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset,
|
|
24895
|
+
this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, alignedIndexByteLen);
|
|
24550
24896
|
const disposeListener = () => {
|
|
24551
24897
|
const entry = this._staticGeometryCache.get(geometry);
|
|
24552
24898
|
if (entry === undefined) {
|
|
@@ -24585,13 +24931,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24585
24931
|
}
|
|
24586
24932
|
}
|
|
24587
24933
|
_ensureIndexCapacity(indexCount) {
|
|
24588
|
-
|
|
24589
|
-
|
|
24590
|
-
|
|
24934
|
+
// GPUQueue.writeBuffer requires the byte count to be a multiple of 4.
|
|
24935
|
+
// Round up: odd Uint16 counts (e.g. a 3-index triangle) would otherwise
|
|
24936
|
+
// produce 6-byte writes which the WebGPU validation layer rejects.
|
|
24937
|
+
const requiredBytes = (indexCount * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
|
|
24938
|
+
if (this._packedIndexData.length * Uint16Array.BYTES_PER_ELEMENT < requiredBytes) {
|
|
24939
|
+
this._packedIndexData = new Uint16Array(Math.max(requiredBytes / Uint16Array.BYTES_PER_ELEMENT, this._packedIndexData.length === 0 ? 2 : this._packedIndexData.length * 2));
|
|
24591
24940
|
}
|
|
24592
24941
|
if (requiredBytes > this._indexBufferCapacity) {
|
|
24593
24942
|
this._indexBuffer?.destroy();
|
|
24594
|
-
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ?
|
|
24943
|
+
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? 4 : this._indexBufferCapacity * 2);
|
|
24595
24944
|
this._indexBuffer = this._device.createBuffer({
|
|
24596
24945
|
size: this._indexBufferCapacity,
|
|
24597
24946
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
@@ -24735,14 +25084,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24735
25084
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
24736
25085
|
});
|
|
24737
25086
|
}
|
|
24738
|
-
// Index buffer
|
|
24739
|
-
const indexBytes = resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT;
|
|
24740
|
-
if (resources.indexData.length <
|
|
24741
|
-
resources.indexData = new Uint16Array(Math.max(
|
|
25087
|
+
// Index buffer — capacity must be 4-byte aligned for GPUQueue.writeBuffer.
|
|
25088
|
+
const indexBytes = (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
|
|
25089
|
+
if (resources.indexData.length * Uint16Array.BYTES_PER_ELEMENT < indexBytes) {
|
|
25090
|
+
resources.indexData = new Uint16Array(Math.max(indexBytes / Uint16Array.BYTES_PER_ELEMENT, resources.indexData.length * 2));
|
|
24742
25091
|
}
|
|
24743
25092
|
if (indexBytes > resources.indexBufferCapacity) {
|
|
24744
25093
|
resources.indexBuffer?.destroy();
|
|
24745
|
-
resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 ||
|
|
25094
|
+
resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 || 4);
|
|
24746
25095
|
resources.indexBuffer = device.createBuffer({
|
|
24747
25096
|
size: resources.indexBufferCapacity,
|
|
24748
25097
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
@@ -24828,19 +25177,24 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24828
25177
|
data[off + 10] = 1;
|
|
24829
25178
|
data[off + 11] = 0;
|
|
24830
25179
|
off += 12;
|
|
24831
|
-
// tint (vec4)
|
|
25180
|
+
// tint (vec4). RGB are 0..255; normalize to 0..1 for the shader multiply
|
|
25181
|
+
// (u_mesh.tint is documented as 0..1, matching the default path above).
|
|
24832
25182
|
const tint = mesh.tint;
|
|
24833
|
-
data[off + 0] = tint.r;
|
|
24834
|
-
data[off + 1] = tint.g;
|
|
24835
|
-
data[off + 2] = tint.b;
|
|
25183
|
+
data[off + 0] = tint.r / 255;
|
|
25184
|
+
data[off + 1] = tint.g / 255;
|
|
25185
|
+
data[off + 2] = tint.b / 255;
|
|
24836
25186
|
data[off + 3] = tint.a;
|
|
24837
25187
|
this._device.queue.writeBuffer(resources.meshUniformBuffer, drawCursor * slotBytes, data);
|
|
24838
25188
|
}
|
|
24839
|
-
_getOrCreateCustomPipeline(resources, blendMode, format) {
|
|
24840
|
-
|
|
25189
|
+
_getOrCreateCustomPipeline(resources, blendMode, format, stencil) {
|
|
25190
|
+
// The stencil dimension keeps the clip and no-clip variants distinct,
|
|
25191
|
+
// mirroring the default and static-batch caches: a stencil pipeline carries
|
|
25192
|
+
// depth/stencil state and is only valid in a pass with the matching
|
|
25193
|
+
// attachment, so the two are never interchangeable.
|
|
25194
|
+
const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
24841
25195
|
let pipeline = resources.pipelines.get(cacheKey);
|
|
24842
25196
|
if (pipeline === undefined) {
|
|
24843
|
-
|
|
25197
|
+
const descriptor = {
|
|
24844
25198
|
layout: resources.pipelineLayout,
|
|
24845
25199
|
vertex: {
|
|
24846
25200
|
module: resources.shaderModule,
|
|
@@ -24872,7 +25226,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
24872
25226
|
topology: 'triangle-list',
|
|
24873
25227
|
cullMode: 'none',
|
|
24874
25228
|
},
|
|
24875
|
-
}
|
|
25229
|
+
};
|
|
25230
|
+
// While a geometric clip is active the coordinator's pass carries a
|
|
25231
|
+
// depth/stencil attachment; the content pipeline must test stencil ==
|
|
25232
|
+
// reference and leave depth/stencil otherwise inert to match it.
|
|
25233
|
+
if (stencil) {
|
|
25234
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
25235
|
+
}
|
|
25236
|
+
pipeline = this._device.createRenderPipeline(descriptor);
|
|
24876
25237
|
resources.pipelines.set(cacheKey, pipeline);
|
|
24877
25238
|
}
|
|
24878
25239
|
return pipeline;
|
|
@@ -25111,6 +25472,13 @@ const initialParticleCapacity = 1;
|
|
|
25111
25472
|
const staticVertexData = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
25112
25473
|
const staticIndexData = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
25113
25474
|
class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
25475
|
+
/**
|
|
25476
|
+
* The particle system's transform is bound as a uniform and each particle is
|
|
25477
|
+
* positioned system-locally, so this renderer never reads the shared transform
|
|
25478
|
+
* storage; the plan player skips writing transform records for particle draws.
|
|
25479
|
+
* @internal
|
|
25480
|
+
*/
|
|
25481
|
+
_consumesSharedTransform = false;
|
|
25114
25482
|
_drawCalls = [];
|
|
25115
25483
|
_drawCallCount = 0;
|
|
25116
25484
|
_uniformData = new Float32Array(uniformByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
@@ -25135,12 +25503,6 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
25135
25503
|
if (backend === null || !(texture instanceof Texture) || texture.source === null || texture.width === 0 || texture.height === 0 || system.liveCount === 0) {
|
|
25136
25504
|
return;
|
|
25137
25505
|
}
|
|
25138
|
-
if (backend._passCoordinator.stencilActive) {
|
|
25139
|
-
// MVP boundary: stencil clipping supports default-material Sprites, Meshes,
|
|
25140
|
-
// and Graphics — not ParticleSystems. Throw at collection time (inside the
|
|
25141
|
-
// clip scope's try) so the push/pop balances.
|
|
25142
|
-
throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. ParticleSystem content under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
|
|
25143
|
-
}
|
|
25144
25506
|
backend.setBlendMode(system.blendMode);
|
|
25145
25507
|
const drawCallIndex = this._drawCallCount++;
|
|
25146
25508
|
const drawCall = this._drawCalls[drawCallIndex];
|
|
@@ -25199,7 +25561,7 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
25199
25561
|
if (particleCount === 0) {
|
|
25200
25562
|
continue;
|
|
25201
25563
|
}
|
|
25202
|
-
const pipeline = this._getPipeline(drawCall.blendMode, backend.renderTargetFormat);
|
|
25564
|
+
const pipeline = this._getPipeline(drawCall.blendMode, backend.renderTargetFormat, backend._passCoordinator.stencilActive);
|
|
25203
25565
|
const textureBinding = backend.getTextureBinding(drawCall.texture);
|
|
25204
25566
|
const textureBindGroup = device.createBindGroup({
|
|
25205
25567
|
layout: this._textureBindGroupLayout,
|
|
@@ -25499,13 +25861,13 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
25499
25861
|
}
|
|
25500
25862
|
return { uvMins: mins, uvMaxs: maxs };
|
|
25501
25863
|
}
|
|
25502
|
-
_getPipeline(blendMode, format) {
|
|
25503
|
-
const pipelineKey = `${blendMode}:${format}`;
|
|
25864
|
+
_getPipeline(blendMode, format, stencil) {
|
|
25865
|
+
const pipelineKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
25504
25866
|
const existingPipeline = this._pipelines.get(pipelineKey);
|
|
25505
25867
|
if (existingPipeline) {
|
|
25506
25868
|
return existingPipeline;
|
|
25507
25869
|
}
|
|
25508
|
-
const
|
|
25870
|
+
const descriptor = {
|
|
25509
25871
|
layout: this._pipelineLayout,
|
|
25510
25872
|
vertex: {
|
|
25511
25873
|
module: this._shaderModule,
|
|
@@ -25573,7 +25935,11 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
25573
25935
|
primitive: {
|
|
25574
25936
|
topology: 'triangle-list',
|
|
25575
25937
|
},
|
|
25576
|
-
}
|
|
25938
|
+
};
|
|
25939
|
+
if (stencil) {
|
|
25940
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
25941
|
+
}
|
|
25942
|
+
const pipeline = this._device.createRenderPipeline(descriptor);
|
|
25577
25943
|
this._pipelines.set(pipelineKey, pipeline);
|
|
25578
25944
|
return pipeline;
|
|
25579
25945
|
}
|
|
@@ -25584,8 +25950,8 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
25584
25950
|
* WebGPU implementation of {@link RenderPassCoordinator}.
|
|
25585
25951
|
*
|
|
25586
25952
|
* Owns the GPU render-pass mechanics (acquire/end the active
|
|
25587
|
-
* `GPURenderPassEncoder`), the clear-vs-load decision, and
|
|
25588
|
-
*
|
|
25953
|
+
* `GPURenderPassEncoder`), the clear-vs-load decision, and geometric stencil
|
|
25954
|
+
* clipping: a per-target `depth24plus-stencil8` attachment
|
|
25589
25955
|
* shared across the multiple submits of a clip scope (via `stencilLoadOp:'load'`)
|
|
25590
25956
|
* plus a position-only stencil-write pipeline. Renderers select stencil-enabled
|
|
25591
25957
|
* content pipelines while {@link stencilActive} is true.
|
|
@@ -25849,8 +26215,16 @@ struct ProjectionUniforms {
|
|
|
25849
26215
|
matrix: mat4x4<f32>,
|
|
25850
26216
|
};
|
|
25851
26217
|
|
|
26218
|
+
struct TransformSlot {
|
|
26219
|
+
m0: vec4<f32>,
|
|
26220
|
+
m1: vec4<f32>,
|
|
26221
|
+
m2: vec4<f32>,
|
|
26222
|
+
};
|
|
26223
|
+
|
|
25852
26224
|
@group(0) @binding(0)
|
|
25853
26225
|
var<uniform> projection: ProjectionUniforms;
|
|
26226
|
+
@group(0) @binding(1)
|
|
26227
|
+
var<storage, read> transforms: array<TransformSlot>;
|
|
25854
26228
|
|
|
25855
26229
|
@group(1) @binding(0)
|
|
25856
26230
|
var spriteTexture0: texture_2d<f32>;
|
|
@@ -25886,16 +26260,17 @@ var spriteSampler6: sampler;
|
|
|
25886
26260
|
@group(1) @binding(15)
|
|
25887
26261
|
var spriteSampler7: sampler;
|
|
25888
26262
|
|
|
25889
|
-
// Per-instance vertex layout (
|
|
26263
|
+
// Per-instance vertex layout (36 bytes per sprite). The four corners
|
|
25890
26264
|
// of the quad are derived from @builtin(vertex_index) 0..3 inside the
|
|
25891
|
-
// vertex shader — there is no per-vertex stream.
|
|
26265
|
+
// vertex shader — there is no per-vertex stream. The world transform is
|
|
26266
|
+
// fetched from the shared transform storage buffer keyed by nodeIndex
|
|
26267
|
+
// instead of being packed inline.
|
|
25892
26268
|
struct VertexInput {
|
|
25893
26269
|
@location(0) localBounds: vec4<f32>, // left, top, right, bottom (local space)
|
|
25894
|
-
@location(1) transformAB: vec3<f32>, // first row of 2D affine
|
|
25895
|
-
@location(2) transformCD: vec3<f32>, // second row of 2D affine
|
|
25896
26270
|
@location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
|
|
25897
26271
|
@location(4) color: vec4<f32>, // RGBA tint
|
|
25898
26272
|
@location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
|
|
26273
|
+
@location(6) nodeIndex: u32, // row into the shared transform storage buffer
|
|
25899
26274
|
};
|
|
25900
26275
|
|
|
25901
26276
|
struct VertexOutput {
|
|
@@ -25918,8 +26293,12 @@ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutp
|
|
|
25918
26293
|
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
25919
26294
|
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
25920
26295
|
|
|
25921
|
-
|
|
25922
|
-
|
|
26296
|
+
// Fetch this instance's world transform from the shared storage buffer,
|
|
26297
|
+
// keyed by nodeIndex: m0 = (a, b, c, d), m1 = (tx, ty, 0, 0). (m2 carries the
|
|
26298
|
+
// node tint, unused here — the sprite keeps its own per-instance color.)
|
|
26299
|
+
let slot = transforms[input.nodeIndex];
|
|
26300
|
+
let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
|
|
26301
|
+
let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
|
|
25923
26302
|
|
|
25924
26303
|
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
25925
26304
|
|
|
@@ -25979,7 +26358,7 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
25979
26358
|
return resolvedSample * input.color;
|
|
25980
26359
|
}
|
|
25981
26360
|
`;
|
|
25982
|
-
const instanceStrideBytes =
|
|
26361
|
+
const instanceStrideBytes = 36;
|
|
25983
26362
|
const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
|
|
25984
26363
|
const projectionByteLength = 64;
|
|
25985
26364
|
const initialBatchCapacity = 32;
|
|
@@ -25997,7 +26376,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
25997
26376
|
_textureBindGroupLayout = null;
|
|
25998
26377
|
_pipelineLayout = null;
|
|
25999
26378
|
_uniformBuffer = null;
|
|
26000
|
-
|
|
26379
|
+
// group(0) bind group = projection UBO + shared transform storage buffer.
|
|
26380
|
+
// Recreated whenever the storage buffer identity changes (capacity growth).
|
|
26381
|
+
_transformBindGroup = null;
|
|
26382
|
+
_transformStorageBuffer = null;
|
|
26001
26383
|
_indexBuffer = null;
|
|
26002
26384
|
_instanceBuffer = null;
|
|
26003
26385
|
_instanceCapacity = 0;
|
|
@@ -26009,6 +26391,9 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26009
26391
|
_textureSlots = new Map();
|
|
26010
26392
|
_slotCount = 0;
|
|
26011
26393
|
_instanceCount = 0;
|
|
26394
|
+
// Highest transform-storage row referenced by the pending batch; drives the
|
|
26395
|
+
// minimum row count uploaded for the storage buffer at flush time.
|
|
26396
|
+
_maxNodeIndex = 0;
|
|
26012
26397
|
_currentBlendMode = null;
|
|
26013
26398
|
// Custom-material state. Per-material pipelines/bind groups are cached; the
|
|
26014
26399
|
// current batch's material/base-texture decide when to flush.
|
|
@@ -26031,6 +26416,13 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26031
26416
|
type: 'uniform',
|
|
26032
26417
|
},
|
|
26033
26418
|
},
|
|
26419
|
+
{
|
|
26420
|
+
binding: 1,
|
|
26421
|
+
visibility: GPUShaderStage.VERTEX,
|
|
26422
|
+
buffer: {
|
|
26423
|
+
type: 'read-only-storage',
|
|
26424
|
+
},
|
|
26425
|
+
},
|
|
26034
26426
|
],
|
|
26035
26427
|
});
|
|
26036
26428
|
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
@@ -26065,17 +26457,9 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26065
26457
|
size: projectionByteLength,
|
|
26066
26458
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
26067
26459
|
});
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
{
|
|
26072
|
-
binding: 0,
|
|
26073
|
-
resource: {
|
|
26074
|
-
buffer: this._uniformBuffer,
|
|
26075
|
-
},
|
|
26076
|
-
},
|
|
26077
|
-
],
|
|
26078
|
-
});
|
|
26460
|
+
// The group(0) bind group also binds the shared transform storage buffer,
|
|
26461
|
+
// whose identity changes when its capacity grows — so it is built lazily in
|
|
26462
|
+
// flush() once the active storage buffer is known.
|
|
26079
26463
|
// Static index buffer for the quad. Allocated once at connect; its
|
|
26080
26464
|
// contents never change.
|
|
26081
26465
|
this._indexBuffer = this._device.createBuffer({
|
|
@@ -26098,7 +26482,8 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26098
26482
|
this._pipelines.clear();
|
|
26099
26483
|
this._instanceBuffer = null;
|
|
26100
26484
|
this._indexBuffer = null;
|
|
26101
|
-
this.
|
|
26485
|
+
this._transformBindGroup = null;
|
|
26486
|
+
this._transformStorageBuffer = null;
|
|
26102
26487
|
this._uniformBuffer = null;
|
|
26103
26488
|
this._pipelineLayout = null;
|
|
26104
26489
|
this._customBaseTextureLayout = null;
|
|
@@ -26112,6 +26497,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26112
26497
|
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
26113
26498
|
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
26114
26499
|
this._instanceCount = 0;
|
|
26500
|
+
this._maxNodeIndex = 0;
|
|
26115
26501
|
this._currentBlendMode = null;
|
|
26116
26502
|
this._currentMaterial = null;
|
|
26117
26503
|
this._currentBaseTexture = null;
|
|
@@ -26129,15 +26515,21 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26129
26515
|
return;
|
|
26130
26516
|
}
|
|
26131
26517
|
const material = sprite.material;
|
|
26518
|
+
// The transform lives in the shared storage buffer, keyed by the draw
|
|
26519
|
+
// command's stable nodeIndex (already packed at the draw-command boundary).
|
|
26520
|
+
// A direct, non-plan `backend.draw(sprite)` has no command — push the
|
|
26521
|
+
// sprite's transform into the buffer and use the freshly-allocated slot.
|
|
26522
|
+
const command = backend.activeDrawCommand;
|
|
26523
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
26132
26524
|
if (material === null) {
|
|
26133
|
-
this._renderDefault(sprite, texture, backend);
|
|
26525
|
+
this._renderDefault(sprite, texture, backend, nodeIndex);
|
|
26134
26526
|
}
|
|
26135
26527
|
else {
|
|
26136
|
-
this._renderCustom(sprite, texture, material, backend);
|
|
26528
|
+
this._renderCustom(sprite, texture, material, backend, nodeIndex);
|
|
26137
26529
|
}
|
|
26138
26530
|
}
|
|
26139
26531
|
/** Default multi-texture path: rotate the base texture through 8 slots. */
|
|
26140
|
-
_renderDefault(sprite, texture, backend) {
|
|
26532
|
+
_renderDefault(sprite, texture, backend, nodeIndex) {
|
|
26141
26533
|
const blendMode = sprite.blendMode;
|
|
26142
26534
|
// Flush triggers: blend-mode change, texture-slot exhaustion, or a custom
|
|
26143
26535
|
// batch still in flight that must drain first.
|
|
@@ -26163,20 +26555,14 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26163
26555
|
// typed-array writes in _packInstance silently fall off the end of a
|
|
26164
26556
|
// too-small buffer.
|
|
26165
26557
|
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
26166
|
-
this._packInstance(sprite, texture, packedSlotFlags);
|
|
26558
|
+
this._packInstance(sprite, texture, packedSlotFlags, nodeIndex);
|
|
26167
26559
|
this._instanceCount++;
|
|
26168
26560
|
}
|
|
26169
26561
|
/** Custom-material path: single base texture on group(1), instanced. */
|
|
26170
|
-
_renderCustom(sprite, texture, material, backend) {
|
|
26562
|
+
_renderCustom(sprite, texture, material, backend, nodeIndex) {
|
|
26171
26563
|
if (material.shader.wgsl === null) {
|
|
26172
26564
|
throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
26173
26565
|
}
|
|
26174
|
-
if (backend._passCoordinator.stencilActive) {
|
|
26175
|
-
// MVP boundary: default-material Sprites, Meshes, and Graphics support the
|
|
26176
|
-
// stencil pipeline variants; a custom-material Sprite does not yet. Throw at
|
|
26177
|
-
// collection time so the clip scope's push/pop balances.
|
|
26178
|
-
throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. A custom-material Sprite under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
|
|
26179
|
-
}
|
|
26180
26566
|
// The material owns its blend mode; the sprite's own blendMode overrides it
|
|
26181
26567
|
// when set away from the default (Normal).
|
|
26182
26568
|
const blendMode = sprite.blendMode === BlendModes.Normal ? material.blendMode : sprite.blendMode;
|
|
@@ -26193,15 +26579,14 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26193
26579
|
backend.setBlendMode(blendMode);
|
|
26194
26580
|
// textureSlot word is unused by custom fragments (base binds to group(1)).
|
|
26195
26581
|
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
26196
|
-
this._packInstance(sprite, texture, 0);
|
|
26582
|
+
this._packInstance(sprite, texture, 0, nodeIndex);
|
|
26197
26583
|
this._instanceCount++;
|
|
26198
26584
|
}
|
|
26199
26585
|
flush() {
|
|
26200
26586
|
const backend = this._backend;
|
|
26201
26587
|
const device = this._device;
|
|
26202
26588
|
const uniformBuffer = this._uniformBuffer;
|
|
26203
|
-
|
|
26204
|
-
if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
|
|
26589
|
+
if (!backend || !device || !uniformBuffer) {
|
|
26205
26590
|
return;
|
|
26206
26591
|
}
|
|
26207
26592
|
if (this._instanceCount === 0 && !backend.clearRequested) {
|
|
@@ -26218,13 +26603,19 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26218
26603
|
const pass = backend._passCoordinator.acquirePass().pass;
|
|
26219
26604
|
if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
|
|
26220
26605
|
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes);
|
|
26606
|
+
// Resolve the shared transform storage buffer (rows uploaded up to the
|
|
26607
|
+
// max nodeIndex referenced by this batch) and bind it alongside the
|
|
26608
|
+
// projection UBO on group(0). Both the default and custom programs fetch
|
|
26609
|
+
// the world transform from it via nodeIndex.
|
|
26610
|
+
const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
|
|
26611
|
+
const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
|
|
26221
26612
|
const material = this._currentMaterial;
|
|
26222
26613
|
const stencil = backend._passCoordinator.stencilActive;
|
|
26223
26614
|
if (material === null) {
|
|
26224
26615
|
const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat, stencil);
|
|
26225
26616
|
const textureBindGroup = this._createTextureBindGroup(device, backend);
|
|
26226
26617
|
pass.setPipeline(pipeline);
|
|
26227
|
-
pass.setBindGroup(0,
|
|
26618
|
+
pass.setBindGroup(0, transformBindGroup);
|
|
26228
26619
|
pass.setBindGroup(1, textureBindGroup);
|
|
26229
26620
|
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
26230
26621
|
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
@@ -26232,7 +26623,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26232
26623
|
}
|
|
26233
26624
|
else {
|
|
26234
26625
|
pass.pushDebugGroup('SpriteMaterial (custom)');
|
|
26235
|
-
this._drawCustomBatch(pass, device, backend, material,
|
|
26626
|
+
this._drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil);
|
|
26236
26627
|
pass.popDebugGroup();
|
|
26237
26628
|
}
|
|
26238
26629
|
backend.stats.batches++;
|
|
@@ -26240,11 +26631,31 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26240
26631
|
}
|
|
26241
26632
|
backend._passCoordinator.endPass();
|
|
26242
26633
|
this._instanceCount = 0;
|
|
26634
|
+
this._maxNodeIndex = 0;
|
|
26243
26635
|
this._resetSlots();
|
|
26244
26636
|
this._currentBlendMode = null;
|
|
26245
26637
|
this._currentMaterial = null;
|
|
26246
26638
|
this._currentBaseTexture = null;
|
|
26247
26639
|
}
|
|
26640
|
+
/**
|
|
26641
|
+
* Build (or reuse) the group(0) bind group pairing the fixed projection UBO
|
|
26642
|
+
* with the shared transform storage buffer. Cached against the storage buffer
|
|
26643
|
+
* identity, which changes only when its capacity grows.
|
|
26644
|
+
*/
|
|
26645
|
+
_getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
|
|
26646
|
+
if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) {
|
|
26647
|
+
return this._transformBindGroup;
|
|
26648
|
+
}
|
|
26649
|
+
this._transformStorageBuffer = storageBuffer;
|
|
26650
|
+
this._transformBindGroup = device.createBindGroup({
|
|
26651
|
+
layout: this._uniformBindGroupLayout,
|
|
26652
|
+
entries: [
|
|
26653
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
26654
|
+
{ binding: 1, resource: { buffer: storageBuffer } },
|
|
26655
|
+
],
|
|
26656
|
+
});
|
|
26657
|
+
return this._transformBindGroup;
|
|
26658
|
+
}
|
|
26248
26659
|
destroy() {
|
|
26249
26660
|
this.disconnect();
|
|
26250
26661
|
}
|
|
@@ -26291,24 +26702,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26291
26702
|
}
|
|
26292
26703
|
await Promise.all(promises);
|
|
26293
26704
|
}
|
|
26294
|
-
_packInstance(sprite, texture, packedSlotFlags) {
|
|
26705
|
+
_packInstance(sprite, texture, packedSlotFlags, nodeIndex) {
|
|
26295
26706
|
const offset = this._instanceCount * wordsPerInstance;
|
|
26296
26707
|
const f32 = this._instanceFloat32;
|
|
26297
26708
|
const u32 = this._instanceUint32;
|
|
26709
|
+
// localBounds: left, top, right, bottom (words 0..3, offset 0)
|
|
26298
26710
|
const bounds = sprite.getLocalBounds();
|
|
26299
26711
|
f32[offset + 0] = bounds.left;
|
|
26300
26712
|
f32[offset + 1] = bounds.top;
|
|
26301
26713
|
f32[offset + 2] = bounds.right;
|
|
26302
26714
|
f32[offset + 3] = bounds.bottom;
|
|
26303
|
-
|
|
26304
|
-
|
|
26305
|
-
f32[offset + 5] = transform.b;
|
|
26306
|
-
f32[offset + 6] = transform.x;
|
|
26307
|
-
f32[offset + 7] = transform.c;
|
|
26308
|
-
f32[offset + 8] = transform.d;
|
|
26309
|
-
f32[offset + 9] = transform.y;
|
|
26310
|
-
// uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
|
|
26311
|
-
// applies the flipY swap so the shader stays orientation-agnostic.
|
|
26715
|
+
// uvBounds: u16x4 normalised, packed into two u32 slots (words 4,5, offset
|
|
26716
|
+
// 16). The CPU applies the flipY swap so the shader stays orientation-agnostic.
|
|
26312
26717
|
const frame = sprite.textureFrame;
|
|
26313
26718
|
const texWidth = texture.width;
|
|
26314
26719
|
const texHeight = texture.height;
|
|
@@ -26319,10 +26724,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26319
26724
|
const flipY = texture instanceof Texture && texture.flipY;
|
|
26320
26725
|
const vMin = flipY ? vMaxRaw : vMinRaw;
|
|
26321
26726
|
const vMax = flipY ? vMinRaw : vMaxRaw;
|
|
26322
|
-
u32[offset +
|
|
26323
|
-
u32[offset +
|
|
26324
|
-
|
|
26325
|
-
u32[offset +
|
|
26727
|
+
u32[offset + 4] = uMin | (vMin << 16);
|
|
26728
|
+
u32[offset + 5] = uMax | (vMax << 16);
|
|
26729
|
+
// color (u8x4 packed) at word 6 (offset 24)
|
|
26730
|
+
u32[offset + 6] = sprite.tint.toRgba();
|
|
26731
|
+
// packedSlotFlags (u32) at word 7 (offset 28)
|
|
26732
|
+
u32[offset + 7] = packedSlotFlags;
|
|
26733
|
+
// nodeIndex (u32) at word 8 (offset 32) — row into the shared transform buffer.
|
|
26734
|
+
const node = nodeIndex >>> 0;
|
|
26735
|
+
u32[offset + 8] = node;
|
|
26736
|
+
if (node > this._maxNodeIndex) {
|
|
26737
|
+
this._maxNodeIndex = node;
|
|
26738
|
+
}
|
|
26326
26739
|
}
|
|
26327
26740
|
_ensureInstanceCapacity(instanceCount) {
|
|
26328
26741
|
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
@@ -26424,29 +26837,24 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26424
26837
|
offset: 0,
|
|
26425
26838
|
format: 'float32x4',
|
|
26426
26839
|
},
|
|
26427
|
-
{
|
|
26428
|
-
shaderLocation: 1,
|
|
26429
|
-
offset: 16,
|
|
26430
|
-
format: 'float32x3',
|
|
26431
|
-
},
|
|
26432
|
-
{
|
|
26433
|
-
shaderLocation: 2,
|
|
26434
|
-
offset: 28,
|
|
26435
|
-
format: 'float32x3',
|
|
26436
|
-
},
|
|
26437
26840
|
{
|
|
26438
26841
|
shaderLocation: 3,
|
|
26439
|
-
offset:
|
|
26842
|
+
offset: 16,
|
|
26440
26843
|
format: 'unorm16x4',
|
|
26441
26844
|
},
|
|
26442
26845
|
{
|
|
26443
26846
|
shaderLocation: 4,
|
|
26444
|
-
offset:
|
|
26847
|
+
offset: 24,
|
|
26445
26848
|
format: 'unorm8x4',
|
|
26446
26849
|
},
|
|
26447
26850
|
{
|
|
26448
26851
|
shaderLocation: 5,
|
|
26449
|
-
offset:
|
|
26852
|
+
offset: 28,
|
|
26853
|
+
format: 'uint32',
|
|
26854
|
+
},
|
|
26855
|
+
{
|
|
26856
|
+
shaderLocation: 6,
|
|
26857
|
+
offset: 32,
|
|
26450
26858
|
format: 'uint32',
|
|
26451
26859
|
},
|
|
26452
26860
|
],
|
|
@@ -26476,14 +26884,14 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26476
26884
|
// ---------------------------------------------------------------------------
|
|
26477
26885
|
// Custom-material path
|
|
26478
26886
|
// ---------------------------------------------------------------------------
|
|
26479
|
-
_drawCustomBatch(pass, device, backend, material,
|
|
26887
|
+
_drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil) {
|
|
26480
26888
|
const resources = this._getOrCreateCustomResources(material, device);
|
|
26481
26889
|
const baseTexture = this._currentBaseTexture ?? Texture.empty;
|
|
26482
26890
|
// Re-built every frame so mutations to material.uniforms.X are picked up.
|
|
26483
26891
|
this._uploadUserUniforms(material, resources, device);
|
|
26484
|
-
const pipeline = this._getOrCreateCustomPipeline(resources, this._currentBlendMode, backend.renderTargetFormat, device);
|
|
26892
|
+
const pipeline = this._getOrCreateCustomPipeline(resources, this._currentBlendMode, backend.renderTargetFormat, stencil, device);
|
|
26485
26893
|
pass.setPipeline(pipeline);
|
|
26486
|
-
pass.setBindGroup(0,
|
|
26894
|
+
pass.setBindGroup(0, transformBindGroup);
|
|
26487
26895
|
pass.setBindGroup(1, this._getCustomBaseTextureBindGroup(resources, backend, baseTexture, device));
|
|
26488
26896
|
pass.setBindGroup(2, this._buildUserBindGroup(material, resources, backend, device));
|
|
26489
26897
|
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
@@ -26500,8 +26908,8 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26500
26908
|
throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
26501
26909
|
}
|
|
26502
26910
|
// The engine owns the vertex stage: prepend the canonical sprite vertex
|
|
26503
|
-
// module (VertexInput/VertexOutput, group(0) projection
|
|
26504
|
-
// texture + sampler) to the material's fragment WGSL.
|
|
26911
|
+
// module (VertexInput/VertexOutput, group(0) projection + transform storage,
|
|
26912
|
+
// group(1) base texture + sampler) to the material's fragment WGSL.
|
|
26505
26913
|
const shaderModule = device.createShaderModule({ code: `${spriteVertexWgsl}\n${wgsl}` });
|
|
26506
26914
|
const userLayout = this._buildUserBindGroupLayout(device, material);
|
|
26507
26915
|
const pipelineLayout = device.createPipelineLayout({
|
|
@@ -26526,13 +26934,13 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26526
26934
|
});
|
|
26527
26935
|
return resources;
|
|
26528
26936
|
}
|
|
26529
|
-
_getOrCreateCustomPipeline(resources, blendMode, format, device) {
|
|
26530
|
-
const cacheKey = `${blendMode}:${format}`;
|
|
26937
|
+
_getOrCreateCustomPipeline(resources, blendMode, format, stencil, device) {
|
|
26938
|
+
const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
26531
26939
|
const existing = resources.pipelines.get(cacheKey);
|
|
26532
26940
|
if (existing !== undefined) {
|
|
26533
26941
|
return existing;
|
|
26534
26942
|
}
|
|
26535
|
-
const
|
|
26943
|
+
const descriptor = {
|
|
26536
26944
|
layout: resources.pipelineLayout,
|
|
26537
26945
|
vertex: {
|
|
26538
26946
|
module: resources.shaderModule,
|
|
@@ -26543,11 +26951,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26543
26951
|
stepMode: 'instance',
|
|
26544
26952
|
attributes: [
|
|
26545
26953
|
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
26546
|
-
{ shaderLocation:
|
|
26547
|
-
{ shaderLocation:
|
|
26548
|
-
{ shaderLocation:
|
|
26549
|
-
{ shaderLocation:
|
|
26550
|
-
{ shaderLocation: 5, offset: 52, format: 'uint32' },
|
|
26954
|
+
{ shaderLocation: 3, offset: 16, format: 'unorm16x4' },
|
|
26955
|
+
{ shaderLocation: 4, offset: 24, format: 'unorm8x4' },
|
|
26956
|
+
{ shaderLocation: 5, offset: 28, format: 'uint32' },
|
|
26957
|
+
{ shaderLocation: 6, offset: 32, format: 'uint32' },
|
|
26551
26958
|
],
|
|
26552
26959
|
},
|
|
26553
26960
|
],
|
|
@@ -26566,7 +26973,11 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
26566
26973
|
primitive: {
|
|
26567
26974
|
topology: 'triangle-list',
|
|
26568
26975
|
},
|
|
26569
|
-
}
|
|
26976
|
+
};
|
|
26977
|
+
if (stencil) {
|
|
26978
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
26979
|
+
}
|
|
26980
|
+
const pipeline = device.createRenderPipeline(descriptor);
|
|
26570
26981
|
resources.pipelines.set(cacheKey, pipeline);
|
|
26571
26982
|
return pipeline;
|
|
26572
26983
|
}
|
|
@@ -26913,6 +27324,13 @@ fn fragmentColor(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
26913
27324
|
* render pass.
|
|
26914
27325
|
*/
|
|
26915
27326
|
class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
27327
|
+
/**
|
|
27328
|
+
* Text packs its world transform into its own per-node data buffer and never
|
|
27329
|
+
* reads the shared transform storage, so the plan player skips writing
|
|
27330
|
+
* transform records for text draws.
|
|
27331
|
+
* @internal
|
|
27332
|
+
*/
|
|
27333
|
+
_consumesSharedTransform = false;
|
|
26916
27334
|
_device = null;
|
|
26917
27335
|
_shaderModule = null;
|
|
26918
27336
|
_frameBindGroupLayout = null;
|
|
@@ -26948,12 +27366,6 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
26948
27366
|
render(node) {
|
|
26949
27367
|
if (!this._device)
|
|
26950
27368
|
throw new Error('WebGpuTextRenderer is not connected to a backend.');
|
|
26951
|
-
if (this.getBackend()._passCoordinator.stencilActive) {
|
|
26952
|
-
// MVP boundary: stencil clipping supports default-material Sprites, Meshes,
|
|
26953
|
-
// and Graphics — not Text. Throw at collection time (inside the clip scope's
|
|
26954
|
-
// try) so the push/pop balances.
|
|
26955
|
-
throw new Error('WebGPU geometry stencil clipping currently supports default-material Sprites, Meshes, and Graphics. Text content under a Geometry clip (RenderNode.clip with a Geometry clipShape) is not supported yet. Use a Rectangle clipShape (scissor) or the WebGL2 backend instead.');
|
|
26956
|
-
}
|
|
26957
27369
|
if (node instanceof Text) {
|
|
26958
27370
|
this._collectText(node);
|
|
26959
27371
|
}
|
|
@@ -27055,6 +27467,7 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27055
27467
|
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, packedV * vertexStrideBytes$1);
|
|
27056
27468
|
device.queue.writeBuffer(this._indexBuffer, 0, this._indexData.buffer, 0, packedI * 2);
|
|
27057
27469
|
const format = backend.renderTargetFormat;
|
|
27470
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
27058
27471
|
const frameBindGroup = this._getFrameBindGroup(device);
|
|
27059
27472
|
// The coordinator owns the GPU pass (load/clear resolution, pass count and
|
|
27060
27473
|
// scissor are applied there) and ends + submits it below.
|
|
@@ -27065,7 +27478,7 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27065
27478
|
let lastTexture = null;
|
|
27066
27479
|
for (const batch of batches) {
|
|
27067
27480
|
if (batch.shaderType !== lastShaderType) {
|
|
27068
|
-
pass.setPipeline(this._getPipeline(batch.shaderType, format));
|
|
27481
|
+
pass.setPipeline(this._getPipeline(batch.shaderType, format, stencil));
|
|
27069
27482
|
pass.setBindGroup(0, frameBindGroup);
|
|
27070
27483
|
lastShaderType = batch.shaderType;
|
|
27071
27484
|
}
|
|
@@ -27098,7 +27511,9 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27098
27511
|
const promises = [];
|
|
27099
27512
|
for (const shaderType of shaderTypes) {
|
|
27100
27513
|
for (const format of formats) {
|
|
27101
|
-
|
|
27514
|
+
// Prewarm only the no-clip variant (matches the _getPipeline cache key
|
|
27515
|
+
// for stencil = false); stencil variants compile lazily under a clip.
|
|
27516
|
+
const key = `${shaderType}:${format}:n`;
|
|
27102
27517
|
if (this._pipelines.has(key))
|
|
27103
27518
|
continue;
|
|
27104
27519
|
promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(shaderType, format)).then(pipeline => {
|
|
@@ -27377,16 +27792,16 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27377
27792
|
return group;
|
|
27378
27793
|
}
|
|
27379
27794
|
// ── Pipeline helpers ─────────────────────────────────────────────────────
|
|
27380
|
-
_getPipeline(shaderType, format) {
|
|
27381
|
-
const key = `${shaderType}:${format}`;
|
|
27795
|
+
_getPipeline(shaderType, format, stencil) {
|
|
27796
|
+
const key = `${shaderType}:${format}:${stencil ? 's' : 'n'}`;
|
|
27382
27797
|
const existing = this._pipelines.get(key);
|
|
27383
27798
|
if (existing)
|
|
27384
27799
|
return existing;
|
|
27385
|
-
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(shaderType, format));
|
|
27800
|
+
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(shaderType, format, stencil));
|
|
27386
27801
|
this._pipelines.set(key, pipeline);
|
|
27387
27802
|
return pipeline;
|
|
27388
27803
|
}
|
|
27389
|
-
_buildPipelineDescriptor(shaderType, format) {
|
|
27804
|
+
_buildPipelineDescriptor(shaderType, format, stencil = false) {
|
|
27390
27805
|
let fragEntry;
|
|
27391
27806
|
if (shaderType === 'sdf') {
|
|
27392
27807
|
fragEntry = 'fragmentSdf';
|
|
@@ -27397,7 +27812,7 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27397
27812
|
else {
|
|
27398
27813
|
fragEntry = 'fragmentColor';
|
|
27399
27814
|
}
|
|
27400
|
-
|
|
27815
|
+
const descriptor = {
|
|
27401
27816
|
label: `WebGpuTextRenderer/${shaderType}`,
|
|
27402
27817
|
layout: this._pipelineLayout,
|
|
27403
27818
|
vertex: {
|
|
@@ -27428,6 +27843,10 @@ class WebGpuTextRenderer extends AbstractWebGpuRenderer {
|
|
|
27428
27843
|
},
|
|
27429
27844
|
primitive: { topology: 'triangle-list' },
|
|
27430
27845
|
};
|
|
27846
|
+
if (stencil) {
|
|
27847
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
27848
|
+
}
|
|
27849
|
+
return descriptor;
|
|
27431
27850
|
}
|
|
27432
27851
|
// ── Capacity helpers ─────────────────────────────────────────────────────
|
|
27433
27852
|
_ensureVertexCapacity(vertexCount) {
|
|
@@ -27471,6 +27890,14 @@ class WebGpuTransformStorage {
|
|
|
27471
27890
|
_storageCapacity = 0;
|
|
27472
27891
|
_storageHash = 0;
|
|
27473
27892
|
_storageCount = -1;
|
|
27893
|
+
/**
|
|
27894
|
+
* Underlying shared transform buffer. Exposed for internal stats / tests
|
|
27895
|
+
* (write, skip and upload counters); not part of any public surface.
|
|
27896
|
+
* @internal
|
|
27897
|
+
*/
|
|
27898
|
+
get buffer() {
|
|
27899
|
+
return this._buffer;
|
|
27900
|
+
}
|
|
27474
27901
|
begin(nodeCount) {
|
|
27475
27902
|
this._buffer.begin(nodeCount);
|
|
27476
27903
|
}
|
|
@@ -27478,27 +27905,51 @@ class WebGpuTransformStorage {
|
|
|
27478
27905
|
const drawable = command.drawable;
|
|
27479
27906
|
this._buffer.write(command.nodeIndex, drawable.getGlobalTransform(), drawable.tint);
|
|
27480
27907
|
}
|
|
27908
|
+
/**
|
|
27909
|
+
* Record that a draw command's transform write was skipped because its
|
|
27910
|
+
* renderer opts out of the shared transform storage. Stats only.
|
|
27911
|
+
* @internal
|
|
27912
|
+
*/
|
|
27913
|
+
recordSkippedWrite() {
|
|
27914
|
+
this._buffer.recordSkippedWrite();
|
|
27915
|
+
}
|
|
27916
|
+
/**
|
|
27917
|
+
* Append a drawable's world transform (+ tint) to the shared buffer and return
|
|
27918
|
+
* the slot it was written to. Used for draws that arrive without a stable
|
|
27919
|
+
* `nodeIndex` — a direct `backend.draw(drawable)` outside the plan player —
|
|
27920
|
+
* so a batch of synthetic draws does not collide on a single row.
|
|
27921
|
+
*/
|
|
27922
|
+
push(drawable) {
|
|
27923
|
+
return this._buffer.push(drawable.getGlobalTransform(), drawable.tint);
|
|
27924
|
+
}
|
|
27925
|
+
/**
|
|
27926
|
+
* Pre-allocate (or grow) the GPU storage buffer to hold at least
|
|
27927
|
+
* `recordCount` transform slots. Called once before render-plan playback so
|
|
27928
|
+
* that later per-group flushes never trigger a mid-frame reallocation.
|
|
27929
|
+
*
|
|
27930
|
+
* If the buffer already covers `recordCount` this is a no-op — no GPU
|
|
27931
|
+
* objects are destroyed or re-created. Capacity only grows, never shrinks.
|
|
27932
|
+
* @internal
|
|
27933
|
+
*/
|
|
27934
|
+
reserve(device, recordCount) {
|
|
27935
|
+
const minCount = Math.max(1, recordCount);
|
|
27936
|
+
const requiredBytes = minCount * slotFloatCount * Float32Array.BYTES_PER_ELEMENT;
|
|
27937
|
+
if (this._storageBuffer !== null && requiredBytes <= this._storageCapacity) {
|
|
27938
|
+
return;
|
|
27939
|
+
}
|
|
27940
|
+
this._growBuffer(device, requiredBytes);
|
|
27941
|
+
}
|
|
27481
27942
|
getBuffer(device, minCount) {
|
|
27482
27943
|
const requiredCount = Math.max(1, minCount);
|
|
27483
27944
|
const requiredBytes = requiredCount * slotFloatCount * Float32Array.BYTES_PER_ELEMENT;
|
|
27484
27945
|
const snapshot = this._buffer.commitSnapshot(requiredCount);
|
|
27485
27946
|
if (this._storageBuffer === null || requiredBytes > this._storageCapacity) {
|
|
27486
|
-
|
|
27487
|
-
while (nextCapacity < requiredBytes) {
|
|
27488
|
-
nextCapacity *= 2;
|
|
27489
|
-
}
|
|
27490
|
-
this._storageBuffer?.destroy();
|
|
27491
|
-
this._storageBuffer = device.createBuffer({
|
|
27492
|
-
size: nextCapacity,
|
|
27493
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
27494
|
-
});
|
|
27495
|
-
this._storageCapacity = nextCapacity;
|
|
27496
|
-
this._storageHash = 0;
|
|
27497
|
-
this._storageCount = -1;
|
|
27947
|
+
this._growBuffer(device, requiredBytes);
|
|
27498
27948
|
}
|
|
27499
27949
|
if (snapshot.changed || snapshot.hash !== this._storageHash || snapshot.count !== this._storageCount) {
|
|
27500
27950
|
const bytes = snapshot.count * slotFloatCount * Float32Array.BYTES_PER_ELEMENT;
|
|
27501
27951
|
device.queue.writeBuffer(this._storageBuffer, 0, this._buffer.data.buffer, this._buffer.data.byteOffset, bytes);
|
|
27952
|
+
this._buffer.recordUpload(snapshot.count);
|
|
27502
27953
|
this._storageHash = snapshot.hash;
|
|
27503
27954
|
this._storageCount = snapshot.count;
|
|
27504
27955
|
}
|
|
@@ -27514,6 +27965,20 @@ class WebGpuTransformStorage {
|
|
|
27514
27965
|
this._storageHash = 0;
|
|
27515
27966
|
this._storageCount = -1;
|
|
27516
27967
|
}
|
|
27968
|
+
_growBuffer(device, requiredBytes) {
|
|
27969
|
+
let nextCapacity = Math.max(this._storageCapacity, slotFloatCount * Float32Array.BYTES_PER_ELEMENT);
|
|
27970
|
+
while (nextCapacity < requiredBytes) {
|
|
27971
|
+
nextCapacity *= 2;
|
|
27972
|
+
}
|
|
27973
|
+
this._storageBuffer?.destroy();
|
|
27974
|
+
this._storageBuffer = device.createBuffer({
|
|
27975
|
+
size: nextCapacity,
|
|
27976
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
27977
|
+
});
|
|
27978
|
+
this._storageCapacity = nextCapacity;
|
|
27979
|
+
this._storageHash = 0;
|
|
27980
|
+
this._storageCount = -1;
|
|
27981
|
+
}
|
|
27517
27982
|
}
|
|
27518
27983
|
|
|
27519
27984
|
/* eslint-disable max-lines */
|
|
@@ -27638,8 +28103,8 @@ class WebGpuBackend {
|
|
|
27638
28103
|
return this._activeDrawCommand;
|
|
27639
28104
|
}
|
|
27640
28105
|
/**
|
|
27641
|
-
* Internal render-pass coordinator. Owns the clear-vs-load decision and
|
|
27642
|
-
*
|
|
28106
|
+
* Internal render-pass coordinator. Owns the clear-vs-load decision and the
|
|
28107
|
+
* active render pass; not part of the public
|
|
27643
28108
|
* {@link RenderBackend} surface.
|
|
27644
28109
|
* @internal
|
|
27645
28110
|
*/
|
|
@@ -27671,14 +28136,46 @@ class WebGpuBackend {
|
|
|
27671
28136
|
}
|
|
27672
28137
|
/** @internal */
|
|
27673
28138
|
_beginDrawPlan(nodeCount) {
|
|
27674
|
-
this._getTransformStorage()
|
|
28139
|
+
const storage = this._getTransformStorage();
|
|
28140
|
+
storage.begin(nodeCount);
|
|
28141
|
+
// Pre-allocate the GPU storage buffer for the full plan before any group
|
|
28142
|
+
// flush runs. Without this, a later flush with a higher maxNodeIndex would
|
|
28143
|
+
// destroy and replace the buffer mid-frame while earlier command buffers
|
|
28144
|
+
// may still reference the old allocation.
|
|
28145
|
+
if (nodeCount > 0 && this._device !== null && !this._deviceLost) {
|
|
28146
|
+
storage.reserve(this._device, nodeCount);
|
|
28147
|
+
}
|
|
27675
28148
|
this._activeDrawCommand = null;
|
|
27676
28149
|
this._drawPlanDepth++;
|
|
27677
28150
|
}
|
|
27678
28151
|
/** @internal */
|
|
28152
|
+
_prepareRenderGroupUpload(group) {
|
|
28153
|
+
// Pack the whole render group's world transforms (+ tint) into the shared
|
|
28154
|
+
// transform storage at the group's upload boundary, keyed by each draw
|
|
28155
|
+
// command's stable nodeIndex. Every draw the player will submit for this
|
|
28156
|
+
// group is covered here, before the group's first draw.
|
|
28157
|
+
//
|
|
28158
|
+
// Renderers that pack their own per-node data (Text, Particle) never read
|
|
28159
|
+
// the shared storage, so their commands are skipped — no consuming draw
|
|
28160
|
+
// ever references their slots (nodeIndex is unique per command).
|
|
28161
|
+
const storage = this._getTransformStorage();
|
|
28162
|
+
const instructions = group.instructions;
|
|
28163
|
+
for (let i = 0; i < instructions.length; i++) {
|
|
28164
|
+
const command = instructions[i];
|
|
28165
|
+
if (drawCommandUsesSharedTransform(command, this)) {
|
|
28166
|
+
storage.writeCommand(command);
|
|
28167
|
+
}
|
|
28168
|
+
else {
|
|
28169
|
+
storage.recordSkippedWrite();
|
|
28170
|
+
}
|
|
28171
|
+
}
|
|
28172
|
+
}
|
|
28173
|
+
/** @internal */
|
|
27679
28174
|
_prepareDrawCommand(command) {
|
|
28175
|
+
// Transform packing now happens at the render-group upload boundary
|
|
28176
|
+
// (`_prepareRenderGroupUpload`); this hook only tracks the active draw so
|
|
28177
|
+
// renderers can read the current command's nodeIndex.
|
|
27680
28178
|
this._activeDrawCommand = command;
|
|
27681
|
-
this._getTransformStorage().writeCommand(command);
|
|
27682
28179
|
}
|
|
27683
28180
|
/** @internal */
|
|
27684
28181
|
_endDrawPlan() {
|
|
@@ -27991,6 +28488,18 @@ class WebGpuBackend {
|
|
|
27991
28488
|
getTransformStorageBuffer(minCount) {
|
|
27992
28489
|
return this._getTransformStorage().getBuffer(this.device, minCount);
|
|
27993
28490
|
}
|
|
28491
|
+
/**
|
|
28492
|
+
* Append a drawable's world transform (+ tint) to the shared transform storage
|
|
28493
|
+
* and return the slot it was written to. Used by instanced renderers for draws
|
|
28494
|
+
* that arrive without a render-group upload boundary — i.e. a direct
|
|
28495
|
+
* `backend.draw(drawable)` outside the plan player (`activeDrawCommand === null`),
|
|
28496
|
+
* where no stable `nodeIndex` was assigned. Each call allocates a fresh slot, so
|
|
28497
|
+
* a batch of synthetic draws does not collide on a single row.
|
|
28498
|
+
* @internal
|
|
28499
|
+
*/
|
|
28500
|
+
_pushTransform(drawable) {
|
|
28501
|
+
return this._getTransformStorage().push(drawable);
|
|
28502
|
+
}
|
|
27994
28503
|
_setActiveRenderer(renderer) {
|
|
27995
28504
|
if (this._renderer !== renderer) {
|
|
27996
28505
|
this._flushActiveRenderer();
|
|
@@ -31486,7 +31995,7 @@ class Loader {
|
|
|
31486
31995
|
return `id:${this._getTypeId(type)}:${discriminator}`;
|
|
31487
31996
|
}
|
|
31488
31997
|
_resolveUrl(path) {
|
|
31489
|
-
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) {
|
|
31998
|
+
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//') || path.startsWith('/')) {
|
|
31490
31999
|
return path;
|
|
31491
32000
|
}
|
|
31492
32001
|
return `${this._basePath}${path}`;
|
|
@@ -35077,7 +35586,10 @@ const lookupSize$4 = 256;
|
|
|
35077
35586
|
*/
|
|
35078
35587
|
class AlphaFadeOverLifetime extends UpdateModule {
|
|
35079
35588
|
curve;
|
|
35080
|
-
constructor(curve
|
|
35589
|
+
constructor(curve = new Curve([
|
|
35590
|
+
{ t: 0, v: 1 },
|
|
35591
|
+
{ t: 1, v: 0 },
|
|
35592
|
+
])) {
|
|
35081
35593
|
super();
|
|
35082
35594
|
this.curve = curve;
|
|
35083
35595
|
}
|
|
@@ -37407,20 +37919,53 @@ class Geometry {
|
|
|
37407
37919
|
|
|
37408
37920
|
const sortedStopOffset = (left, right) => left.offset - right.offset;
|
|
37409
37921
|
/**
|
|
37410
|
-
* CPU-rasterized gradient base
|
|
37922
|
+
* CPU-rasterized gradient base. A {@link Color}-like paint value (not a
|
|
37923
|
+
* {@link Drawable}): it owns a normalized list of color stops plus subclass
|
|
37924
|
+
* geometry and supports the value-object contract — {@link Gradient.clone},
|
|
37925
|
+
* {@link Gradient.copy}, and {@link Gradient.equals}. Stops are cloned on the
|
|
37926
|
+
* way in, clamped to `0..1`, and kept sorted by offset.
|
|
37927
|
+
*
|
|
37928
|
+
* Convert a gradient into a sampleable {@link DataTexture} with
|
|
37929
|
+
* {@link Gradient.toTexture}; wrap that texture in a `Sprite`/`Mesh` to draw it.
|
|
37411
37930
|
*/
|
|
37412
37931
|
class Gradient {
|
|
37413
37932
|
_stops;
|
|
37414
37933
|
_sample = new Float32Array(4);
|
|
37415
37934
|
constructor(stops) {
|
|
37416
|
-
|
|
37417
|
-
throw new Error('Gradient requires at least 2 color stops.');
|
|
37418
|
-
}
|
|
37419
|
-
this._stops = [...stops].map(stop => ({ offset: clamp(stop.offset, 0, 1), color: stop.color.clone() })).sort(sortedStopOffset);
|
|
37935
|
+
this._stops = Gradient._normalizeStops(stops);
|
|
37420
37936
|
}
|
|
37421
37937
|
get stops() {
|
|
37422
37938
|
return this._stops;
|
|
37423
37939
|
}
|
|
37940
|
+
/**
|
|
37941
|
+
* Copy every value (stops and geometry) from a same-type `source` into this
|
|
37942
|
+
* instance, replacing its current state. Returns `this` for chaining.
|
|
37943
|
+
*/
|
|
37944
|
+
copy(source) {
|
|
37945
|
+
this._stops = source._stops.map(stop => ({ offset: stop.offset, color: stop.color.clone() }));
|
|
37946
|
+
this._copyGeometry(source);
|
|
37947
|
+
return this;
|
|
37948
|
+
}
|
|
37949
|
+
/**
|
|
37950
|
+
* Structural value equality: same {@link GradientType}, same ordered stops
|
|
37951
|
+
* (offset and {@link Color}), and same subclass geometry.
|
|
37952
|
+
*/
|
|
37953
|
+
equals(other) {
|
|
37954
|
+
if (this === other) {
|
|
37955
|
+
return true;
|
|
37956
|
+
}
|
|
37957
|
+
if (this.type !== other.type || this._stops.length !== other._stops.length) {
|
|
37958
|
+
return false;
|
|
37959
|
+
}
|
|
37960
|
+
for (let i = 0; i < this._stops.length; i++) {
|
|
37961
|
+
const own = this._stops[i];
|
|
37962
|
+
const their = other._stops[i];
|
|
37963
|
+
if (own.offset !== their.offset || !own.color.equals(their.color)) {
|
|
37964
|
+
return false;
|
|
37965
|
+
}
|
|
37966
|
+
}
|
|
37967
|
+
return this._geometryEquals(other);
|
|
37968
|
+
}
|
|
37424
37969
|
toTexture(width, height, options = {}) {
|
|
37425
37970
|
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
37426
37971
|
throw new Error('Gradient.toTexture() width/height must be positive integers.');
|
|
@@ -37463,6 +38008,19 @@ class Gradient {
|
|
|
37463
38008
|
out[2] = previousColor.b / 255;
|
|
37464
38009
|
out[3] = previousColor.a;
|
|
37465
38010
|
}
|
|
38011
|
+
static _normalizeStops(stops) {
|
|
38012
|
+
if (stops.length < 2) {
|
|
38013
|
+
throw new Error('Gradient requires at least 2 color stops.');
|
|
38014
|
+
}
|
|
38015
|
+
return stops
|
|
38016
|
+
.map(stop => {
|
|
38017
|
+
if (!Number.isFinite(stop.offset)) {
|
|
38018
|
+
throw new Error('Gradient stop offset must be a finite number.');
|
|
38019
|
+
}
|
|
38020
|
+
return { offset: clamp(stop.offset, 0, 1), color: stop.color.clone() };
|
|
38021
|
+
})
|
|
38022
|
+
.sort(sortedStopOffset);
|
|
38023
|
+
}
|
|
37466
38024
|
_toRgba8Texture(width, height, options) {
|
|
37467
38025
|
const texture = new DataTexture({
|
|
37468
38026
|
width,
|
|
@@ -37518,6 +38076,7 @@ const toUnorm8 = (value) => (clamp(value, 0, 1) * 255 + 0.5) | 0;
|
|
|
37518
38076
|
* Linear gradient in UV space projected from `start` to `end`.
|
|
37519
38077
|
*/
|
|
37520
38078
|
class LinearGradient extends Gradient {
|
|
38079
|
+
type = 'linear';
|
|
37521
38080
|
_start;
|
|
37522
38081
|
_end;
|
|
37523
38082
|
constructor(stops, start = [0, 0], end = [1, 0]) {
|
|
@@ -37525,6 +38084,17 @@ class LinearGradient extends Gradient {
|
|
|
37525
38084
|
this._start = [start[0], start[1]];
|
|
37526
38085
|
this._end = [end[0], end[1]];
|
|
37527
38086
|
}
|
|
38087
|
+
/** Projection axis start point in UV space. */
|
|
38088
|
+
get start() {
|
|
38089
|
+
return [this._start[0], this._start[1]];
|
|
38090
|
+
}
|
|
38091
|
+
/** Projection axis end point in UV space. */
|
|
38092
|
+
get end() {
|
|
38093
|
+
return [this._end[0], this._end[1]];
|
|
38094
|
+
}
|
|
38095
|
+
clone() {
|
|
38096
|
+
return new LinearGradient(this.stops, this._start, this._end);
|
|
38097
|
+
}
|
|
37528
38098
|
resolveT(u, v) {
|
|
37529
38099
|
const axisX = this._end[0] - this._start[0];
|
|
37530
38100
|
const axisY = this._end[1] - this._start[1];
|
|
@@ -37534,12 +38104,24 @@ class LinearGradient extends Gradient {
|
|
|
37534
38104
|
}
|
|
37535
38105
|
return ((u - this._start[0]) * axisX + (v - this._start[1]) * axisY) / lengthSquared;
|
|
37536
38106
|
}
|
|
38107
|
+
_copyGeometry(source) {
|
|
38108
|
+
this._start = [source._start[0], source._start[1]];
|
|
38109
|
+
this._end = [source._end[0], source._end[1]];
|
|
38110
|
+
}
|
|
38111
|
+
_geometryEquals(other) {
|
|
38112
|
+
return (other instanceof LinearGradient &&
|
|
38113
|
+
this._start[0] === other._start[0] &&
|
|
38114
|
+
this._start[1] === other._start[1] &&
|
|
38115
|
+
this._end[0] === other._end[0] &&
|
|
38116
|
+
this._end[1] === other._end[1]);
|
|
38117
|
+
}
|
|
37537
38118
|
}
|
|
37538
38119
|
|
|
37539
38120
|
/**
|
|
37540
38121
|
* Radial gradient in UV space around `center` with normalized radius.
|
|
37541
38122
|
*/
|
|
37542
38123
|
class RadialGradient extends Gradient {
|
|
38124
|
+
type = 'radial';
|
|
37543
38125
|
_center;
|
|
37544
38126
|
_radius;
|
|
37545
38127
|
constructor(stops, center = [0.5, 0.5], radius = 0.5) {
|
|
@@ -37547,6 +38129,17 @@ class RadialGradient extends Gradient {
|
|
|
37547
38129
|
this._center = [center[0], center[1]];
|
|
37548
38130
|
this._radius = Math.max(0, radius);
|
|
37549
38131
|
}
|
|
38132
|
+
/** Gradient center in UV space. */
|
|
38133
|
+
get center() {
|
|
38134
|
+
return [this._center[0], this._center[1]];
|
|
38135
|
+
}
|
|
38136
|
+
/** Normalized radius (UV units, clamped to be non-negative). */
|
|
38137
|
+
get radius() {
|
|
38138
|
+
return this._radius;
|
|
38139
|
+
}
|
|
38140
|
+
clone() {
|
|
38141
|
+
return new RadialGradient(this.stops, this._center, this._radius);
|
|
38142
|
+
}
|
|
37550
38143
|
resolveT(u, v) {
|
|
37551
38144
|
if (this._radius <= 0.000001) {
|
|
37552
38145
|
return 1;
|
|
@@ -37555,6 +38148,13 @@ class RadialGradient extends Gradient {
|
|
|
37555
38148
|
const dy = v - this._center[1];
|
|
37556
38149
|
return Math.sqrt(dx * dx + dy * dy) / this._radius;
|
|
37557
38150
|
}
|
|
38151
|
+
_copyGeometry(source) {
|
|
38152
|
+
this._center = [source._center[0], source._center[1]];
|
|
38153
|
+
this._radius = source._radius;
|
|
38154
|
+
}
|
|
38155
|
+
_geometryEquals(other) {
|
|
38156
|
+
return other instanceof RadialGradient && this._center[0] === other._center[0] && this._center[1] === other._center[1] && this._radius === other._radius;
|
|
38157
|
+
}
|
|
37558
38158
|
}
|
|
37559
38159
|
|
|
37560
38160
|
/**
|
|
@@ -37979,23 +38579,43 @@ class SpriteMaterial extends Material {
|
|
|
37979
38579
|
}
|
|
37980
38580
|
}
|
|
37981
38581
|
|
|
38582
|
+
/**
|
|
38583
|
+
* Edge length of the square gradient texture rasterized for gradient paints.
|
|
38584
|
+
* The full 2D UV field is baked so both linear and radial gradients map across
|
|
38585
|
+
* the filled shape's bounding box without per-axis special-casing.
|
|
38586
|
+
*/
|
|
38587
|
+
const gradientTextureSize = 256;
|
|
37982
38588
|
/**
|
|
37983
38589
|
* Immediate-mode 2D shape API backed by {@link Mesh} children.
|
|
37984
38590
|
*
|
|
37985
38591
|
* Each draw call (e.g. `drawCircle`, `drawRectangle`, `drawLine`) appends a
|
|
37986
|
-
* new {@link Mesh} child
|
|
38592
|
+
* new {@link Mesh} child painted with the active fill or stroke style. A style
|
|
38593
|
+
* is either a solid {@link Color} or a {@link Gradient}, assigned through
|
|
38594
|
+
* {@link fillStyle} / {@link strokeStyle}. The {@link fillColor} /
|
|
38595
|
+
* {@link lineColor} accessors are color-only conveniences over those styles.
|
|
37987
38596
|
* The active `lineWidth` controls stroke thickness for path and outline draws.
|
|
37988
38597
|
* Path commands (`moveTo`, `lineTo`, `quadraticCurveTo`, etc.) track a cursor
|
|
37989
38598
|
* point and flush a Mesh on each segment.
|
|
37990
38599
|
*
|
|
38600
|
+
* Gradient styles are rasterized once to a {@link DataTexture} via
|
|
38601
|
+
* {@link Gradient.toTexture} and sampled across each shape's local bounding
|
|
38602
|
+
* box, so {@link LinearGradient} and {@link RadialGradient} render through the
|
|
38603
|
+
* same texture path as a textured Mesh. The textures are owned by the Graphics
|
|
38604
|
+
* and released on {@link clear} / {@link destroy}.
|
|
38605
|
+
*
|
|
37991
38606
|
* Call {@link clear} to remove all child meshes and reset pen state. Because
|
|
37992
38607
|
* each shape is a separate Mesh, `Graphics` inherits full filter, blend,
|
|
37993
38608
|
* tint, and mask support from {@link Container}.
|
|
37994
38609
|
*/
|
|
37995
38610
|
class Graphics extends Container {
|
|
37996
38611
|
_lineWidth = 0;
|
|
37997
|
-
_lineColor = new Color();
|
|
37998
38612
|
_fillColor = new Color();
|
|
38613
|
+
_lineColor = new Color();
|
|
38614
|
+
_fillStyle = this._fillColor;
|
|
38615
|
+
_strokeStyle = this._lineColor;
|
|
38616
|
+
_fillStyleTexture = null;
|
|
38617
|
+
_strokeStyleTexture = null;
|
|
38618
|
+
_ownedTextures = new Set();
|
|
37999
38619
|
_currentPoint = new Vector(0, 0);
|
|
38000
38620
|
get lineWidth() {
|
|
38001
38621
|
return this._lineWidth;
|
|
@@ -38003,17 +38623,59 @@ class Graphics extends Container {
|
|
|
38003
38623
|
set lineWidth(lineWidth) {
|
|
38004
38624
|
this._lineWidth = lineWidth;
|
|
38005
38625
|
}
|
|
38626
|
+
/** Solid stroke color slot: the last solid color assigned to the stroke. */
|
|
38006
38627
|
get lineColor() {
|
|
38007
38628
|
return this._lineColor;
|
|
38008
38629
|
}
|
|
38630
|
+
/**
|
|
38631
|
+
* Convenience solid-color setter for the stroke. Copies `lineColor` into the
|
|
38632
|
+
* color slot and makes it the active {@link strokeStyle}, replacing any
|
|
38633
|
+
* gradient stroke style.
|
|
38634
|
+
*/
|
|
38009
38635
|
set lineColor(lineColor) {
|
|
38010
|
-
this.
|
|
38636
|
+
this.strokeStyle = lineColor;
|
|
38011
38637
|
}
|
|
38638
|
+
/** Solid fill color slot: the last solid color assigned to the fill. */
|
|
38012
38639
|
get fillColor() {
|
|
38013
38640
|
return this._fillColor;
|
|
38014
38641
|
}
|
|
38642
|
+
/**
|
|
38643
|
+
* Convenience solid-color setter for the fill. Copies `fillColor` into the
|
|
38644
|
+
* color slot and makes it the active {@link fillStyle}, replacing any
|
|
38645
|
+
* gradient fill style.
|
|
38646
|
+
*/
|
|
38015
38647
|
set fillColor(fillColor) {
|
|
38016
|
-
this.
|
|
38648
|
+
this.fillStyle = fillColor;
|
|
38649
|
+
}
|
|
38650
|
+
/** Active fill style: a solid {@link Color} or a {@link Gradient}. */
|
|
38651
|
+
get fillStyle() {
|
|
38652
|
+
return this._fillStyle;
|
|
38653
|
+
}
|
|
38654
|
+
/**
|
|
38655
|
+
* Set the fill style. Accepts a solid {@link Color}, a {@link Gradient}
|
|
38656
|
+
* (cloned on assignment and rasterized lazily on first fill), or `null` to
|
|
38657
|
+
* revert to the solid color held by {@link fillColor}. A {@link Color} value
|
|
38658
|
+
* is copied into the {@link fillColor} slot; the most recently assigned style
|
|
38659
|
+
* wins.
|
|
38660
|
+
*/
|
|
38661
|
+
set fillStyle(style) {
|
|
38662
|
+
this._fillStyle = this._resolveStyle(style, this._fillColor);
|
|
38663
|
+
this._fillStyleTexture = null;
|
|
38664
|
+
}
|
|
38665
|
+
/** Active stroke style: a solid {@link Color} or a {@link Gradient}. */
|
|
38666
|
+
get strokeStyle() {
|
|
38667
|
+
return this._strokeStyle;
|
|
38668
|
+
}
|
|
38669
|
+
/**
|
|
38670
|
+
* Set the stroke style. Accepts a solid {@link Color}, a {@link Gradient}
|
|
38671
|
+
* (cloned on assignment and rasterized lazily on first stroke), or `null` to
|
|
38672
|
+
* revert to the solid color held by {@link lineColor}. A {@link Color} value
|
|
38673
|
+
* is copied into the {@link lineColor} slot; the most recently assigned style
|
|
38674
|
+
* wins.
|
|
38675
|
+
*/
|
|
38676
|
+
set strokeStyle(style) {
|
|
38677
|
+
this._strokeStyle = this._resolveStyle(style, this._lineColor);
|
|
38678
|
+
this._strokeStyleTexture = null;
|
|
38017
38679
|
}
|
|
38018
38680
|
get currentPoint() {
|
|
38019
38681
|
return this._currentPoint;
|
|
@@ -38137,19 +38799,19 @@ class Graphics extends Container {
|
|
|
38137
38799
|
/** Draw a stroked line between two explicit points, independent of the current pen position. */
|
|
38138
38800
|
drawLine(startX, startY, endX, endY) {
|
|
38139
38801
|
const data = buildLine(startX, startY, endX, endY, this._lineWidth);
|
|
38140
|
-
this.addChild(this.
|
|
38802
|
+
this.addChild(this._createStrokeMesh(data));
|
|
38141
38803
|
return this;
|
|
38142
38804
|
}
|
|
38143
38805
|
/** Draw a stroked polyline from a flat `[x0,y0, x1,y1, ...]` coordinate array. */
|
|
38144
38806
|
drawPath(path) {
|
|
38145
38807
|
const data = buildPath(path, this._lineWidth);
|
|
38146
|
-
this.addChild(this.
|
|
38808
|
+
this.addChild(this._createStrokeMesh(data));
|
|
38147
38809
|
return this;
|
|
38148
38810
|
}
|
|
38149
38811
|
/** Fill a closed polygon defined by `[x0,y0, x1,y1, ...]` and optionally stroke its outline. */
|
|
38150
38812
|
drawPolygon(path) {
|
|
38151
38813
|
const data = buildPolygon(path);
|
|
38152
|
-
this.addChild(this.
|
|
38814
|
+
this.addChild(this._createFillMesh(data));
|
|
38153
38815
|
if (this._lineWidth > 0) {
|
|
38154
38816
|
this.drawPath(data.points);
|
|
38155
38817
|
}
|
|
@@ -38158,7 +38820,7 @@ class Graphics extends Container {
|
|
|
38158
38820
|
/** Fill a circle and optionally stroke its outline if `lineWidth > 0`. */
|
|
38159
38821
|
drawCircle(centerX, centerY, radius) {
|
|
38160
38822
|
const data = buildCircle(centerX, centerY, radius);
|
|
38161
|
-
this.addChild(this.
|
|
38823
|
+
this.addChild(this._createFillMesh(data));
|
|
38162
38824
|
if (this._lineWidth > 0) {
|
|
38163
38825
|
this.drawPath(data.points);
|
|
38164
38826
|
}
|
|
@@ -38167,7 +38829,7 @@ class Graphics extends Container {
|
|
|
38167
38829
|
/** Fill an ellipse and optionally stroke its outline if `lineWidth > 0`. */
|
|
38168
38830
|
drawEllipse(centerX, centerY, radiusX, radiusY) {
|
|
38169
38831
|
const data = buildEllipse(centerX, centerY, radiusX, radiusY);
|
|
38170
|
-
this.addChild(this.
|
|
38832
|
+
this.addChild(this._createFillMesh(data));
|
|
38171
38833
|
if (this._lineWidth > 0) {
|
|
38172
38834
|
this.drawPath(data.points);
|
|
38173
38835
|
}
|
|
@@ -38176,7 +38838,7 @@ class Graphics extends Container {
|
|
|
38176
38838
|
/** Fill a rectangle and optionally stroke its outline if `lineWidth > 0`. */
|
|
38177
38839
|
drawRectangle(x, y, width, height) {
|
|
38178
38840
|
const data = buildRectangle(x, y, width, height);
|
|
38179
|
-
this.addChild(this.
|
|
38841
|
+
this.addChild(this._createFillMesh(data));
|
|
38180
38842
|
if (this._lineWidth > 0) {
|
|
38181
38843
|
this.drawPath(data.points);
|
|
38182
38844
|
}
|
|
@@ -38188,18 +38850,23 @@ class Graphics extends Container {
|
|
|
38188
38850
|
*/
|
|
38189
38851
|
drawStar(centerX, centerY, points, radius, innerRadius = radius / 2, rotation = 0) {
|
|
38190
38852
|
const data = buildStar(centerX, centerY, points, radius, innerRadius, rotation);
|
|
38191
|
-
this.addChild(this.
|
|
38853
|
+
this.addChild(this._createFillMesh(data));
|
|
38192
38854
|
if (this._lineWidth > 0) {
|
|
38193
38855
|
this.drawPath(data.points);
|
|
38194
38856
|
}
|
|
38195
38857
|
return this;
|
|
38196
38858
|
}
|
|
38197
|
-
/** Remove all child meshes and reset pen state (position,
|
|
38859
|
+
/** Remove all child meshes and reset pen state (position, fill/stroke styles, line width). */
|
|
38198
38860
|
clear() {
|
|
38199
38861
|
this.removeChildren();
|
|
38200
38862
|
this._lineWidth = 0;
|
|
38201
|
-
this._lineColor.copy(Color.black);
|
|
38202
38863
|
this._fillColor.copy(Color.black);
|
|
38864
|
+
this._lineColor.copy(Color.black);
|
|
38865
|
+
this._fillStyle = this._fillColor;
|
|
38866
|
+
this._strokeStyle = this._lineColor;
|
|
38867
|
+
this._fillStyleTexture = null;
|
|
38868
|
+
this._strokeStyleTexture = null;
|
|
38869
|
+
this._destroyOwnedTextures();
|
|
38203
38870
|
this._currentPoint.set(0, 0);
|
|
38204
38871
|
return this;
|
|
38205
38872
|
}
|
|
@@ -38210,7 +38877,38 @@ class Graphics extends Container {
|
|
|
38210
38877
|
this._fillColor.destroy();
|
|
38211
38878
|
this._currentPoint.destroy();
|
|
38212
38879
|
}
|
|
38213
|
-
|
|
38880
|
+
/**
|
|
38881
|
+
* Resolve an assigned style into the stored paint. A {@link Color} is copied
|
|
38882
|
+
* into the solid `colorSlot` (keeping the {@link fillColor} / {@link lineColor}
|
|
38883
|
+
* convenience getters in sync) and that slot is returned; `null` reverts to
|
|
38884
|
+
* the slot; a {@link Gradient} is cloned so later external mutation cannot
|
|
38885
|
+
* change the stored paint.
|
|
38886
|
+
*/
|
|
38887
|
+
_resolveStyle(style, colorSlot) {
|
|
38888
|
+
if (style === null) {
|
|
38889
|
+
return colorSlot;
|
|
38890
|
+
}
|
|
38891
|
+
if (style instanceof Color) {
|
|
38892
|
+
colorSlot.copy(style);
|
|
38893
|
+
return colorSlot;
|
|
38894
|
+
}
|
|
38895
|
+
return style.clone();
|
|
38896
|
+
}
|
|
38897
|
+
_createFillMesh(data) {
|
|
38898
|
+
if (this._fillStyle instanceof Color) {
|
|
38899
|
+
return this._createSolidMesh(data, this._fillStyle);
|
|
38900
|
+
}
|
|
38901
|
+
this._fillStyleTexture ??= this._rasterizeGradient(this._fillStyle);
|
|
38902
|
+
return this._createGradientMesh(data, this._fillStyleTexture);
|
|
38903
|
+
}
|
|
38904
|
+
_createStrokeMesh(data) {
|
|
38905
|
+
if (this._strokeStyle instanceof Color) {
|
|
38906
|
+
return this._createSolidMesh(data, this._strokeStyle);
|
|
38907
|
+
}
|
|
38908
|
+
this._strokeStyleTexture ??= this._rasterizeGradient(this._strokeStyle);
|
|
38909
|
+
return this._createGradientMesh(data, this._strokeStyleTexture);
|
|
38910
|
+
}
|
|
38911
|
+
_createSolidMesh(data, color) {
|
|
38214
38912
|
const mesh = new Mesh({
|
|
38215
38913
|
vertices: data.vertices,
|
|
38216
38914
|
indices: data.indices,
|
|
@@ -38218,7 +38916,66 @@ class Graphics extends Container {
|
|
|
38218
38916
|
mesh.tint = color;
|
|
38219
38917
|
return mesh;
|
|
38220
38918
|
}
|
|
38221
|
-
|
|
38919
|
+
/**
|
|
38920
|
+
* Build a textured mesh whose UVs span the shape's local bounding box, so the
|
|
38921
|
+
* gradient texture samples across the filled/stroked geometry. The default
|
|
38922
|
+
* white tint and vertex color leave the sampled gradient color unmodulated.
|
|
38923
|
+
*/
|
|
38924
|
+
_createGradientMesh(data, texture) {
|
|
38925
|
+
return new Mesh({
|
|
38926
|
+
vertices: data.vertices,
|
|
38927
|
+
indices: data.indices,
|
|
38928
|
+
uvs: computeBoundsUvs(data.vertices),
|
|
38929
|
+
texture,
|
|
38930
|
+
});
|
|
38931
|
+
}
|
|
38932
|
+
_rasterizeGradient(gradient) {
|
|
38933
|
+
const texture = gradient.toTexture(gradientTextureSize, gradientTextureSize, {
|
|
38934
|
+
samplerOptions: { scaleMode: ScaleModes.Linear },
|
|
38935
|
+
});
|
|
38936
|
+
this._ownedTextures.add(texture);
|
|
38937
|
+
return texture;
|
|
38938
|
+
}
|
|
38939
|
+
_destroyOwnedTextures() {
|
|
38940
|
+
for (const texture of this._ownedTextures) {
|
|
38941
|
+
texture.destroy();
|
|
38942
|
+
}
|
|
38943
|
+
this._ownedTextures.clear();
|
|
38944
|
+
}
|
|
38945
|
+
}
|
|
38946
|
+
/**
|
|
38947
|
+
* Normalize each `(x, y)` vertex into `0..1` UV space relative to the flat
|
|
38948
|
+
* vertex array's axis-aligned bounding box. Degenerate (zero-width/height)
|
|
38949
|
+
* spans collapse to `0` to avoid division by zero.
|
|
38950
|
+
*/
|
|
38951
|
+
const computeBoundsUvs = (vertices) => {
|
|
38952
|
+
let minX = Infinity;
|
|
38953
|
+
let minY = Infinity;
|
|
38954
|
+
let maxX = -Infinity;
|
|
38955
|
+
let maxY = -Infinity;
|
|
38956
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
38957
|
+
const x = vertices[i];
|
|
38958
|
+
const y = vertices[i + 1];
|
|
38959
|
+
if (x < minX)
|
|
38960
|
+
minX = x;
|
|
38961
|
+
if (x > maxX)
|
|
38962
|
+
maxX = x;
|
|
38963
|
+
if (y < minY)
|
|
38964
|
+
minY = y;
|
|
38965
|
+
if (y > maxY)
|
|
38966
|
+
maxY = y;
|
|
38967
|
+
}
|
|
38968
|
+
const spanX = maxX - minX;
|
|
38969
|
+
const spanY = maxY - minY;
|
|
38970
|
+
const invX = spanX > 0 ? 1 / spanX : 0;
|
|
38971
|
+
const invY = spanY > 0 ? 1 / spanY : 0;
|
|
38972
|
+
const uvs = new Float32Array(vertices.length);
|
|
38973
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
38974
|
+
uvs[i] = (vertices[i] - minX) * invX;
|
|
38975
|
+
uvs[i + 1] = (vertices[i + 1] - minY) * invY;
|
|
38976
|
+
}
|
|
38977
|
+
return uvs;
|
|
38978
|
+
};
|
|
38222
38979
|
|
|
38223
38980
|
const defaultClipFps = 12;
|
|
38224
38981
|
/**
|