@codexo/exojs-tilemap 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +33 -26
  2. package/dist/esm/ChunkSource.d.ts +33 -0
  3. package/dist/esm/ChunkSource.d.ts.map +1 -0
  4. package/dist/esm/ChunkStreamer.d.ts +98 -0
  5. package/dist/esm/ChunkStreamer.d.ts.map +1 -0
  6. package/dist/esm/ChunkStreamer.js +201 -0
  7. package/dist/esm/ChunkStreamer.js.map +1 -0
  8. package/dist/esm/ImageLayer.d.ts +11 -3
  9. package/dist/esm/ImageLayer.d.ts.map +1 -0
  10. package/dist/esm/ImageLayer.js +71 -64
  11. package/dist/esm/ImageLayer.js.map +1 -1
  12. package/dist/esm/ImageLayerNode.d.ts +89 -0
  13. package/dist/esm/ImageLayerNode.d.ts.map +1 -0
  14. package/dist/esm/ImageLayerNode.js +205 -0
  15. package/dist/esm/ImageLayerNode.js.map +1 -0
  16. package/dist/esm/MapObject.d.ts +70 -0
  17. package/dist/esm/MapObject.d.ts.map +1 -0
  18. package/dist/esm/MapObject.js +34 -0
  19. package/dist/esm/MapObject.js.map +1 -0
  20. package/dist/esm/MapObjectSpawner.d.ts +133 -0
  21. package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
  22. package/dist/esm/MapObjectSpawner.js +147 -0
  23. package/dist/esm/MapObjectSpawner.js.map +1 -0
  24. package/dist/esm/MapSpawnSession.d.ts +53 -0
  25. package/dist/esm/MapSpawnSession.d.ts.map +1 -0
  26. package/dist/esm/MapSpawnSession.js +88 -0
  27. package/dist/esm/MapSpawnSession.js.map +1 -0
  28. package/dist/esm/MapWorld.d.ts +134 -0
  29. package/dist/esm/MapWorld.d.ts.map +1 -0
  30. package/dist/esm/MapWorld.js +150 -0
  31. package/dist/esm/MapWorld.js.map +1 -0
  32. package/dist/esm/MapWorldRuntime.d.ts +182 -0
  33. package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
  34. package/dist/esm/MapWorldRuntime.js +261 -0
  35. package/dist/esm/MapWorldRuntime.js.map +1 -0
  36. package/dist/esm/ObjectLayer.d.ts +93 -7
  37. package/dist/esm/ObjectLayer.d.ts.map +1 -0
  38. package/dist/esm/ObjectLayer.js +184 -167
  39. package/dist/esm/ObjectLayer.js.map +1 -1
  40. package/dist/esm/SampledChunkSource.d.ts +43 -0
  41. package/dist/esm/SampledChunkSource.d.ts.map +1 -0
  42. package/dist/esm/SampledChunkSource.js +48 -0
  43. package/dist/esm/SampledChunkSource.js.map +1 -0
  44. package/dist/esm/TileAnimator.d.ts +12 -2
  45. package/dist/esm/TileAnimator.d.ts.map +1 -0
  46. package/dist/esm/TileAnimator.js +154 -165
  47. package/dist/esm/TileAnimator.js.map +1 -1
  48. package/dist/esm/TileChunk.d.ts +30 -5
  49. package/dist/esm/TileChunk.d.ts.map +1 -0
  50. package/dist/esm/TileChunk.js +190 -188
  51. package/dist/esm/TileChunk.js.map +1 -1
  52. package/dist/esm/TileChunkNode.d.ts +18 -3
  53. package/dist/esm/TileChunkNode.d.ts.map +1 -0
  54. package/dist/esm/TileChunkNode.js +104 -85
  55. package/dist/esm/TileChunkNode.js.map +1 -1
  56. package/dist/esm/TileLayer.d.ts +104 -24
  57. package/dist/esm/TileLayer.d.ts.map +1 -0
  58. package/dist/esm/TileLayer.js +511 -469
  59. package/dist/esm/TileLayer.js.map +1 -1
  60. package/dist/esm/TileLayerNode.d.ts +52 -11
  61. package/dist/esm/TileLayerNode.d.ts.map +1 -0
  62. package/dist/esm/TileLayerNode.js +224 -166
  63. package/dist/esm/TileLayerNode.js.map +1 -1
  64. package/dist/esm/TileMap.d.ts +96 -31
  65. package/dist/esm/TileMap.d.ts.map +1 -0
  66. package/dist/esm/TileMap.js +401 -357
  67. package/dist/esm/TileMap.js.map +1 -1
  68. package/dist/esm/TileMapBand.d.ts +42 -29
  69. package/dist/esm/TileMapBand.d.ts.map +1 -0
  70. package/dist/esm/TileMapBand.js +151 -158
  71. package/dist/esm/TileMapBand.js.map +1 -1
  72. package/dist/esm/TileMapNode.d.ts +34 -18
  73. package/dist/esm/TileMapNode.d.ts.map +1 -0
  74. package/dist/esm/TileMapNode.js +122 -109
  75. package/dist/esm/TileMapNode.js.map +1 -1
  76. package/dist/esm/TileMapView.d.ts +108 -47
  77. package/dist/esm/TileMapView.d.ts.map +1 -0
  78. package/dist/esm/TileMapView.js +385 -338
  79. package/dist/esm/TileMapView.js.map +1 -1
  80. package/dist/esm/TileSet.d.ts +15 -6
  81. package/dist/esm/TileSet.d.ts.map +1 -0
  82. package/dist/esm/TileSet.js +161 -181
  83. package/dist/esm/TileSet.js.map +1 -1
  84. package/dist/esm/WangSet.d.ts +5 -4
  85. package/dist/esm/WangSet.d.ts.map +1 -0
  86. package/dist/esm/WangSet.js +70 -78
  87. package/dist/esm/WangSet.js.map +1 -1
  88. package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
  89. package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
  90. package/dist/esm/WorkerSampledChunkSource.js +117 -0
  91. package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
  92. package/dist/esm/autoTile.d.ts +10 -5
  93. package/dist/esm/autoTile.d.ts.map +1 -0
  94. package/dist/esm/autoTile.js +164 -205
  95. package/dist/esm/autoTile.js.map +1 -1
  96. package/dist/esm/chunkGeometry.d.ts +8 -7
  97. package/dist/esm/chunkGeometry.d.ts.map +1 -0
  98. package/dist/esm/chunkGeometry.js +83 -94
  99. package/dist/esm/chunkGeometry.js.map +1 -1
  100. package/dist/esm/index.d.ts +1 -0
  101. package/dist/esm/index.d.ts.map +1 -0
  102. package/dist/esm/index.js +26 -15
  103. package/dist/esm/nodeBounds.d.ts +20 -0
  104. package/dist/esm/nodeBounds.d.ts.map +1 -0
  105. package/dist/esm/nodeBounds.js +40 -0
  106. package/dist/esm/nodeBounds.js.map +1 -0
  107. package/dist/esm/pixelSnap.d.ts +4 -3
  108. package/dist/esm/pixelSnap.d.ts.map +1 -0
  109. package/dist/esm/pixelSnap.js +18 -16
  110. package/dist/esm/pixelSnap.js.map +1 -1
  111. package/dist/esm/public.d.ts +23 -3
  112. package/dist/esm/public.d.ts.map +1 -0
  113. package/dist/esm/tileCollision.d.ts +190 -0
  114. package/dist/esm/tileCollision.d.ts.map +1 -0
  115. package/dist/esm/tileCollision.js +367 -0
  116. package/dist/esm/tileCollision.js.map +1 -0
  117. package/dist/esm/tileWord.d.ts +12 -0
  118. package/dist/esm/tileWord.d.ts.map +1 -0
  119. package/dist/esm/tileWord.js +16 -0
  120. package/dist/esm/tileWord.js.map +1 -0
  121. package/dist/esm/tilemapExtension.d.ts +4 -3
  122. package/dist/esm/tilemapExtension.d.ts.map +1 -0
  123. package/dist/esm/tilemapExtension.js +47 -47
  124. package/dist/esm/tilemapExtension.js.map +1 -1
  125. package/dist/esm/tilemapSerializers.d.ts +5 -4
  126. package/dist/esm/tilemapSerializers.d.ts.map +1 -0
  127. package/dist/esm/tilemapSerializers.js +48 -41
  128. package/dist/esm/tilemapSerializers.js.map +1 -1
  129. package/dist/esm/types.d.ts +34 -18
  130. package/dist/esm/types.d.ts.map +1 -0
  131. package/dist/esm/types.js +111 -120
  132. package/dist/esm/types.js.map +1 -1
  133. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
  134. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
  135. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
  136. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
  137. package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
  138. package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
  139. package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
  140. package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
  141. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
  142. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
  143. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
  144. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
  145. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
  146. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
  147. package/package.json +8 -15
  148. package/dist/esm/index.js.map +0 -1
  149. package/dist/esm/register.d.ts +0 -1
  150. package/dist/esm/register.js +0 -25
  151. package/dist/esm/register.js.map +0 -1
@@ -1,339 +1,373 @@
1
- import { AbstractWebGl2Renderer, Shader, RenderingPrimitives, createWebGl2ShaderProgram, WebGl2RenderBuffer, BufferTypes, BufferUsage, WebGl2VertexArrayObject } from '@codexo/exojs/renderer-sdk';
1
+ import { TILE_DIAGONAL_BIT, TILE_ROW_MASK } from "../tileWord.js";
2
+ import tile_chunk_default from "./shaders/tile-chunk.frag.js";
3
+ import tile_chunk_default$1 from "./shaders/tile-chunk.vert.js";
4
+ import { AbstractWebGl2Renderer, BlendModes, BufferTypes, BufferUsage, RenderingPrimitives, Shader, WebGl2RenderBuffer, WebGl2VertexArrayObject, createWebGl2ShaderProgram, fillShaderSource, packedGroupChanged, uploadBufferRange, uploadBufferStore } from "@codexo/exojs/renderer-sdk";
2
5
 
3
- // One instance = one tile quad. Layout matches the engine's instanced-quad
4
- // convention (NineSlice/Repeating): float32x4 local rect, unorm16x4 UV bounds,
5
- // unorm8x4 tint, uint32 tile word (transform row + diagonal bit).
6
+ //#region src/webgl2/WebGl2TileChunkRenderer.ts
6
7
  const instanceStrideBytes = 32;
7
8
  const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
8
9
  const transformTextureUnit = 1;
9
- // Tile word packing: transform-buffer row in the low 29 bits, diagonal flip in
10
- // bit 29. flipX/flipY are baked into the UV bounds at write time; the shader
11
- // only needs the diagonal axis swap.
12
- const TILE_ROW_MASK = 0x1fffffff;
13
- const TILE_DIAGONAL_BIT = 0x20000000;
14
- const tileVertexSource = `#version 300 es
15
- precision highp float;
16
- precision highp int;
17
-
18
- // Per-instance attributes (divisor = 1). One entry per tile quad.
19
- // gl_VertexID 0..3 selects which corner of the quad this invocation computes.
20
- layout(location = 0) in vec4 a_quadBounds; // x0, y0, x1, y1 (chunk-local)
21
- layout(location = 1) in vec4 a_uvBounds; // uMin, vMin, uMax, vMax (flipX/Y + texture flipY baked)
22
- layout(location = 2) in vec4 a_color; // RGBA tint (layer opacity in alpha)
23
- layout(location = 3) in uint a_tileWord; // transform row (bits 0..28) | diagonal (bit 29)
24
-
25
- uniform mat3 u_projection;
26
- uniform sampler2D u_transforms; // shared per-frame transform buffer (2 texels/row)
27
-
28
- out vec2 v_texcoord;
29
- out vec4 v_color;
30
-
31
- void main(void) {
32
- // gl_VertexID 0..3 -> corner: 0=TL, 1=TR, 2=BL, 3=BR (TRIANGLE_STRIP order)
33
- int vid = gl_VertexID;
34
- int cornerX = vid & 1;
35
- int cornerY = (vid >> 1) & 1;
36
-
37
- float localX = (cornerX == 0) ? a_quadBounds.x : a_quadBounds.z;
38
- float localY = (cornerY == 0) ? a_quadBounds.y : a_quadBounds.w;
39
-
40
- int row = int(a_tileWord & ${TILE_ROW_MASK}u);
41
- bool diagonal = (a_tileWord & ${TILE_DIAGONAL_BIT}u) != 0u;
42
-
43
- vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0); // a, b, c, d
44
- vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0); // tx, ty, 0, 0
45
-
46
- float worldX = (m0.x * localX) + (m0.y * localY) + m1.x;
47
- float worldY = (m0.z * localX) + (m0.w * localY) + m1.y;
48
-
49
- gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);
50
-
51
- // Tile orientation: the diagonal flip transposes the corner-coordinate axes
52
- // before the UV corner is selected; flipX/flipY are already baked into the
53
- // (uMin,uMax)/(vMin,vMax) ordering by the CPU writer.
54
- int su = cornerX;
55
- int sv = cornerY;
56
- if (diagonal) { int t = su; su = sv; sv = t; }
57
-
58
- float u = (su == 0) ? a_uvBounds.x : a_uvBounds.z;
59
- float v = (sv == 0) ? a_uvBounds.y : a_uvBounds.w;
60
- v_texcoord = vec2(u, v);
61
-
62
- v_color = vec4(a_color.rgb * a_color.a, a_color.a);
63
- }`;
64
- const tileFragmentSource = `#version 300 es
65
- precision highp float;
66
-
67
- uniform sampler2D u_texture;
68
-
69
- in vec2 v_texcoord;
70
- in vec4 v_color;
71
-
72
- layout(location = 0) out vec4 fragColor;
73
-
74
- void main(void) {
75
- vec4 sampleColor = texture(u_texture, v_texcoord);
76
- fragColor = sampleColor * v_color;
77
- }`;
10
+ const identityGroupMat3 = new Float32Array([
11
+ 1,
12
+ 0,
13
+ 0,
14
+ 0,
15
+ 1,
16
+ 0,
17
+ 0,
18
+ 0,
19
+ 1
20
+ ]);
21
+ const tileVertexSource = fillShaderSource(tile_chunk_default$1, {
22
+ tileRowMask: TILE_ROW_MASK,
23
+ tileDiagonalBit: TILE_DIAGONAL_BIT
24
+ });
78
25
  /**
79
- * Instanced WebGL2 renderer for {@link TileChunkNode}. Each tile is one
80
- * instanced quad; tiles are batched by `(shader, tileset texture)` and one
81
- * `drawArraysInstanced` is issued per batch. Per-chunk transforms ride on the
82
- * shared transform buffer (one row per chunk node), so the chunk geometry is
83
- * orientation-neutral and never re-uploaded for a camera pan.
84
- * @internal
85
- */
86
- class WebGl2TileChunkRenderer extends AbstractWebGl2Renderer {
87
- _shader;
88
- _batchSize;
89
- _instanceData;
90
- _instanceFloat32;
91
- _instanceUint32;
92
- _transformUnitScratch = new Int32Array([transformTextureUnit]);
93
- _quadIndex = 0;
94
- _maxNodeIndex = 0;
95
- _currentBlendMode = null;
96
- _currentTexture = null;
97
- _currentView = null;
98
- _currentViewId = -1;
99
- _instanceBuffer = null;
100
- _vao = null;
101
- _connection = null;
102
- constructor(batchSize) {
103
- super();
104
- this._batchSize = batchSize;
105
- this._shader = new Shader(tileVertexSource, tileFragmentSource);
106
- this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes);
107
- this._instanceFloat32 = new Float32Array(this._instanceData);
108
- this._instanceUint32 = new Uint32Array(this._instanceData);
109
- }
110
- render(node) {
111
- const pages = node.pages;
112
- if (pages.length === 0) {
113
- return;
114
- }
115
- const backend = this.getBackend();
116
- const blendMode = node.blendMode;
117
- const tintRgba = node.tint.toRgba();
118
- const command = backend.activeDrawCommand;
119
- const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(node);
120
- for (const page of pages) {
121
- this._renderPage(backend, page.texture, page.quads, blendMode, tintRgba, nodeIndex);
122
- }
123
- }
124
- _renderPage(backend, texture, quads, blendMode, tintRgba, nodeIndex) {
125
- if (quads.length === 0) {
126
- return;
127
- }
128
- const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
129
- const blendModeChanged = blendMode !== this._currentBlendMode;
130
- if (this._quadIndex > 0 && (blendModeChanged || textureChanged || this._quadIndex + quads.length > this._batchSize)) {
131
- this.flush();
132
- }
133
- if (this._currentBlendMode === null || this._currentBlendMode !== blendMode) {
134
- this._currentBlendMode = blendMode;
135
- backend.setBlendMode(blendMode);
136
- }
137
- if (this._currentTexture !== texture) {
138
- this._currentTexture = texture;
139
- backend.bindTexture(texture, 0);
140
- }
141
- const flipY = texture.flipY;
142
- // A chunk page may hold more tiles than the fixed batch buffer; write in
143
- // batch-sized runs, flushing (and re-establishing state) between runs.
144
- let offset = 0;
145
- while (offset < quads.length) {
146
- const remaining = quads.length - offset;
147
- const runSize = Math.min(remaining, this._batchSize);
148
- this._writeRun(quads, offset, runSize, flipY, tintRgba, nodeIndex);
149
- offset += runSize;
150
- if (offset < quads.length) {
151
- this.flush();
152
- this._currentBlendMode = blendMode;
153
- backend.setBlendMode(blendMode);
154
- this._currentTexture = texture;
155
- backend.bindTexture(texture, 0);
156
- }
157
- }
158
- }
159
- _writeRun(quads, offset, count, flipY, tintRgba, nodeIndex) {
160
- const f32 = this._instanceFloat32;
161
- const u32 = this._instanceUint32;
162
- const baseWord = nodeIndex & TILE_ROW_MASK;
163
- for (let i = 0; i < count; i++) {
164
- const q = quads[offset + i];
165
- const idx = this._quadIndex * wordsPerInstance;
166
- f32[idx + 0] = q.x0;
167
- f32[idx + 1] = q.y0;
168
- f32[idx + 2] = q.x1;
169
- f32[idx + 3] = q.y1;
170
- // Bake flipX/flipY into the UV corner ordering; the diagonal axis swap is
171
- // resolved in the shader. Texture flipY (uploaded-flipped atlases) is an
172
- // additional vertical swap that composes with the tile flipY.
173
- const flipX = (q.orient & 1) !== 0;
174
- const tileFlipY = (q.orient & 2) !== 0;
175
- const diagonal = (q.orient & 4) !== 0;
176
- const uA = flipX ? q.u1 : q.u0;
177
- const uB = flipX ? q.u0 : q.u1;
178
- let vA = tileFlipY ? q.v1 : q.v0;
179
- let vB = tileFlipY ? q.v0 : q.v1;
180
- if (flipY) {
181
- const swap = vA;
182
- vA = vB;
183
- vB = swap;
184
- }
185
- const uMin = (uA * 0xffff) & 0xffff;
186
- const vMin = (vA * 0xffff) & 0xffff;
187
- const uMax = (uB * 0xffff) & 0xffff;
188
- const vMax = (vB * 0xffff) & 0xffff;
189
- u32[idx + 4] = uMin | (vMin << 16);
190
- u32[idx + 5] = uMax | (vMax << 16);
191
- u32[idx + 6] = tintRgba;
192
- u32[idx + 7] = (diagonal ? baseWord | TILE_DIAGONAL_BIT : baseWord) >>> 0;
193
- this._quadIndex++;
194
- }
195
- if (nodeIndex > this._maxNodeIndex) {
196
- this._maxNodeIndex = nodeIndex;
197
- }
198
- }
199
- flush() {
200
- const backend = this.getBackendOrNull();
201
- const instanceBuffer = this._instanceBuffer;
202
- const vao = this._vao;
203
- if (this._quadIndex === 0 || backend === null || instanceBuffer === null || vao === null) {
204
- this._quadIndex = 0;
205
- this._maxNodeIndex = 0;
206
- return;
207
- }
208
- const view = backend.view;
209
- if (this._currentView !== view || this._currentViewId !== view.updateId) {
210
- this._currentView = view;
211
- this._currentViewId = view.updateId;
212
- this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
213
- }
214
- if (this._currentTexture !== null) {
215
- this._shader.getUniform('u_texture').setValue(new Int32Array([0]));
216
- }
217
- backend.bindTransformBufferTexture(transformTextureUnit, this._maxNodeIndex + 1);
218
- this._shader.getUniform('u_transforms').setValue(this._transformUnitScratch);
219
- this._shader.sync();
220
- backend.bindVertexArrayObject(vao);
221
- instanceBuffer.upload(this._instanceFloat32.subarray(0, this._quadIndex * wordsPerInstance));
222
- vao.drawInstanced(4, 0, this._quadIndex, RenderingPrimitives.TriangleStrip);
223
- backend.stats.batches++;
224
- backend.stats.drawCalls++;
225
- this._quadIndex = 0;
226
- this._maxNodeIndex = 0;
227
- }
228
- onConnect(backend) {
229
- const gl = backend.context;
230
- this._shader.connect(createWebGl2ShaderProgram(gl));
231
- this._connection = this._createConnection(gl);
232
- this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw).connect(this._createBufferRuntime(this._connection));
233
- this._shader.sync();
234
- this._vao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
235
- .addAttribute(this._instanceBuffer, this._shader.getAttribute('a_quadBounds'), gl.FLOAT, false, instanceStrideBytes, 0, false, 1)
236
- .addAttribute(this._instanceBuffer, this._shader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, instanceStrideBytes, 16, false, 1)
237
- .addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes, 24, false, 1)
238
- .addAttribute(this._instanceBuffer, this._shader.getAttribute('a_tileWord'), gl.UNSIGNED_INT, false, instanceStrideBytes, 28, true, 1)
239
- .connect(this._createVaoRuntime(this._connection));
240
- }
241
- onDisconnect() {
242
- this._shader.disconnect();
243
- this._instanceBuffer?.destroy();
244
- this._instanceBuffer = null;
245
- this._vao?.destroy();
246
- this._vao = null;
247
- this._connection = null;
248
- this._currentBlendMode = null;
249
- this._currentTexture = null;
250
- this._currentView = null;
251
- this._currentViewId = -1;
252
- this._quadIndex = 0;
253
- this._maxNodeIndex = 0;
254
- }
255
- destroy() {
256
- this.disconnect();
257
- this._shader.destroy();
258
- }
259
- _createConnection(gl) {
260
- const vaoHandle = gl.createVertexArray();
261
- if (vaoHandle === null) {
262
- throw new Error('WebGl2TileChunkRenderer: could not create vertex array object.');
263
- }
264
- return { gl, buffers: new Map(), vaoHandle };
265
- }
266
- _createBufferRuntime(connection) {
267
- const handle = connection.gl.createBuffer();
268
- if (handle === null) {
269
- throw new Error('WebGl2TileChunkRenderer: could not create render buffer.');
270
- }
271
- return {
272
- bind: (buffer) => {
273
- connection.gl.bindBuffer(buffer.type, handle);
274
- },
275
- upload: (buffer, offset) => {
276
- const gl = connection.gl;
277
- const data = buffer.data;
278
- const state = connection.buffers.get(buffer);
279
- gl.bindBuffer(buffer.type, handle);
280
- if (state && state.dataByteLength >= data.byteLength) {
281
- gl.bufferSubData(buffer.type, offset, data);
282
- state.dataByteLength = data.byteLength;
283
- }
284
- else {
285
- gl.bufferData(buffer.type, data, buffer.usage);
286
- connection.buffers.set(buffer, { handle, dataByteLength: data.byteLength });
287
- }
288
- },
289
- destroy: (buffer) => {
290
- connection.gl.deleteBuffer(handle);
291
- connection.buffers.delete(buffer);
292
- buffer.disconnect();
293
- },
294
- };
295
- }
296
- _createVaoRuntime(connection) {
297
- let appliedVersion = -1;
298
- return {
299
- bind: (vao) => {
300
- const gl = connection.gl;
301
- gl.bindVertexArray(connection.vaoHandle);
302
- if (appliedVersion !== vao.version) {
303
- let lastBuffer = null;
304
- for (const attribute of vao.attributes) {
305
- if (lastBuffer !== attribute.buffer) {
306
- attribute.buffer.bind();
307
- lastBuffer = attribute.buffer;
308
- }
309
- if (attribute.integer) {
310
- gl.vertexAttribIPointer(attribute.location, attribute.size, attribute.type, attribute.stride, attribute.start);
311
- }
312
- else {
313
- gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
314
- }
315
- gl.enableVertexAttribArray(attribute.location);
316
- gl.vertexAttribDivisor(attribute.location, attribute.divisor);
317
- }
318
- appliedVersion = vao.version;
319
- }
320
- },
321
- unbind: () => {
322
- connection.gl.bindVertexArray(null);
323
- },
324
- draw: (_vao, size, start, type) => {
325
- connection.gl.drawArrays(type, start, size);
326
- },
327
- drawInstanced: (_vao, count, start, instanceCount, type) => {
328
- connection.gl.drawArraysInstanced(type, start, count, instanceCount);
329
- },
330
- destroy: (vao) => {
331
- connection.gl.deleteVertexArray(connection.vaoHandle);
332
- vao.disconnect();
333
- },
334
- };
335
- }
336
- }
26
+ * Instanced WebGL2 renderer for {@link TileChunkNode}. Each tile is one
27
+ * instanced quad; tiles are batched by `(shader, tileset texture)` and one
28
+ * `drawArraysInstanced` is issued per batch. Per-chunk transforms ride on the
29
+ * shared transform buffer (one row per chunk node), so the chunk geometry is
30
+ * orientation-neutral and never re-uploaded for a camera pan.
31
+ * @internal
32
+ */
33
+ var WebGl2TileChunkRenderer = class extends AbstractWebGl2Renderer {
34
+ /**
35
+ * Retained-batch capability opt-in: a tile chunk's per-flush
36
+ * instanced batches (fixed 32-byte layout, tile word at word 7) can be
37
+ * recorded into a group's instruction set and replayed from group-owned
38
+ * resources. Pixel-snapped draws are excluded by the collect-time
39
+ * recordability predicate (and belt-and-braces poisoning in {@link render});
40
+ * tile chunks have no custom-material path to exclude.
41
+ * @internal
42
+ */
43
+ _supportsRetainedBatches = true;
44
+ _shader;
45
+ _batchSize;
46
+ _instanceData;
47
+ _instanceFloat32;
48
+ _instanceUint32;
49
+ _transformUnitScratch = new Int32Array([transformTextureUnit]);
50
+ _baseTextureUnitScratch = new Int32Array([0]);
51
+ _recordTextures = [null];
52
+ _quadIndex = 0;
53
+ _maxNodeIndex = 0;
54
+ _batchNodeCount = 0;
55
+ _nodeBooked = false;
56
+ _currentBlendMode = null;
57
+ _currentTexture = null;
58
+ _currentView = null;
59
+ _currentViewId = -1;
60
+ _hasWrittenGroup = false;
61
+ _writtenGroupData = /* @__PURE__ */ new Float32Array(9);
62
+ _instanceBuffer = null;
63
+ _vao = null;
64
+ _connection = null;
65
+ constructor(batchSize) {
66
+ super();
67
+ this._batchSize = batchSize;
68
+ this._shader = new Shader(tileVertexSource, tile_chunk_default);
69
+ this._instanceData = /* @__PURE__ */ new ArrayBuffer(batchSize * instanceStrideBytes);
70
+ this._instanceFloat32 = new Float32Array(this._instanceData);
71
+ this._instanceUint32 = new Uint32Array(this._instanceData);
72
+ }
73
+ render(node) {
74
+ const pages = node.pages;
75
+ this._nodeBooked = false;
76
+ if (pages.length === 0) return;
77
+ const backend = this.getBackend();
78
+ const blendMode = node.blendMode;
79
+ const tintRgba = node.tint.toRgba8();
80
+ const command = backend.activeDrawCommand;
81
+ const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(node);
82
+ for (const page of pages) this._renderPage(backend, page.texture, page.quads, blendMode, tintRgba, nodeIndex);
83
+ }
84
+ _renderPage(backend, texture, quads, blendMode, tintRgba, nodeIndex) {
85
+ if (quads.length === 0) return;
86
+ const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
87
+ const blendModeChanged = blendMode !== this._currentBlendMode;
88
+ if (this._quadIndex > 0 && (blendModeChanged || textureChanged || this._quadIndex + quads.length > this._batchSize)) this.flush();
89
+ if (this._currentBlendMode === null || this._currentBlendMode !== blendMode) {
90
+ this._currentBlendMode = blendMode;
91
+ backend.setBlendMode(blendMode);
92
+ }
93
+ if (this._currentTexture !== texture) {
94
+ this._currentTexture = texture;
95
+ backend.bindTexture(texture, 0);
96
+ }
97
+ const flipY = texture.flipY;
98
+ let offset = 0;
99
+ while (offset < quads.length) {
100
+ const remaining = quads.length - offset;
101
+ const runSize = Math.min(remaining, this._batchSize);
102
+ this._writeRun(quads, offset, runSize, flipY, tintRgba, nodeIndex);
103
+ offset += runSize;
104
+ if (offset < quads.length) {
105
+ this.flush();
106
+ this._currentBlendMode = blendMode;
107
+ backend.setBlendMode(blendMode);
108
+ this._currentTexture = texture;
109
+ backend.bindTexture(texture, 0);
110
+ }
111
+ }
112
+ }
113
+ _writeRun(quads, offset, count, flipY, tintRgba, nodeIndex) {
114
+ const f32 = this._instanceFloat32;
115
+ const u32 = this._instanceUint32;
116
+ const baseWord = nodeIndex & TILE_ROW_MASK;
117
+ if (!this._nodeBooked) {
118
+ this._nodeBooked = true;
119
+ this._batchNodeCount++;
120
+ }
121
+ for (let i = 0; i < count; i++) {
122
+ const q = quads[offset + i];
123
+ const idx = this._quadIndex * wordsPerInstance;
124
+ f32[idx + 0] = q.x0;
125
+ f32[idx + 1] = q.y0;
126
+ f32[idx + 2] = q.x1;
127
+ f32[idx + 3] = q.y1;
128
+ const flipX = (q.orient & 1) !== 0;
129
+ const tileFlipY = (q.orient & 2) !== 0;
130
+ const diagonal = (q.orient & 4) !== 0;
131
+ const uA = flipX ? q.u1 : q.u0;
132
+ const uB = flipX ? q.u0 : q.u1;
133
+ let vA = tileFlipY ? q.v1 : q.v0;
134
+ let vB = tileFlipY ? q.v0 : q.v1;
135
+ if (flipY) {
136
+ const swap = vA;
137
+ vA = vB;
138
+ vB = swap;
139
+ }
140
+ const uMin = uA * 65535 & 65535;
141
+ const vMin = vA * 65535 & 65535;
142
+ const uMax = uB * 65535 & 65535;
143
+ const vMax = vB * 65535 & 65535;
144
+ u32[idx + 4] = uMin | vMin << 16;
145
+ u32[idx + 5] = uMax | vMax << 16;
146
+ u32[idx + 6] = tintRgba;
147
+ u32[idx + 7] = (diagonal ? baseWord | TILE_DIAGONAL_BIT : baseWord) >>> 0;
148
+ this._quadIndex++;
149
+ }
150
+ if (nodeIndex > this._maxNodeIndex) this._maxNodeIndex = nodeIndex;
151
+ }
152
+ flush() {
153
+ const backend = this.getBackendOrNull();
154
+ const instanceBuffer = this._instanceBuffer;
155
+ const vao = this._vao;
156
+ if (this._quadIndex === 0 || backend === null || instanceBuffer === null || vao === null) {
157
+ this._quadIndex = 0;
158
+ this._maxNodeIndex = 0;
159
+ this._batchNodeCount = 0;
160
+ return;
161
+ }
162
+ this._stageViewUniforms(backend);
163
+ if (this._currentTexture !== null) this._shader.getUniform("u_texture").setValue(this._baseTextureUnitScratch);
164
+ backend.bindTransformBufferTexture(transformTextureUnit, this._maxNodeIndex + 1);
165
+ this._shader.getUniform("u_transforms").setValue(this._transformUnitScratch);
166
+ this._shader.sync();
167
+ backend.bindVertexArrayObject(vao);
168
+ instanceBuffer.upload(this._instanceFloat32, 0, this._quadIndex * wordsPerInstance);
169
+ vao.drawInstanced(4, 0, this._quadIndex, RenderingPrimitives.TriangleStrip);
170
+ backend.stats.batches++;
171
+ backend.stats.drawCalls++;
172
+ if (backend._isRetainedCapturing && this._currentTexture !== null) {
173
+ this._recordTextures[0] = this._currentTexture;
174
+ backend._recordRetainedBatch(this, this._instanceUint32.subarray(0, this._quadIndex * wordsPerInstance), this._quadIndex, this._currentBlendMode ?? BlendModes.Normal, this._recordTextures, 1, null, null, this._batchNodeCount);
175
+ }
176
+ this._quadIndex = 0;
177
+ this._maxNodeIndex = 0;
178
+ this._batchNodeCount = 0;
179
+ }
180
+ /**
181
+ * Stage `u_projection` (live view) and `u_group` (live composed group
182
+ * matrix) on the shader, guarded by cached view/group stamps. Shared by the
183
+ * live flush path and retained-batch replay - replay resolves exactly the
184
+ * same live state a slow-path flush would.
185
+ */
186
+ _stageViewUniforms(backend) {
187
+ const view = backend.view;
188
+ if (this._currentView !== view || this._currentViewId !== view.updateId) {
189
+ this._currentView = view;
190
+ this._currentViewId = view.updateId;
191
+ this._shader.getUniform("u_projection").setValue(view.getTransform().toArray(false));
192
+ }
193
+ const groupTransform = backend.renderGroupTransform;
194
+ const groupData = groupTransform !== null ? groupTransform.toArray(false) : identityGroupMat3;
195
+ if (!this._hasWrittenGroup || packedGroupChanged(groupData, this._writtenGroupData, 0)) {
196
+ this._shader.getUniform("u_group").setValue(groupData);
197
+ this._writtenGroupData.set(groupData);
198
+ this._hasWrittenGroup = true;
199
+ }
200
+ backend._stageViewportUniform(this._shader);
201
+ }
202
+ /** @internal See {@link WebGl2RetainedBatchReplayer._scanRetainedNodeIndexRange}. */
203
+ _scanRetainedNodeIndexRange(payload, range) {
204
+ const words = payload.bundle.instanceWords;
205
+ const start = payload.byteOffset / Uint32Array.BYTES_PER_ELEMENT;
206
+ for (let i = 0; i < payload.instanceCount; i++) {
207
+ const row = words[start + i * wordsPerInstance + 7] & TILE_ROW_MASK;
208
+ if (row < range.min) range.min = row;
209
+ if (row > range.max) range.max = row;
210
+ }
211
+ }
212
+ /** @internal See {@link WebGl2RetainedBatchReplayer._rebaseRetainedNodeIndices} (group-local indices). */
213
+ _rebaseRetainedNodeIndices(payload, base) {
214
+ const words = payload.bundle.instanceWords;
215
+ const start = payload.byteOffset / Uint32Array.BYTES_PER_ELEMENT;
216
+ for (let i = 0; i < payload.instanceCount; i++) {
217
+ const index = start + i * wordsPerInstance + 7;
218
+ const word = words[index];
219
+ words[index] = (word & TILE_DIAGONAL_BIT | (word & TILE_ROW_MASK) - base & TILE_ROW_MASK) >>> 0;
220
+ }
221
+ }
222
+ /**
223
+ * Point the batch VAO's per-instance attributes at the bundle's persistent
224
+ * instance buffer, based at the batch's byte offset (WebGL2 has no
225
+ * baseInstance, hence one small VAO per recorded batch). Same attribute
226
+ * set/locations as the live VAO in {@link onConnect}.
227
+ * @internal
228
+ */
229
+ _configureRetainedVao(payload) {
230
+ const gl = this.getBackend().context;
231
+ const buffer = payload.bundle.instanceBuffer;
232
+ const vao = payload.vao;
233
+ if (buffer === null || vao === null) throw new Error("WebGl2TileChunkRenderer: retained batch VAO configuration requires an uploaded bundle.");
234
+ const base = payload.byteOffset;
235
+ vao.addAttribute(buffer, this._shader.getAttribute("a_quadBounds"), gl.FLOAT, false, instanceStrideBytes, base + 0, false, 1).addAttribute(buffer, this._shader.getAttribute("a_uvBounds"), gl.UNSIGNED_SHORT, true, instanceStrideBytes, base + 16, false, 1).addAttribute(buffer, this._shader.getAttribute("a_color"), gl.UNSIGNED_BYTE, true, instanceStrideBytes, base + 24, false, 1).addAttribute(buffer, this._shader.getAttribute("a_tileWord"), gl.UNSIGNED_INT, false, instanceStrideBytes, base + 28, true, 1);
236
+ }
237
+ /**
238
+ * Replay one recorded batch: all STATE is resolved live - blend mode via
239
+ * the backend's dedup, `u_projection`/`u_group` from the live view + live
240
+ * composed group matrix (the camera-pan / group-move win), the single
241
+ * tileset texture bound to unit 0 by recorded slot order - and only the
242
+ * DATA is cached: the instance bytes in the bundle buffer (bound through
243
+ * the per-batch VAO) and the group-owned transform texture on the shared
244
+ * transform unit. The backend hook flushed any pending live batch before
245
+ * dispatching here and bumps the stats from the instruction descriptor.
246
+ * @internal
247
+ */
248
+ _replayRetainedBatch(payload) {
249
+ const backend = this.getBackendOrNull();
250
+ const vao = payload.vao;
251
+ const transformTexture = payload.bundle.transformTexture;
252
+ if (backend === null || vao === null || transformTexture === null) return;
253
+ if (payload.blendMode !== this._currentBlendMode) this._currentBlendMode = payload.blendMode;
254
+ backend.setBlendMode(payload.blendMode);
255
+ this._stageViewUniforms(backend);
256
+ const textures = payload.textures;
257
+ for (let i = 0; i < textures.length; i++) backend.bindTexture(textures[i], i);
258
+ if (textures.length > 0) this._currentTexture = textures[0];
259
+ this._shader.getUniform("u_texture").setValue(this._baseTextureUnitScratch);
260
+ backend.bindTexture(transformTexture, transformTextureUnit);
261
+ this._shader.getUniform("u_transforms").setValue(this._transformUnitScratch);
262
+ this._shader.sync();
263
+ backend.bindVertexArrayObject(vao);
264
+ vao.drawInstanced(4, 0, payload.instanceCount, RenderingPrimitives.TriangleStrip);
265
+ }
266
+ onConnect(backend) {
267
+ const gl = backend.context;
268
+ this._shader.connect(createWebGl2ShaderProgram(gl));
269
+ this._connection = this._createConnection(gl);
270
+ this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw).connect(this._createBufferRuntime(this._connection));
271
+ this._shader.sync();
272
+ this._vao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip).addAttribute(this._instanceBuffer, this._shader.getAttribute("a_quadBounds"), gl.FLOAT, false, instanceStrideBytes, 0, false, 1).addAttribute(this._instanceBuffer, this._shader.getAttribute("a_uvBounds"), gl.UNSIGNED_SHORT, true, instanceStrideBytes, 16, false, 1).addAttribute(this._instanceBuffer, this._shader.getAttribute("a_color"), gl.UNSIGNED_BYTE, true, instanceStrideBytes, 24, false, 1).addAttribute(this._instanceBuffer, this._shader.getAttribute("a_tileWord"), gl.UNSIGNED_INT, false, instanceStrideBytes, 28, true, 1).connect(this._createVaoRuntime(this._connection));
273
+ }
274
+ onDisconnect() {
275
+ this._shader.disconnect();
276
+ this._instanceBuffer?.destroy();
277
+ this._instanceBuffer = null;
278
+ this._vao?.destroy();
279
+ this._vao = null;
280
+ this._connection = null;
281
+ this._currentBlendMode = null;
282
+ this._currentTexture = null;
283
+ this._currentView = null;
284
+ this._currentViewId = -1;
285
+ this._hasWrittenGroup = false;
286
+ this._quadIndex = 0;
287
+ this._maxNodeIndex = 0;
288
+ this._batchNodeCount = 0;
289
+ this._nodeBooked = false;
290
+ }
291
+ destroy() {
292
+ this.disconnect();
293
+ this._shader.destroy();
294
+ }
295
+ _createConnection(gl) {
296
+ const vaoHandle = gl.createVertexArray();
297
+ if (vaoHandle === null) throw new Error("WebGl2TileChunkRenderer: could not create vertex array object.");
298
+ return {
299
+ gl,
300
+ buffers: /* @__PURE__ */ new Map(),
301
+ vaoHandle
302
+ };
303
+ }
304
+ _createBufferRuntime(connection) {
305
+ const handle = connection.gl.createBuffer();
306
+ if (handle === null) throw new Error("WebGl2TileChunkRenderer: could not create render buffer.");
307
+ return {
308
+ bind: (buffer) => {
309
+ connection.gl.bindBuffer(buffer.type, handle);
310
+ },
311
+ upload: (buffer, offset) => {
312
+ const gl = connection.gl;
313
+ const state = connection.buffers.get(buffer);
314
+ gl.bindBuffer(buffer.type, handle);
315
+ if (state && state.dataByteLength >= buffer.uploadByteLength) {
316
+ uploadBufferRange(gl, buffer, offset);
317
+ state.dataByteLength = buffer.uploadByteLength;
318
+ } else {
319
+ uploadBufferStore(gl, buffer);
320
+ connection.buffers.set(buffer, {
321
+ handle,
322
+ dataByteLength: buffer.uploadByteLength
323
+ });
324
+ }
325
+ },
326
+ destroy: (buffer) => {
327
+ connection.gl.deleteBuffer(handle);
328
+ connection.buffers.delete(buffer);
329
+ buffer.disconnect();
330
+ }
331
+ };
332
+ }
333
+ _createVaoRuntime(connection) {
334
+ let appliedVersion = -1;
335
+ return {
336
+ bind: (vao) => {
337
+ const gl = connection.gl;
338
+ gl.bindVertexArray(connection.vaoHandle);
339
+ if (appliedVersion !== vao.version) {
340
+ let lastBuffer = null;
341
+ for (const attribute of vao.attributes) {
342
+ if (lastBuffer !== attribute.buffer) {
343
+ attribute.buffer.bind();
344
+ lastBuffer = attribute.buffer;
345
+ }
346
+ if (attribute.integer) gl.vertexAttribIPointer(attribute.location, attribute.size, attribute.type, attribute.stride, attribute.start);
347
+ else gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
348
+ gl.enableVertexAttribArray(attribute.location);
349
+ gl.vertexAttribDivisor(attribute.location, attribute.divisor);
350
+ }
351
+ appliedVersion = vao.version;
352
+ }
353
+ },
354
+ unbind: () => {
355
+ connection.gl.bindVertexArray(null);
356
+ },
357
+ draw: (_vao, size, start, type) => {
358
+ connection.gl.drawArrays(type, start, size);
359
+ },
360
+ drawInstanced: (_vao, count, start, instanceCount, type) => {
361
+ connection.gl.drawArraysInstanced(type, start, count, instanceCount);
362
+ },
363
+ destroy: (vao) => {
364
+ connection.gl.deleteVertexArray(connection.vaoHandle);
365
+ vao.disconnect();
366
+ }
367
+ };
368
+ }
369
+ };
337
370
 
371
+ //#endregion
338
372
  export { WebGl2TileChunkRenderer };
339
- //# sourceMappingURL=WebGl2TileChunkRenderer.js.map
373
+ //# sourceMappingURL=WebGl2TileChunkRenderer.js.map