@lijuhong1981/three.instancedsprite 1.1.0 → 2.0.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.
@@ -2,10 +2,10 @@ import Check from "@lijuhong1981/jscheck/src/Check.js";
2
2
  import {
3
3
  MathUtils,
4
4
  Object3D,
5
- Texture
6
5
  } from "three";
7
6
  import InstancedSprite from "./InstancedSprite.js";
8
7
  import InstancedSpriteMesh from "./InstancedSpriteMesh.js";
8
+ import TextureAtlas from "./TextureAtlas.js";
9
9
 
10
10
  /**
11
11
  * @import InstancedSpriteOptions from "./InstancedSprite.js";
@@ -19,18 +19,22 @@ const imageCache = new Map();
19
19
 
20
20
  /**
21
21
  * InstancedSpriteCollection类,批量管理InstancedSprite实例
22
- *
22
+ *
23
23
  * * 继承自Object3D
24
- * * 根据InstancedSprite的图像属性生成InstancedSpriteMaterial和InstancedSpriteMesh,
25
- * * 管理InstancedSprite与InstancedSpriteMesh实例
24
+ * * 使用纹理图集将不同图片打包,一张图集对应一个 InstancedSpriteMesh(一次 draw call)
25
+ * * 图集扩容到上限后会新建图集与 Mesh
26
26
  * @extends Object3D
27
27
  */
28
28
  class InstancedSpriteCollection extends Object3D {
29
29
  /**
30
- * @param {boolean} [useNodeMaterial=false] - 是否使用TSL的NodeMaterial,默认false
30
+ * @param {object} [options] - 配置项
31
+ * @param {boolean} [options.useNodeMaterial=false] - 是否使用TSL的NodeMaterial(WebGPU),默认false
32
+ * @param {number} [options.initialSize=1024] - 图集初始边长
33
+ * @param {number} [options.maxSize=8192] - 图集最大边长
34
+ * @param {number} [options.padding=2] - 子图间距
31
35
  * @constructor
32
36
  */
33
- constructor(useNodeMaterial = false) {
37
+ constructor(options = {}) {
34
38
  super();
35
39
  /**
36
40
  * 是否使用TSL的NodeMaterial,默认false
@@ -38,25 +42,49 @@ class InstancedSpriteCollection extends Object3D {
38
42
  * @readonly
39
43
  * @default false
40
44
  */
41
- this.useNodeMaterial = useNodeMaterial;
45
+ this.useNodeMaterial = options.useNodeMaterial === true;
42
46
  /**
43
47
  * 对象类型标识
44
48
  * @type {string}
45
49
  * @readonly
46
50
  */
47
51
  this.type = "InstancedSpriteCollection";
48
- // this.frustumCulled = false;
49
52
  /**
50
53
  * @type {Array<InstancedSprite>}
51
54
  * @ignore
52
55
  */
53
56
  this._instancedSprites = [];
54
57
  /**
55
- * @type {Map<HTMLImageElement|HTMLCanvasElement, InstancedSpriteMesh>}
58
+ * @type {Map<string, InstancedSprite>}
56
59
  * @ignore
57
60
  */
58
- this._meshes = new Map();
61
+ this._spriteByUuid = new Map();
62
+ /**
63
+ * 图集与对应 Mesh 的列表,按创建顺序排列
64
+ * @type {Array<{atlas:TextureAtlas, mesh:InstancedSpriteMesh}>}
65
+ * @ignore
66
+ */
67
+ this._atlasList = [];
68
+ /**
69
+ * 图片源到所在图集条目的索引,避免同一张图片被重复打包进不同图集
70
+ * @type {Map<HTMLImageElement|HTMLCanvasElement, {atlas:TextureAtlas, mesh:InstancedSpriteMesh}>}
71
+ * @ignore
72
+ */
73
+ this._imageEntry = new Map();
74
+ /**
75
+ * 图集配置项,新建图集时使用
76
+ * @type {object}
77
+ * @ignore
78
+ */
79
+ this._atlasOptions = options;
59
80
  this._depthTest = true;
81
+ this._depthWrite = true;
82
+ /**
83
+ * 预留容量,在创建新Mesh时应用,undefined表示不预留
84
+ * @type {number|undefined}
85
+ * @ignore
86
+ */
87
+ this._reservedCapacity = undefined;
60
88
  }
61
89
  /**
62
90
  * InstancedSpriteCollection对象标识
@@ -71,11 +99,23 @@ class InstancedSpriteCollection extends Object3D {
71
99
  */
72
100
  get depthTest() { return this._depthTest; }
73
101
  set depthTest(value) {
74
- Check.typeOf.boolean(value, 'depthTest');
102
+ Check.typeOf.boolean('depthTest', value);
75
103
  this._depthTest = value;
76
- const meshes = this._meshes.values();
77
- for (const mesh of meshes) {
78
- mesh.depthTest = value;
104
+ for (const entry of this._atlasList) {
105
+ entry.mesh.depthTest = value;
106
+ }
107
+ }
108
+ /**
109
+ * 深度写入开关,默认为true;多个半透明 Sprite 重叠时,开启深度写入可能导致排序瑕疵(后方 Sprite 被错误遮挡),关闭可缓解,适合半透明标签/粒子等场景
110
+ * @type {boolean}
111
+ * @default true
112
+ */
113
+ get depthWrite() { return this._depthWrite; }
114
+ set depthWrite(value) {
115
+ Check.typeOf.boolean('depthWrite', value);
116
+ this._depthWrite = value;
117
+ for (const entry of this._atlasList) {
118
+ entry.mesh.depthWrite = value;
79
119
  }
80
120
  }
81
121
  /**
@@ -88,35 +128,7 @@ class InstancedSpriteCollection extends Object3D {
88
128
  source.src = MathUtils.generateUUID();
89
129
  if (!imageCache.has(source.src))
90
130
  imageCache.set(source.src, source);
91
- let mesh = this._meshes.get(source);
92
- if (!mesh) {
93
- const texture = new Texture();
94
- if (source instanceof HTMLCanvasElement || (source instanceof HTMLImageElement && source.complete)) {
95
- texture.image = source;
96
- texture.needsUpdate = true;
97
- } else {
98
- const onload = () => {
99
- source.removeEventListener('load', onload);
100
- texture.image = source;
101
- texture.needsUpdate = true;
102
- sprite.imageSize.set(source.width, source.height);
103
- };
104
- source.addEventListener('load', onload);
105
- }
106
- mesh = new InstancedSpriteMesh(this, texture);
107
- mesh.depthTest = this._depthTest;
108
- this._meshes.set(source, mesh);
109
- super.add(mesh);
110
- }
111
- if (sprite._mesh === mesh)
112
- return;
113
- // instancedSprite如果已经有所属的mesh了,则从所属的Mesh中移除
114
- sprite.remove();
115
- mesh.add(sprite);
116
- sprite._image = source;
117
- if (source.width !== 0 && source.height !== 0) {
118
- sprite.imageSize.set(source.width, source.height);
119
- }
131
+ this._setImageAtlas(source, sprite);
120
132
  } else if (typeof source === 'string') {
121
133
  let image = imageCache.get(source);
122
134
  if (!image) {
@@ -129,6 +141,95 @@ class InstancedSpriteCollection extends Object3D {
129
141
  throw new Error('Invalid image source type ' + source);
130
142
  }
131
143
  }
144
+ /**
145
+ * 将图片打包进图集并加入对应 Mesh
146
+ * @param {HTMLImageElement|HTMLCanvasElement} source
147
+ * @param {InstancedSprite} sprite
148
+ * @private
149
+ */
150
+ _setImageAtlas(source, sprite) {
151
+ sprite._image = source;
152
+ const loaded = source instanceof HTMLCanvasElement || (source.width > 0 && source.height > 0);
153
+ if (loaded) {
154
+ this._assignAtlas(source, sprite);
155
+ } else {
156
+ const onload = () => {
157
+ source.removeEventListener('load', onload);
158
+ // 图片加载完成前 sprite 可能已被移除,此时不再加入图集
159
+ if (this._spriteByUuid.get(sprite.uuid) === sprite)
160
+ this._assignAtlas(source, sprite);
161
+ };
162
+ source.addEventListener('load', onload);
163
+ }
164
+ }
165
+ /**
166
+ * 将已加载的图片打包进图集,设置 sprite 的 uvRect 与 imageSize,并加入图集 Mesh
167
+ * @param {HTMLImageElement|HTMLCanvasElement} source
168
+ * @param {InstancedSprite} sprite
169
+ * @private
170
+ */
171
+ _assignAtlas(source, sprite) {
172
+ // 已打包过的图片复用其所在图集,避免把同一张图重复打进不同图集
173
+ let entry = this._imageEntry.get(source);
174
+ if (!entry) {
175
+ entry = this._atlasList[this._atlasList.length - 1];
176
+ let rect = entry ? entry.atlas.add(source) : null;
177
+ if (rect === null && entry) {
178
+ // 图集已满,尝试扩容
179
+ if (entry.atlas.grow()) {
180
+ // 扩容会重建纹理,需同步更新 Mesh 材质引用的纹理
181
+ entry.mesh.material.texture = entry.atlas.texture;
182
+ this._recomputeUvRects(entry);
183
+ rect = entry.atlas.add(source);
184
+ }
185
+ }
186
+ if (rect === null) {
187
+ // 图集已达最大尺寸,新建图集与 Mesh
188
+ entry = this._createAtlasEntry();
189
+ this._atlasList.push(entry);
190
+ rect = entry.atlas.add(source);
191
+ }
192
+ this._imageEntry.set(source, entry);
193
+ }
194
+ const rect = entry.atlas.getRect(source);
195
+ const uv = entry.atlas.getUvRect(rect);
196
+ sprite.uvRect.set(uv[0], uv[1], uv[2], uv[3]);
197
+ sprite.imageSize.set(source.width, source.height);
198
+ sprite._remove();
199
+ entry.mesh.add(sprite);
200
+ }
201
+ /**
202
+ * 图集扩容后重新归一化该图集内所有 sprite 的 uvRect
203
+ * @param {{atlas:TextureAtlas, mesh:InstancedSpriteMesh}} entry
204
+ * @private
205
+ */
206
+ _recomputeUvRects(entry) {
207
+ for (const sprite of this._instancedSprites) {
208
+ if (sprite._mesh === entry.mesh) {
209
+ const rect = entry.atlas.getRect(sprite._image);
210
+ if (rect) {
211
+ const uv = entry.atlas.getUvRect(rect);
212
+ sprite.uvRect.set(uv[0], uv[1], uv[2], uv[3]);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * 创建新的图集与对应 Mesh
219
+ * @returns {{atlas:TextureAtlas, mesh:InstancedSpriteMesh}}
220
+ * @private
221
+ */
222
+ _createAtlasEntry() {
223
+ const atlas = new TextureAtlas(this._atlasOptions);
224
+ const mesh = new InstancedSpriteMesh(this, atlas.texture);
225
+ mesh.depthTest = this._depthTest;
226
+ mesh.depthWrite = this._depthWrite;
227
+ if (this._reservedCapacity !== undefined) {
228
+ mesh.reserve(this._reservedCapacity);
229
+ }
230
+ super.add(mesh);
231
+ return { atlas, mesh };
232
+ }
132
233
  /**
133
234
  * InstancedSprite实例数组
134
235
  * @type {Array<InstancedSprite>}
@@ -147,7 +248,7 @@ class InstancedSpriteCollection extends Object3D {
147
248
  }
148
249
  /**
149
250
  * 根据索引获取InstancedSprite实例
150
- * @param {number} index
251
+ * @param {number} index
151
252
  * @returns {InstancedSprite|undefined}
152
253
  */
153
254
  get(index) {
@@ -159,19 +260,17 @@ class InstancedSpriteCollection extends Object3D {
159
260
  * @returns {InstancedSprite|undefined}
160
261
  */
161
262
  getByUuid(uuid) {
162
- for (const sprite of this._instancedSprites) {
163
- if (sprite.uuid === uuid)
164
- return sprite;
165
- }
263
+ return this._spriteByUuid.get(uuid);
166
264
  }
167
265
  /**
168
266
  * 添加InstancedSprite
169
- * @type {InstancedSpriteConstructorOptions} options
267
+ * @param {InstancedSpriteOptions} options - 初始化配置项
170
268
  * @returns {InstancedSprite}
171
269
  */
172
270
  add(options = {}) {
173
271
  const sprite = new InstancedSprite(options, this);
174
272
  this._instancedSprites.push(sprite);
273
+ this._spriteByUuid.set(sprite.uuid, sprite);
175
274
  return sprite;
176
275
  }
177
276
  /**
@@ -183,12 +282,13 @@ class InstancedSpriteCollection extends Object3D {
183
282
  const index = this._instancedSprites.indexOf(sprite);
184
283
  if (index !== -1) {
185
284
  this._instancedSprites.splice(index, 1);
285
+ this._spriteByUuid.delete(sprite.uuid);
186
286
  sprite._remove();
187
287
  }
188
288
  return this;
189
289
  }
190
290
  /**
191
- * 移除所有InstancedSprite,并清空所有Mesh的实例数据(Mesh本身保留以便复用)
291
+ * 移除所有InstancedSprite,并清空所有Mesh的实例数据(Mesh与图集保留以便复用)
192
292
  * @returns {InstancedSpriteCollection}
193
293
  */
194
294
  clear() {
@@ -197,9 +297,22 @@ class InstancedSpriteCollection extends Object3D {
197
297
  sprite._instanceIndex = -1;
198
298
  }
199
299
  this._instancedSprites.length = 0;
200
- const meshes = this._meshes.values();
201
- for (const mesh of meshes) {
202
- mesh.clear();
300
+ this._spriteByUuid.clear();
301
+ for (const entry of this._atlasList) {
302
+ entry.mesh.clear();
303
+ }
304
+ return this;
305
+ }
306
+ /**
307
+ * 预分配所有Mesh的容量,避免后续动态扩容
308
+ * @param {number} capacity - 需要预留的实例数量
309
+ * @returns {InstancedSpriteCollection}
310
+ */
311
+ reserve(capacity) {
312
+ Check.typeOf.number('capacity', capacity);
313
+ this._reservedCapacity = capacity;
314
+ for (const entry of this._atlasList) {
315
+ entry.mesh.reserve(capacity);
203
316
  }
204
317
  return this;
205
318
  }
@@ -219,24 +332,43 @@ class InstancedSpriteCollection extends Object3D {
219
332
  * @param {Array<Object>} intersects - The target array that holds the intersection points.
220
333
  */
221
334
  raycast(raycaster, intersects) {
222
- const meshes = this._meshes.values();
223
- for (const mesh of meshes) {
224
- mesh.raycast(raycaster, intersects);
335
+ for (const entry of this._atlasList) {
336
+ entry.mesh.raycast(raycaster, intersects);
225
337
  }
226
338
  }
227
339
  /**
228
340
  * 每帧更新
229
341
  * @private
230
- */
342
+ */
231
343
  update() {
232
344
  if (!this.visible) return;
233
345
 
234
- const meshes = this._meshes.values();
235
- for (const mesh of meshes) {
236
- mesh.update();
346
+ for (const entry of this._atlasList) {
347
+ entry.mesh.update();
237
348
  }
238
349
  }
350
+ /**
351
+ * 释放所有 GPU 资源(几何体、材质、图集纹理),并移除所有子 Mesh
352
+ * @returns {InstancedSpriteCollection}
353
+ */
354
+ dispose() {
355
+ this.clear();
356
+ for (const entry of this._atlasList) {
357
+ entry.mesh.dispose();
358
+ entry.atlas.texture.dispose();
359
+ }
360
+ this._atlasList.length = 0;
361
+ this._imageEntry.clear();
362
+ super.clear();
363
+ return this;
364
+ }
239
365
  };
240
366
 
241
367
  export default InstancedSpriteCollection;
242
- export { InstancedSpriteCollection };
368
+ export { InstancedSpriteCollection };
369
+ /**
370
+ * InstancedSpriteCollection 的别名。
371
+ * @class
372
+ * @name BillboardCollection
373
+ */
374
+ export const BillboardCollection = InstancedSpriteCollection;
@@ -14,6 +14,7 @@ attribute vec4 aCenterAndSize; // xy: 锚点中心(0-1), zw: 图片宽高(像素
14
14
  attribute vec3 aScaleAndRotationAndSizeAttenuation; // x: 缩放, y: 旋转(弧度), z: 大小跟随相机深度(0/1)
15
15
  attribute vec4 aColorAndOpacity; // RGBA颜色
16
16
  attribute vec4 aPickColorAndEnabled; // xyz:拾取颜色,w:是否启用拾取颜色(0/1)
17
+ attribute vec4 aUvRect; // xy: UV偏移, zw: UV缩放(非图集模式为(0,0,1,1))
17
18
 
18
19
  varying float vShow;
19
20
  varying vec2 vUv;
@@ -31,7 +32,7 @@ void main() {
31
32
 
32
33
  // --- 2. 输出varying变量 ---
33
34
  vShow = show;
34
- vUv = uv;
35
+ vUv = aUvRect.xy + uv * aUvRect.zw;
35
36
  vColorAndOpacity = aColorAndOpacity;
36
37
  vPickColorAndEnabled = aPickColorAndEnabled;
37
38
 
@@ -144,4 +145,10 @@ class InstancedSpriteMaterial extends ShaderMaterial {
144
145
  };
145
146
 
146
147
  export default InstancedSpriteMaterial;
147
- export { InstancedSpriteMaterial };
148
+ export { InstancedSpriteMaterial };
149
+ /**
150
+ * InstancedSpriteMaterial 的别名。
151
+ * @class
152
+ * @name BillboardMaterial
153
+ */
154
+ export const BillboardMaterial = InstancedSpriteMaterial;