@lijuhong1981/three.instancedsprite 1.1.0 → 2.0.1

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.
@@ -49,10 +49,35 @@ const AttributesItemSize = Object.freeze({
49
49
  aScaleAndRotationAndSizeAttenuation: 3,
50
50
  aColorAndOpacity: 4,
51
51
  aPickColorAndEnabled: 4,
52
+ aUvRect: 4,
52
53
  });
54
+ /**
55
+ * 将指定实例的属性片段标记为需要更新,仅上传该实例对应的数据范围(增量上传)
56
+ * @param {InstancedSpriteMesh} mesh
57
+ * @param {string} name - attribute名称
58
+ * @param {number} index - 实例索引
59
+ * @param {number} itemSize - 单个实例占用的float数量
60
+ * @ignore
61
+ */
62
+ function markUpdateRange(mesh, name, index, itemSize) {
63
+ const attribute = mesh.geometry.attributes[name];
64
+ attribute.addUpdateRange(index * itemSize, itemSize);
65
+ attribute.needsUpdate = true;
66
+ }
67
+ /**
68
+ * 将指定实例属性标记为全量更新(整块重传)
69
+ * @param {InstancedSpriteMesh} mesh
70
+ * @param {string} name - attribute名称
71
+ * @ignore
72
+ */
73
+ function markFullUpdate(mesh, name) {
74
+ const attribute = mesh.geometry.attributes[name];
75
+ attribute.clearUpdateRanges();
76
+ attribute.needsUpdate = true;
77
+ }
53
78
  /**
54
79
  * 检查并更新一个InstancedSprite实例的数据到对应的InstancedBufferAttribute位置
55
- * @param {InstancedSpriteMesh} mesh
80
+ * @param {InstancedSpriteMesh} mesh
56
81
  * @param {InstancedSprite} sprite
57
82
  * @ignore
58
83
  */
@@ -71,6 +96,7 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
71
96
  const opacityChanged = (sprite._opacity !== sprite.opacity);
72
97
  const pickColorChanged = (sprite._pickColor.equals(sprite.pickColor) === false);
73
98
  const enablePickColorChanged = (sprite._enablePickColor !== sprite.enablePickColor);
99
+ const uvRectChanged = (sprite._uvRect.equals(sprite.uvRect) === false);
74
100
 
75
101
  let idx = 0;
76
102
 
@@ -86,7 +112,7 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
86
112
  sprite._show = sprite.show;
87
113
  attributesData.aPositionAndShow[idx + 3] = sprite._show ? 1 : 0;
88
114
  }
89
- mesh.geometry.attributes.aPositionAndShow.needsUpdate = true;
115
+ markUpdateRange(mesh, 'aPositionAndShow', index, AttributesItemSize.aPositionAndShow);
90
116
  }
91
117
 
92
118
  if (centerChanged || imageSizeChanged) {
@@ -101,7 +127,7 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
101
127
  attributesData.aCenterAndSize[idx + 2] = sprite._imageSize.x;
102
128
  attributesData.aCenterAndSize[idx + 3] = sprite._imageSize.y;
103
129
  }
104
- mesh.geometry.attributes.aCenterAndSize.needsUpdate = true;
130
+ markUpdateRange(mesh, 'aCenterAndSize', index, AttributesItemSize.aCenterAndSize);
105
131
  }
106
132
 
107
133
  if (scaleChanged || rotationChanged || sizeAttenuationChanged) {
@@ -118,7 +144,7 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
118
144
  sprite._sizeAttenuation = sprite.sizeAttenuation;
119
145
  attributesData.aScaleAndRotationAndSizeAttenuation[idx + 2] = sprite._sizeAttenuation ? 1 : 0;
120
146
  }
121
- mesh.geometry.attributes.aScaleAndRotationAndSizeAttenuation.needsUpdate = true;
147
+ markUpdateRange(mesh, 'aScaleAndRotationAndSizeAttenuation', index, AttributesItemSize.aScaleAndRotationAndSizeAttenuation);
122
148
  }
123
149
 
124
150
  if (colorChanged || opacityChanged) {
@@ -133,7 +159,7 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
133
159
  sprite._opacity = sprite.opacity;
134
160
  attributesData.aColorAndOpacity[idx + 3] = sprite._opacity;
135
161
  }
136
- mesh.geometry.attributes.aColorAndOpacity.needsUpdate = true;
162
+ markUpdateRange(mesh, 'aColorAndOpacity', index, AttributesItemSize.aColorAndOpacity);
137
163
  }
138
164
 
139
165
  if (pickColorChanged || enablePickColorChanged) {
@@ -148,14 +174,24 @@ function checkAndUpdateInstancedSprite(mesh, sprite) {
148
174
  sprite._enablePickColor = sprite.enablePickColor;
149
175
  attributesData.aPickColorAndEnabled[idx + 3] = sprite._enablePickColor ? 1 : 0;
150
176
  }
151
- mesh.geometry.attributes.aPickColorAndEnabled.needsUpdate = true;
177
+ markUpdateRange(mesh, 'aPickColorAndEnabled', index, AttributesItemSize.aPickColorAndEnabled);
178
+ }
179
+
180
+ if (uvRectChanged) {
181
+ idx = index * AttributesItemSize.aUvRect;
182
+ sprite._uvRect.copy(sprite.uvRect);
183
+ attributesData.aUvRect[idx] = sprite._uvRect.x;
184
+ attributesData.aUvRect[idx + 1] = sprite._uvRect.y;
185
+ attributesData.aUvRect[idx + 2] = sprite._uvRect.z;
186
+ attributesData.aUvRect[idx + 3] = sprite._uvRect.w;
187
+ markUpdateRange(mesh, 'aUvRect', index, AttributesItemSize.aUvRect);
152
188
  }
153
189
  };
154
190
  /**
155
191
  * 添加一个InstancedSprite实例的数据到InstancedBufferAttribute中对应的位置
156
192
  * @param {InstancedSpriteMesh} mesh
157
193
  * @param {InstancedSprite} sprite
158
- * @param {number} index
194
+ * @param {number} index
159
195
  * @ignore
160
196
  */
161
197
  function addInstancedSprite(mesh, sprite, index) {
@@ -170,7 +206,7 @@ function addInstancedSprite(mesh, sprite, index) {
170
206
  attributesData.aPositionAndShow[idx + 2] = sprite._position.z;
171
207
  sprite._show = sprite.show;
172
208
  attributesData.aPositionAndShow[idx + 3] = sprite._show ? 1 : 0;
173
- mesh.geometry.attributes.aPositionAndShow.needsUpdate = true;
209
+ markFullUpdate(mesh, 'aPositionAndShow');
174
210
 
175
211
  idx = index * AttributesItemSize.aCenterAndSize;
176
212
  sprite._center.copy(sprite.center);
@@ -179,7 +215,7 @@ function addInstancedSprite(mesh, sprite, index) {
179
215
  sprite._imageSize.copy(sprite.imageSize);
180
216
  attributesData.aCenterAndSize[idx + 2] = sprite._imageSize.x;
181
217
  attributesData.aCenterAndSize[idx + 3] = sprite._imageSize.y;
182
- mesh.geometry.attributes.aCenterAndSize.needsUpdate = true;
218
+ markFullUpdate(mesh, 'aCenterAndSize');
183
219
 
184
220
  idx = index * AttributesItemSize.aScaleAndRotationAndSizeAttenuation;
185
221
  sprite._scale = sprite.scale;
@@ -188,7 +224,7 @@ function addInstancedSprite(mesh, sprite, index) {
188
224
  attributesData.aScaleAndRotationAndSizeAttenuation[idx + 1] = sprite._rotation;
189
225
  sprite._sizeAttenuation = sprite.sizeAttenuation;
190
226
  attributesData.aScaleAndRotationAndSizeAttenuation[idx + 2] = sprite._sizeAttenuation ? 1 : 0;
191
- mesh.geometry.attributes.aScaleAndRotationAndSizeAttenuation.needsUpdate = true;
227
+ markFullUpdate(mesh, 'aScaleAndRotationAndSizeAttenuation');
192
228
 
193
229
  idx = index * AttributesItemSize.aColorAndOpacity;
194
230
  sprite._color.copy(sprite.color);
@@ -197,18 +233,28 @@ function addInstancedSprite(mesh, sprite, index) {
197
233
  attributesData.aColorAndOpacity[idx + 2] = sprite._color.b;
198
234
  sprite._opacity = sprite.opacity;
199
235
  attributesData.aColorAndOpacity[idx + 3] = sprite._opacity;
200
- mesh.geometry.attributes.aColorAndOpacity.needsUpdate = true;
236
+ markFullUpdate(mesh, 'aColorAndOpacity');
237
+
238
+ idx = index * AttributesItemSize.aUvRect;
239
+ sprite._uvRect.copy(sprite.uvRect);
240
+ attributesData.aUvRect[idx] = sprite._uvRect.x;
241
+ attributesData.aUvRect[idx + 1] = sprite._uvRect.y;
242
+ attributesData.aUvRect[idx + 2] = sprite._uvRect.z;
243
+ attributesData.aUvRect[idx + 3] = sprite._uvRect.w;
244
+ markFullUpdate(mesh, 'aUvRect');
201
245
  };
202
246
 
203
247
  /**
204
248
  * InstancedSpriteMesh类,基于InstancedBufferGeometry实现的高性能InstancedSprite渲染组件
205
- *
249
+ *
250
+ * 一张图集纹理对应一个 InstancedSpriteMesh,所有使用该图集的 Sprite 由其统一绘制
251
+ *
206
252
  * @extends {Mesh}
207
253
  */
208
254
  class InstancedSpriteMesh extends Mesh {
209
255
  /**
210
256
  * @param {InstancedSpriteCollection} collection - 所属的InstancedSpriteCollection实例,必填
211
- * @param {Texture} texture - 材质图像纹理,必填
257
+ * @param {Texture} texture - 图集纹理,必填
212
258
  * @constructor
213
259
  */
214
260
  constructor(collection, texture) {
@@ -223,6 +269,12 @@ class InstancedSpriteMesh extends Mesh {
223
269
  * @readonly
224
270
  */
225
271
  this.type = "InstancedSpriteMesh";
272
+ /**
273
+ * 实例化几何体的包围球基于单位四边形(位于原点),不包含实例位置,视锥剔除会把远离原点的实例整批误剔除,因此禁用
274
+ * @type {boolean}
275
+ * @default false
276
+ */
277
+ this.frustumCulled = false;
226
278
  /**
227
279
  * @type {Array<InstancedSprite>}
228
280
  * @ignore
@@ -250,7 +302,9 @@ class InstancedSpriteMesh extends Mesh {
250
302
  * @ignore
251
303
  */
252
304
  this._maxCapacity = 0;
253
- this._enaureCapacity(64);
305
+ this._ensureCapacity(1024);
306
+ // 初始无实例,绘制数量为0(_ensureCapacity 只负责扩容,不改变实例数量)
307
+ this.geometry.instanceCount = 0;
254
308
  }
255
309
  /**
256
310
  * InstancedSpriteMesh对象标识
@@ -268,6 +322,16 @@ class InstancedSpriteMesh extends Mesh {
268
322
  Check.typeOf.boolean('depthTest', value);
269
323
  this.material.depthTest = value;
270
324
  }
325
+ /**
326
+ * 深度写入开关,默认为true;多个半透明 Sprite 重叠时,开启深度写入可能导致排序瑕疵(后方 Sprite 被错误遮挡),关闭可缓解,适合半透明标签/粒子等场景
327
+ * @type {boolean}
328
+ * @default true
329
+ */
330
+ get depthWrite() { return this.material.depthWrite; }
331
+ set depthWrite(value) {
332
+ Check.typeOf.boolean('depthWrite', value);
333
+ this.material.depthWrite = value;
334
+ }
271
335
  /**
272
336
  * InstancedSprite对象数组
273
337
  * @type {Array<InstancedSprite>}
@@ -279,30 +343,36 @@ class InstancedSpriteMesh extends Mesh {
279
343
  * @param {number} required - 需要容纳的InstancedSprite数量
280
344
  * @private
281
345
  */
282
- _enaureCapacity(required) {
283
- if (required < this._maxCapacity) return;
346
+ _ensureCapacity(required) {
347
+ if (required <= this._maxCapacity) return;
284
348
 
285
- const newCapacity = Math.max(required, this._maxCapacity + 64);
349
+ // 线性扩容(+1024)保证内存浪费有界;Three.js 不支持原地扩容 buffer,需新建数组与 attribute
350
+ const newCapacity = Math.max(required, this._maxCapacity + 1024);
286
351
 
287
352
  for (const [name, itemSize] of Object.entries(AttributesItemSize)) {
288
- const newArray = new Float32Array(newCapacity * itemSize);
289
353
  const oldArray = this._attributesData[name];
354
+ const newArray = new Float32Array(newCapacity * itemSize);
290
355
  if (oldArray) {
291
- const copyLen = Math.min(oldArray.length, newArray.length);
292
- newArray.set(oldArray.subarray(0, copyLen));
356
+ newArray.set(oldArray.subarray(0, Math.min(oldArray.length, newArray.length)));
293
357
  }
294
358
  this._attributesData[name] = newArray;
295
- }
296
359
 
297
- // 创建 InstancedBufferAttribute 对象
298
- for (const [name, itemSize] of Object.entries(AttributesItemSize)) {
299
- const attr = new InstancedBufferAttribute(this._attributesData[name], itemSize);
300
- this.geometry.setAttribute(name, attr);
360
+ const attribute = new InstancedBufferAttribute(newArray, itemSize);
361
+ this.geometry.setAttribute(name, attribute);
301
362
  }
302
363
 
303
- this.geometry.instanceCount = required;
304
364
  this._maxCapacity = newCapacity;
305
365
  }
366
+ /**
367
+ * 预分配容量,避免后续动态扩容(不改变当前实际绘制的实例数量)
368
+ * @param {number} capacity - 需要预留的实例数量
369
+ * @returns {InstancedSpriteMesh}
370
+ */
371
+ reserve(capacity) {
372
+ Check.typeOf.number('capacity', capacity);
373
+ this._ensureCapacity(capacity);
374
+ return this;
375
+ }
306
376
  /**
307
377
  * 添加一个InstancedSprite
308
378
  * @param {InstancedSprite} sprite - 要添加的InstancedSprite实例
@@ -369,33 +439,40 @@ class InstancedSpriteMesh extends Mesh {
369
439
 
370
440
  // 计算最终需要的容量
371
441
  const requiredCapacity = instancedSprites.length + addInstancedSprites.length - removeInstancedSprites.length;
372
- this._enaureCapacity(requiredCapacity);
442
+ this._ensureCapacity(requiredCapacity);
373
443
 
374
444
  if (removeInstancedSprites.length > 0) {
375
- // 如果有需要移除的InstancedSprite,先从instancedSprites数组中移除
445
+ // 末位换入(swap-and-pop)移除:把最后一个 sprite 换到被移除的位置,O(k) 而非 O(n) 全量重写
446
+ const movedSprites = [];
376
447
  for (const sprite of removeInstancedSprites) {
377
- const index = instancedSprites.indexOf(sprite);
378
- if (index !== -1) {
379
- instancedSprites.splice(index, 1); //从instancedSprites数组中移除
380
- sprite._mesh = undefined;
381
- sprite._instanceIndex = -1;
448
+ const index = sprite._instanceIndex;
449
+ if (index === -1)
450
+ continue; // 已被处理过(例如换入后又在本帧被移除)
451
+
452
+ const last = instancedSprites[instancedSprites.length - 1];
453
+ instancedSprites.pop();
454
+ if (last !== sprite) {
455
+ instancedSprites[index] = last;
456
+ last._instanceIndex = index; // 立即更新,避免后续删除用到陈旧索引
457
+ movedSprites.push(last);
382
458
  }
383
- // else
384
- // console.warn('InstancedSprite not found in InstancedSpriteMesh during removal');
459
+ sprite._instanceIndex = -1;
460
+ sprite._mesh = undefined;
385
461
  }
386
462
  removeInstancedSprites.length = 0;
387
463
 
388
- // 如果有需要添加的InstancedSprite,则加入instancedSprites数组中
389
- if (addInstancedSprites.length > 0) {
390
- instancedSprites.push(...addInstancedSprites);
391
- addInstancedSprites.length = 0;
464
+ // 重写被移动 sprite 的数据到新位置
465
+ for (const sprite of movedSprites) {
466
+ if (sprite._instanceIndex !== -1) {
467
+ addInstancedSprite(this, sprite, sprite._instanceIndex);
468
+ }
392
469
  }
393
470
 
394
- // 重新写入所有InstancedSprite的属性数据到InstancedBufferAttribute中
395
- const length = instancedSprites.length;
396
- for (let i = 0; i < length; i++) {
397
- const sprite = instancedSprites[i];
398
- addInstancedSprite(this, sprite, i);
471
+ // 添加新 sprite
472
+ while (addInstancedSprites.length > 0) {
473
+ const sprite = addInstancedSprites.shift();
474
+ addInstancedSprite(this, sprite, instancedSprites.length);
475
+ instancedSprites.push(sprite);
399
476
  }
400
477
  } else {
401
478
  // 检查并更新属性值有变化的InstancedSprite
@@ -413,6 +490,9 @@ class InstancedSpriteMesh extends Mesh {
413
490
 
414
491
  this.geometry.instanceCount = instancedSprites.length; //更新instanceCount
415
492
  }
493
+ /**
494
+ * 释放 GPU 资源
495
+ */
416
496
  dispose() {
417
497
  this.clear();
418
498
  this.geometry.dispose();
@@ -422,4 +502,9 @@ class InstancedSpriteMesh extends Mesh {
422
502
 
423
503
  export default InstancedSpriteMesh;
424
504
  export { InstancedSpriteMesh };
425
-
505
+ /**
506
+ * InstancedSpriteMesh 的别名。
507
+ * @class
508
+ * @name BillboardMesh
509
+ */
510
+ export const BillboardMesh = InstancedSpriteMesh;
@@ -43,6 +43,7 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
43
43
  const aScaleAndRotationAndSizeAttenuation = attribute('aScaleAndRotationAndSizeAttenuation', 'vec3');
44
44
  const aColorAndOpacity = attribute('aColorAndOpacity', 'vec4');
45
45
  const aPickColorAndEnabled = attribute('aPickColorAndEnabled', 'vec4');
46
+ const aUvRect = attribute('aUvRect', 'vec4');
46
47
 
47
48
  // ============================================================
48
49
  // Varying 变量 — 从顶点着色器传递到片元着色器
@@ -67,7 +68,7 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
67
68
  // --- 2. 输出 varying ---
68
69
  vShow.assign(show);
69
70
  vColorAndOpacity.assign(aColorAndOpacity);
70
- vUv.assign(uv());
71
+ vUv.assign(aUvRect.xy.add(uv().mul(aUvRect.zw)));
71
72
  vPickColorAndEnabled.assign(aPickColorAndEnabled);
72
73
 
73
74
  // --- 3. 计算模型视图位置 ---
@@ -84,8 +85,8 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
84
85
 
85
86
  If(isVisible, () => {
86
87
  // --- 4. sizeAttenuation:大小跟随相机深度 ---
87
- // 检查是否为透视投影: 透视投影矩阵中 m[2][3] (elements[11]) == -1
88
- const scaleFactor = sizeAttenuation.lessThan(0.5).and(cameraProjectionMatrix.element(11).equal(-1))
88
+ // 检查是否为透视投影:透视投影矩阵 m[2][3] (elements[11]) == -1
89
+ const scaleFactor = sizeAttenuation.lessThan(0.5).and(cameraProjectionMatrix.element(2).w.equal(-1))
89
90
  .select(scale.mul(depth), scale);
90
91
 
91
92
  // --- 5. 坐标对齐 ---
@@ -119,7 +120,7 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
119
120
  });
120
121
 
121
122
  return clipPosition;
122
- }).once();
123
+ }).once()();
123
124
 
124
125
  // ============================================================
125
126
  // 片元着色器 (fragmentNode)
@@ -145,7 +146,7 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
145
146
  });
146
147
 
147
148
  return diffuseColor;
148
- }).once();
149
+ }).once()();
149
150
  }
150
151
  /**
151
152
  * 图片纹理
@@ -161,3 +162,9 @@ class InstancedSpriteNodeMaterial extends NodeMaterial {
161
162
 
162
163
  export default InstancedSpriteNodeMaterial;
163
164
  export { InstancedSpriteNodeMaterial };
165
+ /**
166
+ * InstancedSpriteNodeMaterial 的别名。
167
+ * @class
168
+ * @name BillboardNodeMaterial
169
+ */
170
+ export const BillboardNodeMaterial = InstancedSpriteNodeMaterial;
@@ -0,0 +1,125 @@
1
+ import { CanvasTexture } from "three";
2
+
3
+ /**
4
+ * 纹理图集,将多张图片按行(shelf)打包进一张 Canvas 纹理,用于合并 draw call
5
+ */
6
+ class TextureAtlas {
7
+ /**
8
+ * @param {object} [options]
9
+ * @param {number} [options.initialSize=1024] 初始边长(正方形,单位像素)
10
+ * @param {number} [options.maxSize=8192] 最大边长,自动扩容到该值后不再增大
11
+ * @param {number} [options.padding=2] 子图间距,防止线性过滤时边缘渗色
12
+ */
13
+ constructor(options = {}) {
14
+ this.initialSize = options.initialSize || 1024;
15
+ this.maxSize = options.maxSize || 8192;
16
+ this.padding = options.padding !== undefined ? options.padding : 2;
17
+
18
+ this.size = this.initialSize;
19
+ this.canvas = document.createElement('canvas');
20
+ this.canvas.width = this.size;
21
+ this.canvas.height = this.size;
22
+ this.ctx = this.canvas.getContext('2d');
23
+
24
+ this.texture = new CanvasTexture(this.canvas);
25
+ this.texture.needsUpdate = true;
26
+
27
+ this._cursorX = this.padding;
28
+ this._cursorY = this.padding;
29
+ this._rowHeight = 0;
30
+ this._rects = new Map(); // source -> {x, y, w, h} 像素矩形
31
+ }
32
+ /**
33
+ * 将图片加入图集,返回其像素矩形;已加入过则返回缓存;图集整体已满则返回null
34
+ * @param {HTMLImageElement|HTMLCanvasElement} source
35
+ * @returns {{x:number,y:number,w:number,h:number}|null}
36
+ */
37
+ add(source) {
38
+ const cached = this._rects.get(source);
39
+ if (cached) return cached;
40
+
41
+ const w = source.width;
42
+ const h = source.height;
43
+ const stepW = w + this.padding * 2;
44
+ const stepH = h + this.padding * 2;
45
+
46
+ // 单张图片超过当前图集尺寸时,先扩容到能容纳
47
+ while (stepW > this.size || stepH > this.size) {
48
+ if (!this.grow()) {
49
+ throw new Error('TextureAtlas: 图片尺寸超过图集最大尺寸');
50
+ }
51
+ }
52
+
53
+ // 当前行放不下则换行
54
+ if (this._cursorX + stepW > this.size) {
55
+ this._cursorX = this.padding;
56
+ this._cursorY += this._rowHeight;
57
+ this._rowHeight = 0;
58
+ }
59
+ // 图集整体已满
60
+ if (this._cursorY + stepH > this.size) {
61
+ return null;
62
+ }
63
+
64
+ const x = this._cursorX + this.padding;
65
+ const y = this._cursorY + this.padding;
66
+ const rect = { x, y, w, h };
67
+ this._rects.set(source, rect);
68
+
69
+ this.ctx.drawImage(source, x, y, w, h);
70
+
71
+ this._cursorX += stepW;
72
+ this._rowHeight = Math.max(this._rowHeight, stepH);
73
+
74
+ this.texture.needsUpdate = true;
75
+ return rect;
76
+ }
77
+ /**
78
+ * 获取图片已分配的像素矩形
79
+ * @param {HTMLImageElement|HTMLCanvasElement} source
80
+ * @returns {{x:number,y:number,w:number,h:number}|undefined}
81
+ */
82
+ getRect(source) {
83
+ return this._rects.get(source);
84
+ }
85
+ /**
86
+ * 扩容图集(边长翻倍),成功返回true;已达最大尺寸返回false
87
+ * @returns {boolean}
88
+ */
89
+ grow() {
90
+ const newSize = this.size * 2;
91
+ if (newSize > this.maxSize) return false;
92
+
93
+ const newCanvas = document.createElement('canvas');
94
+ newCanvas.width = newSize;
95
+ newCanvas.height = newSize;
96
+ const newCtx = newCanvas.getContext('2d');
97
+ newCtx.drawImage(this.canvas, 0, 0);
98
+
99
+ this.canvas = newCanvas;
100
+ this.ctx = newCtx;
101
+ this.size = newSize;
102
+
103
+ // 尺寸变化必须重建纹理:WebGL2 用 texStorage 分配固定尺寸,替换 image 后 texSubImage2D 无法扩容
104
+ this.texture.dispose();
105
+ this.texture = new CanvasTexture(this.canvas);
106
+ this.texture.needsUpdate = true;
107
+ return true;
108
+ }
109
+ /**
110
+ * 将像素矩形转为归一化 UV(uOffset, vOffset, uScale, vScale)
111
+ * @param {{x:number,y:number,w:number,h:number}} rect
112
+ * @returns {Array<number>}
113
+ */
114
+ getUvRect(rect) {
115
+ return [
116
+ rect.x / this.size,
117
+ 1 - (rect.y + rect.h) / this.size, // flipY=true 时纹理上下翻转,v 坐标需反转
118
+ rect.w / this.size,
119
+ rect.h / this.size,
120
+ ];
121
+ }
122
+ }
123
+
124
+ export default TextureAtlas;
125
+ export { TextureAtlas };
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- *.js text eol=lf
2
- *.md text eol=lf