@codexo/exojs 0.6.1 → 0.6.2
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 +35 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rendering/index.d.ts +3 -0
- package/dist/esm/rendering/mesh/Mesh.d.ts +69 -0
- package/dist/esm/rendering/mesh/Mesh.js +114 -0
- package/dist/esm/rendering/mesh/Mesh.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +3 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +27 -0
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +242 -0
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.frag.js.map +1 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +4 -0
- package/dist/esm/rendering/webgl2/glsl/mesh.vert.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +3 -0
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +40 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +439 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -0
- package/dist/exo.esm.js +2277 -1500
- package/dist/exo.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/exo.esm.js
CHANGED
|
@@ -5638,9 +5638,9 @@ class WebGl2VertexArrayObject {
|
|
|
5638
5638
|
}
|
|
5639
5639
|
}
|
|
5640
5640
|
|
|
5641
|
-
var vertexSource$
|
|
5641
|
+
var vertexSource$4 = "#version 300 es\nprecision lowp float;\nprecision lowp 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 = 1) in vec3 a_transformAB; // a, b, x — first row of 2D affine\nlayout(location = 2) in vec3 a_transformCD; // c, d, y — second row\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;\n\nuniform mat3 u_projection;\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 // Apply the per-instance affine transform: world = M * (localX, localY, 1)\n float worldX = (a_transformAB.x * localX) + (a_transformAB.y * localY) + a_transformAB.z;\n float worldY = (a_transformCD.x * localX) + (a_transformCD.y * localY) + a_transformCD.z;\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";
|
|
5642
5642
|
|
|
5643
|
-
var fragmentSource$
|
|
5643
|
+
var fragmentSource$4 = "#version 300 es\r\nprecision lowp float;\r\nprecision lowp int;\r\n\r\n// Multi-texture sprite batching: up to 8 textures bound per draw call,\r\n// each fragment selects its source via a flat-interpolated slot index.\r\n//\r\n// GLSL ES 3.0 forbids non-constant array-of-sampler indexing unless the\r\n// expression is dynamically uniform — which a per-vertex slot is not\r\n// once different triangles in the same batch carry different slots. The\r\n// if/else chain below dispatches statically and dodges that constraint.\r\n\r\nuniform sampler2D u_texture0;\r\nuniform sampler2D u_texture1;\r\nuniform sampler2D u_texture2;\r\nuniform sampler2D u_texture3;\r\nuniform sampler2D u_texture4;\r\nuniform sampler2D u_texture5;\r\nuniform sampler2D u_texture6;\r\nuniform sampler2D u_texture7;\r\n\r\nin vec2 v_texcoord;\r\nin vec4 v_color;\r\nflat in uint v_textureSlot;\r\n\r\nlayout(location = 0) out vec4 fragColor;\r\n\r\nvoid main(void) {\r\n vec4 sampleColor;\r\n\r\n if (v_textureSlot == 0u) {\r\n sampleColor = texture(u_texture0, v_texcoord);\r\n } else if (v_textureSlot == 1u) {\r\n sampleColor = texture(u_texture1, v_texcoord);\r\n } else if (v_textureSlot == 2u) {\r\n sampleColor = texture(u_texture2, v_texcoord);\r\n } else if (v_textureSlot == 3u) {\r\n sampleColor = texture(u_texture3, v_texcoord);\r\n } else if (v_textureSlot == 4u) {\r\n sampleColor = texture(u_texture4, v_texcoord);\r\n } else if (v_textureSlot == 5u) {\r\n sampleColor = texture(u_texture5, v_texcoord);\r\n } else if (v_textureSlot == 6u) {\r\n sampleColor = texture(u_texture6, v_texcoord);\r\n } else {\r\n sampleColor = texture(u_texture7, v_texcoord);\r\n }\r\n\r\n fragColor = sampleColor * v_color;\r\n}\r\n";
|
|
5644
5644
|
|
|
5645
5645
|
/**
|
|
5646
5646
|
* Instanced sprite renderer for WebGL2.
|
|
@@ -5688,7 +5688,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
5688
5688
|
constructor(batchSize) {
|
|
5689
5689
|
super();
|
|
5690
5690
|
this._batchSize = batchSize;
|
|
5691
|
-
this._shader = new Shader(vertexSource$
|
|
5691
|
+
this._shader = new Shader(vertexSource$4, fragmentSource$4);
|
|
5692
5692
|
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$3);
|
|
5693
5693
|
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
5694
5694
|
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
@@ -5911,226 +5911,385 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
5911
5911
|
}
|
|
5912
5912
|
}
|
|
5913
5913
|
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
* the per-instance buffer. The next `flush()` issues a single
|
|
5925
|
-
* `drawElementsInstanced` for that system.
|
|
5926
|
-
*
|
|
5927
|
-
* Per-instance layout (24 bytes per particle, 4 attributes):
|
|
5928
|
-
* ```
|
|
5929
|
-
* translation f32x2 (offset 0, 8 bytes) particle position (system-local)
|
|
5930
|
-
* scale f32x2 (offset 8, 8 bytes)
|
|
5931
|
-
* rotation f32 (offset 16, 4 bytes) degrees
|
|
5932
|
-
* color u8x4 (offset 20, 4 bytes) RGBA tint, normalised
|
|
5933
|
-
* ```
|
|
5934
|
-
*
|
|
5935
|
-
* vs the previous per-vertex layout (36 bytes per vertex × 4 verts =
|
|
5936
|
-
* 144 bytes per particle), this is an 83% bandwidth reduction and
|
|
5937
|
-
* roughly 80% fewer CPU writes per particle (one pack call vs four
|
|
5938
|
-
* duplicated vertex writes plus per-corner UV coords).
|
|
5939
|
-
*
|
|
5940
|
-
* The system transform stays as a uniform — mixing systems in one
|
|
5941
|
-
* batch would require either a per-instance transform matrix (more
|
|
5942
|
-
* bandwidth) or per-particle texture-slot indexing (multi-texture
|
|
5943
|
-
* support similar to the sprite renderer). Both are follow-ups; the
|
|
5944
|
-
* current pattern keeps the renderer focused on the per-particle win.
|
|
5945
|
-
*/
|
|
5946
|
-
const instanceStrideBytes$2 = 24;
|
|
5947
|
-
const wordsPerInstance$1 = instanceStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
|
|
5948
|
-
const indicesPerQuad = 6;
|
|
5949
|
-
const quadIndices$2 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
5950
|
-
class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
5951
|
-
_shader;
|
|
5952
|
-
_batchSize;
|
|
5953
|
-
_instanceData;
|
|
5954
|
-
_instanceFloat32;
|
|
5955
|
-
_instanceUint32;
|
|
5956
|
-
_instanceCount = 0;
|
|
5957
|
-
_currentTexture = null;
|
|
5958
|
-
_currentBlendMode = null;
|
|
5959
|
-
_currentView = null;
|
|
5960
|
-
_currentViewId = -1;
|
|
5961
|
-
_instanceBuffer = null;
|
|
5962
|
-
_indexBuffer = null;
|
|
5963
|
-
_vao = null;
|
|
5964
|
-
_connection = null;
|
|
5965
|
-
constructor(batchSize) {
|
|
5966
|
-
super();
|
|
5967
|
-
this._batchSize = batchSize;
|
|
5968
|
-
this._shader = new Shader(vertexSource$2, fragmentSource$2);
|
|
5969
|
-
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$2);
|
|
5970
|
-
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
5971
|
-
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
5914
|
+
const createQuadIndices = (size) => {
|
|
5915
|
+
const data = new Uint16Array(size * 6);
|
|
5916
|
+
const len = data.length;
|
|
5917
|
+
for (let i = 0, offset = 0; i < len; i += 6, offset += 4) {
|
|
5918
|
+
data[i] = offset;
|
|
5919
|
+
data[i + 1] = offset + 1;
|
|
5920
|
+
data[i + 2] = offset + 2;
|
|
5921
|
+
data[i + 3] = offset;
|
|
5922
|
+
data[i + 4] = offset + 2;
|
|
5923
|
+
data[i + 5] = offset + 3;
|
|
5972
5924
|
}
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
f32[offset + 4] = particle.rotation;
|
|
6014
|
-
u32[offset + 5] = particle.tint.toRgba();
|
|
5925
|
+
return data;
|
|
5926
|
+
};
|
|
5927
|
+
const createCanvas = (options = {}) => {
|
|
5928
|
+
const { canvas, fillStyle, width, height } = options;
|
|
5929
|
+
const newCanvas = canvas ?? document.createElement('canvas');
|
|
5930
|
+
const context = newCanvas.getContext('2d');
|
|
5931
|
+
newCanvas.width = width ?? 10;
|
|
5932
|
+
newCanvas.height = height ?? 10;
|
|
5933
|
+
context.fillStyle = fillStyle ?? '#6495ed';
|
|
5934
|
+
context.fillRect(0, 0, newCanvas.width, newCanvas.height);
|
|
5935
|
+
return newCanvas;
|
|
5936
|
+
};
|
|
5937
|
+
const heightCache = new Map();
|
|
5938
|
+
const determineFontHeight = (font) => {
|
|
5939
|
+
if (!heightCache.has(font)) {
|
|
5940
|
+
const body = document.body;
|
|
5941
|
+
const dummy = document.createElement('div');
|
|
5942
|
+
dummy.appendChild(document.createTextNode('M'));
|
|
5943
|
+
dummy.setAttribute('style', `font: ${font};position:absolute;top:0;left:0`);
|
|
5944
|
+
body.appendChild(dummy);
|
|
5945
|
+
heightCache.set(font, dummy.offsetHeight);
|
|
5946
|
+
body.removeChild(dummy);
|
|
5947
|
+
}
|
|
5948
|
+
return heightCache.get(font);
|
|
5949
|
+
};
|
|
5950
|
+
|
|
5951
|
+
class Texture {
|
|
5952
|
+
static _black = null;
|
|
5953
|
+
static _white = null;
|
|
5954
|
+
static defaultSamplerOptions = {
|
|
5955
|
+
scaleMode: ScaleModes.Linear,
|
|
5956
|
+
wrapMode: WrapModes.ClampToEdge,
|
|
5957
|
+
premultiplyAlpha: true,
|
|
5958
|
+
generateMipMap: true,
|
|
5959
|
+
flipY: false,
|
|
5960
|
+
};
|
|
5961
|
+
static empty = new Texture(null);
|
|
5962
|
+
static get black() {
|
|
5963
|
+
if (Texture._black === null) {
|
|
5964
|
+
Texture._black = new Texture(createCanvas({ fillStyle: '#000' }));
|
|
6015
5965
|
}
|
|
6016
|
-
|
|
6017
|
-
return this;
|
|
5966
|
+
return Texture._black;
|
|
6018
5967
|
}
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
const indexBuffer = this._indexBuffer;
|
|
6023
|
-
const vao = this._vao;
|
|
6024
|
-
if (this._instanceCount === 0 || backend === null || instanceBuffer === null || indexBuffer === null || vao === null) {
|
|
6025
|
-
return;
|
|
5968
|
+
static get white() {
|
|
5969
|
+
if (Texture._white === null) {
|
|
5970
|
+
Texture._white = new Texture(createCanvas({ fillStyle: '#fff' }));
|
|
6026
5971
|
}
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
5972
|
+
return Texture._white;
|
|
5973
|
+
}
|
|
5974
|
+
_version = 0;
|
|
5975
|
+
_source = null;
|
|
5976
|
+
_size = new Size(0, 0);
|
|
5977
|
+
_destroyListeners = new Set();
|
|
5978
|
+
_scaleMode;
|
|
5979
|
+
_wrapMode;
|
|
5980
|
+
_premultiplyAlpha = false;
|
|
5981
|
+
_generateMipMap = false;
|
|
5982
|
+
_flipY = false;
|
|
5983
|
+
constructor(source = null, options) {
|
|
5984
|
+
const { scaleMode, wrapMode, premultiplyAlpha, generateMipMap, flipY } = { ...Texture.defaultSamplerOptions, ...options };
|
|
5985
|
+
this._scaleMode = scaleMode;
|
|
5986
|
+
this._wrapMode = wrapMode;
|
|
5987
|
+
this._premultiplyAlpha = premultiplyAlpha;
|
|
5988
|
+
this._generateMipMap = generateMipMap;
|
|
5989
|
+
this._flipY = flipY;
|
|
5990
|
+
if (source !== null) {
|
|
5991
|
+
this.setSource(source);
|
|
6034
5992
|
}
|
|
6035
|
-
this._shader.sync();
|
|
6036
|
-
backend.bindVertexArrayObject(vao);
|
|
6037
|
-
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._instanceCount * wordsPerInstance$1));
|
|
6038
|
-
vao.drawInstanced(indicesPerQuad, 0, this._instanceCount, RenderingPrimitives.Triangles);
|
|
6039
|
-
backend.stats.batches++;
|
|
6040
|
-
backend.stats.drawCalls++;
|
|
6041
|
-
this._instanceCount = 0;
|
|
6042
5993
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
6046
|
-
this._connection = this._createConnection(gl);
|
|
6047
|
-
this._indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$2, BufferUsage.StaticDraw)
|
|
6048
|
-
.connect(this._createBufferRuntime(this._connection));
|
|
6049
|
-
this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw)
|
|
6050
|
-
.connect(this._createBufferRuntime(this._connection));
|
|
6051
|
-
this._shader.sync();
|
|
6052
|
-
this._vao = new WebGl2VertexArrayObject()
|
|
6053
|
-
.addIndex(this._indexBuffer)
|
|
6054
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_translation'), gl.FLOAT, false, instanceStrideBytes$2, 0, false, 1)
|
|
6055
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_scale'), gl.FLOAT, false, instanceStrideBytes$2, 8, false, 1)
|
|
6056
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_rotation'), gl.FLOAT, false, instanceStrideBytes$2, 16, false, 1)
|
|
6057
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$2, 20, false, 1)
|
|
6058
|
-
.connect(this._createVaoRuntime(this._connection));
|
|
5994
|
+
get source() {
|
|
5995
|
+
return this._source;
|
|
6059
5996
|
}
|
|
6060
|
-
|
|
6061
|
-
this.
|
|
6062
|
-
this._instanceBuffer?.destroy();
|
|
6063
|
-
this._instanceBuffer = null;
|
|
6064
|
-
this._indexBuffer?.destroy();
|
|
6065
|
-
this._indexBuffer = null;
|
|
6066
|
-
this._vao?.destroy();
|
|
6067
|
-
this._vao = null;
|
|
6068
|
-
this._connection = null;
|
|
6069
|
-
this._currentTexture = null;
|
|
6070
|
-
this._currentBlendMode = null;
|
|
6071
|
-
this._currentView = null;
|
|
6072
|
-
this._currentViewId = -1;
|
|
6073
|
-
this._instanceCount = 0;
|
|
5997
|
+
set source(source) {
|
|
5998
|
+
this.setSource(source);
|
|
6074
5999
|
}
|
|
6075
|
-
|
|
6076
|
-
this.
|
|
6077
|
-
this._shader.destroy();
|
|
6000
|
+
get size() {
|
|
6001
|
+
return this._size;
|
|
6078
6002
|
}
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
* (uMin, vMin, uMax, vMax) bounds the new vertex shader expects.
|
|
6082
|
-
* Already accounts for flipY (the system's `texCoords` baked it in).
|
|
6083
|
-
*
|
|
6084
|
-
* The four packed u32s, by corner index, are:
|
|
6085
|
-
* [0] TL = (uMin/uMax | vMin/vMax depending on flipY)
|
|
6086
|
-
* [1] TR
|
|
6087
|
-
* [2] BR
|
|
6088
|
-
* [3] BL
|
|
6089
|
-
* We need (uMin, vMin, uMax, vMax) — the corner-extreme values.
|
|
6090
|
-
*/
|
|
6091
|
-
_uvBoundsScratch = new Float32Array(4);
|
|
6092
|
-
_unpackUvBounds(system) {
|
|
6093
|
-
const texCoords = system.texCoords;
|
|
6094
|
-
// Each packed u32: low 16 bits = U normalised to 0..65535,
|
|
6095
|
-
// high 16 bits = V normalised.
|
|
6096
|
-
const uTopLeft = (texCoords[0] & 0xFFFF) / 0xFFFF;
|
|
6097
|
-
const vTopLeft = ((texCoords[0] >>> 16) & 0xFFFF) / 0xFFFF;
|
|
6098
|
-
const uBottomRight = (texCoords[2] & 0xFFFF) / 0xFFFF;
|
|
6099
|
-
const vBottomRight = ((texCoords[2] >>> 16) & 0xFFFF) / 0xFFFF;
|
|
6100
|
-
// For flipY: TL.v becomes vMax (was minY → maxY). The shader picks
|
|
6101
|
-
// (cornerY == 0 ? vMin : vMax); writing TL.v into vMin and BR.v into
|
|
6102
|
-
// vMax matches the original per-corner ordering whether or not flipY
|
|
6103
|
-
// was applied at texCoords pack time.
|
|
6104
|
-
this._uvBoundsScratch[0] = uTopLeft;
|
|
6105
|
-
this._uvBoundsScratch[1] = vTopLeft;
|
|
6106
|
-
this._uvBoundsScratch[2] = uBottomRight;
|
|
6107
|
-
this._uvBoundsScratch[3] = vBottomRight;
|
|
6108
|
-
return this._uvBoundsScratch;
|
|
6003
|
+
set size(size) {
|
|
6004
|
+
this.setSize(size.width, size.height);
|
|
6109
6005
|
}
|
|
6110
|
-
|
|
6006
|
+
get width() {
|
|
6007
|
+
return this._size.width;
|
|
6008
|
+
}
|
|
6009
|
+
set width(width) {
|
|
6010
|
+
this.setSize(width, this.height);
|
|
6011
|
+
}
|
|
6012
|
+
get height() {
|
|
6013
|
+
return this._size.height;
|
|
6014
|
+
}
|
|
6015
|
+
set height(height) {
|
|
6016
|
+
this.setSize(this.width, height);
|
|
6017
|
+
}
|
|
6018
|
+
get scaleMode() {
|
|
6019
|
+
return this._scaleMode;
|
|
6020
|
+
}
|
|
6021
|
+
set scaleMode(scaleMode) {
|
|
6022
|
+
this.setScaleMode(scaleMode);
|
|
6023
|
+
}
|
|
6024
|
+
get wrapMode() {
|
|
6025
|
+
return this._wrapMode;
|
|
6026
|
+
}
|
|
6027
|
+
set wrapMode(wrapMode) {
|
|
6028
|
+
this.setWrapMode(wrapMode);
|
|
6029
|
+
}
|
|
6030
|
+
get premultiplyAlpha() {
|
|
6031
|
+
return this._premultiplyAlpha;
|
|
6032
|
+
}
|
|
6033
|
+
set premultiplyAlpha(premultiplyAlpha) {
|
|
6034
|
+
this.setPremultiplyAlpha(premultiplyAlpha);
|
|
6035
|
+
}
|
|
6036
|
+
get generateMipMap() {
|
|
6037
|
+
return this._generateMipMap;
|
|
6038
|
+
}
|
|
6039
|
+
set generateMipMap(generateMipMap) {
|
|
6040
|
+
this._generateMipMap = generateMipMap;
|
|
6041
|
+
}
|
|
6042
|
+
get flipY() {
|
|
6043
|
+
return this._flipY;
|
|
6044
|
+
}
|
|
6045
|
+
set flipY(flipY) {
|
|
6046
|
+
this._flipY = flipY;
|
|
6047
|
+
}
|
|
6048
|
+
get powerOfTwo() {
|
|
6049
|
+
return isPowerOfTwo(this.width) && isPowerOfTwo(this.height);
|
|
6050
|
+
}
|
|
6051
|
+
get version() {
|
|
6052
|
+
return this._version;
|
|
6053
|
+
}
|
|
6054
|
+
addDestroyListener(listener) {
|
|
6055
|
+
this._destroyListeners.add(listener);
|
|
6056
|
+
return this;
|
|
6057
|
+
}
|
|
6058
|
+
removeDestroyListener(listener) {
|
|
6059
|
+
this._destroyListeners.delete(listener);
|
|
6060
|
+
return this;
|
|
6061
|
+
}
|
|
6062
|
+
setScaleMode(scaleMode) {
|
|
6063
|
+
if (this._scaleMode !== scaleMode) {
|
|
6064
|
+
this._scaleMode = scaleMode;
|
|
6065
|
+
this._touch();
|
|
6066
|
+
}
|
|
6067
|
+
return this;
|
|
6068
|
+
}
|
|
6069
|
+
setWrapMode(wrapMode) {
|
|
6070
|
+
if (this._wrapMode !== wrapMode) {
|
|
6071
|
+
this._wrapMode = wrapMode;
|
|
6072
|
+
this._touch();
|
|
6073
|
+
}
|
|
6074
|
+
return this;
|
|
6075
|
+
}
|
|
6076
|
+
setPremultiplyAlpha(premultiplyAlpha) {
|
|
6077
|
+
if (this._premultiplyAlpha !== premultiplyAlpha) {
|
|
6078
|
+
this._premultiplyAlpha = premultiplyAlpha;
|
|
6079
|
+
this._touch();
|
|
6080
|
+
}
|
|
6081
|
+
return this;
|
|
6082
|
+
}
|
|
6083
|
+
setSource(source) {
|
|
6084
|
+
if (this._source !== source) {
|
|
6085
|
+
this._source = source;
|
|
6086
|
+
this.updateSource();
|
|
6087
|
+
}
|
|
6088
|
+
return this;
|
|
6089
|
+
}
|
|
6090
|
+
updateSource() {
|
|
6091
|
+
const { width, height } = getTextureSourceSize(this._source);
|
|
6092
|
+
this.setSize(width, height);
|
|
6093
|
+
this._touch();
|
|
6094
|
+
return this;
|
|
6095
|
+
}
|
|
6096
|
+
setSize(width, height) {
|
|
6097
|
+
if (!this._size.equals({ width, height })) {
|
|
6098
|
+
this._size.set(width, height);
|
|
6099
|
+
this._touch();
|
|
6100
|
+
}
|
|
6101
|
+
return this;
|
|
6102
|
+
}
|
|
6103
|
+
destroy() {
|
|
6104
|
+
for (const listener of Array.from(this._destroyListeners)) {
|
|
6105
|
+
listener();
|
|
6106
|
+
}
|
|
6107
|
+
this._destroyListeners.clear();
|
|
6108
|
+
this._size.destroy();
|
|
6109
|
+
this._source = null;
|
|
6110
|
+
}
|
|
6111
|
+
_touch() {
|
|
6112
|
+
this._version++;
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
var vertexSource$3 = "#version 300 es\nprecision lowp float;\n\nlayout(location = 0) in vec2 a_position;\nlayout(location = 1) in vec2 a_texcoord;\nlayout(location = 2) in vec4 a_color;\n\nuniform mat3 u_projection;\nuniform mat3 u_translation;\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n gl_Position = vec4((u_projection * u_translation * vec3(a_position, 1.0)).xy, 0.0, 1.0);\n v_texcoord = a_texcoord;\n v_color = a_color;\n}\n";
|
|
6117
|
+
|
|
6118
|
+
var fragmentSource$3 = "#version 300 es\nprecision lowp float;\n\nuniform sampler2D u_texture;\nuniform vec4 u_tint;\n\nin vec2 v_texcoord;\nin vec4 v_color;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main(void) {\n vec4 base = texture(u_texture, v_texcoord) * v_color * u_tint;\n fragColor = vec4(base.rgb * base.a, base.a);\n}\n";
|
|
6119
|
+
|
|
6120
|
+
// Per-vertex layout (20 bytes):
|
|
6121
|
+
// position: vec2 f32 (offset 0, 8 bytes)
|
|
6122
|
+
// texcoord: vec2 f32 (offset 8, 8 bytes)
|
|
6123
|
+
// color: u8x4 norm (offset 16, 4 bytes)
|
|
6124
|
+
const vertexStrideBytes$5 = 20;
|
|
6125
|
+
const vertexStrideWords$1 = vertexStrideBytes$5 / 4;
|
|
6126
|
+
const initialVertexCapacity = 64;
|
|
6127
|
+
const initialIndexCapacity = 192;
|
|
6128
|
+
const defaultVertexColor = 0xFFFFFFFF; // white, full alpha
|
|
6129
|
+
class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
6130
|
+
_shader = new Shader(vertexSource$3, fragmentSource$3);
|
|
6131
|
+
_tintScratch = new Float32Array(4);
|
|
6132
|
+
_textureUnitScratch = new Int32Array([0]);
|
|
6133
|
+
_vertexCapacity = initialVertexCapacity;
|
|
6134
|
+
_indexCapacity = initialIndexCapacity;
|
|
6135
|
+
_vertexData = new ArrayBuffer(initialVertexCapacity * vertexStrideBytes$5);
|
|
6136
|
+
_float32View = new Float32Array(this._vertexData);
|
|
6137
|
+
_uint32View = new Uint32Array(this._vertexData);
|
|
6138
|
+
_indexData = new Uint16Array(initialIndexCapacity);
|
|
6139
|
+
_connection = null;
|
|
6140
|
+
_currentBlendMode = null;
|
|
6141
|
+
_currentView = null;
|
|
6142
|
+
_viewId = -1;
|
|
6143
|
+
render(mesh) {
|
|
6144
|
+
const connection = this._connection;
|
|
6145
|
+
if (!connection) {
|
|
6146
|
+
throw new Error('WebGl2MeshRenderer is not connected to a backend.');
|
|
6147
|
+
}
|
|
6148
|
+
const vertexCount = mesh.vertexCount;
|
|
6149
|
+
if (vertexCount === 0) {
|
|
6150
|
+
return;
|
|
6151
|
+
}
|
|
6152
|
+
const backend = this.getBackend();
|
|
6153
|
+
const blendMode = mesh.blendMode;
|
|
6154
|
+
if (blendMode !== this._currentBlendMode) {
|
|
6155
|
+
this._currentBlendMode = blendMode;
|
|
6156
|
+
backend.setBlendMode(blendMode);
|
|
6157
|
+
}
|
|
6158
|
+
const view = backend.view;
|
|
6159
|
+
if (this._currentView !== view || this._viewId !== view.updateId) {
|
|
6160
|
+
this._currentView = view;
|
|
6161
|
+
this._viewId = view.updateId;
|
|
6162
|
+
this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
|
|
6163
|
+
}
|
|
6164
|
+
this._shader.getUniform('u_translation').setValue(mesh.getGlobalTransform().toArray(false));
|
|
6165
|
+
const tint = mesh.tint;
|
|
6166
|
+
this._tintScratch[0] = tint.red;
|
|
6167
|
+
this._tintScratch[1] = tint.green;
|
|
6168
|
+
this._tintScratch[2] = tint.blue;
|
|
6169
|
+
this._tintScratch[3] = tint.alpha;
|
|
6170
|
+
this._shader.getUniform('u_tint').setValue(this._tintScratch);
|
|
6171
|
+
// The fragment shader always samples u_texture; meshes without an
|
|
6172
|
+
// explicit texture get the engine's 1x1 white default so the shader
|
|
6173
|
+
// path stays branchless.
|
|
6174
|
+
const meshTexture = mesh.texture ?? Texture.white;
|
|
6175
|
+
this._shader.getUniform('u_texture').setValue(this._textureUnitScratch);
|
|
6176
|
+
backend.bindTexture(meshTexture, 0);
|
|
6177
|
+
this._ensureVertexCapacity(vertexCount);
|
|
6178
|
+
const positions = mesh.vertices;
|
|
6179
|
+
const uvs = mesh.uvs;
|
|
6180
|
+
const colors = mesh.colors;
|
|
6181
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
6182
|
+
const word = i * vertexStrideWords$1;
|
|
6183
|
+
const pair = i * 2;
|
|
6184
|
+
this._float32View[word] = positions[pair];
|
|
6185
|
+
this._float32View[word + 1] = positions[pair + 1];
|
|
6186
|
+
if (uvs !== null) {
|
|
6187
|
+
this._float32View[word + 2] = uvs[pair];
|
|
6188
|
+
this._float32View[word + 3] = uvs[pair + 1];
|
|
6189
|
+
}
|
|
6190
|
+
else {
|
|
6191
|
+
this._float32View[word + 2] = 0;
|
|
6192
|
+
this._float32View[word + 3] = 0;
|
|
6193
|
+
}
|
|
6194
|
+
this._uint32View[word + 4] = colors !== null ? colors[i] : defaultVertexColor;
|
|
6195
|
+
}
|
|
6196
|
+
const indexCount = mesh.indexCount;
|
|
6197
|
+
this._ensureIndexCapacity(indexCount);
|
|
6198
|
+
if (mesh.indices !== null) {
|
|
6199
|
+
this._indexData.set(mesh.indices, 0);
|
|
6200
|
+
}
|
|
6201
|
+
else {
|
|
6202
|
+
for (let i = 0; i < indexCount; i++) {
|
|
6203
|
+
this._indexData[i] = i;
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
this._shader.sync();
|
|
6207
|
+
backend.bindVertexArrayObject(connection.vao);
|
|
6208
|
+
connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords$1));
|
|
6209
|
+
connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
|
|
6210
|
+
connection.vao.draw(indexCount, 0, RenderingPrimitives.Triangles);
|
|
6211
|
+
backend.stats.batches++;
|
|
6212
|
+
backend.stats.drawCalls++;
|
|
6213
|
+
}
|
|
6214
|
+
flush() {
|
|
6215
|
+
// Mesh draws are immediate per-mesh; nothing to batch.
|
|
6216
|
+
}
|
|
6217
|
+
destroy() {
|
|
6218
|
+
this.disconnect();
|
|
6219
|
+
this._shader.destroy();
|
|
6220
|
+
this._currentBlendMode = null;
|
|
6221
|
+
this._currentView = null;
|
|
6222
|
+
}
|
|
6223
|
+
onConnect(backend) {
|
|
6224
|
+
const gl = backend.context;
|
|
6111
6225
|
const vaoHandle = gl.createVertexArray();
|
|
6112
6226
|
if (vaoHandle === null) {
|
|
6113
|
-
throw new Error('
|
|
6227
|
+
throw new Error('Could not create vertex array object.');
|
|
6114
6228
|
}
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6229
|
+
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
6230
|
+
const buffers = new Map();
|
|
6231
|
+
const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, this._indexData, BufferUsage.DynamicDraw)
|
|
6232
|
+
.connect(this._createBufferRuntime(gl, buffers));
|
|
6233
|
+
const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
|
|
6234
|
+
.connect(this._createBufferRuntime(gl, buffers));
|
|
6235
|
+
// Force the shader's first finalize so attribute locations are
|
|
6236
|
+
// available immediately (the async-compile path defers extraction
|
|
6237
|
+
// to first sync(), but we need attributes here for VAO setup).
|
|
6238
|
+
this._shader.sync();
|
|
6239
|
+
const vao = new WebGl2VertexArrayObject()
|
|
6240
|
+
.addIndex(indexBuffer)
|
|
6241
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$5, 0)
|
|
6242
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$5, 8)
|
|
6243
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$5, 16)
|
|
6244
|
+
.connect(this._createVaoRuntime(gl, vaoHandle));
|
|
6245
|
+
this._connection = { gl, vao, vertexBuffer, indexBuffer };
|
|
6120
6246
|
}
|
|
6121
|
-
|
|
6122
|
-
const
|
|
6247
|
+
onDisconnect() {
|
|
6248
|
+
const connection = this._connection;
|
|
6249
|
+
if (!connection) {
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
this._shader.disconnect();
|
|
6253
|
+
connection.indexBuffer.destroy();
|
|
6254
|
+
connection.vertexBuffer.destroy();
|
|
6255
|
+
connection.vao.destroy();
|
|
6256
|
+
this._connection = null;
|
|
6257
|
+
this._currentBlendMode = null;
|
|
6258
|
+
this._currentView = null;
|
|
6259
|
+
this._viewId = -1;
|
|
6260
|
+
}
|
|
6261
|
+
_ensureVertexCapacity(vertexCount) {
|
|
6262
|
+
if (vertexCount <= this._vertexCapacity) {
|
|
6263
|
+
return;
|
|
6264
|
+
}
|
|
6265
|
+
while (this._vertexCapacity < vertexCount) {
|
|
6266
|
+
this._vertexCapacity *= 2;
|
|
6267
|
+
}
|
|
6268
|
+
this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$5);
|
|
6269
|
+
this._float32View = new Float32Array(this._vertexData);
|
|
6270
|
+
this._uint32View = new Uint32Array(this._vertexData);
|
|
6271
|
+
}
|
|
6272
|
+
_ensureIndexCapacity(indexCount) {
|
|
6273
|
+
if (indexCount <= this._indexCapacity) {
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
6276
|
+
while (this._indexCapacity < indexCount) {
|
|
6277
|
+
this._indexCapacity *= 2;
|
|
6278
|
+
}
|
|
6279
|
+
this._indexData = new Uint16Array(this._indexCapacity);
|
|
6280
|
+
}
|
|
6281
|
+
_createBufferRuntime(gl, buffers) {
|
|
6282
|
+
const handle = gl.createBuffer();
|
|
6123
6283
|
if (handle === null) {
|
|
6124
|
-
throw new Error('
|
|
6284
|
+
throw new Error('Could not create render buffer.');
|
|
6125
6285
|
}
|
|
6126
6286
|
return {
|
|
6127
6287
|
bind: (buffer) => {
|
|
6128
|
-
|
|
6288
|
+
gl.bindBuffer(buffer.type, handle);
|
|
6129
6289
|
},
|
|
6130
6290
|
upload: (buffer, offset) => {
|
|
6131
|
-
const
|
|
6291
|
+
const state = buffers.get(buffer);
|
|
6132
6292
|
const data = buffer.data;
|
|
6133
|
-
const state = connection.buffers.get(buffer);
|
|
6134
6293
|
gl.bindBuffer(buffer.type, handle);
|
|
6135
6294
|
if (state && state.dataByteLength >= data.byteLength) {
|
|
6136
6295
|
gl.bufferSubData(buffer.type, offset, data);
|
|
@@ -6138,22 +6297,21 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
|
6138
6297
|
}
|
|
6139
6298
|
else {
|
|
6140
6299
|
gl.bufferData(buffer.type, data, buffer.usage);
|
|
6141
|
-
|
|
6300
|
+
buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
6142
6301
|
}
|
|
6143
6302
|
},
|
|
6144
6303
|
destroy: (buffer) => {
|
|
6145
|
-
|
|
6146
|
-
|
|
6304
|
+
gl.deleteBuffer(handle);
|
|
6305
|
+
buffers.delete(buffer);
|
|
6147
6306
|
buffer.disconnect();
|
|
6148
6307
|
},
|
|
6149
6308
|
};
|
|
6150
6309
|
}
|
|
6151
|
-
_createVaoRuntime(
|
|
6310
|
+
_createVaoRuntime(gl, vaoHandle) {
|
|
6152
6311
|
let appliedVersion = -1;
|
|
6153
6312
|
return {
|
|
6154
6313
|
bind: (vao) => {
|
|
6155
|
-
|
|
6156
|
-
gl.bindVertexArray(connection.vaoHandle);
|
|
6314
|
+
gl.bindVertexArray(vaoHandle);
|
|
6157
6315
|
if (appliedVersion !== vao.version) {
|
|
6158
6316
|
let lastBuffer = null;
|
|
6159
6317
|
for (const attribute of vao.attributes) {
|
|
@@ -6161,14 +6319,8 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
|
6161
6319
|
attribute.buffer.bind();
|
|
6162
6320
|
lastBuffer = attribute.buffer;
|
|
6163
6321
|
}
|
|
6164
|
-
|
|
6165
|
-
gl.vertexAttribIPointer(attribute.location, attribute.size, attribute.type, attribute.stride, attribute.start);
|
|
6166
|
-
}
|
|
6167
|
-
else {
|
|
6168
|
-
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
|
|
6169
|
-
}
|
|
6322
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
|
|
6170
6323
|
gl.enableVertexAttribArray(attribute.location);
|
|
6171
|
-
gl.vertexAttribDivisor(attribute.location, attribute.divisor);
|
|
6172
6324
|
}
|
|
6173
6325
|
if (vao.indexBuffer) {
|
|
6174
6326
|
vao.indexBuffer.bind();
|
|
@@ -6177,10 +6329,9 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
|
6177
6329
|
}
|
|
6178
6330
|
},
|
|
6179
6331
|
unbind: () => {
|
|
6180
|
-
|
|
6332
|
+
gl.bindVertexArray(null);
|
|
6181
6333
|
},
|
|
6182
6334
|
draw: (vao, size, start, type) => {
|
|
6183
|
-
const gl = connection.gl;
|
|
6184
6335
|
if (vao.indexBuffer) {
|
|
6185
6336
|
gl.drawElements(type, size, gl.UNSIGNED_SHORT, start);
|
|
6186
6337
|
}
|
|
@@ -6188,181 +6339,234 @@ class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
|
6188
6339
|
gl.drawArrays(type, start, size);
|
|
6189
6340
|
}
|
|
6190
6341
|
},
|
|
6191
|
-
drawInstanced: (vao, count, start, instanceCount, type) => {
|
|
6192
|
-
const gl = connection.gl;
|
|
6193
|
-
if (vao.indexBuffer) {
|
|
6194
|
-
gl.drawElementsInstanced(type, count, gl.UNSIGNED_SHORT, start, instanceCount);
|
|
6195
|
-
}
|
|
6196
|
-
else {
|
|
6197
|
-
gl.drawArraysInstanced(type, start, count, instanceCount);
|
|
6198
|
-
}
|
|
6199
|
-
},
|
|
6200
6342
|
destroy: (vao) => {
|
|
6201
|
-
|
|
6343
|
+
gl.deleteVertexArray(vaoHandle);
|
|
6202
6344
|
vao.disconnect();
|
|
6203
6345
|
},
|
|
6204
6346
|
};
|
|
6205
6347
|
}
|
|
6206
6348
|
}
|
|
6207
6349
|
|
|
6208
|
-
var vertexSource$
|
|
6350
|
+
var vertexSource$2 = "#version 300 es\nprecision lowp float;\nprecision lowp int;\n\n// Per-instance attributes (one entry per particle, 24 bytes total).\nlayout(location = 0) in vec2 a_translation; // particle position in system-local space\nlayout(location = 1) in vec2 a_scale; // particle scale\nlayout(location = 2) in float a_rotation; // particle rotation in degrees\nlayout(location = 3) in vec4 a_color; // RGBA tint\n\nuniform mat3 u_projection;\nuniform mat3 u_systemTransform;\nuniform vec4 u_localBounds; // left, top, right, bottom (system.vertices)\nuniform vec4 u_uvBounds; // uMin, vMin, uMax, vMax (flipY-swapped)\n\nout vec2 v_texcoord;\nout vec4 v_color;\n\nvoid main(void) {\n // Static index buffer is [0,1,2,0,2,3] (triangle-list), so gl_VertexID 0..3\n // maps to TL, TR, BR, BL via the same bit math the sprite renderer uses.\n int vid = gl_VertexID;\n int cornerX = ((vid + 1) >> 1) & 1;\n int cornerY = vid >> 1;\n\n float localX = (cornerX == 0) ? u_localBounds.x : u_localBounds.z;\n float localY = (cornerY == 0) ? u_localBounds.y : u_localBounds.w;\n\n // Per-particle scale + rotation.\n vec2 rotation = vec2(sin(radians(a_rotation)), cos(radians(a_rotation)));\n vec2 transformed = vec2(\n (localX * (a_scale.x * rotation.y)) + (localY * (a_scale.y * rotation.x)),\n (localX * (a_scale.x * -rotation.x)) + (localY * (a_scale.y * rotation.y))\n );\n\n vec3 worldPos = vec3(transformed + a_translation, 1.0);\n\n gl_Position = vec4((u_projection * u_systemTransform * worldPos).xy, 0.0, 1.0);\n\n float u = (cornerX == 0) ? u_uvBounds.x : u_uvBounds.z;\n float v = (cornerY == 0) ? u_uvBounds.y : u_uvBounds.w;\n v_texcoord = vec2(u, v);\n\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\n}\n";
|
|
6209
6351
|
|
|
6210
|
-
var fragmentSource$
|
|
6352
|
+
var fragmentSource$2 = "#version 300 es\r\nprecision lowp float;\r\n\r\nuniform sampler2D u_texture;\r\n\r\nin vec2 v_texcoord;\r\nin vec4 v_color;\r\n\r\nlayout(location = 0) out vec4 fragColor;\r\n\r\nvoid main(void) {\r\n fragColor = texture(u_texture, v_texcoord) * v_color;\r\n}\r\n";
|
|
6211
6353
|
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6354
|
+
/**
|
|
6355
|
+
* Instanced particle renderer for WebGL2.
|
|
6356
|
+
*
|
|
6357
|
+
* One ParticleSystem = one batch. Each `render(system)` flushes any
|
|
6358
|
+
* pending batch, sets the system-level uniforms (transform, local
|
|
6359
|
+
* bounds, UV bounds, texture), and packs every active particle into
|
|
6360
|
+
* the per-instance buffer. The next `flush()` issues a single
|
|
6361
|
+
* `drawElementsInstanced` for that system.
|
|
6362
|
+
*
|
|
6363
|
+
* Per-instance layout (24 bytes per particle, 4 attributes):
|
|
6364
|
+
* ```
|
|
6365
|
+
* translation f32x2 (offset 0, 8 bytes) particle position (system-local)
|
|
6366
|
+
* scale f32x2 (offset 8, 8 bytes)
|
|
6367
|
+
* rotation f32 (offset 16, 4 bytes) degrees
|
|
6368
|
+
* color u8x4 (offset 20, 4 bytes) RGBA tint, normalised
|
|
6369
|
+
* ```
|
|
6370
|
+
*
|
|
6371
|
+
* vs the previous per-vertex layout (36 bytes per vertex × 4 verts =
|
|
6372
|
+
* 144 bytes per particle), this is an 83% bandwidth reduction and
|
|
6373
|
+
* roughly 80% fewer CPU writes per particle (one pack call vs four
|
|
6374
|
+
* duplicated vertex writes plus per-corner UV coords).
|
|
6375
|
+
*
|
|
6376
|
+
* The system transform stays as a uniform — mixing systems in one
|
|
6377
|
+
* batch would require either a per-instance transform matrix (more
|
|
6378
|
+
* bandwidth) or per-particle texture-slot indexing (multi-texture
|
|
6379
|
+
* support similar to the sprite renderer). Both are follow-ups; the
|
|
6380
|
+
* current pattern keeps the renderer focused on the per-particle win.
|
|
6381
|
+
*/
|
|
6382
|
+
const instanceStrideBytes$2 = 24;
|
|
6383
|
+
const wordsPerInstance$1 = instanceStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
|
|
6384
|
+
const indicesPerQuad = 6;
|
|
6385
|
+
const quadIndices$2 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
6386
|
+
class WebGl2ParticleRenderer extends AbstractWebGl2Renderer {
|
|
6387
|
+
_shader;
|
|
6388
|
+
_batchSize;
|
|
6389
|
+
_instanceData;
|
|
6390
|
+
_instanceFloat32;
|
|
6391
|
+
_instanceUint32;
|
|
6392
|
+
_instanceCount = 0;
|
|
6393
|
+
_currentTexture = null;
|
|
6224
6394
|
_currentBlendMode = null;
|
|
6225
6395
|
_currentView = null;
|
|
6226
|
-
|
|
6396
|
+
_currentViewId = -1;
|
|
6397
|
+
_instanceBuffer = null;
|
|
6398
|
+
_indexBuffer = null;
|
|
6399
|
+
_vao = null;
|
|
6400
|
+
_connection = null;
|
|
6227
6401
|
constructor(batchSize) {
|
|
6228
6402
|
super();
|
|
6229
|
-
this.
|
|
6230
|
-
this.
|
|
6231
|
-
this.
|
|
6232
|
-
this.
|
|
6233
|
-
this.
|
|
6234
|
-
this._uint32View = new Uint32Array(this._vertexData);
|
|
6403
|
+
this._batchSize = batchSize;
|
|
6404
|
+
this._shader = new Shader(vertexSource$2, fragmentSource$2);
|
|
6405
|
+
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$2);
|
|
6406
|
+
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
6407
|
+
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
6235
6408
|
}
|
|
6236
|
-
render(
|
|
6237
|
-
const connection = this._connection;
|
|
6238
|
-
if (!connection) {
|
|
6239
|
-
throw new Error('Renderer not connected');
|
|
6240
|
-
}
|
|
6409
|
+
render(system) {
|
|
6241
6410
|
const backend = this.getBackend();
|
|
6242
|
-
const {
|
|
6243
|
-
const
|
|
6244
|
-
const
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6411
|
+
const { texture, particles, blendMode } = system;
|
|
6412
|
+
const textureChanged = texture !== this._currentTexture;
|
|
6413
|
+
const blendModeChanged = blendMode !== this._currentBlendMode;
|
|
6414
|
+
// System transform / texture / UV / local-bounds are uniforms, so
|
|
6415
|
+
// mixing systems in one batch is invalid. Flush any prior system
|
|
6416
|
+
// before setting up this one.
|
|
6417
|
+
this.flush();
|
|
6418
|
+
if (textureChanged) {
|
|
6419
|
+
this._currentTexture = texture;
|
|
6420
|
+
backend.bindTexture(texture);
|
|
6249
6421
|
}
|
|
6250
|
-
|
|
6251
|
-
this._ensureIndexCapacity(indexCount);
|
|
6252
|
-
if (blendMode !== this._currentBlendMode) {
|
|
6422
|
+
if (blendModeChanged) {
|
|
6253
6423
|
this._currentBlendMode = blendMode;
|
|
6254
6424
|
backend.setBlendMode(blendMode);
|
|
6255
6425
|
}
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6426
|
+
// System-level uniforms are set before packing so the eventual
|
|
6427
|
+
// flush() can sync them in one go.
|
|
6428
|
+
const localBounds = system.vertices;
|
|
6429
|
+
const uvBounds = this._unpackUvBounds(system);
|
|
6430
|
+
this._shader
|
|
6431
|
+
.getUniform('u_systemTransform')
|
|
6432
|
+
.setValue(system.getGlobalTransform().toArray(false));
|
|
6433
|
+
this._shader
|
|
6434
|
+
.getUniform('u_localBounds')
|
|
6435
|
+
.setValue(localBounds);
|
|
6436
|
+
this._shader
|
|
6437
|
+
.getUniform('u_uvBounds')
|
|
6438
|
+
.setValue(uvBounds);
|
|
6439
|
+
const f32 = this._instanceFloat32;
|
|
6440
|
+
const u32 = this._instanceUint32;
|
|
6441
|
+
const limit = Math.min(particles.length, this._batchSize);
|
|
6442
|
+
for (let i = 0; i < limit; i++) {
|
|
6443
|
+
const particle = particles[i];
|
|
6444
|
+
const offset = i * wordsPerInstance$1;
|
|
6445
|
+
f32[offset + 0] = particle.position.x;
|
|
6446
|
+
f32[offset + 1] = particle.position.y;
|
|
6447
|
+
f32[offset + 2] = particle.scale.x;
|
|
6448
|
+
f32[offset + 3] = particle.scale.y;
|
|
6449
|
+
f32[offset + 4] = particle.rotation;
|
|
6450
|
+
u32[offset + 5] = particle.tint.toRgba();
|
|
6270
6451
|
}
|
|
6271
|
-
|
|
6272
|
-
|
|
6452
|
+
this._instanceCount = limit;
|
|
6453
|
+
return this;
|
|
6454
|
+
}
|
|
6455
|
+
flush() {
|
|
6456
|
+
const backend = this.getBackendOrNull();
|
|
6457
|
+
const instanceBuffer = this._instanceBuffer;
|
|
6458
|
+
const indexBuffer = this._indexBuffer;
|
|
6459
|
+
const vao = this._vao;
|
|
6460
|
+
if (this._instanceCount === 0 || backend === null || instanceBuffer === null || indexBuffer === null || vao === null) {
|
|
6461
|
+
return;
|
|
6273
6462
|
}
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6463
|
+
const view = backend.view;
|
|
6464
|
+
if (this._currentView !== view || this._currentViewId !== view.updateId) {
|
|
6465
|
+
this._currentView = view;
|
|
6466
|
+
this._currentViewId = view.updateId;
|
|
6467
|
+
this._shader
|
|
6468
|
+
.getUniform('u_projection')
|
|
6469
|
+
.setValue(view.getTransform().toArray(false));
|
|
6278
6470
|
}
|
|
6279
6471
|
this._shader.sync();
|
|
6280
|
-
backend.bindVertexArrayObject(
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
connection.vao.draw(indexCount, 0, drawMode);
|
|
6472
|
+
backend.bindVertexArrayObject(vao);
|
|
6473
|
+
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._instanceCount * wordsPerInstance$1));
|
|
6474
|
+
vao.drawInstanced(indicesPerQuad, 0, this._instanceCount, RenderingPrimitives.Triangles);
|
|
6284
6475
|
backend.stats.batches++;
|
|
6285
6476
|
backend.stats.drawCalls++;
|
|
6286
|
-
|
|
6287
|
-
flush() {
|
|
6288
|
-
// Primitive rendering is immediate per shape in this bridge stage.
|
|
6289
|
-
}
|
|
6290
|
-
destroy() {
|
|
6291
|
-
this.disconnect();
|
|
6292
|
-
this._shader.destroy();
|
|
6293
|
-
this._currentBlendMode = null;
|
|
6294
|
-
this._currentView = null;
|
|
6477
|
+
this._instanceCount = 0;
|
|
6295
6478
|
}
|
|
6296
6479
|
onConnect(backend) {
|
|
6297
6480
|
const gl = backend.context;
|
|
6298
|
-
const vaoHandle = gl.createVertexArray();
|
|
6299
6481
|
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
.connect(this._createBufferRuntime(gl, buffers));
|
|
6306
|
-
const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
|
|
6307
|
-
.connect(this._createBufferRuntime(gl, buffers));
|
|
6308
|
-
// Force shader finalize so the attribute table is populated. The
|
|
6309
|
-
// async-compile path defers attribute extraction from initialize()
|
|
6310
|
-
// to first sync(); without this nudge, getAttribute() below would
|
|
6311
|
-
// throw "Attribute 'a_position' is not available".
|
|
6482
|
+
this._connection = this._createConnection(gl);
|
|
6483
|
+
this._indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$2, BufferUsage.StaticDraw)
|
|
6484
|
+
.connect(this._createBufferRuntime(this._connection));
|
|
6485
|
+
this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw)
|
|
6486
|
+
.connect(this._createBufferRuntime(this._connection));
|
|
6312
6487
|
this._shader.sync();
|
|
6313
|
-
|
|
6314
|
-
.addIndex(
|
|
6315
|
-
.addAttribute(
|
|
6316
|
-
.addAttribute(
|
|
6317
|
-
.
|
|
6318
|
-
|
|
6488
|
+
this._vao = new WebGl2VertexArrayObject()
|
|
6489
|
+
.addIndex(this._indexBuffer)
|
|
6490
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_translation'), gl.FLOAT, false, instanceStrideBytes$2, 0, false, 1)
|
|
6491
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_scale'), gl.FLOAT, false, instanceStrideBytes$2, 8, false, 1)
|
|
6492
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_rotation'), gl.FLOAT, false, instanceStrideBytes$2, 16, false, 1)
|
|
6493
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$2, 20, false, 1)
|
|
6494
|
+
.connect(this._createVaoRuntime(this._connection));
|
|
6319
6495
|
}
|
|
6320
6496
|
onDisconnect() {
|
|
6321
|
-
const connection = this._connection;
|
|
6322
|
-
if (!connection) {
|
|
6323
|
-
return;
|
|
6324
|
-
}
|
|
6325
6497
|
this._shader.disconnect();
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6498
|
+
this._instanceBuffer?.destroy();
|
|
6499
|
+
this._instanceBuffer = null;
|
|
6500
|
+
this._indexBuffer?.destroy();
|
|
6501
|
+
this._indexBuffer = null;
|
|
6502
|
+
this._vao?.destroy();
|
|
6503
|
+
this._vao = null;
|
|
6329
6504
|
this._connection = null;
|
|
6505
|
+
this._currentTexture = null;
|
|
6330
6506
|
this._currentBlendMode = null;
|
|
6331
6507
|
this._currentView = null;
|
|
6332
|
-
this.
|
|
6333
|
-
|
|
6334
|
-
_ensureVertexCapacity(vertexCount) {
|
|
6335
|
-
if (vertexCount <= this._vertexCapacity) {
|
|
6336
|
-
return;
|
|
6337
|
-
}
|
|
6338
|
-
while (this._vertexCapacity < vertexCount) {
|
|
6339
|
-
this._vertexCapacity *= 2;
|
|
6340
|
-
}
|
|
6341
|
-
this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$3);
|
|
6342
|
-
this._float32View = new Float32Array(this._vertexData);
|
|
6343
|
-
this._uint32View = new Uint32Array(this._vertexData);
|
|
6508
|
+
this._currentViewId = -1;
|
|
6509
|
+
this._instanceCount = 0;
|
|
6344
6510
|
}
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
}
|
|
6349
|
-
while (this._indexCapacity < indexCount) {
|
|
6350
|
-
this._indexCapacity *= 2;
|
|
6351
|
-
}
|
|
6352
|
-
this._indexData = new Uint16Array(this._indexCapacity);
|
|
6511
|
+
destroy() {
|
|
6512
|
+
this.disconnect();
|
|
6513
|
+
this._shader.destroy();
|
|
6353
6514
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6515
|
+
/**
|
|
6516
|
+
* Convert the system's per-corner packed-u32 texCoords into the
|
|
6517
|
+
* (uMin, vMin, uMax, vMax) bounds the new vertex shader expects.
|
|
6518
|
+
* Already accounts for flipY (the system's `texCoords` baked it in).
|
|
6519
|
+
*
|
|
6520
|
+
* The four packed u32s, by corner index, are:
|
|
6521
|
+
* [0] TL = (uMin/uMax | vMin/vMax depending on flipY)
|
|
6522
|
+
* [1] TR
|
|
6523
|
+
* [2] BR
|
|
6524
|
+
* [3] BL
|
|
6525
|
+
* We need (uMin, vMin, uMax, vMax) — the corner-extreme values.
|
|
6526
|
+
*/
|
|
6527
|
+
_uvBoundsScratch = new Float32Array(4);
|
|
6528
|
+
_unpackUvBounds(system) {
|
|
6529
|
+
const texCoords = system.texCoords;
|
|
6530
|
+
// Each packed u32: low 16 bits = U normalised to 0..65535,
|
|
6531
|
+
// high 16 bits = V normalised.
|
|
6532
|
+
const uTopLeft = (texCoords[0] & 0xFFFF) / 0xFFFF;
|
|
6533
|
+
const vTopLeft = ((texCoords[0] >>> 16) & 0xFFFF) / 0xFFFF;
|
|
6534
|
+
const uBottomRight = (texCoords[2] & 0xFFFF) / 0xFFFF;
|
|
6535
|
+
const vBottomRight = ((texCoords[2] >>> 16) & 0xFFFF) / 0xFFFF;
|
|
6536
|
+
// For flipY: TL.v becomes vMax (was minY → maxY). The shader picks
|
|
6537
|
+
// (cornerY == 0 ? vMin : vMax); writing TL.v into vMin and BR.v into
|
|
6538
|
+
// vMax matches the original per-corner ordering whether or not flipY
|
|
6539
|
+
// was applied at texCoords pack time.
|
|
6540
|
+
this._uvBoundsScratch[0] = uTopLeft;
|
|
6541
|
+
this._uvBoundsScratch[1] = vTopLeft;
|
|
6542
|
+
this._uvBoundsScratch[2] = uBottomRight;
|
|
6543
|
+
this._uvBoundsScratch[3] = vBottomRight;
|
|
6544
|
+
return this._uvBoundsScratch;
|
|
6545
|
+
}
|
|
6546
|
+
_createConnection(gl) {
|
|
6547
|
+
const vaoHandle = gl.createVertexArray();
|
|
6548
|
+
if (vaoHandle === null) {
|
|
6549
|
+
throw new Error('WebGl2ParticleRenderer: could not create vertex array object.');
|
|
6550
|
+
}
|
|
6551
|
+
return {
|
|
6552
|
+
gl,
|
|
6553
|
+
buffers: new Map(),
|
|
6554
|
+
vaoHandle,
|
|
6555
|
+
};
|
|
6556
|
+
}
|
|
6557
|
+
_createBufferRuntime(connection) {
|
|
6558
|
+
const handle = connection.gl.createBuffer();
|
|
6356
6559
|
if (handle === null) {
|
|
6357
|
-
throw new Error('
|
|
6560
|
+
throw new Error('WebGl2ParticleRenderer: could not create render buffer.');
|
|
6358
6561
|
}
|
|
6359
6562
|
return {
|
|
6360
6563
|
bind: (buffer) => {
|
|
6361
|
-
gl.bindBuffer(buffer.type, handle);
|
|
6564
|
+
connection.gl.bindBuffer(buffer.type, handle);
|
|
6362
6565
|
},
|
|
6363
6566
|
upload: (buffer, offset) => {
|
|
6364
|
-
const
|
|
6567
|
+
const gl = connection.gl;
|
|
6365
6568
|
const data = buffer.data;
|
|
6569
|
+
const state = connection.buffers.get(buffer);
|
|
6366
6570
|
gl.bindBuffer(buffer.type, handle);
|
|
6367
6571
|
if (state && state.dataByteLength >= data.byteLength) {
|
|
6368
6572
|
gl.bufferSubData(buffer.type, offset, data);
|
|
@@ -6370,21 +6574,22 @@ class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
|
|
|
6370
6574
|
}
|
|
6371
6575
|
else {
|
|
6372
6576
|
gl.bufferData(buffer.type, data, buffer.usage);
|
|
6373
|
-
buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
6577
|
+
connection.buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
6374
6578
|
}
|
|
6375
6579
|
},
|
|
6376
6580
|
destroy: (buffer) => {
|
|
6377
|
-
gl.deleteBuffer(handle);
|
|
6378
|
-
buffers.delete(buffer);
|
|
6581
|
+
connection.gl.deleteBuffer(handle);
|
|
6582
|
+
connection.buffers.delete(buffer);
|
|
6379
6583
|
buffer.disconnect();
|
|
6380
6584
|
},
|
|
6381
6585
|
};
|
|
6382
6586
|
}
|
|
6383
|
-
_createVaoRuntime(
|
|
6587
|
+
_createVaoRuntime(connection) {
|
|
6384
6588
|
let appliedVersion = -1;
|
|
6385
6589
|
return {
|
|
6386
6590
|
bind: (vao) => {
|
|
6387
|
-
gl.
|
|
6591
|
+
const gl = connection.gl;
|
|
6592
|
+
gl.bindVertexArray(connection.vaoHandle);
|
|
6388
6593
|
if (appliedVersion !== vao.version) {
|
|
6389
6594
|
let lastBuffer = null;
|
|
6390
6595
|
for (const attribute of vao.attributes) {
|
|
@@ -6392,8 +6597,14 @@ class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
|
|
|
6392
6597
|
attribute.buffer.bind();
|
|
6393
6598
|
lastBuffer = attribute.buffer;
|
|
6394
6599
|
}
|
|
6395
|
-
|
|
6600
|
+
if (attribute.integer) {
|
|
6601
|
+
gl.vertexAttribIPointer(attribute.location, attribute.size, attribute.type, attribute.stride, attribute.start);
|
|
6602
|
+
}
|
|
6603
|
+
else {
|
|
6604
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
|
|
6605
|
+
}
|
|
6396
6606
|
gl.enableVertexAttribArray(attribute.location);
|
|
6607
|
+
gl.vertexAttribDivisor(attribute.location, attribute.divisor);
|
|
6397
6608
|
}
|
|
6398
6609
|
if (vao.indexBuffer) {
|
|
6399
6610
|
vao.indexBuffer.bind();
|
|
@@ -6402,9 +6613,10 @@ class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
|
|
|
6402
6613
|
}
|
|
6403
6614
|
},
|
|
6404
6615
|
unbind: () => {
|
|
6405
|
-
gl.bindVertexArray(null);
|
|
6616
|
+
connection.gl.bindVertexArray(null);
|
|
6406
6617
|
},
|
|
6407
6618
|
draw: (vao, size, start, type) => {
|
|
6619
|
+
const gl = connection.gl;
|
|
6408
6620
|
if (vao.indexBuffer) {
|
|
6409
6621
|
gl.drawElements(type, size, gl.UNSIGNED_SHORT, start);
|
|
6410
6622
|
}
|
|
@@ -6412,111 +6624,335 @@ class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
|
|
|
6412
6624
|
gl.drawArrays(type, start, size);
|
|
6413
6625
|
}
|
|
6414
6626
|
},
|
|
6627
|
+
drawInstanced: (vao, count, start, instanceCount, type) => {
|
|
6628
|
+
const gl = connection.gl;
|
|
6629
|
+
if (vao.indexBuffer) {
|
|
6630
|
+
gl.drawElementsInstanced(type, count, gl.UNSIGNED_SHORT, start, instanceCount);
|
|
6631
|
+
}
|
|
6632
|
+
else {
|
|
6633
|
+
gl.drawArraysInstanced(type, start, count, instanceCount);
|
|
6634
|
+
}
|
|
6635
|
+
},
|
|
6415
6636
|
destroy: (vao) => {
|
|
6416
|
-
gl.deleteVertexArray(vaoHandle);
|
|
6637
|
+
connection.gl.deleteVertexArray(connection.vaoHandle);
|
|
6417
6638
|
vao.disconnect();
|
|
6418
6639
|
},
|
|
6419
6640
|
};
|
|
6420
6641
|
}
|
|
6421
6642
|
}
|
|
6422
6643
|
|
|
6423
|
-
var vertexSource = "#version 300 es\r\nprecision lowp float;\r\n\r\nlayout(location = 0) in vec2 a_position;\r\nlayout(location = 1) in
|
|
6644
|
+
var vertexSource$1 = "#version 300 es\r\nprecision lowp float;\r\n\r\nlayout(location = 0) in vec2 a_position;\r\nlayout(location = 1) in vec4 a_color;\r\n\r\nuniform mat3 u_projection;\r\nuniform mat3 u_translation;\r\n\r\nout vec4 v_color;\r\n\r\nvoid main(void) {\r\n gl_Position = vec4((u_projection * u_translation * vec3(a_position, 1.0)).xy, 0.0, 1.0);\r\n v_color = vec4(a_color.rgb * a_color.a, a_color.a);\r\n}\r\n";
|
|
6424
6645
|
|
|
6425
|
-
var fragmentSource = "#version 300 es\r\nprecision lowp float;\r\n\r\
|
|
6646
|
+
var fragmentSource$1 = "#version 300 es\r\nprecision lowp float;\r\n\r\nlayout(location = 0) out vec4 fragColor;\r\n\r\nin vec4 v_color;\r\n\r\nvoid main(void) {\r\n fragColor = v_color;\r\n}\r\n";
|
|
6426
6647
|
|
|
6427
|
-
|
|
6428
|
-
const vertexStrideBytes$
|
|
6429
|
-
const
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
* Intentionally not a {@link AbstractWebGl2Renderer} subclass: this
|
|
6439
|
-
* compositor is invoked directly by the manager for non-Drawable
|
|
6440
|
-
* compositing operations and never participates in the renderer
|
|
6441
|
-
* registry dispatch path.
|
|
6442
|
-
*/
|
|
6443
|
-
class WebGl2MaskCompositor {
|
|
6444
|
-
_shader = new Shader(vertexSource, fragmentSource);
|
|
6445
|
-
_vertexData = new ArrayBuffer(4 * vertexStrideBytes$2);
|
|
6446
|
-
_float32View = new Float32Array(this._vertexData);
|
|
6447
|
-
_contentSamplerSlot = new Int32Array([0]);
|
|
6448
|
-
_maskSamplerSlot = new Int32Array([1]);
|
|
6648
|
+
const minBatchVertexSize = 4;
|
|
6649
|
+
const vertexStrideBytes$4 = 12;
|
|
6650
|
+
const vertexStrideWords = vertexStrideBytes$4 / 4;
|
|
6651
|
+
class WebGl2PrimitiveRenderer extends AbstractWebGl2Renderer {
|
|
6652
|
+
_vertexCapacity;
|
|
6653
|
+
_indexCapacity;
|
|
6654
|
+
_vertexData;
|
|
6655
|
+
_indexData;
|
|
6656
|
+
_float32View;
|
|
6657
|
+
_uint32View;
|
|
6658
|
+
_shader = new Shader(vertexSource$1, fragmentSource$1);
|
|
6449
6659
|
_connection = null;
|
|
6450
|
-
|
|
6451
|
-
|
|
6660
|
+
_currentBlendMode = null;
|
|
6661
|
+
_currentView = null;
|
|
6662
|
+
_viewId = -1;
|
|
6663
|
+
constructor(batchSize) {
|
|
6664
|
+
super();
|
|
6665
|
+
this._vertexCapacity = Math.max(minBatchVertexSize, batchSize);
|
|
6666
|
+
this._indexCapacity = Math.max(6, this._vertexCapacity * 3);
|
|
6667
|
+
this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
|
|
6668
|
+
this._indexData = new Uint16Array(this._indexCapacity);
|
|
6669
|
+
this._float32View = new Float32Array(this._vertexData);
|
|
6670
|
+
this._uint32View = new Uint32Array(this._vertexData);
|
|
6671
|
+
}
|
|
6672
|
+
render(shape) {
|
|
6673
|
+
const connection = this._connection;
|
|
6674
|
+
if (!connection) {
|
|
6675
|
+
throw new Error('Renderer not connected');
|
|
6676
|
+
}
|
|
6677
|
+
const backend = this.getBackend();
|
|
6678
|
+
const { geometry, drawMode, color, blendMode } = shape;
|
|
6679
|
+
const vertices = geometry.vertices;
|
|
6680
|
+
const sourceIndices = geometry.indices;
|
|
6681
|
+
const vertexCount = vertices.length / 2;
|
|
6682
|
+
const indexCount = sourceIndices.length > 0 ? sourceIndices.length : vertexCount;
|
|
6683
|
+
if (vertexCount === 0 || indexCount === 0) {
|
|
6452
6684
|
return;
|
|
6453
6685
|
}
|
|
6686
|
+
this._ensureVertexCapacity(vertexCount);
|
|
6687
|
+
this._ensureIndexCapacity(indexCount);
|
|
6688
|
+
if (blendMode !== this._currentBlendMode) {
|
|
6689
|
+
this._currentBlendMode = blendMode;
|
|
6690
|
+
backend.setBlendMode(blendMode);
|
|
6691
|
+
}
|
|
6692
|
+
const view = backend.view;
|
|
6693
|
+
if (this._currentView !== view || this._viewId !== view.updateId) {
|
|
6694
|
+
this._currentView = view;
|
|
6695
|
+
this._viewId = view.updateId;
|
|
6696
|
+
this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
|
|
6697
|
+
}
|
|
6698
|
+
this._shader.getUniform('u_translation').setValue(shape.getGlobalTransform().toArray(false));
|
|
6699
|
+
const packedColor = color.toRgba();
|
|
6700
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
6701
|
+
const sourceIndex = i * 2;
|
|
6702
|
+
const targetIndex = i * vertexStrideWords;
|
|
6703
|
+
this._float32View[targetIndex] = vertices[sourceIndex];
|
|
6704
|
+
this._float32View[targetIndex + 1] = vertices[sourceIndex + 1];
|
|
6705
|
+
this._uint32View[targetIndex + 2] = packedColor;
|
|
6706
|
+
}
|
|
6707
|
+
if (sourceIndices.length > 0) {
|
|
6708
|
+
this._indexData.set(sourceIndices, 0);
|
|
6709
|
+
}
|
|
6710
|
+
else {
|
|
6711
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
6712
|
+
this._indexData[i] = i;
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
this._shader.sync();
|
|
6716
|
+
backend.bindVertexArrayObject(connection.vao);
|
|
6717
|
+
connection.vertexBuffer.upload(this._float32View.subarray(0, vertexCount * vertexStrideWords));
|
|
6718
|
+
connection.indexBuffer.upload(this._indexData.subarray(0, indexCount));
|
|
6719
|
+
connection.vao.draw(indexCount, 0, drawMode);
|
|
6720
|
+
backend.stats.batches++;
|
|
6721
|
+
backend.stats.drawCalls++;
|
|
6722
|
+
}
|
|
6723
|
+
flush() {
|
|
6724
|
+
// Primitive rendering is immediate per shape in this bridge stage.
|
|
6725
|
+
}
|
|
6726
|
+
destroy() {
|
|
6727
|
+
this.disconnect();
|
|
6728
|
+
this._shader.destroy();
|
|
6729
|
+
this._currentBlendMode = null;
|
|
6730
|
+
this._currentView = null;
|
|
6731
|
+
}
|
|
6732
|
+
onConnect(backend) {
|
|
6454
6733
|
const gl = backend.context;
|
|
6455
6734
|
const vaoHandle = gl.createVertexArray();
|
|
6735
|
+
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
6456
6736
|
if (vaoHandle === null) {
|
|
6457
|
-
throw new Error('
|
|
6737
|
+
throw new Error('Could not create vertex array object.');
|
|
6458
6738
|
}
|
|
6459
|
-
|
|
6460
|
-
const
|
|
6461
|
-
|
|
6462
|
-
.connect(this._createBufferRuntime(gl, bufferHandles));
|
|
6739
|
+
const buffers = new Map();
|
|
6740
|
+
const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, this._indexData, BufferUsage.DynamicDraw)
|
|
6741
|
+
.connect(this._createBufferRuntime(gl, buffers));
|
|
6463
6742
|
const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
|
|
6464
|
-
.connect(this._createBufferRuntime(gl,
|
|
6465
|
-
// Force shader finalize so
|
|
6466
|
-
//
|
|
6467
|
-
//
|
|
6743
|
+
.connect(this._createBufferRuntime(gl, buffers));
|
|
6744
|
+
// Force shader finalize so the attribute table is populated. The
|
|
6745
|
+
// async-compile path defers attribute extraction from initialize()
|
|
6746
|
+
// to first sync(); without this nudge, getAttribute() below would
|
|
6747
|
+
// throw "Attribute 'a_position' is not available".
|
|
6468
6748
|
this._shader.sync();
|
|
6469
6749
|
const vao = new WebGl2VertexArrayObject()
|
|
6470
6750
|
.addIndex(indexBuffer)
|
|
6471
|
-
.addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$
|
|
6472
|
-
.addAttribute(vertexBuffer, this._shader.getAttribute('
|
|
6751
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$4, 0)
|
|
6752
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, vertexStrideBytes$4, 8)
|
|
6473
6753
|
.connect(this._createVaoRuntime(gl, vaoHandle));
|
|
6474
|
-
this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer
|
|
6754
|
+
this._connection = { gl, buffers, vaoHandle, vao, indexBuffer, vertexBuffer };
|
|
6475
6755
|
}
|
|
6476
|
-
|
|
6756
|
+
onDisconnect() {
|
|
6477
6757
|
const connection = this._connection;
|
|
6478
|
-
if (connection
|
|
6758
|
+
if (!connection) {
|
|
6479
6759
|
return;
|
|
6480
6760
|
}
|
|
6761
|
+
this._shader.disconnect();
|
|
6481
6762
|
connection.indexBuffer.destroy();
|
|
6482
6763
|
connection.vertexBuffer.destroy();
|
|
6483
6764
|
connection.vao.destroy();
|
|
6484
|
-
this._shader.disconnect();
|
|
6485
6765
|
this._connection = null;
|
|
6766
|
+
this._currentBlendMode = null;
|
|
6767
|
+
this._currentView = null;
|
|
6768
|
+
this._viewId = -1;
|
|
6486
6769
|
}
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
throw new Error('WebGl2MaskCompositor: not connected.');
|
|
6770
|
+
_ensureVertexCapacity(vertexCount) {
|
|
6771
|
+
if (vertexCount <= this._vertexCapacity) {
|
|
6772
|
+
return;
|
|
6491
6773
|
}
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
this.
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6774
|
+
while (this._vertexCapacity < vertexCount) {
|
|
6775
|
+
this._vertexCapacity *= 2;
|
|
6776
|
+
}
|
|
6777
|
+
this._vertexData = new ArrayBuffer(this._vertexCapacity * vertexStrideBytes$4);
|
|
6778
|
+
this._float32View = new Float32Array(this._vertexData);
|
|
6779
|
+
this._uint32View = new Uint32Array(this._vertexData);
|
|
6780
|
+
}
|
|
6781
|
+
_ensureIndexCapacity(indexCount) {
|
|
6782
|
+
if (indexCount <= this._indexCapacity) {
|
|
6783
|
+
return;
|
|
6784
|
+
}
|
|
6785
|
+
while (this._indexCapacity < indexCount) {
|
|
6786
|
+
this._indexCapacity *= 2;
|
|
6787
|
+
}
|
|
6788
|
+
this._indexData = new Uint16Array(this._indexCapacity);
|
|
6789
|
+
}
|
|
6790
|
+
_createBufferRuntime(gl, buffers) {
|
|
6791
|
+
const handle = gl.createBuffer();
|
|
6792
|
+
if (handle === null) {
|
|
6793
|
+
throw new Error('Could not create render buffer.');
|
|
6794
|
+
}
|
|
6795
|
+
return {
|
|
6796
|
+
bind: (buffer) => {
|
|
6797
|
+
gl.bindBuffer(buffer.type, handle);
|
|
6798
|
+
},
|
|
6799
|
+
upload: (buffer, offset) => {
|
|
6800
|
+
const state = buffers.get(buffer);
|
|
6801
|
+
const data = buffer.data;
|
|
6802
|
+
gl.bindBuffer(buffer.type, handle);
|
|
6803
|
+
if (state && state.dataByteLength >= data.byteLength) {
|
|
6804
|
+
gl.bufferSubData(buffer.type, offset, data);
|
|
6805
|
+
state.dataByteLength = data.byteLength;
|
|
6806
|
+
}
|
|
6807
|
+
else {
|
|
6808
|
+
gl.bufferData(buffer.type, data, buffer.usage);
|
|
6809
|
+
buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
6810
|
+
}
|
|
6811
|
+
},
|
|
6812
|
+
destroy: (buffer) => {
|
|
6813
|
+
gl.deleteBuffer(handle);
|
|
6814
|
+
buffers.delete(buffer);
|
|
6815
|
+
buffer.disconnect();
|
|
6816
|
+
},
|
|
6817
|
+
};
|
|
6818
|
+
}
|
|
6819
|
+
_createVaoRuntime(gl, vaoHandle) {
|
|
6820
|
+
let appliedVersion = -1;
|
|
6821
|
+
return {
|
|
6822
|
+
bind: (vao) => {
|
|
6823
|
+
gl.bindVertexArray(vaoHandle);
|
|
6824
|
+
if (appliedVersion !== vao.version) {
|
|
6825
|
+
let lastBuffer = null;
|
|
6826
|
+
for (const attribute of vao.attributes) {
|
|
6827
|
+
if (lastBuffer !== attribute.buffer) {
|
|
6828
|
+
attribute.buffer.bind();
|
|
6829
|
+
lastBuffer = attribute.buffer;
|
|
6830
|
+
}
|
|
6831
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
|
|
6832
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
6833
|
+
}
|
|
6834
|
+
if (vao.indexBuffer) {
|
|
6835
|
+
vao.indexBuffer.bind();
|
|
6836
|
+
}
|
|
6837
|
+
appliedVersion = vao.version;
|
|
6838
|
+
}
|
|
6839
|
+
},
|
|
6840
|
+
unbind: () => {
|
|
6841
|
+
gl.bindVertexArray(null);
|
|
6842
|
+
},
|
|
6843
|
+
draw: (vao, size, start, type) => {
|
|
6844
|
+
if (vao.indexBuffer) {
|
|
6845
|
+
gl.drawElements(type, size, gl.UNSIGNED_SHORT, start);
|
|
6846
|
+
}
|
|
6847
|
+
else {
|
|
6848
|
+
gl.drawArrays(type, start, size);
|
|
6849
|
+
}
|
|
6850
|
+
},
|
|
6851
|
+
destroy: (vao) => {
|
|
6852
|
+
gl.deleteVertexArray(vaoHandle);
|
|
6853
|
+
vao.disconnect();
|
|
6854
|
+
},
|
|
6855
|
+
};
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6859
|
+
var vertexSource = "#version 300 es\r\nprecision lowp float;\r\n\r\nlayout(location = 0) in vec2 a_position;\r\nlayout(location = 1) in vec2 a_texcoord;\r\n\r\nuniform mat3 u_projection;\r\n\r\nout vec2 v_texcoord;\r\n\r\nvoid main(void) {\r\n gl_Position = vec4((u_projection * vec3(a_position, 1.0)).xy, 0.0, 1.0);\r\n v_texcoord = a_texcoord;\r\n}\r\n";
|
|
6860
|
+
|
|
6861
|
+
var fragmentSource = "#version 300 es\r\nprecision lowp float;\r\n\r\nuniform sampler2D u_content;\r\nuniform sampler2D u_mask;\r\n\r\nin vec2 v_texcoord;\r\n\r\nlayout(location = 0) out vec4 fragColor;\r\n\r\nvoid main(void) {\r\n vec4 contentColor = texture(u_content, v_texcoord);\r\n float maskAlpha = texture(u_mask, v_texcoord).a;\r\n\r\n fragColor = vec4(contentColor.rgb * maskAlpha, contentColor.a * maskAlpha);\r\n}\r\n";
|
|
6862
|
+
|
|
6863
|
+
// 4 floats per vertex: position(x, y) + texcoord(u, v).
|
|
6864
|
+
const vertexStrideBytes$3 = 16;
|
|
6865
|
+
const quadIndices$1 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
6866
|
+
/**
|
|
6867
|
+
* Single-quad two-texture compositor used by `WebGl2Backend.composeWithAlphaMask`.
|
|
6868
|
+
*
|
|
6869
|
+
* Renders the content texture onto the active render target with each
|
|
6870
|
+
* output texel's alpha multiplied by the mask texture's alpha at the
|
|
6871
|
+
* same UV. Both textures are sampled with stretched-fit UVs over the
|
|
6872
|
+
* destination rectangle.
|
|
6873
|
+
*
|
|
6874
|
+
* Intentionally not a {@link AbstractWebGl2Renderer} subclass: this
|
|
6875
|
+
* compositor is invoked directly by the manager for non-Drawable
|
|
6876
|
+
* compositing operations and never participates in the renderer
|
|
6877
|
+
* registry dispatch path.
|
|
6878
|
+
*/
|
|
6879
|
+
class WebGl2MaskCompositor {
|
|
6880
|
+
_shader = new Shader(vertexSource, fragmentSource);
|
|
6881
|
+
_vertexData = new ArrayBuffer(4 * vertexStrideBytes$3);
|
|
6882
|
+
_float32View = new Float32Array(this._vertexData);
|
|
6883
|
+
_contentSamplerSlot = new Int32Array([0]);
|
|
6884
|
+
_maskSamplerSlot = new Int32Array([1]);
|
|
6885
|
+
_connection = null;
|
|
6886
|
+
connect(backend) {
|
|
6887
|
+
if (this._connection !== null) {
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
const gl = backend.context;
|
|
6891
|
+
const vaoHandle = gl.createVertexArray();
|
|
6892
|
+
if (vaoHandle === null) {
|
|
6893
|
+
throw new Error('WebGl2MaskCompositor: could not create vertex array object.');
|
|
6894
|
+
}
|
|
6895
|
+
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
6896
|
+
const bufferHandles = new Map();
|
|
6897
|
+
const indexBuffer = new WebGl2RenderBuffer(BufferTypes.ElementArrayBuffer, quadIndices$1, BufferUsage.StaticDraw)
|
|
6898
|
+
.connect(this._createBufferRuntime(gl, bufferHandles));
|
|
6899
|
+
const vertexBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._vertexData, BufferUsage.DynamicDraw)
|
|
6900
|
+
.connect(this._createBufferRuntime(gl, bufferHandles));
|
|
6901
|
+
// Force shader finalize so getAttribute() below sees a populated
|
|
6902
|
+
// attribute table; the async-compile path defers extraction until
|
|
6903
|
+
// the first sync() call.
|
|
6904
|
+
this._shader.sync();
|
|
6905
|
+
const vao = new WebGl2VertexArrayObject()
|
|
6906
|
+
.addIndex(indexBuffer)
|
|
6907
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_position'), gl.FLOAT, false, vertexStrideBytes$3, 0)
|
|
6908
|
+
.addAttribute(vertexBuffer, this._shader.getAttribute('a_texcoord'), gl.FLOAT, false, vertexStrideBytes$3, 8)
|
|
6909
|
+
.connect(this._createVaoRuntime(gl, vaoHandle));
|
|
6910
|
+
this._connection = { gl, vaoHandle, vao, indexBuffer, vertexBuffer, bufferHandles };
|
|
6911
|
+
}
|
|
6912
|
+
disconnect() {
|
|
6913
|
+
const connection = this._connection;
|
|
6914
|
+
if (connection === null) {
|
|
6915
|
+
return;
|
|
6916
|
+
}
|
|
6917
|
+
connection.indexBuffer.destroy();
|
|
6918
|
+
connection.vertexBuffer.destroy();
|
|
6919
|
+
connection.vao.destroy();
|
|
6920
|
+
this._shader.disconnect();
|
|
6921
|
+
this._connection = null;
|
|
6922
|
+
}
|
|
6923
|
+
compose(backend, content, mask, x, y, width, height, blendMode) {
|
|
6924
|
+
const connection = this._connection;
|
|
6925
|
+
if (connection === null) {
|
|
6926
|
+
throw new Error('WebGl2MaskCompositor: not connected.');
|
|
6927
|
+
}
|
|
6928
|
+
// Update the quad vertices for this destination rect. UVs are 0..1
|
|
6929
|
+
// mapped over (left, top) → (right, bottom) with an explicit Y-flip
|
|
6930
|
+
// for render-texture sampling: the mask path samples textures
|
|
6931
|
+
// already authored as RGBA in render-texture orientation.
|
|
6932
|
+
this._writeQuadVertices(x, y, x + width, y + height);
|
|
6933
|
+
// Bind the compositor program. Setting projection + sampler uniforms
|
|
6934
|
+
// each call because they need to match the current render target.
|
|
6935
|
+
backend.bindShader(this._shader);
|
|
6936
|
+
const view = backend.view;
|
|
6937
|
+
const projection = view.getTransform().toArray(false);
|
|
6938
|
+
this._shader.getUniform('u_projection').setValue(projection);
|
|
6939
|
+
this._shader.getUniform('u_content').setValue(this._contentSamplerSlot);
|
|
6940
|
+
this._shader.getUniform('u_mask').setValue(this._maskSamplerSlot);
|
|
6941
|
+
this._shader.sync();
|
|
6942
|
+
backend.bindTexture(content, 0);
|
|
6943
|
+
backend.bindTexture(mask, 1);
|
|
6944
|
+
backend.setBlendMode(blendMode);
|
|
6945
|
+
backend.bindVertexArrayObject(connection.vao);
|
|
6946
|
+
connection.vertexBuffer.upload(this._float32View);
|
|
6947
|
+
connection.vao.draw(6, 0);
|
|
6948
|
+
backend.stats.batches++;
|
|
6949
|
+
backend.stats.drawCalls++;
|
|
6950
|
+
// Reset the active texture unit to 0 to avoid leaking unit 1 into
|
|
6951
|
+
// subsequent renderer state.
|
|
6952
|
+
backend.bindTexture(null, 1);
|
|
6953
|
+
}
|
|
6954
|
+
_writeQuadVertices(left, top, right, bottom) {
|
|
6955
|
+
const view = this._float32View;
|
|
6520
6956
|
// Vertex 0: top-left (UV 0, 0)
|
|
6521
6957
|
view[0] = left;
|
|
6522
6958
|
view[1] = top;
|
|
@@ -6761,6 +7197,116 @@ class Sprite extends Drawable {
|
|
|
6761
7197
|
}
|
|
6762
7198
|
RenderNode.setInternalSpriteFactory(() => new Sprite(null));
|
|
6763
7199
|
|
|
7200
|
+
/**
|
|
7201
|
+
* Arbitrary 2D triangle-mesh primitive.
|
|
7202
|
+
*
|
|
7203
|
+
* `Mesh` lives alongside {@link Sprite} as a public Drawable: it has the
|
|
7204
|
+
* same transform (position/rotation/scale/origin), tint, blendMode,
|
|
7205
|
+
* filters, masks, and cacheAsBitmap — but the geometry it renders is
|
|
7206
|
+
* user-supplied rather than implied by a texture frame. The intended use
|
|
7207
|
+
* cases are:
|
|
7208
|
+
*
|
|
7209
|
+
* - Custom-shape sprites whose silhouette isn't a quad (badges, speech
|
|
7210
|
+
* bubbles, region overlays).
|
|
7211
|
+
* - Deformable visuals (rope/ribbon, banner, water surface): mutate the
|
|
7212
|
+
* vertex array between frames and the GPU re-tessellates nothing —
|
|
7213
|
+
* only the transform changes per frame.
|
|
7214
|
+
* - Particles or trails with custom geometry per emitter.
|
|
7215
|
+
*
|
|
7216
|
+
* The mesh data is **immutable after construction** in v1: vertex /
|
|
7217
|
+
* index / UV / color arrays are exposed as readonly references. Mutate
|
|
7218
|
+
* the underlying typed arrays in-place if you need per-frame updates,
|
|
7219
|
+
* but the array lengths and topology cannot change. Texture is the only
|
|
7220
|
+
* post-construction mutable property.
|
|
7221
|
+
*
|
|
7222
|
+
* The vertex stream is a flat `Float32Array` of (x, y) pairs in local
|
|
7223
|
+
* space. The mesh's local bounds are computed once at construction from
|
|
7224
|
+
* the AABB of those vertices and used by the cull pass. Re-computing
|
|
7225
|
+
* after in-place mutation is the caller's responsibility (call
|
|
7226
|
+
* `recomputeLocalBounds()`).
|
|
7227
|
+
*/
|
|
7228
|
+
class Mesh extends Drawable {
|
|
7229
|
+
vertices;
|
|
7230
|
+
indices;
|
|
7231
|
+
uvs;
|
|
7232
|
+
colors;
|
|
7233
|
+
_texture;
|
|
7234
|
+
constructor(options) {
|
|
7235
|
+
super();
|
|
7236
|
+
const { vertices, indices = null, uvs = null, colors = null, texture = null } = options;
|
|
7237
|
+
if (vertices.length === 0 || vertices.length % 2 !== 0) {
|
|
7238
|
+
throw new Error(`Mesh vertices must be a non-empty flat array of (x,y) pairs (got length ${vertices.length}).`);
|
|
7239
|
+
}
|
|
7240
|
+
const vertexCount = vertices.length / 2;
|
|
7241
|
+
if (vertexCount < 3) {
|
|
7242
|
+
throw new Error(`Mesh requires at least 3 vertices (got ${vertexCount}).`);
|
|
7243
|
+
}
|
|
7244
|
+
if (uvs !== null && uvs.length !== vertices.length) {
|
|
7245
|
+
throw new Error(`Mesh uvs length ${uvs.length} must equal vertices length ${vertices.length}.`);
|
|
7246
|
+
}
|
|
7247
|
+
if (colors !== null && colors.length !== vertexCount) {
|
|
7248
|
+
throw new Error(`Mesh colors length ${colors.length} must equal vertex count ${vertexCount}.`);
|
|
7249
|
+
}
|
|
7250
|
+
if (indices !== null) {
|
|
7251
|
+
if (indices.length === 0 || indices.length % 3 !== 0) {
|
|
7252
|
+
throw new Error(`Mesh indices must be a non-empty multiple of 3 (got length ${indices.length}).`);
|
|
7253
|
+
}
|
|
7254
|
+
for (let i = 0; i < indices.length; i++) {
|
|
7255
|
+
if (indices[i] >= vertexCount) {
|
|
7256
|
+
throw new Error(`Mesh index ${indices[i]} at position ${i} is out of range for vertex count ${vertexCount}.`);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
else if (vertexCount % 3 !== 0) {
|
|
7261
|
+
throw new Error(`Non-indexed Mesh requires a vertex count that is a multiple of 3 (got ${vertexCount}).`);
|
|
7262
|
+
}
|
|
7263
|
+
this.vertices = vertices;
|
|
7264
|
+
this.indices = indices;
|
|
7265
|
+
this.uvs = uvs;
|
|
7266
|
+
this.colors = colors;
|
|
7267
|
+
this._texture = texture;
|
|
7268
|
+
this.recomputeLocalBounds();
|
|
7269
|
+
}
|
|
7270
|
+
get vertexCount() {
|
|
7271
|
+
return this.vertices.length / 2;
|
|
7272
|
+
}
|
|
7273
|
+
get indexCount() {
|
|
7274
|
+
return this.indices?.length ?? this.vertexCount;
|
|
7275
|
+
}
|
|
7276
|
+
get texture() {
|
|
7277
|
+
return this._texture;
|
|
7278
|
+
}
|
|
7279
|
+
set texture(texture) {
|
|
7280
|
+
this._texture = texture;
|
|
7281
|
+
this.invalidateCache();
|
|
7282
|
+
}
|
|
7283
|
+
/**
|
|
7284
|
+
* Recompute the local AABB from the current vertex array. Call after
|
|
7285
|
+
* mutating `vertices` in place to keep culling correct; otherwise the
|
|
7286
|
+
* bounds the cull pass sees will be the AABB at construction time.
|
|
7287
|
+
*/
|
|
7288
|
+
recomputeLocalBounds() {
|
|
7289
|
+
let minX = Infinity;
|
|
7290
|
+
let minY = Infinity;
|
|
7291
|
+
let maxX = -Infinity;
|
|
7292
|
+
let maxY = -Infinity;
|
|
7293
|
+
for (let i = 0; i < this.vertices.length; i += 2) {
|
|
7294
|
+
const x = this.vertices[i];
|
|
7295
|
+
const y = this.vertices[i + 1];
|
|
7296
|
+
if (x < minX)
|
|
7297
|
+
minX = x;
|
|
7298
|
+
if (x > maxX)
|
|
7299
|
+
maxX = x;
|
|
7300
|
+
if (y < minY)
|
|
7301
|
+
minY = y;
|
|
7302
|
+
if (y > maxY)
|
|
7303
|
+
maxY = y;
|
|
7304
|
+
}
|
|
7305
|
+
this.localBounds.set(minX, minY, maxX - minX, maxY - minY);
|
|
7306
|
+
return this;
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
|
|
6764
7310
|
class Particle {
|
|
6765
7311
|
_totalLifetime = Time.oneSecond.clone();
|
|
6766
7312
|
_elapsedLifetime = Time.zero.clone();
|
|
@@ -7040,208 +7586,6 @@ class ParticleSystem extends Drawable {
|
|
|
7040
7586
|
}
|
|
7041
7587
|
}
|
|
7042
7588
|
|
|
7043
|
-
const createQuadIndices = (size) => {
|
|
7044
|
-
const data = new Uint16Array(size * 6);
|
|
7045
|
-
const len = data.length;
|
|
7046
|
-
for (let i = 0, offset = 0; i < len; i += 6, offset += 4) {
|
|
7047
|
-
data[i] = offset;
|
|
7048
|
-
data[i + 1] = offset + 1;
|
|
7049
|
-
data[i + 2] = offset + 2;
|
|
7050
|
-
data[i + 3] = offset;
|
|
7051
|
-
data[i + 4] = offset + 2;
|
|
7052
|
-
data[i + 5] = offset + 3;
|
|
7053
|
-
}
|
|
7054
|
-
return data;
|
|
7055
|
-
};
|
|
7056
|
-
const createCanvas = (options = {}) => {
|
|
7057
|
-
const { canvas, fillStyle, width, height } = options;
|
|
7058
|
-
const newCanvas = canvas ?? document.createElement('canvas');
|
|
7059
|
-
const context = newCanvas.getContext('2d');
|
|
7060
|
-
newCanvas.width = width ?? 10;
|
|
7061
|
-
newCanvas.height = height ?? 10;
|
|
7062
|
-
context.fillStyle = fillStyle ?? '#6495ed';
|
|
7063
|
-
context.fillRect(0, 0, newCanvas.width, newCanvas.height);
|
|
7064
|
-
return newCanvas;
|
|
7065
|
-
};
|
|
7066
|
-
const heightCache = new Map();
|
|
7067
|
-
const determineFontHeight = (font) => {
|
|
7068
|
-
if (!heightCache.has(font)) {
|
|
7069
|
-
const body = document.body;
|
|
7070
|
-
const dummy = document.createElement('div');
|
|
7071
|
-
dummy.appendChild(document.createTextNode('M'));
|
|
7072
|
-
dummy.setAttribute('style', `font: ${font};position:absolute;top:0;left:0`);
|
|
7073
|
-
body.appendChild(dummy);
|
|
7074
|
-
heightCache.set(font, dummy.offsetHeight);
|
|
7075
|
-
body.removeChild(dummy);
|
|
7076
|
-
}
|
|
7077
|
-
return heightCache.get(font);
|
|
7078
|
-
};
|
|
7079
|
-
|
|
7080
|
-
class Texture {
|
|
7081
|
-
static _black = null;
|
|
7082
|
-
static _white = null;
|
|
7083
|
-
static defaultSamplerOptions = {
|
|
7084
|
-
scaleMode: ScaleModes.Linear,
|
|
7085
|
-
wrapMode: WrapModes.ClampToEdge,
|
|
7086
|
-
premultiplyAlpha: true,
|
|
7087
|
-
generateMipMap: true,
|
|
7088
|
-
flipY: false,
|
|
7089
|
-
};
|
|
7090
|
-
static empty = new Texture(null);
|
|
7091
|
-
static get black() {
|
|
7092
|
-
if (Texture._black === null) {
|
|
7093
|
-
Texture._black = new Texture(createCanvas({ fillStyle: '#000' }));
|
|
7094
|
-
}
|
|
7095
|
-
return Texture._black;
|
|
7096
|
-
}
|
|
7097
|
-
static get white() {
|
|
7098
|
-
if (Texture._white === null) {
|
|
7099
|
-
Texture._white = new Texture(createCanvas({ fillStyle: '#fff' }));
|
|
7100
|
-
}
|
|
7101
|
-
return Texture._white;
|
|
7102
|
-
}
|
|
7103
|
-
_version = 0;
|
|
7104
|
-
_source = null;
|
|
7105
|
-
_size = new Size(0, 0);
|
|
7106
|
-
_destroyListeners = new Set();
|
|
7107
|
-
_scaleMode;
|
|
7108
|
-
_wrapMode;
|
|
7109
|
-
_premultiplyAlpha = false;
|
|
7110
|
-
_generateMipMap = false;
|
|
7111
|
-
_flipY = false;
|
|
7112
|
-
constructor(source = null, options) {
|
|
7113
|
-
const { scaleMode, wrapMode, premultiplyAlpha, generateMipMap, flipY } = { ...Texture.defaultSamplerOptions, ...options };
|
|
7114
|
-
this._scaleMode = scaleMode;
|
|
7115
|
-
this._wrapMode = wrapMode;
|
|
7116
|
-
this._premultiplyAlpha = premultiplyAlpha;
|
|
7117
|
-
this._generateMipMap = generateMipMap;
|
|
7118
|
-
this._flipY = flipY;
|
|
7119
|
-
if (source !== null) {
|
|
7120
|
-
this.setSource(source);
|
|
7121
|
-
}
|
|
7122
|
-
}
|
|
7123
|
-
get source() {
|
|
7124
|
-
return this._source;
|
|
7125
|
-
}
|
|
7126
|
-
set source(source) {
|
|
7127
|
-
this.setSource(source);
|
|
7128
|
-
}
|
|
7129
|
-
get size() {
|
|
7130
|
-
return this._size;
|
|
7131
|
-
}
|
|
7132
|
-
set size(size) {
|
|
7133
|
-
this.setSize(size.width, size.height);
|
|
7134
|
-
}
|
|
7135
|
-
get width() {
|
|
7136
|
-
return this._size.width;
|
|
7137
|
-
}
|
|
7138
|
-
set width(width) {
|
|
7139
|
-
this.setSize(width, this.height);
|
|
7140
|
-
}
|
|
7141
|
-
get height() {
|
|
7142
|
-
return this._size.height;
|
|
7143
|
-
}
|
|
7144
|
-
set height(height) {
|
|
7145
|
-
this.setSize(this.width, height);
|
|
7146
|
-
}
|
|
7147
|
-
get scaleMode() {
|
|
7148
|
-
return this._scaleMode;
|
|
7149
|
-
}
|
|
7150
|
-
set scaleMode(scaleMode) {
|
|
7151
|
-
this.setScaleMode(scaleMode);
|
|
7152
|
-
}
|
|
7153
|
-
get wrapMode() {
|
|
7154
|
-
return this._wrapMode;
|
|
7155
|
-
}
|
|
7156
|
-
set wrapMode(wrapMode) {
|
|
7157
|
-
this.setWrapMode(wrapMode);
|
|
7158
|
-
}
|
|
7159
|
-
get premultiplyAlpha() {
|
|
7160
|
-
return this._premultiplyAlpha;
|
|
7161
|
-
}
|
|
7162
|
-
set premultiplyAlpha(premultiplyAlpha) {
|
|
7163
|
-
this.setPremultiplyAlpha(premultiplyAlpha);
|
|
7164
|
-
}
|
|
7165
|
-
get generateMipMap() {
|
|
7166
|
-
return this._generateMipMap;
|
|
7167
|
-
}
|
|
7168
|
-
set generateMipMap(generateMipMap) {
|
|
7169
|
-
this._generateMipMap = generateMipMap;
|
|
7170
|
-
}
|
|
7171
|
-
get flipY() {
|
|
7172
|
-
return this._flipY;
|
|
7173
|
-
}
|
|
7174
|
-
set flipY(flipY) {
|
|
7175
|
-
this._flipY = flipY;
|
|
7176
|
-
}
|
|
7177
|
-
get powerOfTwo() {
|
|
7178
|
-
return isPowerOfTwo(this.width) && isPowerOfTwo(this.height);
|
|
7179
|
-
}
|
|
7180
|
-
get version() {
|
|
7181
|
-
return this._version;
|
|
7182
|
-
}
|
|
7183
|
-
addDestroyListener(listener) {
|
|
7184
|
-
this._destroyListeners.add(listener);
|
|
7185
|
-
return this;
|
|
7186
|
-
}
|
|
7187
|
-
removeDestroyListener(listener) {
|
|
7188
|
-
this._destroyListeners.delete(listener);
|
|
7189
|
-
return this;
|
|
7190
|
-
}
|
|
7191
|
-
setScaleMode(scaleMode) {
|
|
7192
|
-
if (this._scaleMode !== scaleMode) {
|
|
7193
|
-
this._scaleMode = scaleMode;
|
|
7194
|
-
this._touch();
|
|
7195
|
-
}
|
|
7196
|
-
return this;
|
|
7197
|
-
}
|
|
7198
|
-
setWrapMode(wrapMode) {
|
|
7199
|
-
if (this._wrapMode !== wrapMode) {
|
|
7200
|
-
this._wrapMode = wrapMode;
|
|
7201
|
-
this._touch();
|
|
7202
|
-
}
|
|
7203
|
-
return this;
|
|
7204
|
-
}
|
|
7205
|
-
setPremultiplyAlpha(premultiplyAlpha) {
|
|
7206
|
-
if (this._premultiplyAlpha !== premultiplyAlpha) {
|
|
7207
|
-
this._premultiplyAlpha = premultiplyAlpha;
|
|
7208
|
-
this._touch();
|
|
7209
|
-
}
|
|
7210
|
-
return this;
|
|
7211
|
-
}
|
|
7212
|
-
setSource(source) {
|
|
7213
|
-
if (this._source !== source) {
|
|
7214
|
-
this._source = source;
|
|
7215
|
-
this.updateSource();
|
|
7216
|
-
}
|
|
7217
|
-
return this;
|
|
7218
|
-
}
|
|
7219
|
-
updateSource() {
|
|
7220
|
-
const { width, height } = getTextureSourceSize(this._source);
|
|
7221
|
-
this.setSize(width, height);
|
|
7222
|
-
this._touch();
|
|
7223
|
-
return this;
|
|
7224
|
-
}
|
|
7225
|
-
setSize(width, height) {
|
|
7226
|
-
if (!this._size.equals({ width, height })) {
|
|
7227
|
-
this._size.set(width, height);
|
|
7228
|
-
this._touch();
|
|
7229
|
-
}
|
|
7230
|
-
return this;
|
|
7231
|
-
}
|
|
7232
|
-
destroy() {
|
|
7233
|
-
for (const listener of Array.from(this._destroyListeners)) {
|
|
7234
|
-
listener();
|
|
7235
|
-
}
|
|
7236
|
-
this._destroyListeners.clear();
|
|
7237
|
-
this._size.destroy();
|
|
7238
|
-
this._source = null;
|
|
7239
|
-
}
|
|
7240
|
-
_touch() {
|
|
7241
|
-
this._version++;
|
|
7242
|
-
}
|
|
7243
|
-
}
|
|
7244
|
-
|
|
7245
7589
|
/**
|
|
7246
7590
|
* Instance-based renderer registry.
|
|
7247
7591
|
*
|
|
@@ -7427,6 +7771,7 @@ class WebGl2Backend {
|
|
|
7427
7771
|
this._setupContext();
|
|
7428
7772
|
this._addEvents();
|
|
7429
7773
|
this.rendererRegistry.registerRenderer(Sprite, new WebGl2SpriteRenderer(spriteRendererBatchSize));
|
|
7774
|
+
this.rendererRegistry.registerRenderer(Mesh, new WebGl2MeshRenderer());
|
|
7430
7775
|
this.rendererRegistry.registerRenderer(ParticleSystem, new WebGl2ParticleRenderer(particleRendererBatchSize));
|
|
7431
7776
|
this.rendererRegistry.registerRenderer(DrawableShape, new WebGl2PrimitiveRenderer(primitiveRendererBatchSize));
|
|
7432
7777
|
this.rendererRegistry.connect(this);
|
|
@@ -8076,334 +8421,994 @@ function getWebGpuBlendState(blendMode) {
|
|
|
8076
8421
|
},
|
|
8077
8422
|
};
|
|
8078
8423
|
}
|
|
8079
|
-
}
|
|
8080
|
-
|
|
8081
|
-
/// <reference types="@webgpu/types" />
|
|
8082
|
-
const primitiveShaderSource = `
|
|
8083
|
-
struct VertexInput {
|
|
8084
|
-
@location(0) position: vec4<f32>,
|
|
8085
|
-
@location(1) color: vec4<f32>,
|
|
8086
|
-
};
|
|
8087
|
-
|
|
8088
|
-
struct VertexOutput {
|
|
8089
|
-
@builtin(position) position: vec4<f32>,
|
|
8090
|
-
@location(0) color: vec4<f32>,
|
|
8091
|
-
};
|
|
8092
|
-
|
|
8093
|
-
@vertex
|
|
8094
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
8095
|
-
var output: VertexOutput;
|
|
8096
|
-
|
|
8097
|
-
output.position = input.position;
|
|
8098
|
-
output.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
|
|
8099
|
-
|
|
8100
|
-
return output;
|
|
8101
|
-
}
|
|
8102
|
-
|
|
8103
|
-
@fragment
|
|
8104
|
-
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
8105
|
-
return input.color;
|
|
8106
|
-
}
|
|
8107
|
-
`;
|
|
8108
|
-
// 4 floats (pre-transformed clip-space position) + 1 u32 (color) = 20 bytes.
|
|
8109
|
-
// The CPU applies (view * shape.globalTransform) to each vertex before writing
|
|
8110
|
-
// it into the vertex buffer, so the shader outputs the position as-is. This
|
|
8111
|
-
// matches the sprite renderer's approach and eliminates the need for a per-
|
|
8112
|
-
// drawcall uniform binding.
|
|
8113
|
-
const vertexStrideBytes$
|
|
8114
|
-
const wordsPerVertex = vertexStrideBytes$
|
|
8115
|
-
class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
8116
|
-
_combinedTransform = new Matrix();
|
|
8117
|
-
_drawCalls = [];
|
|
8118
|
-
_drawCallCount = 0;
|
|
8119
|
-
_pipelines = new Map();
|
|
8120
|
-
_device = null;
|
|
8121
|
-
_shaderModule = null;
|
|
8122
|
-
_pipelineLayout = null;
|
|
8123
|
-
_vertexBuffer = null;
|
|
8124
|
-
_indexBuffer = null;
|
|
8125
|
-
_vertexBufferCapacity = 0;
|
|
8126
|
-
_indexBufferCapacity = 0;
|
|
8127
|
-
_vertexData = new ArrayBuffer(0);
|
|
8128
|
-
_float32View = new Float32Array(this._vertexData);
|
|
8129
|
-
_uint32View = new Uint32Array(this._vertexData);
|
|
8130
|
-
_packedIndexData = new Uint16Array(0);
|
|
8131
|
-
_generatedIndexData = new Uint16Array(0);
|
|
8132
|
-
_sequentialIndexData = new Uint16Array(0);
|
|
8133
|
-
render(shape) {
|
|
8424
|
+
}
|
|
8425
|
+
|
|
8426
|
+
/// <reference types="@webgpu/types" />
|
|
8427
|
+
const primitiveShaderSource = `
|
|
8428
|
+
struct VertexInput {
|
|
8429
|
+
@location(0) position: vec4<f32>,
|
|
8430
|
+
@location(1) color: vec4<f32>,
|
|
8431
|
+
};
|
|
8432
|
+
|
|
8433
|
+
struct VertexOutput {
|
|
8434
|
+
@builtin(position) position: vec4<f32>,
|
|
8435
|
+
@location(0) color: vec4<f32>,
|
|
8436
|
+
};
|
|
8437
|
+
|
|
8438
|
+
@vertex
|
|
8439
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
8440
|
+
var output: VertexOutput;
|
|
8441
|
+
|
|
8442
|
+
output.position = input.position;
|
|
8443
|
+
output.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
|
|
8444
|
+
|
|
8445
|
+
return output;
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
@fragment
|
|
8449
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
8450
|
+
return input.color;
|
|
8451
|
+
}
|
|
8452
|
+
`;
|
|
8453
|
+
// 4 floats (pre-transformed clip-space position) + 1 u32 (color) = 20 bytes.
|
|
8454
|
+
// The CPU applies (view * shape.globalTransform) to each vertex before writing
|
|
8455
|
+
// it into the vertex buffer, so the shader outputs the position as-is. This
|
|
8456
|
+
// matches the sprite renderer's approach and eliminates the need for a per-
|
|
8457
|
+
// drawcall uniform binding.
|
|
8458
|
+
const vertexStrideBytes$2 = 20;
|
|
8459
|
+
const wordsPerVertex$1 = vertexStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
|
|
8460
|
+
class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
8461
|
+
_combinedTransform = new Matrix();
|
|
8462
|
+
_drawCalls = [];
|
|
8463
|
+
_drawCallCount = 0;
|
|
8464
|
+
_pipelines = new Map();
|
|
8465
|
+
_device = null;
|
|
8466
|
+
_shaderModule = null;
|
|
8467
|
+
_pipelineLayout = null;
|
|
8468
|
+
_vertexBuffer = null;
|
|
8469
|
+
_indexBuffer = null;
|
|
8470
|
+
_vertexBufferCapacity = 0;
|
|
8471
|
+
_indexBufferCapacity = 0;
|
|
8472
|
+
_vertexData = new ArrayBuffer(0);
|
|
8473
|
+
_float32View = new Float32Array(this._vertexData);
|
|
8474
|
+
_uint32View = new Uint32Array(this._vertexData);
|
|
8475
|
+
_packedIndexData = new Uint16Array(0);
|
|
8476
|
+
_generatedIndexData = new Uint16Array(0);
|
|
8477
|
+
_sequentialIndexData = new Uint16Array(0);
|
|
8478
|
+
render(shape) {
|
|
8479
|
+
const backend = this._backend;
|
|
8480
|
+
if (backend === null) {
|
|
8481
|
+
throw new Error('Renderer not connected');
|
|
8482
|
+
}
|
|
8483
|
+
if (shape.drawMode !== RenderingPrimitives.Points
|
|
8484
|
+
&& shape.drawMode !== RenderingPrimitives.Lines
|
|
8485
|
+
&& shape.drawMode !== RenderingPrimitives.LineLoop
|
|
8486
|
+
&& shape.drawMode !== RenderingPrimitives.LineStrip
|
|
8487
|
+
&& shape.drawMode !== RenderingPrimitives.Triangles
|
|
8488
|
+
&& shape.drawMode !== RenderingPrimitives.TriangleFan
|
|
8489
|
+
&& shape.drawMode !== RenderingPrimitives.TriangleStrip) {
|
|
8490
|
+
throw new Error(`WebGPU primitive renderer does not support draw mode "${shape.drawMode}" yet.`);
|
|
8491
|
+
}
|
|
8492
|
+
backend.setBlendMode(shape.blendMode);
|
|
8493
|
+
if (shape.geometry.vertices.length === 0) {
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
const drawCallIndex = this._drawCallCount++;
|
|
8497
|
+
const drawCall = this._drawCalls[drawCallIndex];
|
|
8498
|
+
if (drawCall) {
|
|
8499
|
+
drawCall.shape = shape;
|
|
8500
|
+
drawCall.blendMode = shape.blendMode;
|
|
8501
|
+
}
|
|
8502
|
+
else {
|
|
8503
|
+
this._drawCalls.push({
|
|
8504
|
+
shape,
|
|
8505
|
+
blendMode: shape.blendMode,
|
|
8506
|
+
});
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
flush() {
|
|
8510
|
+
const backend = this._backend;
|
|
8511
|
+
const device = this._device;
|
|
8512
|
+
if (!backend || !device) {
|
|
8513
|
+
return;
|
|
8514
|
+
}
|
|
8515
|
+
if (this._drawCallCount === 0 && !backend.clearRequested) {
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
const scissor = backend.getScissorRect();
|
|
8519
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
8520
|
+
// Phase 1: resolve drawcalls and record each one's offsets into the
|
|
8521
|
+
// shared packed buffers. Transform gets baked into the vertex data
|
|
8522
|
+
// during phase 2 so no per-drawcall uniform binding is needed.
|
|
8523
|
+
const plan = [];
|
|
8524
|
+
const resolvedDrawCalls = [];
|
|
8525
|
+
let totalVertices = 0;
|
|
8526
|
+
let totalIndices = 0;
|
|
8527
|
+
if (this._drawCallCount > 0 && !maskClipsAll) {
|
|
8528
|
+
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
8529
|
+
const drawCall = this._drawCalls[drawCallIndex];
|
|
8530
|
+
const shape = drawCall.shape;
|
|
8531
|
+
const resolved = this._resolveDrawCall(shape);
|
|
8532
|
+
resolvedDrawCalls.push(resolved);
|
|
8533
|
+
if (resolved === null) {
|
|
8534
|
+
continue;
|
|
8535
|
+
}
|
|
8536
|
+
const pipeline = this._getPipeline({
|
|
8537
|
+
topology: resolved.topology,
|
|
8538
|
+
usesStripIndex: resolved.usesStripIndex,
|
|
8539
|
+
blendMode: drawCall.blendMode,
|
|
8540
|
+
format: backend.renderTargetFormat,
|
|
8541
|
+
});
|
|
8542
|
+
plan.push({
|
|
8543
|
+
pipeline,
|
|
8544
|
+
vertexByteOffset: totalVertices * vertexStrideBytes$2,
|
|
8545
|
+
vertexCount: resolved.vertexCount,
|
|
8546
|
+
indexByteOffset: totalIndices * Uint16Array.BYTES_PER_ELEMENT,
|
|
8547
|
+
indexCount: resolved.indexCount,
|
|
8548
|
+
});
|
|
8549
|
+
totalVertices += resolved.vertexCount;
|
|
8550
|
+
totalIndices += resolved.indexCount;
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
// If nothing will actually render, still honor a pending clear with
|
|
8554
|
+
// a single empty pass so createColorAttachment consumes the clear
|
|
8555
|
+
// state exactly once.
|
|
8556
|
+
if (plan.length === 0) {
|
|
8557
|
+
if (backend.clearRequested) {
|
|
8558
|
+
const encoder = device.createCommandEncoder();
|
|
8559
|
+
const pass = encoder.beginRenderPass({
|
|
8560
|
+
colorAttachments: [backend.createColorAttachment()],
|
|
8561
|
+
});
|
|
8562
|
+
backend.stats.renderPasses++;
|
|
8563
|
+
pass.end();
|
|
8564
|
+
backend.submit(encoder.finish());
|
|
8565
|
+
}
|
|
8566
|
+
this._drawCallCount = 0;
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8569
|
+
// Phase 2: size GPU buffers for the whole-frame totals, then pack
|
|
8570
|
+
// every drawcall's CPU-side data. _writeShapeVertices applies
|
|
8571
|
+
// (view * shape.globalTransform) per-vertex so the shader simply
|
|
8572
|
+
// outputs input.position unchanged.
|
|
8573
|
+
this._ensureVertexCapacity(totalVertices);
|
|
8574
|
+
if (totalIndices > 0) {
|
|
8575
|
+
this._ensureIndexCapacity(totalIndices);
|
|
8576
|
+
if (this._packedIndexData.length < totalIndices) {
|
|
8577
|
+
this._packedIndexData = new Uint16Array(Math.max(totalIndices, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
{
|
|
8581
|
+
let vOffset = 0;
|
|
8582
|
+
let iOffset = 0;
|
|
8583
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
8584
|
+
const resolved = resolvedDrawCalls[i];
|
|
8585
|
+
if (resolved === null) {
|
|
8586
|
+
continue;
|
|
8587
|
+
}
|
|
8588
|
+
const drawCall = this._drawCalls[i];
|
|
8589
|
+
const shape = drawCall.shape;
|
|
8590
|
+
this._writeShapeVertices(backend, shape, vOffset);
|
|
8591
|
+
if (resolved.indices !== null && resolved.indexCount > 0) {
|
|
8592
|
+
this._packedIndexData.set(resolved.indices.subarray(0, resolved.indexCount), iOffset);
|
|
8593
|
+
iOffset += resolved.indexCount;
|
|
8594
|
+
}
|
|
8595
|
+
vOffset += resolved.vertexCount;
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
// Phase 3: single writeBuffer per GPU buffer covers the whole frame.
|
|
8599
|
+
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$2);
|
|
8600
|
+
if (totalIndices > 0) {
|
|
8601
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
8602
|
+
}
|
|
8603
|
+
// Phase 4: single render pass. Per-draw state is just pipeline and
|
|
8604
|
+
// vertex/index subrange offsets — the transform has already been
|
|
8605
|
+
// baked into the vertex data.
|
|
8606
|
+
const encoder = device.createCommandEncoder();
|
|
8607
|
+
const pass = encoder.beginRenderPass({
|
|
8608
|
+
colorAttachments: [backend.createColorAttachment()],
|
|
8609
|
+
});
|
|
8610
|
+
backend.stats.renderPasses++;
|
|
8611
|
+
if (scissor !== null) {
|
|
8612
|
+
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
8613
|
+
}
|
|
8614
|
+
for (const planned of plan) {
|
|
8615
|
+
pass.setPipeline(planned.pipeline);
|
|
8616
|
+
pass.setVertexBuffer(0, this._vertexBuffer, planned.vertexByteOffset);
|
|
8617
|
+
if (planned.indexCount > 0) {
|
|
8618
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16', planned.indexByteOffset);
|
|
8619
|
+
pass.drawIndexed(planned.indexCount);
|
|
8620
|
+
}
|
|
8621
|
+
else {
|
|
8622
|
+
pass.draw(planned.vertexCount);
|
|
8623
|
+
}
|
|
8624
|
+
backend.stats.batches++;
|
|
8625
|
+
backend.stats.drawCalls++;
|
|
8626
|
+
}
|
|
8627
|
+
pass.end();
|
|
8628
|
+
backend.submit(encoder.finish());
|
|
8629
|
+
this._drawCallCount = 0;
|
|
8630
|
+
}
|
|
8631
|
+
destroy() {
|
|
8632
|
+
this.disconnect();
|
|
8633
|
+
this._combinedTransform.destroy();
|
|
8634
|
+
}
|
|
8635
|
+
onConnect(backend) {
|
|
8636
|
+
this._backend = backend;
|
|
8637
|
+
this._device = this._backend.device;
|
|
8638
|
+
this._shaderModule = this._device.createShaderModule({ code: primitiveShaderSource });
|
|
8639
|
+
// Transform is applied per-vertex on the CPU, so no uniform binding
|
|
8640
|
+
// is needed — the shader outputs input.position directly.
|
|
8641
|
+
this._pipelineLayout = this._device.createPipelineLayout({
|
|
8642
|
+
bindGroupLayouts: [],
|
|
8643
|
+
});
|
|
8644
|
+
}
|
|
8645
|
+
onDisconnect() {
|
|
8646
|
+
this.flush();
|
|
8647
|
+
this._destroyBuffers();
|
|
8648
|
+
this._pipelines.clear();
|
|
8649
|
+
this._pipelineLayout = null;
|
|
8650
|
+
this._shaderModule = null;
|
|
8651
|
+
this._device = null;
|
|
8652
|
+
this._backend = null;
|
|
8653
|
+
this._drawCallCount = 0;
|
|
8654
|
+
}
|
|
8655
|
+
_writeShapeVertices(backend, shape, vertexStart) {
|
|
8656
|
+
// Matrix.combine is `other * this` (see Matrix.rotate and
|
|
8657
|
+
// SceneNode.getGlobalTransform, both of which chain via
|
|
8658
|
+
// local.combine(parent.global) to yield parent.global * local).
|
|
8659
|
+
//
|
|
8660
|
+
// We need view * global applied to a local vertex, so start with
|
|
8661
|
+
// global and combine with view — that gives
|
|
8662
|
+
// _combinedTransform = view * global.
|
|
8663
|
+
const matrix = this._combinedTransform
|
|
8664
|
+
.copy(shape.getGlobalTransform())
|
|
8665
|
+
.combine(backend.view.getTransform());
|
|
8666
|
+
// Match the original uniform-based WGSL layout exactly.
|
|
8667
|
+
//
|
|
8668
|
+
// The shader packs the Matrix's 9 fields into a 4x4 mat (column-major
|
|
8669
|
+
// in WGSL):
|
|
8670
|
+
// col 0 = [a, c, 0, 0]
|
|
8671
|
+
// col 1 = [b, d, 0, 0]
|
|
8672
|
+
// col 2 = [0, 0, 1, 0]
|
|
8673
|
+
// col 3 = [x, y, 0, z]
|
|
8674
|
+
//
|
|
8675
|
+
// Multiplied by vec4(px, py, 0, 1):
|
|
8676
|
+
// out = col0*px + col1*py + col2*0 + col3*1
|
|
8677
|
+
// out.x = a*px + b*py + x
|
|
8678
|
+
// out.y = c*px + d*py + y
|
|
8679
|
+
// out.z = 0
|
|
8680
|
+
// out.w = z
|
|
8681
|
+
//
|
|
8682
|
+
// The Matrix class represents the affine matrix in the order
|
|
8683
|
+
// [a b x]
|
|
8684
|
+
// [c d y]
|
|
8685
|
+
// [e f z]
|
|
8686
|
+
// so a/b/c/d are rotation+scale (note: b on the TOP row, c on the
|
|
8687
|
+
// LEFT column, not the other way around) and x/y/z the translation /
|
|
8688
|
+
// w component. Matrix.toArray(false) confirms this layout.
|
|
8689
|
+
const a = matrix.a;
|
|
8690
|
+
const b = matrix.b;
|
|
8691
|
+
const c = matrix.c;
|
|
8692
|
+
const d = matrix.d;
|
|
8693
|
+
const tx = matrix.x;
|
|
8694
|
+
const ty = matrix.y;
|
|
8695
|
+
const tw = matrix.z;
|
|
8696
|
+
const color = shape.color.toRgba();
|
|
8697
|
+
const vertices = shape.geometry.vertices;
|
|
8698
|
+
const vertexCount = vertices.length / 2;
|
|
8699
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
8700
|
+
const sourceIndex = i * 2;
|
|
8701
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex$1;
|
|
8702
|
+
const px = vertices[sourceIndex];
|
|
8703
|
+
const py = vertices[sourceIndex + 1];
|
|
8704
|
+
this._float32View[targetIndex + 0] = a * px + b * py + tx;
|
|
8705
|
+
this._float32View[targetIndex + 1] = c * px + d * py + ty;
|
|
8706
|
+
this._float32View[targetIndex + 2] = 0;
|
|
8707
|
+
this._float32View[targetIndex + 3] = tw;
|
|
8708
|
+
this._uint32View[targetIndex + 4] = color;
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
_ensureVertexCapacity(vertexCount) {
|
|
8712
|
+
const requiredBytes = vertexCount * vertexStrideBytes$2;
|
|
8713
|
+
if (requiredBytes > this._vertexData.byteLength) {
|
|
8714
|
+
const byteLength = Math.max(requiredBytes, this._vertexData.byteLength === 0 ? vertexStrideBytes$2 : this._vertexData.byteLength * 2);
|
|
8715
|
+
this._vertexData = new ArrayBuffer(byteLength);
|
|
8716
|
+
this._float32View = new Float32Array(this._vertexData);
|
|
8717
|
+
this._uint32View = new Uint32Array(this._vertexData);
|
|
8718
|
+
}
|
|
8719
|
+
if (requiredBytes > this._vertexBufferCapacity) {
|
|
8720
|
+
this._vertexBuffer?.destroy();
|
|
8721
|
+
this._vertexBufferCapacity = Math.max(requiredBytes, this._vertexBufferCapacity === 0 ? vertexStrideBytes$2 : this._vertexBufferCapacity * 2);
|
|
8722
|
+
this._vertexBuffer = this._device.createBuffer({
|
|
8723
|
+
size: this._vertexBufferCapacity,
|
|
8724
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
8725
|
+
});
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
_ensureIndexCapacity(indexCount) {
|
|
8729
|
+
const requiredBytes = indexCount * Uint16Array.BYTES_PER_ELEMENT;
|
|
8730
|
+
if (requiredBytes > this._indexBufferCapacity) {
|
|
8731
|
+
this._indexBuffer?.destroy();
|
|
8732
|
+
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
|
|
8733
|
+
this._indexBuffer = this._device.createBuffer({
|
|
8734
|
+
size: this._indexBufferCapacity,
|
|
8735
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
8736
|
+
});
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
_getPipeline(key) {
|
|
8740
|
+
const pipelineKey = `${key.topology}:${key.usesStripIndex ? 1 : 0}:${key.blendMode}:${key.format}`;
|
|
8741
|
+
const existingPipeline = this._pipelines.get(pipelineKey);
|
|
8742
|
+
if (existingPipeline) {
|
|
8743
|
+
return existingPipeline;
|
|
8744
|
+
}
|
|
8745
|
+
const pipeline = this._device.createRenderPipeline({
|
|
8746
|
+
layout: this._pipelineLayout,
|
|
8747
|
+
vertex: {
|
|
8748
|
+
module: this._shaderModule,
|
|
8749
|
+
entryPoint: 'vertexMain',
|
|
8750
|
+
buffers: [{
|
|
8751
|
+
arrayStride: vertexStrideBytes$2,
|
|
8752
|
+
attributes: [{
|
|
8753
|
+
shaderLocation: 0,
|
|
8754
|
+
offset: 0,
|
|
8755
|
+
format: 'float32x4',
|
|
8756
|
+
}, {
|
|
8757
|
+
shaderLocation: 1,
|
|
8758
|
+
offset: 16,
|
|
8759
|
+
format: 'unorm8x4',
|
|
8760
|
+
}],
|
|
8761
|
+
}],
|
|
8762
|
+
},
|
|
8763
|
+
fragment: {
|
|
8764
|
+
module: this._shaderModule,
|
|
8765
|
+
entryPoint: 'fragmentMain',
|
|
8766
|
+
targets: [{
|
|
8767
|
+
format: key.format,
|
|
8768
|
+
blend: getWebGpuBlendState(key.blendMode),
|
|
8769
|
+
writeMask: GPUColorWrite.ALL,
|
|
8770
|
+
}],
|
|
8771
|
+
},
|
|
8772
|
+
primitive: {
|
|
8773
|
+
topology: key.topology,
|
|
8774
|
+
stripIndexFormat: key.usesStripIndex ? 'uint16' : undefined,
|
|
8775
|
+
},
|
|
8776
|
+
});
|
|
8777
|
+
this._pipelines.set(pipelineKey, pipeline);
|
|
8778
|
+
return pipeline;
|
|
8779
|
+
}
|
|
8780
|
+
_getTopology(drawMode) {
|
|
8781
|
+
switch (drawMode) {
|
|
8782
|
+
case RenderingPrimitives.Points:
|
|
8783
|
+
return 'point-list';
|
|
8784
|
+
case RenderingPrimitives.Lines:
|
|
8785
|
+
return 'line-list';
|
|
8786
|
+
case RenderingPrimitives.LineLoop:
|
|
8787
|
+
case RenderingPrimitives.LineStrip:
|
|
8788
|
+
return 'line-strip';
|
|
8789
|
+
case RenderingPrimitives.Triangles:
|
|
8790
|
+
case RenderingPrimitives.TriangleFan:
|
|
8791
|
+
return 'triangle-list';
|
|
8792
|
+
case RenderingPrimitives.TriangleStrip:
|
|
8793
|
+
return 'triangle-strip';
|
|
8794
|
+
default:
|
|
8795
|
+
throw new Error(`WebGPU primitive renderer does not support draw mode "${drawMode}" yet.`);
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
_resolveDrawCall(shape) {
|
|
8799
|
+
const vertices = shape.geometry.vertices;
|
|
8800
|
+
const vertexCount = vertices.length / 2;
|
|
8801
|
+
if (vertexCount === 0) {
|
|
8802
|
+
return null;
|
|
8803
|
+
}
|
|
8804
|
+
switch (shape.drawMode) {
|
|
8805
|
+
case RenderingPrimitives.LineLoop:
|
|
8806
|
+
return this._resolveLineLoopDrawCall(shape.geometry.indices, vertexCount);
|
|
8807
|
+
case RenderingPrimitives.TriangleFan:
|
|
8808
|
+
return this._resolveTriangleFanDrawCall(shape.geometry.indices, vertexCount);
|
|
8809
|
+
default: {
|
|
8810
|
+
const indices = shape.geometry.indices;
|
|
8811
|
+
const topology = this._getTopology(shape.drawMode);
|
|
8812
|
+
const indexCount = indices.length;
|
|
8813
|
+
const usesStripIndex = indexCount > 0 && (shape.drawMode === RenderingPrimitives.LineStrip
|
|
8814
|
+
|| shape.drawMode === RenderingPrimitives.TriangleStrip);
|
|
8815
|
+
if (indexCount > 0) {
|
|
8816
|
+
return {
|
|
8817
|
+
topology,
|
|
8818
|
+
usesStripIndex,
|
|
8819
|
+
vertexCount,
|
|
8820
|
+
indices,
|
|
8821
|
+
indexCount,
|
|
8822
|
+
};
|
|
8823
|
+
}
|
|
8824
|
+
return {
|
|
8825
|
+
topology,
|
|
8826
|
+
usesStripIndex,
|
|
8827
|
+
vertexCount,
|
|
8828
|
+
indices: null,
|
|
8829
|
+
indexCount: 0,
|
|
8830
|
+
};
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
_resolveLineLoopDrawCall(indices, vertexCount) {
|
|
8835
|
+
const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
|
|
8836
|
+
const sourceCount = sourceIndices.length;
|
|
8837
|
+
if (sourceCount < 2) {
|
|
8838
|
+
return null;
|
|
8839
|
+
}
|
|
8840
|
+
const loopIndexCount = sourceCount + 1;
|
|
8841
|
+
const generatedIndices = this._ensureGeneratedIndexCapacity(loopIndexCount);
|
|
8842
|
+
generatedIndices.set(sourceIndices.subarray(0, sourceCount), 0);
|
|
8843
|
+
generatedIndices[sourceCount] = sourceIndices[0];
|
|
8844
|
+
return {
|
|
8845
|
+
topology: 'line-strip',
|
|
8846
|
+
usesStripIndex: true,
|
|
8847
|
+
vertexCount,
|
|
8848
|
+
indices: generatedIndices,
|
|
8849
|
+
indexCount: loopIndexCount,
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
_resolveTriangleFanDrawCall(indices, vertexCount) {
|
|
8853
|
+
const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
|
|
8854
|
+
const sourceCount = sourceIndices.length;
|
|
8855
|
+
if (sourceCount < 3) {
|
|
8856
|
+
return null;
|
|
8857
|
+
}
|
|
8858
|
+
const indexCount = (sourceCount - 2) * 3;
|
|
8859
|
+
const generatedIndices = this._ensureGeneratedIndexCapacity(indexCount);
|
|
8860
|
+
let targetIndex = 0;
|
|
8861
|
+
for (let index = 1; index < sourceCount - 1; index++) {
|
|
8862
|
+
generatedIndices[targetIndex++] = sourceIndices[0];
|
|
8863
|
+
generatedIndices[targetIndex++] = sourceIndices[index];
|
|
8864
|
+
generatedIndices[targetIndex++] = sourceIndices[index + 1];
|
|
8865
|
+
}
|
|
8866
|
+
return {
|
|
8867
|
+
topology: 'triangle-list',
|
|
8868
|
+
usesStripIndex: false,
|
|
8869
|
+
vertexCount,
|
|
8870
|
+
indices: generatedIndices,
|
|
8871
|
+
indexCount,
|
|
8872
|
+
};
|
|
8873
|
+
}
|
|
8874
|
+
_getSequentialIndices(vertexCount) {
|
|
8875
|
+
if (vertexCount > this._sequentialIndexData.length) {
|
|
8876
|
+
let nextLength = Math.max(1, this._sequentialIndexData.length);
|
|
8877
|
+
while (nextLength < vertexCount) {
|
|
8878
|
+
nextLength *= 2;
|
|
8879
|
+
}
|
|
8880
|
+
this._sequentialIndexData = new Uint16Array(nextLength);
|
|
8881
|
+
}
|
|
8882
|
+
for (let index = 0; index < vertexCount; index++) {
|
|
8883
|
+
this._sequentialIndexData[index] = index;
|
|
8884
|
+
}
|
|
8885
|
+
return this._sequentialIndexData.subarray(0, vertexCount);
|
|
8886
|
+
}
|
|
8887
|
+
_ensureGeneratedIndexCapacity(indexCount) {
|
|
8888
|
+
if (indexCount > this._generatedIndexData.length) {
|
|
8889
|
+
let nextLength = Math.max(1, this._generatedIndexData.length);
|
|
8890
|
+
while (nextLength < indexCount) {
|
|
8891
|
+
nextLength *= 2;
|
|
8892
|
+
}
|
|
8893
|
+
this._generatedIndexData = new Uint16Array(nextLength);
|
|
8894
|
+
}
|
|
8895
|
+
return this._generatedIndexData.subarray(0, indexCount);
|
|
8896
|
+
}
|
|
8897
|
+
_destroyBuffers() {
|
|
8898
|
+
this._vertexBuffer?.destroy();
|
|
8899
|
+
this._indexBuffer?.destroy();
|
|
8900
|
+
this._vertexBuffer = null;
|
|
8901
|
+
this._indexBuffer = null;
|
|
8902
|
+
this._vertexBufferCapacity = 0;
|
|
8903
|
+
this._indexBufferCapacity = 0;
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
|
|
8907
|
+
/// <reference types="@webgpu/types" />
|
|
8908
|
+
const spriteShaderSource = `
|
|
8909
|
+
struct ProjectionUniforms {
|
|
8910
|
+
matrix: mat4x4<f32>,
|
|
8911
|
+
};
|
|
8912
|
+
|
|
8913
|
+
@group(0) @binding(0)
|
|
8914
|
+
var<uniform> projection: ProjectionUniforms;
|
|
8915
|
+
|
|
8916
|
+
@group(1) @binding(0)
|
|
8917
|
+
var spriteTexture0: texture_2d<f32>;
|
|
8918
|
+
@group(1) @binding(1)
|
|
8919
|
+
var spriteTexture1: texture_2d<f32>;
|
|
8920
|
+
@group(1) @binding(2)
|
|
8921
|
+
var spriteTexture2: texture_2d<f32>;
|
|
8922
|
+
@group(1) @binding(3)
|
|
8923
|
+
var spriteTexture3: texture_2d<f32>;
|
|
8924
|
+
@group(1) @binding(4)
|
|
8925
|
+
var spriteTexture4: texture_2d<f32>;
|
|
8926
|
+
@group(1) @binding(5)
|
|
8927
|
+
var spriteTexture5: texture_2d<f32>;
|
|
8928
|
+
@group(1) @binding(6)
|
|
8929
|
+
var spriteTexture6: texture_2d<f32>;
|
|
8930
|
+
@group(1) @binding(7)
|
|
8931
|
+
var spriteTexture7: texture_2d<f32>;
|
|
8932
|
+
|
|
8933
|
+
@group(1) @binding(8)
|
|
8934
|
+
var spriteSampler0: sampler;
|
|
8935
|
+
@group(1) @binding(9)
|
|
8936
|
+
var spriteSampler1: sampler;
|
|
8937
|
+
@group(1) @binding(10)
|
|
8938
|
+
var spriteSampler2: sampler;
|
|
8939
|
+
@group(1) @binding(11)
|
|
8940
|
+
var spriteSampler3: sampler;
|
|
8941
|
+
@group(1) @binding(12)
|
|
8942
|
+
var spriteSampler4: sampler;
|
|
8943
|
+
@group(1) @binding(13)
|
|
8944
|
+
var spriteSampler5: sampler;
|
|
8945
|
+
@group(1) @binding(14)
|
|
8946
|
+
var spriteSampler6: sampler;
|
|
8947
|
+
@group(1) @binding(15)
|
|
8948
|
+
var spriteSampler7: sampler;
|
|
8949
|
+
|
|
8950
|
+
// Per-instance vertex layout (56 bytes per sprite). The four corners
|
|
8951
|
+
// of the quad are derived from @builtin(vertex_index) 0..3 inside the
|
|
8952
|
+
// vertex shader — there is no per-vertex stream.
|
|
8953
|
+
struct VertexInput {
|
|
8954
|
+
@location(0) localBounds: vec4<f32>, // left, top, right, bottom (local space)
|
|
8955
|
+
@location(1) transformAB: vec3<f32>, // first row of 2D affine
|
|
8956
|
+
@location(2) transformCD: vec3<f32>, // second row of 2D affine
|
|
8957
|
+
@location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
|
|
8958
|
+
@location(4) color: vec4<f32>, // RGBA tint
|
|
8959
|
+
@location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
|
|
8960
|
+
};
|
|
8961
|
+
|
|
8962
|
+
struct VertexOutput {
|
|
8963
|
+
@builtin(position) position: vec4<f32>,
|
|
8964
|
+
@location(0) texcoord: vec2<f32>,
|
|
8965
|
+
@location(1) color: vec4<f32>,
|
|
8966
|
+
@location(2) @interpolate(flat) premultiplySample: u32,
|
|
8967
|
+
@location(3) @interpolate(flat) textureSlot: u32,
|
|
8968
|
+
};
|
|
8969
|
+
|
|
8970
|
+
@vertex
|
|
8971
|
+
fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
8972
|
+
var output: VertexOutput;
|
|
8973
|
+
|
|
8974
|
+
// vid 0..3 → corners in TL, TR, BR, BL order (matches the static index
|
|
8975
|
+
// buffer [0,1,2,0,2,3] used for indexed triangle-list drawing).
|
|
8976
|
+
let cornerX = ((vid + 1u) >> 1u) & 1u;
|
|
8977
|
+
let cornerY = vid >> 1u;
|
|
8978
|
+
|
|
8979
|
+
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
8980
|
+
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
8981
|
+
|
|
8982
|
+
let worldX = input.transformAB.x * localX + input.transformAB.y * localY + input.transformAB.z;
|
|
8983
|
+
let worldY = input.transformCD.x * localX + input.transformCD.y * localY + input.transformCD.z;
|
|
8984
|
+
|
|
8985
|
+
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
8986
|
+
|
|
8987
|
+
let u = select(input.uvBounds.x, input.uvBounds.z, cornerX == 1u);
|
|
8988
|
+
let v = select(input.uvBounds.y, input.uvBounds.w, cornerY == 1u);
|
|
8989
|
+
output.texcoord = vec2<f32>(u, v);
|
|
8990
|
+
|
|
8991
|
+
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
8992
|
+
output.textureSlot = input.packedSlotFlags & 0xFFu;
|
|
8993
|
+
output.premultiplySample = (input.packedSlotFlags >> 8u) & 1u;
|
|
8994
|
+
|
|
8995
|
+
return output;
|
|
8996
|
+
}
|
|
8997
|
+
|
|
8998
|
+
fn sampleTexture(slot: u32, uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
|
|
8999
|
+
switch slot {
|
|
9000
|
+
case 0u: {
|
|
9001
|
+
return textureSampleGrad(spriteTexture0, spriteSampler0, uv, ddx, ddy);
|
|
9002
|
+
}
|
|
9003
|
+
case 1u: {
|
|
9004
|
+
return textureSampleGrad(spriteTexture1, spriteSampler1, uv, ddx, ddy);
|
|
9005
|
+
}
|
|
9006
|
+
case 2u: {
|
|
9007
|
+
return textureSampleGrad(spriteTexture2, spriteSampler2, uv, ddx, ddy);
|
|
9008
|
+
}
|
|
9009
|
+
case 3u: {
|
|
9010
|
+
return textureSampleGrad(spriteTexture3, spriteSampler3, uv, ddx, ddy);
|
|
9011
|
+
}
|
|
9012
|
+
case 4u: {
|
|
9013
|
+
return textureSampleGrad(spriteTexture4, spriteSampler4, uv, ddx, ddy);
|
|
9014
|
+
}
|
|
9015
|
+
case 5u: {
|
|
9016
|
+
return textureSampleGrad(spriteTexture5, spriteSampler5, uv, ddx, ddy);
|
|
9017
|
+
}
|
|
9018
|
+
case 6u: {
|
|
9019
|
+
return textureSampleGrad(spriteTexture6, spriteSampler6, uv, ddx, ddy);
|
|
9020
|
+
}
|
|
9021
|
+
default: {
|
|
9022
|
+
return textureSampleGrad(spriteTexture7, spriteSampler7, uv, ddx, ddy);
|
|
9023
|
+
}
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
|
|
9027
|
+
@fragment
|
|
9028
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
9029
|
+
// Compute screen-space derivatives in uniform control flow before the
|
|
9030
|
+
// per-slot switch. WGSL requires textureSample (implicit LOD) to run in
|
|
9031
|
+
// uniform control flow, which multi-texture batching breaks because the
|
|
9032
|
+
// slot varies per fragment. textureSampleGrad takes explicit derivatives
|
|
9033
|
+
// and is valid regardless of control-flow uniformity, while preserving
|
|
9034
|
+
// mipmap-correct LOD when sprites use mipmapped textures.
|
|
9035
|
+
let ddx = dpdx(input.texcoord);
|
|
9036
|
+
let ddy = dpdy(input.texcoord);
|
|
9037
|
+
let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
|
|
9038
|
+
let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
|
|
9039
|
+
|
|
9040
|
+
return resolvedSample * input.color;
|
|
9041
|
+
}
|
|
9042
|
+
`;
|
|
9043
|
+
const instanceStrideBytes$1 = 56;
|
|
9044
|
+
const wordsPerInstance = instanceStrideBytes$1 / Uint32Array.BYTES_PER_ELEMENT;
|
|
9045
|
+
const projectionByteLength = 64;
|
|
9046
|
+
const initialBatchCapacity = 32;
|
|
9047
|
+
const maxBatchTextures = 8;
|
|
9048
|
+
const indicesPerSprite = 6;
|
|
9049
|
+
// Static index buffer: two triangles forming a quad, vertex IDs 0..3 in
|
|
9050
|
+
// TL/TR/BR/BL order so the WGSL `cornerX/cornerY` derivation matches.
|
|
9051
|
+
const quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
9052
|
+
class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
9053
|
+
_projectionData = new Float32Array(projectionByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
9054
|
+
_device = null;
|
|
9055
|
+
_shaderModule = null;
|
|
9056
|
+
_uniformBindGroupLayout = null;
|
|
9057
|
+
_textureBindGroupLayout = null;
|
|
9058
|
+
_pipelineLayout = null;
|
|
9059
|
+
_uniformBuffer = null;
|
|
9060
|
+
_uniformBindGroup = null;
|
|
9061
|
+
_indexBuffer = null;
|
|
9062
|
+
_instanceBuffer = null;
|
|
9063
|
+
_instanceCapacity = 0;
|
|
9064
|
+
_instanceData = new ArrayBuffer(0);
|
|
9065
|
+
_instanceFloat32 = new Float32Array(this._instanceData);
|
|
9066
|
+
_instanceUint32 = new Uint32Array(this._instanceData);
|
|
9067
|
+
_pipelines = new Map();
|
|
9068
|
+
_activeTextures = new Array(maxBatchTextures).fill(null);
|
|
9069
|
+
_textureSlots = new Map();
|
|
9070
|
+
_slotCount = 0;
|
|
9071
|
+
_instanceCount = 0;
|
|
9072
|
+
_currentBlendMode = null;
|
|
9073
|
+
onConnect(backend) {
|
|
9074
|
+
if (this._device) {
|
|
9075
|
+
return;
|
|
9076
|
+
}
|
|
9077
|
+
this._device = backend.device;
|
|
9078
|
+
this._shaderModule = this._device.createShaderModule({ code: spriteShaderSource });
|
|
9079
|
+
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
9080
|
+
entries: [{
|
|
9081
|
+
binding: 0,
|
|
9082
|
+
visibility: GPUShaderStage.VERTEX,
|
|
9083
|
+
buffer: {
|
|
9084
|
+
type: 'uniform',
|
|
9085
|
+
},
|
|
9086
|
+
}],
|
|
9087
|
+
});
|
|
9088
|
+
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
9089
|
+
entries: [
|
|
9090
|
+
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
9091
|
+
binding: index,
|
|
9092
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
9093
|
+
texture: {
|
|
9094
|
+
sampleType: 'float',
|
|
9095
|
+
},
|
|
9096
|
+
})),
|
|
9097
|
+
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
9098
|
+
binding: maxBatchTextures + index,
|
|
9099
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
9100
|
+
sampler: {
|
|
9101
|
+
type: 'filtering',
|
|
9102
|
+
},
|
|
9103
|
+
})),
|
|
9104
|
+
],
|
|
9105
|
+
});
|
|
9106
|
+
this._pipelineLayout = this._device.createPipelineLayout({
|
|
9107
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
9108
|
+
});
|
|
9109
|
+
this._uniformBuffer = this._device.createBuffer({
|
|
9110
|
+
size: projectionByteLength,
|
|
9111
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9112
|
+
});
|
|
9113
|
+
this._uniformBindGroup = this._device.createBindGroup({
|
|
9114
|
+
layout: this._uniformBindGroupLayout,
|
|
9115
|
+
entries: [{
|
|
9116
|
+
binding: 0,
|
|
9117
|
+
resource: {
|
|
9118
|
+
buffer: this._uniformBuffer,
|
|
9119
|
+
},
|
|
9120
|
+
}],
|
|
9121
|
+
});
|
|
9122
|
+
// Static index buffer for the quad. Allocated once at connect; its
|
|
9123
|
+
// contents never change.
|
|
9124
|
+
this._indexBuffer = this._device.createBuffer({
|
|
9125
|
+
size: quadIndices.byteLength,
|
|
9126
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
9127
|
+
});
|
|
9128
|
+
this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
|
|
9129
|
+
}
|
|
9130
|
+
onDisconnect() {
|
|
9131
|
+
this._instanceBuffer?.destroy();
|
|
9132
|
+
this._indexBuffer?.destroy();
|
|
9133
|
+
this._uniformBuffer?.destroy();
|
|
9134
|
+
this._pipelines.clear();
|
|
9135
|
+
this._instanceBuffer = null;
|
|
9136
|
+
this._indexBuffer = null;
|
|
9137
|
+
this._uniformBindGroup = null;
|
|
9138
|
+
this._uniformBuffer = null;
|
|
9139
|
+
this._pipelineLayout = null;
|
|
9140
|
+
this._textureBindGroupLayout = null;
|
|
9141
|
+
this._uniformBindGroupLayout = null;
|
|
9142
|
+
this._shaderModule = null;
|
|
9143
|
+
this._device = null;
|
|
9144
|
+
this._backend = null;
|
|
9145
|
+
this._instanceCapacity = 0;
|
|
9146
|
+
this._instanceData = new ArrayBuffer(0);
|
|
9147
|
+
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
9148
|
+
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
9149
|
+
this._instanceCount = 0;
|
|
9150
|
+
this._currentBlendMode = null;
|
|
9151
|
+
this._resetSlots();
|
|
9152
|
+
}
|
|
9153
|
+
render(sprite) {
|
|
8134
9154
|
const backend = this._backend;
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
&&
|
|
8142
|
-
&& shape.drawMode !== RenderingPrimitives.Triangles
|
|
8143
|
-
&& shape.drawMode !== RenderingPrimitives.TriangleFan
|
|
8144
|
-
&& shape.drawMode !== RenderingPrimitives.TriangleStrip) {
|
|
8145
|
-
throw new Error(`WebGPU primitive renderer does not support draw mode "${shape.drawMode}" yet.`);
|
|
8146
|
-
}
|
|
8147
|
-
backend.setBlendMode(shape.blendMode);
|
|
8148
|
-
if (shape.geometry.vertices.length === 0) {
|
|
9155
|
+
const texture = sprite.texture;
|
|
9156
|
+
// Same early-out conditions as the deferred renderer used to apply.
|
|
9157
|
+
if (backend === null
|
|
9158
|
+
|| (!(texture instanceof Texture) && !(texture instanceof RenderTexture))
|
|
9159
|
+
|| texture.width === 0
|
|
9160
|
+
|| texture.height === 0
|
|
9161
|
+
|| (texture instanceof Texture && texture.source === null)) {
|
|
8149
9162
|
return;
|
|
8150
9163
|
}
|
|
8151
|
-
const
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
9164
|
+
const blendMode = sprite.blendMode;
|
|
9165
|
+
// Flush triggers: blend-mode change, instance buffer full at current
|
|
9166
|
+
// capacity (we'll grow on next render), or texture-slot exhaustion.
|
|
9167
|
+
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
9168
|
+
const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
|
|
9169
|
+
if (blendModeChanged || slotExhausted) {
|
|
9170
|
+
this.flush();
|
|
8156
9171
|
}
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
9172
|
+
this._currentBlendMode = blendMode;
|
|
9173
|
+
backend.setBlendMode(blendMode);
|
|
9174
|
+
// Resolve / assign texture slot.
|
|
9175
|
+
let slot = this._textureSlots.get(texture);
|
|
9176
|
+
if (slot === undefined) {
|
|
9177
|
+
slot = this._slotCount++;
|
|
9178
|
+
this._textureSlots.set(texture, slot);
|
|
9179
|
+
this._activeTextures[slot] = texture;
|
|
8162
9180
|
}
|
|
9181
|
+
const premultiplySample = backend.shouldPremultiplyTextureSample(texture) ? 1 : 0;
|
|
9182
|
+
const packedSlotFlags = slot | (premultiplySample << 8);
|
|
9183
|
+
// Ensure capacity covers the new entry BEFORE packing — otherwise the
|
|
9184
|
+
// typed-array writes in _packInstance silently fall off the end of a
|
|
9185
|
+
// too-small buffer.
|
|
9186
|
+
this._ensureInstanceCapacity(this._instanceCount + 1);
|
|
9187
|
+
this._packInstance(sprite, texture, packedSlotFlags);
|
|
9188
|
+
this._instanceCount++;
|
|
8163
9189
|
}
|
|
8164
9190
|
flush() {
|
|
8165
9191
|
const backend = this._backend;
|
|
8166
9192
|
const device = this._device;
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
if (this._drawCallCount === 0 && !backend.clearRequested) {
|
|
9193
|
+
const uniformBuffer = this._uniformBuffer;
|
|
9194
|
+
const uniformBindGroup = this._uniformBindGroup;
|
|
9195
|
+
if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
|
|
8171
9196
|
return;
|
|
8172
9197
|
}
|
|
8173
|
-
|
|
8174
|
-
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
8175
|
-
// Phase 1: resolve drawcalls and record each one's offsets into the
|
|
8176
|
-
// shared packed buffers. Transform gets baked into the vertex data
|
|
8177
|
-
// during phase 2 so no per-drawcall uniform binding is needed.
|
|
8178
|
-
const plan = [];
|
|
8179
|
-
const resolvedDrawCalls = [];
|
|
8180
|
-
let totalVertices = 0;
|
|
8181
|
-
let totalIndices = 0;
|
|
8182
|
-
if (this._drawCallCount > 0 && !maskClipsAll) {
|
|
8183
|
-
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
8184
|
-
const drawCall = this._drawCalls[drawCallIndex];
|
|
8185
|
-
const shape = drawCall.shape;
|
|
8186
|
-
const resolved = this._resolveDrawCall(shape);
|
|
8187
|
-
resolvedDrawCalls.push(resolved);
|
|
8188
|
-
if (resolved === null) {
|
|
8189
|
-
continue;
|
|
8190
|
-
}
|
|
8191
|
-
const pipeline = this._getPipeline({
|
|
8192
|
-
topology: resolved.topology,
|
|
8193
|
-
usesStripIndex: resolved.usesStripIndex,
|
|
8194
|
-
blendMode: drawCall.blendMode,
|
|
8195
|
-
format: backend.renderTargetFormat,
|
|
8196
|
-
});
|
|
8197
|
-
plan.push({
|
|
8198
|
-
pipeline,
|
|
8199
|
-
vertexByteOffset: totalVertices * vertexStrideBytes$1,
|
|
8200
|
-
vertexCount: resolved.vertexCount,
|
|
8201
|
-
indexByteOffset: totalIndices * Uint16Array.BYTES_PER_ELEMENT,
|
|
8202
|
-
indexCount: resolved.indexCount,
|
|
8203
|
-
});
|
|
8204
|
-
totalVertices += resolved.vertexCount;
|
|
8205
|
-
totalIndices += resolved.indexCount;
|
|
8206
|
-
}
|
|
8207
|
-
}
|
|
8208
|
-
// If nothing will actually render, still honor a pending clear with
|
|
8209
|
-
// a single empty pass so createColorAttachment consumes the clear
|
|
8210
|
-
// state exactly once.
|
|
8211
|
-
if (plan.length === 0) {
|
|
8212
|
-
if (backend.clearRequested) {
|
|
8213
|
-
const encoder = device.createCommandEncoder();
|
|
8214
|
-
const pass = encoder.beginRenderPass({
|
|
8215
|
-
colorAttachments: [backend.createColorAttachment()],
|
|
8216
|
-
});
|
|
8217
|
-
backend.stats.renderPasses++;
|
|
8218
|
-
pass.end();
|
|
8219
|
-
backend.submit(encoder.finish());
|
|
8220
|
-
}
|
|
8221
|
-
this._drawCallCount = 0;
|
|
9198
|
+
if (this._instanceCount === 0 && !backend.clearRequested) {
|
|
8222
9199
|
return;
|
|
8223
9200
|
}
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
this._packedIndexData = new Uint16Array(Math.max(totalIndices, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
|
|
8233
|
-
}
|
|
8234
|
-
}
|
|
8235
|
-
{
|
|
8236
|
-
let vOffset = 0;
|
|
8237
|
-
let iOffset = 0;
|
|
8238
|
-
for (let i = 0; i < this._drawCallCount; i++) {
|
|
8239
|
-
const resolved = resolvedDrawCalls[i];
|
|
8240
|
-
if (resolved === null) {
|
|
8241
|
-
continue;
|
|
8242
|
-
}
|
|
8243
|
-
const drawCall = this._drawCalls[i];
|
|
8244
|
-
const shape = drawCall.shape;
|
|
8245
|
-
this._writeShapeVertices(backend, shape, vOffset);
|
|
8246
|
-
if (resolved.indices !== null && resolved.indexCount > 0) {
|
|
8247
|
-
this._packedIndexData.set(resolved.indices.subarray(0, resolved.indexCount), iOffset);
|
|
8248
|
-
iOffset += resolved.indexCount;
|
|
8249
|
-
}
|
|
8250
|
-
vOffset += resolved.vertexCount;
|
|
8251
|
-
}
|
|
8252
|
-
}
|
|
8253
|
-
// Phase 3: single writeBuffer per GPU buffer covers the whole frame.
|
|
8254
|
-
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$1);
|
|
8255
|
-
if (totalIndices > 0) {
|
|
8256
|
-
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
8257
|
-
}
|
|
8258
|
-
// Phase 4: single render pass. Per-draw state is just pipeline and
|
|
8259
|
-
// vertex/index subrange offsets — the transform has already been
|
|
8260
|
-
// baked into the vertex data.
|
|
9201
|
+
const viewMatrix = backend.view.getTransform();
|
|
9202
|
+
this._projectionData.set([
|
|
9203
|
+
viewMatrix.a, viewMatrix.c, 0, 0,
|
|
9204
|
+
viewMatrix.b, viewMatrix.d, 0, 0,
|
|
9205
|
+
0, 0, 1, 0,
|
|
9206
|
+
viewMatrix.x, viewMatrix.y, 0, viewMatrix.z,
|
|
9207
|
+
]);
|
|
9208
|
+
device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
|
|
8261
9209
|
const encoder = device.createCommandEncoder();
|
|
8262
9210
|
const pass = encoder.beginRenderPass({
|
|
8263
9211
|
colorAttachments: [backend.createColorAttachment()],
|
|
8264
9212
|
});
|
|
8265
9213
|
backend.stats.renderPasses++;
|
|
8266
|
-
|
|
9214
|
+
const scissor = backend.getScissorRect();
|
|
9215
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
9216
|
+
if (scissor !== null && !maskClipsAll) {
|
|
8267
9217
|
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
8268
9218
|
}
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
9219
|
+
if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
|
|
9220
|
+
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes$1);
|
|
9221
|
+
const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat);
|
|
9222
|
+
const textureBindGroup = this._createTextureBindGroup(device, backend);
|
|
9223
|
+
pass.setPipeline(pipeline);
|
|
9224
|
+
pass.setBindGroup(0, uniformBindGroup);
|
|
9225
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
9226
|
+
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
9227
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
9228
|
+
pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
|
|
9229
|
+
backend.stats.batches++;
|
|
9230
|
+
backend.stats.drawCalls++;
|
|
9231
|
+
}
|
|
9232
|
+
pass.end();
|
|
9233
|
+
backend.submit(encoder.finish());
|
|
9234
|
+
this._instanceCount = 0;
|
|
9235
|
+
this._resetSlots();
|
|
9236
|
+
this._currentBlendMode = null;
|
|
9237
|
+
}
|
|
9238
|
+
destroy() {
|
|
9239
|
+
this.disconnect();
|
|
9240
|
+
}
|
|
9241
|
+
/**
|
|
9242
|
+
* Pre-create render pipelines for every blend-mode × target-format
|
|
9243
|
+
* combination this renderer can produce, asynchronously and in
|
|
9244
|
+
* parallel. Called from the render manager's init path so by the time
|
|
9245
|
+
* the first frame draws, all pipelines exist in cache.
|
|
9246
|
+
*
|
|
9247
|
+
* Without prewarm, the first draw of any new (blendMode, format)
|
|
9248
|
+
* combination would fall back to the synchronous _getPipeline() path,
|
|
9249
|
+
* which blocks while the WebGPU implementation compiles WGSL and
|
|
9250
|
+
* sets up the pipeline state object — typically tens of milliseconds.
|
|
9251
|
+
*/
|
|
9252
|
+
async prewarmPipelines(formats) {
|
|
9253
|
+
const device = this._device;
|
|
9254
|
+
if (!device || !this._shaderModule || !this._pipelineLayout) {
|
|
9255
|
+
return;
|
|
9256
|
+
}
|
|
9257
|
+
if (typeof device.createRenderPipelineAsync !== 'function') {
|
|
9258
|
+
return;
|
|
9259
|
+
}
|
|
9260
|
+
const blendModes = [
|
|
9261
|
+
BlendModes.Normal,
|
|
9262
|
+
BlendModes.Additive,
|
|
9263
|
+
BlendModes.Subtract,
|
|
9264
|
+
BlendModes.Multiply,
|
|
9265
|
+
BlendModes.Screen,
|
|
9266
|
+
];
|
|
9267
|
+
const promises = [];
|
|
9268
|
+
for (const blendMode of blendModes) {
|
|
9269
|
+
for (const format of formats) {
|
|
9270
|
+
const pipelineKey = `${blendMode}:${format}`;
|
|
9271
|
+
if (this._pipelines.has(pipelineKey)) {
|
|
9272
|
+
continue;
|
|
9273
|
+
}
|
|
9274
|
+
const promise = device
|
|
9275
|
+
.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format))
|
|
9276
|
+
.then((pipeline) => {
|
|
9277
|
+
this._pipelines.set(pipelineKey, pipeline);
|
|
9278
|
+
});
|
|
9279
|
+
promises.push(promise);
|
|
8278
9280
|
}
|
|
8279
|
-
backend.stats.batches++;
|
|
8280
|
-
backend.stats.drawCalls++;
|
|
8281
9281
|
}
|
|
8282
|
-
|
|
8283
|
-
backend.submit(encoder.finish());
|
|
8284
|
-
this._drawCallCount = 0;
|
|
9282
|
+
await Promise.all(promises);
|
|
8285
9283
|
}
|
|
8286
|
-
|
|
8287
|
-
this.
|
|
8288
|
-
this.
|
|
9284
|
+
_packInstance(sprite, texture, packedSlotFlags) {
|
|
9285
|
+
const offset = this._instanceCount * wordsPerInstance;
|
|
9286
|
+
const f32 = this._instanceFloat32;
|
|
9287
|
+
const u32 = this._instanceUint32;
|
|
9288
|
+
const bounds = sprite.getLocalBounds();
|
|
9289
|
+
f32[offset + 0] = bounds.left;
|
|
9290
|
+
f32[offset + 1] = bounds.top;
|
|
9291
|
+
f32[offset + 2] = bounds.right;
|
|
9292
|
+
f32[offset + 3] = bounds.bottom;
|
|
9293
|
+
const transform = sprite.getGlobalTransform();
|
|
9294
|
+
f32[offset + 4] = transform.a;
|
|
9295
|
+
f32[offset + 5] = transform.b;
|
|
9296
|
+
f32[offset + 6] = transform.x;
|
|
9297
|
+
f32[offset + 7] = transform.c;
|
|
9298
|
+
f32[offset + 8] = transform.d;
|
|
9299
|
+
f32[offset + 9] = transform.y;
|
|
9300
|
+
// uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
|
|
9301
|
+
// applies the flipY swap so the shader stays orientation-agnostic.
|
|
9302
|
+
const frame = sprite.textureFrame;
|
|
9303
|
+
const texWidth = texture.width;
|
|
9304
|
+
const texHeight = texture.height;
|
|
9305
|
+
const uMin = ((frame.left / texWidth) * 0xFFFF) & 0xFFFF;
|
|
9306
|
+
const uMax = ((frame.right / texWidth) * 0xFFFF) & 0xFFFF;
|
|
9307
|
+
const vMinRaw = ((frame.top / texHeight) * 0xFFFF) & 0xFFFF;
|
|
9308
|
+
const vMaxRaw = ((frame.bottom / texHeight) * 0xFFFF) & 0xFFFF;
|
|
9309
|
+
const flipY = texture instanceof Texture && texture.flipY;
|
|
9310
|
+
const vMin = flipY ? vMaxRaw : vMinRaw;
|
|
9311
|
+
const vMax = flipY ? vMinRaw : vMaxRaw;
|
|
9312
|
+
u32[offset + 10] = uMin | (vMin << 16);
|
|
9313
|
+
u32[offset + 11] = uMax | (vMax << 16);
|
|
9314
|
+
u32[offset + 12] = sprite.tint.toRgba();
|
|
9315
|
+
u32[offset + 13] = packedSlotFlags;
|
|
8289
9316
|
}
|
|
8290
|
-
|
|
8291
|
-
this.
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
9317
|
+
_ensureInstanceCapacity(instanceCount) {
|
|
9318
|
+
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
9319
|
+
return;
|
|
9320
|
+
}
|
|
9321
|
+
let nextCapacity = Math.max(this._instanceCapacity, initialBatchCapacity);
|
|
9322
|
+
while (nextCapacity < instanceCount) {
|
|
9323
|
+
nextCapacity *= 2;
|
|
9324
|
+
}
|
|
9325
|
+
const oldData = this._instanceData;
|
|
9326
|
+
// Preserve any already-packed instances. _instanceCount is bounded by
|
|
9327
|
+
// the previous capacity, but oldData may be the initial 0-byte buffer
|
|
9328
|
+
// — clamp to its actual byteLength to avoid out-of-range typed-array
|
|
9329
|
+
// construction.
|
|
9330
|
+
const carryBytes = Math.min(this._instanceCount * instanceStrideBytes$1, oldData.byteLength);
|
|
9331
|
+
const instanceData = new ArrayBuffer(nextCapacity * instanceStrideBytes$1);
|
|
9332
|
+
if (carryBytes > 0) {
|
|
9333
|
+
new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
|
|
9334
|
+
}
|
|
9335
|
+
const instanceBuffer = this._device.createBuffer({
|
|
9336
|
+
size: instanceData.byteLength,
|
|
9337
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
8298
9338
|
});
|
|
9339
|
+
this._instanceBuffer?.destroy();
|
|
9340
|
+
this._instanceCapacity = nextCapacity;
|
|
9341
|
+
this._instanceData = instanceData;
|
|
9342
|
+
this._instanceFloat32 = new Float32Array(instanceData);
|
|
9343
|
+
this._instanceUint32 = new Uint32Array(instanceData);
|
|
9344
|
+
this._instanceBuffer = instanceBuffer;
|
|
8299
9345
|
}
|
|
8300
|
-
|
|
8301
|
-
this.
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
this._backend = null;
|
|
8308
|
-
this._drawCallCount = 0;
|
|
8309
|
-
}
|
|
8310
|
-
_writeShapeVertices(backend, shape, vertexStart) {
|
|
8311
|
-
// Matrix.combine is `other * this` (see Matrix.rotate and
|
|
8312
|
-
// SceneNode.getGlobalTransform, both of which chain via
|
|
8313
|
-
// local.combine(parent.global) to yield parent.global * local).
|
|
8314
|
-
//
|
|
8315
|
-
// We need view * global applied to a local vertex, so start with
|
|
8316
|
-
// global and combine with view — that gives
|
|
8317
|
-
// _combinedTransform = view * global.
|
|
8318
|
-
const matrix = this._combinedTransform
|
|
8319
|
-
.copy(shape.getGlobalTransform())
|
|
8320
|
-
.combine(backend.view.getTransform());
|
|
8321
|
-
// Match the original uniform-based WGSL layout exactly.
|
|
8322
|
-
//
|
|
8323
|
-
// The shader packs the Matrix's 9 fields into a 4x4 mat (column-major
|
|
8324
|
-
// in WGSL):
|
|
8325
|
-
// col 0 = [a, c, 0, 0]
|
|
8326
|
-
// col 1 = [b, d, 0, 0]
|
|
8327
|
-
// col 2 = [0, 0, 1, 0]
|
|
8328
|
-
// col 3 = [x, y, 0, z]
|
|
8329
|
-
//
|
|
8330
|
-
// Multiplied by vec4(px, py, 0, 1):
|
|
8331
|
-
// out = col0*px + col1*py + col2*0 + col3*1
|
|
8332
|
-
// out.x = a*px + b*py + x
|
|
8333
|
-
// out.y = c*px + d*py + y
|
|
8334
|
-
// out.z = 0
|
|
8335
|
-
// out.w = z
|
|
8336
|
-
//
|
|
8337
|
-
// The Matrix class represents the affine matrix in the order
|
|
8338
|
-
// [a b x]
|
|
8339
|
-
// [c d y]
|
|
8340
|
-
// [e f z]
|
|
8341
|
-
// so a/b/c/d are rotation+scale (note: b on the TOP row, c on the
|
|
8342
|
-
// LEFT column, not the other way around) and x/y/z the translation /
|
|
8343
|
-
// w component. Matrix.toArray(false) confirms this layout.
|
|
8344
|
-
const a = matrix.a;
|
|
8345
|
-
const b = matrix.b;
|
|
8346
|
-
const c = matrix.c;
|
|
8347
|
-
const d = matrix.d;
|
|
8348
|
-
const tx = matrix.x;
|
|
8349
|
-
const ty = matrix.y;
|
|
8350
|
-
const tw = matrix.z;
|
|
8351
|
-
const color = shape.color.toRgba();
|
|
8352
|
-
const vertices = shape.geometry.vertices;
|
|
8353
|
-
const vertexCount = vertices.length / 2;
|
|
8354
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
8355
|
-
const sourceIndex = i * 2;
|
|
8356
|
-
const targetIndex = (vertexStart + i) * wordsPerVertex;
|
|
8357
|
-
const px = vertices[sourceIndex];
|
|
8358
|
-
const py = vertices[sourceIndex + 1];
|
|
8359
|
-
this._float32View[targetIndex + 0] = a * px + b * py + tx;
|
|
8360
|
-
this._float32View[targetIndex + 1] = c * px + d * py + ty;
|
|
8361
|
-
this._float32View[targetIndex + 2] = 0;
|
|
8362
|
-
this._float32View[targetIndex + 3] = tw;
|
|
8363
|
-
this._uint32View[targetIndex + 4] = color;
|
|
9346
|
+
_resetSlots() {
|
|
9347
|
+
if (this._slotCount > 0) {
|
|
9348
|
+
for (let i = 0; i < this._slotCount; i++) {
|
|
9349
|
+
this._activeTextures[i] = null;
|
|
9350
|
+
}
|
|
9351
|
+
this._textureSlots.clear();
|
|
9352
|
+
this._slotCount = 0;
|
|
8364
9353
|
}
|
|
8365
9354
|
}
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
9355
|
+
_createTextureBindGroup(device, backend) {
|
|
9356
|
+
// Slots beyond the active count get the slot-0 texture as a filler so
|
|
9357
|
+
// the bind-group layout always sees N valid texture views and samplers.
|
|
9358
|
+
// The fragment shader's switch only ever dispatches to the active slot
|
|
9359
|
+
// count, so unsampled fillers cost nothing visually.
|
|
9360
|
+
const fallbackTexture = this._activeTextures[0] ?? Texture.empty;
|
|
9361
|
+
const fallbackBinding = backend.getTextureBinding(fallbackTexture);
|
|
9362
|
+
const resolvedBindings = new Array(maxBatchTextures);
|
|
9363
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
9364
|
+
const texture = this._activeTextures[i] ?? fallbackTexture;
|
|
9365
|
+
resolvedBindings[i] = texture === fallbackTexture
|
|
9366
|
+
? fallbackBinding
|
|
9367
|
+
: backend.getTextureBinding(texture);
|
|
8373
9368
|
}
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
9369
|
+
const entries = [];
|
|
9370
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
9371
|
+
entries.push({
|
|
9372
|
+
binding: i,
|
|
9373
|
+
resource: resolvedBindings[i].view,
|
|
8380
9374
|
});
|
|
8381
9375
|
}
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
this._indexBuffer?.destroy();
|
|
8387
|
-
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
|
|
8388
|
-
this._indexBuffer = this._device.createBuffer({
|
|
8389
|
-
size: this._indexBufferCapacity,
|
|
8390
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
9376
|
+
for (let i = 0; i < maxBatchTextures; i++) {
|
|
9377
|
+
entries.push({
|
|
9378
|
+
binding: maxBatchTextures + i,
|
|
9379
|
+
resource: resolvedBindings[i].sampler,
|
|
8391
9380
|
});
|
|
8392
9381
|
}
|
|
9382
|
+
return device.createBindGroup({
|
|
9383
|
+
layout: this._textureBindGroupLayout,
|
|
9384
|
+
entries,
|
|
9385
|
+
});
|
|
8393
9386
|
}
|
|
8394
|
-
_getPipeline(
|
|
8395
|
-
const pipelineKey = `${
|
|
9387
|
+
_getPipeline(blendMode, format) {
|
|
9388
|
+
const pipelineKey = `${blendMode}:${format}`;
|
|
8396
9389
|
const existingPipeline = this._pipelines.get(pipelineKey);
|
|
8397
9390
|
if (existingPipeline) {
|
|
8398
9391
|
return existingPipeline;
|
|
8399
9392
|
}
|
|
8400
|
-
|
|
9393
|
+
if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
|
|
9394
|
+
throw new Error('Renderer has to be connected first!');
|
|
9395
|
+
}
|
|
9396
|
+
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
|
|
9397
|
+
this._pipelines.set(pipelineKey, pipeline);
|
|
9398
|
+
return pipeline;
|
|
9399
|
+
}
|
|
9400
|
+
_buildPipelineDescriptor(blendMode, format) {
|
|
9401
|
+
if (!this._shaderModule || !this._pipelineLayout) {
|
|
9402
|
+
throw new Error('Renderer has to be connected first!');
|
|
9403
|
+
}
|
|
9404
|
+
return {
|
|
8401
9405
|
layout: this._pipelineLayout,
|
|
8402
9406
|
vertex: {
|
|
8403
9407
|
module: this._shaderModule,
|
|
8404
9408
|
entryPoint: 'vertexMain',
|
|
8405
9409
|
buffers: [{
|
|
8406
|
-
arrayStride:
|
|
9410
|
+
arrayStride: instanceStrideBytes$1,
|
|
9411
|
+
stepMode: 'instance',
|
|
8407
9412
|
attributes: [{
|
|
8408
9413
|
shaderLocation: 0,
|
|
8409
9414
|
offset: 0,
|
|
@@ -8411,7 +9416,23 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
8411
9416
|
}, {
|
|
8412
9417
|
shaderLocation: 1,
|
|
8413
9418
|
offset: 16,
|
|
9419
|
+
format: 'float32x3',
|
|
9420
|
+
}, {
|
|
9421
|
+
shaderLocation: 2,
|
|
9422
|
+
offset: 28,
|
|
9423
|
+
format: 'float32x3',
|
|
9424
|
+
}, {
|
|
9425
|
+
shaderLocation: 3,
|
|
9426
|
+
offset: 40,
|
|
9427
|
+
format: 'unorm16x4',
|
|
9428
|
+
}, {
|
|
9429
|
+
shaderLocation: 4,
|
|
9430
|
+
offset: 48,
|
|
8414
9431
|
format: 'unorm8x4',
|
|
9432
|
+
}, {
|
|
9433
|
+
shaderLocation: 5,
|
|
9434
|
+
offset: 52,
|
|
9435
|
+
format: 'uint32',
|
|
8415
9436
|
}],
|
|
8416
9437
|
}],
|
|
8417
9438
|
},
|
|
@@ -8419,491 +9440,233 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
8419
9440
|
module: this._shaderModule,
|
|
8420
9441
|
entryPoint: 'fragmentMain',
|
|
8421
9442
|
targets: [{
|
|
8422
|
-
format
|
|
8423
|
-
blend: getWebGpuBlendState(
|
|
9443
|
+
format,
|
|
9444
|
+
blend: getWebGpuBlendState(blendMode),
|
|
8424
9445
|
writeMask: GPUColorWrite.ALL,
|
|
8425
9446
|
}],
|
|
8426
9447
|
},
|
|
8427
9448
|
primitive: {
|
|
8428
|
-
topology:
|
|
8429
|
-
stripIndexFormat: key.usesStripIndex ? 'uint16' : undefined,
|
|
9449
|
+
topology: 'triangle-list',
|
|
8430
9450
|
},
|
|
8431
|
-
});
|
|
8432
|
-
this._pipelines.set(pipelineKey, pipeline);
|
|
8433
|
-
return pipeline;
|
|
8434
|
-
}
|
|
8435
|
-
_getTopology(drawMode) {
|
|
8436
|
-
switch (drawMode) {
|
|
8437
|
-
case RenderingPrimitives.Points:
|
|
8438
|
-
return 'point-list';
|
|
8439
|
-
case RenderingPrimitives.Lines:
|
|
8440
|
-
return 'line-list';
|
|
8441
|
-
case RenderingPrimitives.LineLoop:
|
|
8442
|
-
case RenderingPrimitives.LineStrip:
|
|
8443
|
-
return 'line-strip';
|
|
8444
|
-
case RenderingPrimitives.Triangles:
|
|
8445
|
-
case RenderingPrimitives.TriangleFan:
|
|
8446
|
-
return 'triangle-list';
|
|
8447
|
-
case RenderingPrimitives.TriangleStrip:
|
|
8448
|
-
return 'triangle-strip';
|
|
8449
|
-
default:
|
|
8450
|
-
throw new Error(`WebGPU primitive renderer does not support draw mode "${drawMode}" yet.`);
|
|
8451
|
-
}
|
|
8452
|
-
}
|
|
8453
|
-
_resolveDrawCall(shape) {
|
|
8454
|
-
const vertices = shape.geometry.vertices;
|
|
8455
|
-
const vertexCount = vertices.length / 2;
|
|
8456
|
-
if (vertexCount === 0) {
|
|
8457
|
-
return null;
|
|
8458
|
-
}
|
|
8459
|
-
switch (shape.drawMode) {
|
|
8460
|
-
case RenderingPrimitives.LineLoop:
|
|
8461
|
-
return this._resolveLineLoopDrawCall(shape.geometry.indices, vertexCount);
|
|
8462
|
-
case RenderingPrimitives.TriangleFan:
|
|
8463
|
-
return this._resolveTriangleFanDrawCall(shape.geometry.indices, vertexCount);
|
|
8464
|
-
default: {
|
|
8465
|
-
const indices = shape.geometry.indices;
|
|
8466
|
-
const topology = this._getTopology(shape.drawMode);
|
|
8467
|
-
const indexCount = indices.length;
|
|
8468
|
-
const usesStripIndex = indexCount > 0 && (shape.drawMode === RenderingPrimitives.LineStrip
|
|
8469
|
-
|| shape.drawMode === RenderingPrimitives.TriangleStrip);
|
|
8470
|
-
if (indexCount > 0) {
|
|
8471
|
-
return {
|
|
8472
|
-
topology,
|
|
8473
|
-
usesStripIndex,
|
|
8474
|
-
vertexCount,
|
|
8475
|
-
indices,
|
|
8476
|
-
indexCount,
|
|
8477
|
-
};
|
|
8478
|
-
}
|
|
8479
|
-
return {
|
|
8480
|
-
topology,
|
|
8481
|
-
usesStripIndex,
|
|
8482
|
-
vertexCount,
|
|
8483
|
-
indices: null,
|
|
8484
|
-
indexCount: 0,
|
|
8485
|
-
};
|
|
8486
|
-
}
|
|
8487
|
-
}
|
|
8488
|
-
}
|
|
8489
|
-
_resolveLineLoopDrawCall(indices, vertexCount) {
|
|
8490
|
-
const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
|
|
8491
|
-
const sourceCount = sourceIndices.length;
|
|
8492
|
-
if (sourceCount < 2) {
|
|
8493
|
-
return null;
|
|
8494
|
-
}
|
|
8495
|
-
const loopIndexCount = sourceCount + 1;
|
|
8496
|
-
const generatedIndices = this._ensureGeneratedIndexCapacity(loopIndexCount);
|
|
8497
|
-
generatedIndices.set(sourceIndices.subarray(0, sourceCount), 0);
|
|
8498
|
-
generatedIndices[sourceCount] = sourceIndices[0];
|
|
8499
|
-
return {
|
|
8500
|
-
topology: 'line-strip',
|
|
8501
|
-
usesStripIndex: true,
|
|
8502
|
-
vertexCount,
|
|
8503
|
-
indices: generatedIndices,
|
|
8504
|
-
indexCount: loopIndexCount,
|
|
8505
|
-
};
|
|
8506
|
-
}
|
|
8507
|
-
_resolveTriangleFanDrawCall(indices, vertexCount) {
|
|
8508
|
-
const sourceIndices = indices.length > 0 ? indices : this._getSequentialIndices(vertexCount);
|
|
8509
|
-
const sourceCount = sourceIndices.length;
|
|
8510
|
-
if (sourceCount < 3) {
|
|
8511
|
-
return null;
|
|
8512
|
-
}
|
|
8513
|
-
const indexCount = (sourceCount - 2) * 3;
|
|
8514
|
-
const generatedIndices = this._ensureGeneratedIndexCapacity(indexCount);
|
|
8515
|
-
let targetIndex = 0;
|
|
8516
|
-
for (let index = 1; index < sourceCount - 1; index++) {
|
|
8517
|
-
generatedIndices[targetIndex++] = sourceIndices[0];
|
|
8518
|
-
generatedIndices[targetIndex++] = sourceIndices[index];
|
|
8519
|
-
generatedIndices[targetIndex++] = sourceIndices[index + 1];
|
|
8520
|
-
}
|
|
8521
|
-
return {
|
|
8522
|
-
topology: 'triangle-list',
|
|
8523
|
-
usesStripIndex: false,
|
|
8524
|
-
vertexCount,
|
|
8525
|
-
indices: generatedIndices,
|
|
8526
|
-
indexCount,
|
|
8527
9451
|
};
|
|
8528
9452
|
}
|
|
8529
|
-
_getSequentialIndices(vertexCount) {
|
|
8530
|
-
if (vertexCount > this._sequentialIndexData.length) {
|
|
8531
|
-
let nextLength = Math.max(1, this._sequentialIndexData.length);
|
|
8532
|
-
while (nextLength < vertexCount) {
|
|
8533
|
-
nextLength *= 2;
|
|
8534
|
-
}
|
|
8535
|
-
this._sequentialIndexData = new Uint16Array(nextLength);
|
|
8536
|
-
}
|
|
8537
|
-
for (let index = 0; index < vertexCount; index++) {
|
|
8538
|
-
this._sequentialIndexData[index] = index;
|
|
8539
|
-
}
|
|
8540
|
-
return this._sequentialIndexData.subarray(0, vertexCount);
|
|
8541
|
-
}
|
|
8542
|
-
_ensureGeneratedIndexCapacity(indexCount) {
|
|
8543
|
-
if (indexCount > this._generatedIndexData.length) {
|
|
8544
|
-
let nextLength = Math.max(1, this._generatedIndexData.length);
|
|
8545
|
-
while (nextLength < indexCount) {
|
|
8546
|
-
nextLength *= 2;
|
|
8547
|
-
}
|
|
8548
|
-
this._generatedIndexData = new Uint16Array(nextLength);
|
|
8549
|
-
}
|
|
8550
|
-
return this._generatedIndexData.subarray(0, indexCount);
|
|
8551
|
-
}
|
|
8552
|
-
_destroyBuffers() {
|
|
8553
|
-
this._vertexBuffer?.destroy();
|
|
8554
|
-
this._indexBuffer?.destroy();
|
|
8555
|
-
this._vertexBuffer = null;
|
|
8556
|
-
this._indexBuffer = null;
|
|
8557
|
-
this._vertexBufferCapacity = 0;
|
|
8558
|
-
this._indexBufferCapacity = 0;
|
|
8559
|
-
}
|
|
8560
|
-
}
|
|
8561
|
-
|
|
8562
|
-
/// <reference types="@webgpu/types" />
|
|
8563
|
-
const spriteShaderSource = `
|
|
8564
|
-
struct ProjectionUniforms {
|
|
8565
|
-
matrix: mat4x4<f32>,
|
|
8566
|
-
};
|
|
8567
|
-
|
|
8568
|
-
@group(0) @binding(0)
|
|
8569
|
-
var<uniform> projection: ProjectionUniforms;
|
|
8570
|
-
|
|
8571
|
-
@group(1) @binding(0)
|
|
8572
|
-
var spriteTexture0: texture_2d<f32>;
|
|
8573
|
-
@group(1) @binding(1)
|
|
8574
|
-
var spriteTexture1: texture_2d<f32>;
|
|
8575
|
-
@group(1) @binding(2)
|
|
8576
|
-
var spriteTexture2: texture_2d<f32>;
|
|
8577
|
-
@group(1) @binding(3)
|
|
8578
|
-
var spriteTexture3: texture_2d<f32>;
|
|
8579
|
-
@group(1) @binding(4)
|
|
8580
|
-
var spriteTexture4: texture_2d<f32>;
|
|
8581
|
-
@group(1) @binding(5)
|
|
8582
|
-
var spriteTexture5: texture_2d<f32>;
|
|
8583
|
-
@group(1) @binding(6)
|
|
8584
|
-
var spriteTexture6: texture_2d<f32>;
|
|
8585
|
-
@group(1) @binding(7)
|
|
8586
|
-
var spriteTexture7: texture_2d<f32>;
|
|
8587
|
-
|
|
8588
|
-
@group(1) @binding(8)
|
|
8589
|
-
var spriteSampler0: sampler;
|
|
8590
|
-
@group(1) @binding(9)
|
|
8591
|
-
var spriteSampler1: sampler;
|
|
8592
|
-
@group(1) @binding(10)
|
|
8593
|
-
var spriteSampler2: sampler;
|
|
8594
|
-
@group(1) @binding(11)
|
|
8595
|
-
var spriteSampler3: sampler;
|
|
8596
|
-
@group(1) @binding(12)
|
|
8597
|
-
var spriteSampler4: sampler;
|
|
8598
|
-
@group(1) @binding(13)
|
|
8599
|
-
var spriteSampler5: sampler;
|
|
8600
|
-
@group(1) @binding(14)
|
|
8601
|
-
var spriteSampler6: sampler;
|
|
8602
|
-
@group(1) @binding(15)
|
|
8603
|
-
var spriteSampler7: sampler;
|
|
8604
|
-
|
|
8605
|
-
// Per-instance vertex layout (56 bytes per sprite). The four corners
|
|
8606
|
-
// of the quad are derived from @builtin(vertex_index) 0..3 inside the
|
|
8607
|
-
// vertex shader — there is no per-vertex stream.
|
|
8608
|
-
struct VertexInput {
|
|
8609
|
-
@location(0) localBounds: vec4<f32>, // left, top, right, bottom (local space)
|
|
8610
|
-
@location(1) transformAB: vec3<f32>, // first row of 2D affine
|
|
8611
|
-
@location(2) transformCD: vec3<f32>, // second row of 2D affine
|
|
8612
|
-
@location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
|
|
8613
|
-
@location(4) color: vec4<f32>, // RGBA tint
|
|
8614
|
-
@location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
|
|
8615
|
-
};
|
|
8616
|
-
|
|
8617
|
-
struct VertexOutput {
|
|
8618
|
-
@builtin(position) position: vec4<f32>,
|
|
8619
|
-
@location(0) texcoord: vec2<f32>,
|
|
8620
|
-
@location(1) color: vec4<f32>,
|
|
8621
|
-
@location(2) @interpolate(flat) premultiplySample: u32,
|
|
8622
|
-
@location(3) @interpolate(flat) textureSlot: u32,
|
|
8623
|
-
};
|
|
8624
|
-
|
|
8625
|
-
@vertex
|
|
8626
|
-
fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
8627
|
-
var output: VertexOutput;
|
|
8628
|
-
|
|
8629
|
-
// vid 0..3 → corners in TL, TR, BR, BL order (matches the static index
|
|
8630
|
-
// buffer [0,1,2,0,2,3] used for indexed triangle-list drawing).
|
|
8631
|
-
let cornerX = ((vid + 1u) >> 1u) & 1u;
|
|
8632
|
-
let cornerY = vid >> 1u;
|
|
8633
|
-
|
|
8634
|
-
let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
|
|
8635
|
-
let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
|
|
8636
|
-
|
|
8637
|
-
let worldX = input.transformAB.x * localX + input.transformAB.y * localY + input.transformAB.z;
|
|
8638
|
-
let worldY = input.transformCD.x * localX + input.transformCD.y * localY + input.transformCD.z;
|
|
8639
|
-
|
|
8640
|
-
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
8641
|
-
|
|
8642
|
-
let u = select(input.uvBounds.x, input.uvBounds.z, cornerX == 1u);
|
|
8643
|
-
let v = select(input.uvBounds.y, input.uvBounds.w, cornerY == 1u);
|
|
8644
|
-
output.texcoord = vec2<f32>(u, v);
|
|
8645
|
-
|
|
8646
|
-
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
8647
|
-
output.textureSlot = input.packedSlotFlags & 0xFFu;
|
|
8648
|
-
output.premultiplySample = (input.packedSlotFlags >> 8u) & 1u;
|
|
8649
|
-
|
|
8650
|
-
return output;
|
|
8651
9453
|
}
|
|
8652
9454
|
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
9455
|
+
/// <reference types="@webgpu/types" />
|
|
9456
|
+
const meshShaderSource = `
|
|
9457
|
+
struct VertexInput {
|
|
9458
|
+
@location(0) position: vec2<f32>,
|
|
9459
|
+
@location(1) texcoord: vec2<f32>,
|
|
9460
|
+
@location(2) color: vec4<f32>,
|
|
9461
|
+
};
|
|
9462
|
+
|
|
9463
|
+
struct VertexOutput {
|
|
9464
|
+
@builtin(position) position: vec4<f32>,
|
|
9465
|
+
@location(0) texcoord: vec2<f32>,
|
|
9466
|
+
@location(1) color: vec4<f32>,
|
|
9467
|
+
@location(2) @interpolate(flat) premultiplySample: u32,
|
|
9468
|
+
};
|
|
9469
|
+
|
|
9470
|
+
struct TintUniform {
|
|
9471
|
+
tint: vec4<f32>,
|
|
9472
|
+
flags: vec4<f32>,
|
|
9473
|
+
};
|
|
9474
|
+
|
|
9475
|
+
@group(0) @binding(0) var<uniform> uniforms: TintUniform;
|
|
9476
|
+
|
|
9477
|
+
@group(1) @binding(0) var meshTexture: texture_2d<f32>;
|
|
9478
|
+
@group(1) @binding(1) var meshSampler: sampler;
|
|
9479
|
+
|
|
9480
|
+
@vertex
|
|
9481
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
9482
|
+
var output: VertexOutput;
|
|
9483
|
+
output.position = vec4<f32>(input.position, 0.0, 1.0);
|
|
9484
|
+
output.texcoord = input.texcoord;
|
|
9485
|
+
output.color = input.color;
|
|
9486
|
+
output.premultiplySample = u32(uniforms.flags.x);
|
|
9487
|
+
return output;
|
|
8680
9488
|
}
|
|
8681
9489
|
|
|
8682
9490
|
@fragment
|
|
8683
9491
|
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
8684
|
-
|
|
8685
|
-
// per-slot switch. WGSL requires textureSample (implicit LOD) to run in
|
|
8686
|
-
// uniform control flow, which multi-texture batching breaks because the
|
|
8687
|
-
// slot varies per fragment. textureSampleGrad takes explicit derivatives
|
|
8688
|
-
// and is valid regardless of control-flow uniformity, while preserving
|
|
8689
|
-
// mipmap-correct LOD when sprites use mipmapped textures.
|
|
8690
|
-
let ddx = dpdx(input.texcoord);
|
|
8691
|
-
let ddy = dpdy(input.texcoord);
|
|
8692
|
-
let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
|
|
9492
|
+
let sample = textureSample(meshTexture, meshSampler, input.texcoord);
|
|
8693
9493
|
let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
|
|
8694
|
-
|
|
8695
|
-
return
|
|
9494
|
+
let modulated = resolvedSample * input.color * uniforms.tint;
|
|
9495
|
+
return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
|
|
8696
9496
|
}
|
|
8697
9497
|
`;
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
const
|
|
8702
|
-
const
|
|
8703
|
-
const
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
9498
|
+
// Per-vertex layout (20 bytes): pos f32x2 + uv f32x2 + color u8x4-norm.
|
|
9499
|
+
// CPU bakes the (view * globalTransform) into position so the vertex
|
|
9500
|
+
// shader stays branchless and uniform-free except for the per-mesh tint.
|
|
9501
|
+
const vertexStrideBytes$1 = 20;
|
|
9502
|
+
const wordsPerVertex = vertexStrideBytes$1 / 4;
|
|
9503
|
+
const tintByteLength = 32; // vec4 tint + vec4 flags (only flags.x used)
|
|
9504
|
+
class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
9505
|
+
_combinedTransform = new Matrix();
|
|
9506
|
+
_drawCalls = [];
|
|
9507
|
+
_pipelines = new Map();
|
|
9508
|
+
_textureBindGroups = new Map();
|
|
8709
9509
|
_device = null;
|
|
8710
9510
|
_shaderModule = null;
|
|
8711
9511
|
_uniformBindGroupLayout = null;
|
|
8712
9512
|
_textureBindGroupLayout = null;
|
|
8713
9513
|
_pipelineLayout = null;
|
|
9514
|
+
_vertexBuffer = null;
|
|
9515
|
+
_indexBuffer = null;
|
|
8714
9516
|
_uniformBuffer = null;
|
|
8715
9517
|
_uniformBindGroup = null;
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
_instanceCount = 0;
|
|
8727
|
-
_currentBlendMode = null;
|
|
8728
|
-
onConnect(backend) {
|
|
8729
|
-
if (this._device) {
|
|
8730
|
-
return;
|
|
8731
|
-
}
|
|
8732
|
-
this._device = backend.device;
|
|
8733
|
-
this._shaderModule = this._device.createShaderModule({ code: spriteShaderSource });
|
|
8734
|
-
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
8735
|
-
entries: [{
|
|
8736
|
-
binding: 0,
|
|
8737
|
-
visibility: GPUShaderStage.VERTEX,
|
|
8738
|
-
buffer: {
|
|
8739
|
-
type: 'uniform',
|
|
8740
|
-
},
|
|
8741
|
-
}],
|
|
8742
|
-
});
|
|
8743
|
-
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
8744
|
-
entries: [
|
|
8745
|
-
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
8746
|
-
binding: index,
|
|
8747
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
8748
|
-
texture: {
|
|
8749
|
-
sampleType: 'float',
|
|
8750
|
-
},
|
|
8751
|
-
})),
|
|
8752
|
-
...Array.from({ length: maxBatchTextures }, (_, index) => ({
|
|
8753
|
-
binding: maxBatchTextures + index,
|
|
8754
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
8755
|
-
sampler: {
|
|
8756
|
-
type: 'filtering',
|
|
8757
|
-
},
|
|
8758
|
-
})),
|
|
8759
|
-
],
|
|
8760
|
-
});
|
|
8761
|
-
this._pipelineLayout = this._device.createPipelineLayout({
|
|
8762
|
-
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
8763
|
-
});
|
|
8764
|
-
this._uniformBuffer = this._device.createBuffer({
|
|
8765
|
-
size: projectionByteLength,
|
|
8766
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8767
|
-
});
|
|
8768
|
-
this._uniformBindGroup = this._device.createBindGroup({
|
|
8769
|
-
layout: this._uniformBindGroupLayout,
|
|
8770
|
-
entries: [{
|
|
8771
|
-
binding: 0,
|
|
8772
|
-
resource: {
|
|
8773
|
-
buffer: this._uniformBuffer,
|
|
8774
|
-
},
|
|
8775
|
-
}],
|
|
8776
|
-
});
|
|
8777
|
-
// Static index buffer for the quad. Allocated once at connect; its
|
|
8778
|
-
// contents never change.
|
|
8779
|
-
this._indexBuffer = this._device.createBuffer({
|
|
8780
|
-
size: quadIndices.byteLength,
|
|
8781
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
8782
|
-
});
|
|
8783
|
-
this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
|
|
8784
|
-
}
|
|
8785
|
-
onDisconnect() {
|
|
8786
|
-
this._instanceBuffer?.destroy();
|
|
8787
|
-
this._indexBuffer?.destroy();
|
|
8788
|
-
this._uniformBuffer?.destroy();
|
|
8789
|
-
this._pipelines.clear();
|
|
8790
|
-
this._instanceBuffer = null;
|
|
8791
|
-
this._indexBuffer = null;
|
|
8792
|
-
this._uniformBindGroup = null;
|
|
8793
|
-
this._uniformBuffer = null;
|
|
8794
|
-
this._pipelineLayout = null;
|
|
8795
|
-
this._textureBindGroupLayout = null;
|
|
8796
|
-
this._uniformBindGroupLayout = null;
|
|
8797
|
-
this._shaderModule = null;
|
|
8798
|
-
this._device = null;
|
|
8799
|
-
this._backend = null;
|
|
8800
|
-
this._instanceCapacity = 0;
|
|
8801
|
-
this._instanceData = new ArrayBuffer(0);
|
|
8802
|
-
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
8803
|
-
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
8804
|
-
this._instanceCount = 0;
|
|
8805
|
-
this._currentBlendMode = null;
|
|
8806
|
-
this._resetSlots();
|
|
8807
|
-
}
|
|
8808
|
-
render(sprite) {
|
|
9518
|
+
_uniformAlignment = 256;
|
|
9519
|
+
_vertexBufferCapacity = 0;
|
|
9520
|
+
_indexBufferCapacity = 0;
|
|
9521
|
+
_uniformBufferCapacity = 0;
|
|
9522
|
+
_vertexData = new ArrayBuffer(0);
|
|
9523
|
+
_float32View = new Float32Array(this._vertexData);
|
|
9524
|
+
_uint32View = new Uint32Array(this._vertexData);
|
|
9525
|
+
_packedIndexData = new Uint16Array(0);
|
|
9526
|
+
_drawCallCount = 0;
|
|
9527
|
+
render(mesh) {
|
|
8809
9528
|
const backend = this._backend;
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
if (backend === null
|
|
8813
|
-
|| (!(texture instanceof Texture) && !(texture instanceof RenderTexture))
|
|
8814
|
-
|| texture.width === 0
|
|
8815
|
-
|| texture.height === 0
|
|
8816
|
-
|| (texture instanceof Texture && texture.source === null)) {
|
|
8817
|
-
return;
|
|
9529
|
+
if (backend === null) {
|
|
9530
|
+
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
8818
9531
|
}
|
|
8819
|
-
const
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
8823
|
-
const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
|
|
8824
|
-
if (blendModeChanged || slotExhausted) {
|
|
8825
|
-
this.flush();
|
|
9532
|
+
const vertexCount = mesh.vertexCount;
|
|
9533
|
+
if (vertexCount === 0) {
|
|
9534
|
+
return;
|
|
8826
9535
|
}
|
|
8827
|
-
|
|
9536
|
+
const blendMode = mesh.blendMode;
|
|
8828
9537
|
backend.setBlendMode(blendMode);
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
const
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
9538
|
+
const meshTexture = mesh.texture ?? Texture.white;
|
|
9539
|
+
// Texture.white is a 1x1 canvas-backed Texture; backend.shouldPremultiplyTextureSample
|
|
9540
|
+
// expects RenderTexture-or-Texture. Both branches are valid here.
|
|
9541
|
+
const premultiplySample = backend.shouldPremultiplyTextureSample(meshTexture);
|
|
9542
|
+
const indexCount = mesh.indexCount;
|
|
9543
|
+
// Plan offsets within the shared per-frame buffers; actual data
|
|
9544
|
+
// packing happens in flush() after all drawcalls are known so a
|
|
9545
|
+
// single writeBuffer per resource covers the whole frame.
|
|
9546
|
+
const drawCall = {
|
|
9547
|
+
mesh,
|
|
9548
|
+
blendMode,
|
|
9549
|
+
texture: meshTexture,
|
|
9550
|
+
premultiplySample,
|
|
9551
|
+
vertexByteOffset: 0,
|
|
9552
|
+
vertexCount,
|
|
9553
|
+
indexByteOffset: 0,
|
|
9554
|
+
indexCount,
|
|
9555
|
+
};
|
|
9556
|
+
// Use mutable record (interface readonly is for type safety against
|
|
9557
|
+
// callers; the renderer fills these slots in flush()).
|
|
9558
|
+
this._drawCalls[this._drawCallCount++] = drawCall;
|
|
8844
9559
|
}
|
|
8845
9560
|
flush() {
|
|
8846
9561
|
const backend = this._backend;
|
|
8847
9562
|
const device = this._device;
|
|
8848
|
-
|
|
8849
|
-
const uniformBindGroup = this._uniformBindGroup;
|
|
8850
|
-
if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
|
|
9563
|
+
if (!backend || !device) {
|
|
8851
9564
|
return;
|
|
8852
9565
|
}
|
|
8853
|
-
if (this.
|
|
9566
|
+
if (this._drawCallCount === 0 && !backend.clearRequested) {
|
|
8854
9567
|
return;
|
|
8855
9568
|
}
|
|
8856
|
-
const
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
9569
|
+
const scissor = backend.getScissorRect();
|
|
9570
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
9571
|
+
if (this._drawCallCount === 0 || maskClipsAll) {
|
|
9572
|
+
// Honor a pending clear with an empty pass so createColorAttachment
|
|
9573
|
+
// consumes the clear-state once.
|
|
9574
|
+
if (backend.clearRequested) {
|
|
9575
|
+
const encoder = device.createCommandEncoder();
|
|
9576
|
+
const pass = encoder.beginRenderPass({
|
|
9577
|
+
colorAttachments: [backend.createColorAttachment()],
|
|
9578
|
+
});
|
|
9579
|
+
backend.stats.renderPasses++;
|
|
9580
|
+
pass.end();
|
|
9581
|
+
backend.submit(encoder.finish());
|
|
9582
|
+
}
|
|
9583
|
+
this._drawCallCount = 0;
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9586
|
+
// Phase 1: compute layout offsets for the whole frame.
|
|
9587
|
+
let totalVertices = 0;
|
|
9588
|
+
let totalIndices = 0;
|
|
9589
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
9590
|
+
const dc = this._drawCalls[i];
|
|
9591
|
+
dc.vertexByteOffset = totalVertices * vertexStrideBytes$1;
|
|
9592
|
+
dc.indexByteOffset = totalIndices * Uint16Array.BYTES_PER_ELEMENT;
|
|
9593
|
+
totalVertices += dc.vertexCount;
|
|
9594
|
+
totalIndices += dc.indexCount;
|
|
9595
|
+
}
|
|
9596
|
+
// Phase 2: ensure capacities for the totals.
|
|
9597
|
+
this._ensureVertexCapacity(totalVertices);
|
|
9598
|
+
this._ensureIndexCapacity(totalIndices);
|
|
9599
|
+
this._ensureUniformCapacity(this._drawCallCount);
|
|
9600
|
+
// Phase 3: pack vertex + index + uniform CPU-side data.
|
|
9601
|
+
const uniformBytes = this._drawCallCount * this._uniformAlignment;
|
|
9602
|
+
const uniformData = new ArrayBuffer(uniformBytes);
|
|
9603
|
+
const uniformF32 = new Float32Array(uniformData);
|
|
9604
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
9605
|
+
const dc = this._drawCalls[i];
|
|
9606
|
+
this._writeMeshVertices(backend, dc.mesh, dc.vertexByteOffset / vertexStrideBytes$1);
|
|
9607
|
+
if (dc.mesh.indices !== null) {
|
|
9608
|
+
this._packedIndexData.set(dc.mesh.indices, dc.indexByteOffset / Uint16Array.BYTES_PER_ELEMENT);
|
|
9609
|
+
}
|
|
9610
|
+
else {
|
|
9611
|
+
const start = dc.indexByteOffset / Uint16Array.BYTES_PER_ELEMENT;
|
|
9612
|
+
for (let j = 0; j < dc.indexCount; j++) {
|
|
9613
|
+
this._packedIndexData[start + j] = j;
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
const uniformOffsetWords = (i * this._uniformAlignment) / Float32Array.BYTES_PER_ELEMENT;
|
|
9617
|
+
const tint = dc.mesh.tint;
|
|
9618
|
+
uniformF32[uniformOffsetWords + 0] = tint.red;
|
|
9619
|
+
uniformF32[uniformOffsetWords + 1] = tint.green;
|
|
9620
|
+
uniformF32[uniformOffsetWords + 2] = tint.blue;
|
|
9621
|
+
uniformF32[uniformOffsetWords + 3] = tint.alpha;
|
|
9622
|
+
uniformF32[uniformOffsetWords + 4] = dc.premultiplySample ? 1 : 0;
|
|
9623
|
+
uniformF32[uniformOffsetWords + 5] = 0;
|
|
9624
|
+
uniformF32[uniformOffsetWords + 6] = 0;
|
|
9625
|
+
uniformF32[uniformOffsetWords + 7] = 0;
|
|
9626
|
+
}
|
|
9627
|
+
// Phase 4: single writeBuffer per resource for the whole frame.
|
|
9628
|
+
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$1);
|
|
9629
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
9630
|
+
device.queue.writeBuffer(this._uniformBuffer, 0, uniformData, 0, uniformBytes);
|
|
9631
|
+
// Phase 5: single render pass with one drawIndexed per mesh.
|
|
8864
9632
|
const encoder = device.createCommandEncoder();
|
|
8865
9633
|
const pass = encoder.beginRenderPass({
|
|
8866
9634
|
colorAttachments: [backend.createColorAttachment()],
|
|
8867
9635
|
});
|
|
8868
9636
|
backend.stats.renderPasses++;
|
|
8869
|
-
|
|
8870
|
-
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
8871
|
-
if (scissor !== null && !maskClipsAll) {
|
|
9637
|
+
if (scissor !== null) {
|
|
8872
9638
|
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
8873
9639
|
}
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
9640
|
+
let lastBlendMode = null;
|
|
9641
|
+
let lastFormat = null;
|
|
9642
|
+
let lastTexture = null;
|
|
9643
|
+
const renderTargetFormat = backend.renderTargetFormat;
|
|
9644
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
9645
|
+
const dc = this._drawCalls[i];
|
|
9646
|
+
if (dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat) {
|
|
9647
|
+
lastBlendMode = dc.blendMode;
|
|
9648
|
+
lastFormat = renderTargetFormat;
|
|
9649
|
+
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
|
|
9650
|
+
}
|
|
9651
|
+
pass.setBindGroup(0, this._uniformBindGroup, [i * this._uniformAlignment]);
|
|
9652
|
+
if (dc.texture !== lastTexture) {
|
|
9653
|
+
lastTexture = dc.texture;
|
|
9654
|
+
pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
|
|
9655
|
+
}
|
|
9656
|
+
pass.setVertexBuffer(0, this._vertexBuffer, dc.vertexByteOffset);
|
|
9657
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16', dc.indexByteOffset);
|
|
9658
|
+
pass.drawIndexed(dc.indexCount);
|
|
8884
9659
|
backend.stats.batches++;
|
|
8885
9660
|
backend.stats.drawCalls++;
|
|
8886
9661
|
}
|
|
8887
|
-
pass.end();
|
|
8888
|
-
backend.submit(encoder.finish());
|
|
8889
|
-
this.
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
}
|
|
8896
|
-
/**
|
|
8897
|
-
* Pre-create render pipelines for every blend-mode × target-format
|
|
8898
|
-
* combination this renderer can produce, asynchronously and in
|
|
8899
|
-
* parallel. Called from the render manager's init path so by the time
|
|
8900
|
-
* the first frame draws, all pipelines exist in cache.
|
|
8901
|
-
*
|
|
8902
|
-
* Without prewarm, the first draw of any new (blendMode, format)
|
|
8903
|
-
* combination would fall back to the synchronous _getPipeline() path,
|
|
8904
|
-
* which blocks while the WebGPU implementation compiles WGSL and
|
|
8905
|
-
* sets up the pipeline state object — typically tens of milliseconds.
|
|
8906
|
-
*/
|
|
9662
|
+
pass.end();
|
|
9663
|
+
backend.submit(encoder.finish());
|
|
9664
|
+
this._drawCallCount = 0;
|
|
9665
|
+
}
|
|
9666
|
+
destroy() {
|
|
9667
|
+
this.disconnect();
|
|
9668
|
+
this._combinedTransform.destroy();
|
|
9669
|
+
}
|
|
8907
9670
|
async prewarmPipelines(formats) {
|
|
8908
9671
|
const device = this._device;
|
|
8909
9672
|
if (!device || !this._shaderModule || !this._pipelineLayout) {
|
|
@@ -8922,173 +9685,121 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
8922
9685
|
const promises = [];
|
|
8923
9686
|
for (const blendMode of blendModes) {
|
|
8924
9687
|
for (const format of formats) {
|
|
8925
|
-
const
|
|
8926
|
-
if (this._pipelines.has(
|
|
9688
|
+
const key = `${blendMode}:${format}`;
|
|
9689
|
+
if (this._pipelines.has(key))
|
|
8927
9690
|
continue;
|
|
8928
|
-
|
|
8929
|
-
const promise = device
|
|
8930
|
-
.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format))
|
|
9691
|
+
promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format))
|
|
8931
9692
|
.then((pipeline) => {
|
|
8932
|
-
this._pipelines.set(
|
|
8933
|
-
});
|
|
8934
|
-
promises.push(promise);
|
|
9693
|
+
this._pipelines.set(key, pipeline);
|
|
9694
|
+
}));
|
|
8935
9695
|
}
|
|
8936
9696
|
}
|
|
8937
9697
|
await Promise.all(promises);
|
|
8938
9698
|
}
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
const f32 = this._instanceFloat32;
|
|
8942
|
-
const u32 = this._instanceUint32;
|
|
8943
|
-
const bounds = sprite.getLocalBounds();
|
|
8944
|
-
f32[offset + 0] = bounds.left;
|
|
8945
|
-
f32[offset + 1] = bounds.top;
|
|
8946
|
-
f32[offset + 2] = bounds.right;
|
|
8947
|
-
f32[offset + 3] = bounds.bottom;
|
|
8948
|
-
const transform = sprite.getGlobalTransform();
|
|
8949
|
-
f32[offset + 4] = transform.a;
|
|
8950
|
-
f32[offset + 5] = transform.b;
|
|
8951
|
-
f32[offset + 6] = transform.x;
|
|
8952
|
-
f32[offset + 7] = transform.c;
|
|
8953
|
-
f32[offset + 8] = transform.d;
|
|
8954
|
-
f32[offset + 9] = transform.y;
|
|
8955
|
-
// uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
|
|
8956
|
-
// applies the flipY swap so the shader stays orientation-agnostic.
|
|
8957
|
-
const frame = sprite.textureFrame;
|
|
8958
|
-
const texWidth = texture.width;
|
|
8959
|
-
const texHeight = texture.height;
|
|
8960
|
-
const uMin = ((frame.left / texWidth) * 0xFFFF) & 0xFFFF;
|
|
8961
|
-
const uMax = ((frame.right / texWidth) * 0xFFFF) & 0xFFFF;
|
|
8962
|
-
const vMinRaw = ((frame.top / texHeight) * 0xFFFF) & 0xFFFF;
|
|
8963
|
-
const vMaxRaw = ((frame.bottom / texHeight) * 0xFFFF) & 0xFFFF;
|
|
8964
|
-
const flipY = texture instanceof Texture && texture.flipY;
|
|
8965
|
-
const vMin = flipY ? vMaxRaw : vMinRaw;
|
|
8966
|
-
const vMax = flipY ? vMinRaw : vMaxRaw;
|
|
8967
|
-
u32[offset + 10] = uMin | (vMin << 16);
|
|
8968
|
-
u32[offset + 11] = uMax | (vMax << 16);
|
|
8969
|
-
u32[offset + 12] = sprite.tint.toRgba();
|
|
8970
|
-
u32[offset + 13] = packedSlotFlags;
|
|
8971
|
-
}
|
|
8972
|
-
_ensureInstanceCapacity(instanceCount) {
|
|
8973
|
-
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
9699
|
+
onConnect(backend) {
|
|
9700
|
+
if (this._device) {
|
|
8974
9701
|
return;
|
|
8975
9702
|
}
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
9703
|
+
this._device = backend.device;
|
|
9704
|
+
this._shaderModule = this._device.createShaderModule({ code: meshShaderSource });
|
|
9705
|
+
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
9706
|
+
entries: [{
|
|
9707
|
+
binding: 0,
|
|
9708
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
9709
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
9710
|
+
}],
|
|
9711
|
+
});
|
|
9712
|
+
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
9713
|
+
entries: [
|
|
9714
|
+
{
|
|
9715
|
+
binding: 0,
|
|
9716
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
9717
|
+
texture: { sampleType: 'float' },
|
|
9718
|
+
},
|
|
9719
|
+
{
|
|
9720
|
+
binding: 1,
|
|
9721
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
9722
|
+
sampler: { type: 'filtering' },
|
|
9723
|
+
},
|
|
9724
|
+
],
|
|
9725
|
+
});
|
|
9726
|
+
this._pipelineLayout = this._device.createPipelineLayout({
|
|
9727
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
8993
9728
|
});
|
|
8994
|
-
this._instanceBuffer?.destroy();
|
|
8995
|
-
this._instanceCapacity = nextCapacity;
|
|
8996
|
-
this._instanceData = instanceData;
|
|
8997
|
-
this._instanceFloat32 = new Float32Array(instanceData);
|
|
8998
|
-
this._instanceUint32 = new Uint32Array(instanceData);
|
|
8999
|
-
this._instanceBuffer = instanceBuffer;
|
|
9000
9729
|
}
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9730
|
+
onDisconnect() {
|
|
9731
|
+
this.flush();
|
|
9732
|
+
this._vertexBuffer?.destroy();
|
|
9733
|
+
this._indexBuffer?.destroy();
|
|
9734
|
+
this._uniformBuffer?.destroy();
|
|
9735
|
+
this._pipelines.clear();
|
|
9736
|
+
this._textureBindGroups.clear();
|
|
9737
|
+
this._vertexBuffer = null;
|
|
9738
|
+
this._indexBuffer = null;
|
|
9739
|
+
this._uniformBuffer = null;
|
|
9740
|
+
this._uniformBindGroup = null;
|
|
9741
|
+
this._pipelineLayout = null;
|
|
9742
|
+
this._textureBindGroupLayout = null;
|
|
9743
|
+
this._uniformBindGroupLayout = null;
|
|
9744
|
+
this._shaderModule = null;
|
|
9745
|
+
this._device = null;
|
|
9746
|
+
this._backend = null;
|
|
9747
|
+
this._drawCallCount = 0;
|
|
9748
|
+
this._vertexBufferCapacity = 0;
|
|
9749
|
+
this._indexBufferCapacity = 0;
|
|
9750
|
+
this._uniformBufferCapacity = 0;
|
|
9009
9751
|
}
|
|
9010
|
-
|
|
9011
|
-
//
|
|
9012
|
-
// the
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
const
|
|
9017
|
-
const
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
const
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9752
|
+
_writeMeshVertices(backend, mesh, vertexStart) {
|
|
9753
|
+
// Bake (view * globalTransform) into vertex positions on the CPU,
|
|
9754
|
+
// matching the primitive renderer's no-uniforms approach.
|
|
9755
|
+
const matrix = this._combinedTransform
|
|
9756
|
+
.copy(mesh.getGlobalTransform())
|
|
9757
|
+
.combine(backend.view.getTransform());
|
|
9758
|
+
const a = matrix.a;
|
|
9759
|
+
const b = matrix.b;
|
|
9760
|
+
const c = matrix.c;
|
|
9761
|
+
const d = matrix.d;
|
|
9762
|
+
const tx = matrix.x;
|
|
9763
|
+
const ty = matrix.y;
|
|
9764
|
+
const vertices = mesh.vertices;
|
|
9765
|
+
const uvs = mesh.uvs;
|
|
9766
|
+
const colors = mesh.colors;
|
|
9767
|
+
const vertexCount = mesh.vertexCount;
|
|
9768
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
9769
|
+
const sourceIndex = i * 2;
|
|
9770
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex;
|
|
9771
|
+
const px = vertices[sourceIndex];
|
|
9772
|
+
const py = vertices[sourceIndex + 1];
|
|
9773
|
+
this._float32View[targetIndex + 0] = a * px + b * py + tx;
|
|
9774
|
+
this._float32View[targetIndex + 1] = c * px + d * py + ty;
|
|
9775
|
+
this._float32View[targetIndex + 2] = uvs !== null ? uvs[sourceIndex] : 0;
|
|
9776
|
+
this._float32View[targetIndex + 3] = uvs !== null ? uvs[sourceIndex + 1] : 0;
|
|
9777
|
+
this._uint32View[targetIndex + 4] = colors !== null ? colors[i] : 0xFFFFFFFF;
|
|
9036
9778
|
}
|
|
9037
|
-
return device.createBindGroup({
|
|
9038
|
-
layout: this._textureBindGroupLayout,
|
|
9039
|
-
entries,
|
|
9040
|
-
});
|
|
9041
9779
|
}
|
|
9042
|
-
_getPipeline(
|
|
9043
|
-
const
|
|
9044
|
-
|
|
9045
|
-
if (
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
|
|
9049
|
-
throw new Error('Renderer has to be connected first!');
|
|
9780
|
+
_getPipeline(key) {
|
|
9781
|
+
const cacheKey = `${key.blendMode}:${key.format}`;
|
|
9782
|
+
let pipeline = this._pipelines.get(cacheKey);
|
|
9783
|
+
if (!pipeline) {
|
|
9784
|
+
pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format));
|
|
9785
|
+
this._pipelines.set(cacheKey, pipeline);
|
|
9050
9786
|
}
|
|
9051
|
-
const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
|
|
9052
|
-
this._pipelines.set(pipelineKey, pipeline);
|
|
9053
9787
|
return pipeline;
|
|
9054
9788
|
}
|
|
9055
9789
|
_buildPipelineDescriptor(blendMode, format) {
|
|
9056
|
-
if (!this._shaderModule || !this._pipelineLayout) {
|
|
9057
|
-
throw new Error('Renderer has to be connected first!');
|
|
9058
|
-
}
|
|
9059
9790
|
return {
|
|
9060
9791
|
layout: this._pipelineLayout,
|
|
9061
9792
|
vertex: {
|
|
9062
9793
|
module: this._shaderModule,
|
|
9063
9794
|
entryPoint: 'vertexMain',
|
|
9064
9795
|
buffers: [{
|
|
9065
|
-
arrayStride:
|
|
9066
|
-
stepMode: '
|
|
9067
|
-
attributes: [
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
shaderLocation: 1,
|
|
9073
|
-
offset: 16,
|
|
9074
|
-
format: 'float32x3',
|
|
9075
|
-
}, {
|
|
9076
|
-
shaderLocation: 2,
|
|
9077
|
-
offset: 28,
|
|
9078
|
-
format: 'float32x3',
|
|
9079
|
-
}, {
|
|
9080
|
-
shaderLocation: 3,
|
|
9081
|
-
offset: 40,
|
|
9082
|
-
format: 'unorm16x4',
|
|
9083
|
-
}, {
|
|
9084
|
-
shaderLocation: 4,
|
|
9085
|
-
offset: 48,
|
|
9086
|
-
format: 'unorm8x4',
|
|
9087
|
-
}, {
|
|
9088
|
-
shaderLocation: 5,
|
|
9089
|
-
offset: 52,
|
|
9090
|
-
format: 'uint32',
|
|
9091
|
-
}],
|
|
9796
|
+
arrayStride: vertexStrideBytes$1,
|
|
9797
|
+
stepMode: 'vertex',
|
|
9798
|
+
attributes: [
|
|
9799
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
|
|
9800
|
+
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
|
|
9801
|
+
{ shaderLocation: 2, offset: 16, format: 'unorm8x4' },
|
|
9802
|
+
],
|
|
9092
9803
|
}],
|
|
9093
9804
|
},
|
|
9094
9805
|
fragment: {
|
|
@@ -9102,9 +9813,74 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
9102
9813
|
},
|
|
9103
9814
|
primitive: {
|
|
9104
9815
|
topology: 'triangle-list',
|
|
9816
|
+
cullMode: 'none',
|
|
9105
9817
|
},
|
|
9106
9818
|
};
|
|
9107
9819
|
}
|
|
9820
|
+
_getTextureBindGroup(backend, texture) {
|
|
9821
|
+
let group = this._textureBindGroups.get(texture);
|
|
9822
|
+
if (!group) {
|
|
9823
|
+
const binding = backend.getTextureBinding(texture);
|
|
9824
|
+
group = this._device.createBindGroup({
|
|
9825
|
+
layout: this._textureBindGroupLayout,
|
|
9826
|
+
entries: [
|
|
9827
|
+
{ binding: 0, resource: binding.view },
|
|
9828
|
+
{ binding: 1, resource: binding.sampler },
|
|
9829
|
+
],
|
|
9830
|
+
});
|
|
9831
|
+
this._textureBindGroups.set(texture, group);
|
|
9832
|
+
}
|
|
9833
|
+
return group;
|
|
9834
|
+
}
|
|
9835
|
+
_ensureVertexCapacity(vertexCount) {
|
|
9836
|
+
const requiredBytes = vertexCount * vertexStrideBytes$1;
|
|
9837
|
+
if (requiredBytes > this._vertexData.byteLength) {
|
|
9838
|
+
const byteLength = Math.max(requiredBytes, this._vertexData.byteLength === 0 ? vertexStrideBytes$1 : this._vertexData.byteLength * 2);
|
|
9839
|
+
this._vertexData = new ArrayBuffer(byteLength);
|
|
9840
|
+
this._float32View = new Float32Array(this._vertexData);
|
|
9841
|
+
this._uint32View = new Uint32Array(this._vertexData);
|
|
9842
|
+
}
|
|
9843
|
+
if (requiredBytes > this._vertexBufferCapacity) {
|
|
9844
|
+
this._vertexBuffer?.destroy();
|
|
9845
|
+
this._vertexBufferCapacity = Math.max(requiredBytes, this._vertexBufferCapacity === 0 ? vertexStrideBytes$1 : this._vertexBufferCapacity * 2);
|
|
9846
|
+
this._vertexBuffer = this._device.createBuffer({
|
|
9847
|
+
size: this._vertexBufferCapacity,
|
|
9848
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
9849
|
+
});
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
_ensureIndexCapacity(indexCount) {
|
|
9853
|
+
const requiredBytes = indexCount * Uint16Array.BYTES_PER_ELEMENT;
|
|
9854
|
+
if (this._packedIndexData.length < indexCount) {
|
|
9855
|
+
this._packedIndexData = new Uint16Array(Math.max(indexCount, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
|
|
9856
|
+
}
|
|
9857
|
+
if (requiredBytes > this._indexBufferCapacity) {
|
|
9858
|
+
this._indexBuffer?.destroy();
|
|
9859
|
+
this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
|
|
9860
|
+
this._indexBuffer = this._device.createBuffer({
|
|
9861
|
+
size: this._indexBufferCapacity,
|
|
9862
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
9863
|
+
});
|
|
9864
|
+
}
|
|
9865
|
+
}
|
|
9866
|
+
_ensureUniformCapacity(drawCallCount) {
|
|
9867
|
+
const requiredBytes = drawCallCount * this._uniformAlignment;
|
|
9868
|
+
if (requiredBytes > this._uniformBufferCapacity) {
|
|
9869
|
+
this._uniformBuffer?.destroy();
|
|
9870
|
+
this._uniformBufferCapacity = Math.max(requiredBytes, this._uniformBufferCapacity === 0 ? this._uniformAlignment : this._uniformBufferCapacity * 2);
|
|
9871
|
+
this._uniformBuffer = this._device.createBuffer({
|
|
9872
|
+
size: this._uniformBufferCapacity,
|
|
9873
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9874
|
+
});
|
|
9875
|
+
this._uniformBindGroup = this._device.createBindGroup({
|
|
9876
|
+
layout: this._uniformBindGroupLayout,
|
|
9877
|
+
entries: [{
|
|
9878
|
+
binding: 0,
|
|
9879
|
+
resource: { buffer: this._uniformBuffer, size: tintByteLength },
|
|
9880
|
+
}],
|
|
9881
|
+
});
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9108
9884
|
}
|
|
9109
9885
|
|
|
9110
9886
|
/// <reference types="@webgpu/types" />
|
|
@@ -9844,6 +10620,7 @@ class WebGpuBackend {
|
|
|
9844
10620
|
}
|
|
9845
10621
|
this.rendererRegistry.registerRenderer(DrawableShape, new WebGpuPrimitiveRenderer());
|
|
9846
10622
|
this.rendererRegistry.registerRenderer(Sprite, new WebGpuSpriteRenderer());
|
|
10623
|
+
this.rendererRegistry.registerRenderer(Mesh, new WebGpuMeshRenderer());
|
|
9847
10624
|
this.rendererRegistry.registerRenderer(ParticleSystem, new WebGpuParticleRenderer());
|
|
9848
10625
|
this.resize(width, height);
|
|
9849
10626
|
}
|
|
@@ -18208,5 +18985,5 @@ const createRapierPhysicsWorld = async (options = {}) => {
|
|
|
18208
18985
|
return await RapierPhysicsWorld.create(options);
|
|
18209
18986
|
};
|
|
18210
18987
|
|
|
18211
|
-
export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, ChannelOffset, ChannelSize, Circle, CircleGeometry, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DrawableShape, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Geometry, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, RapierPhysicsBinding, RapierPhysicsWorld, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2ParticleRenderer, WebGl2PrimitiveRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuParticleRenderer, WebGpuPrimitiveRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRapierPhysicsWorld, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, lerp, matchesIds, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign$1 as sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
|
|
18988
|
+
export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, ChannelOffset, ChannelSize, Circle, CircleGeometry, Clock, CollisionType, Color, ColorAffector, ColorFilter, Container, Drawable, DrawableShape, Ellipse, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Geometry, Graphics, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, Particle, ParticleOptions, ParticleSystem, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, RapierPhysicsBinding, RapierPhysicsWorld, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2PrimitiveRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuPrimitiveRenderer, WebGpuSpriteRenderer, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRapierPhysicsWorld, createRenderStats, createWebGl2ShaderProgram, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, lerp, matchesIds, milliseconds, minutes, noop$1 as noop, normalizeIds, onAudioContextReady, parseGamepadDescriptor, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign$1 as sign, stopEvent, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, tau, trimRotation, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
|
|
18212
18989
|
//# sourceMappingURL=exo.esm.js.map
|