@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.
- package/API.md +207 -11
- package/README.md +28 -7
- package/index.js +1 -0
- package/package.json +11 -4
- package/src/InstancedSprite.js +14 -1
- package/src/InstancedSpriteCollection.js +193 -61
- package/src/InstancedSpriteMaterial.js +9 -2
- package/src/InstancedSpriteMesh.js +130 -45
- package/src/InstancedSpriteNodeMaterial.js +12 -5
- package/src/TextureAtlas.js +125 -0
- package/.gitattributes +0 -2
|
@@ -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
|
-
* *
|
|
25
|
-
* *
|
|
24
|
+
* * 使用纹理图集将不同图片打包,一张图集对应一个 InstancedSpriteMesh(一次 draw call)
|
|
25
|
+
* * 图集扩容到上限后会新建图集与 Mesh
|
|
26
26
|
* @extends Object3D
|
|
27
27
|
*/
|
|
28
28
|
class InstancedSpriteCollection extends Object3D {
|
|
29
29
|
/**
|
|
30
|
-
* @param {
|
|
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(
|
|
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<
|
|
58
|
+
* @type {Map<string, InstancedSprite>}
|
|
56
59
|
* @ignore
|
|
57
60
|
*/
|
|
58
|
-
this.
|
|
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(
|
|
102
|
+
Check.typeOf.boolean('depthTest', value);
|
|
75
103
|
this._depthTest = value;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
if (sprite.uuid === uuid)
|
|
164
|
-
return sprite;
|
|
165
|
-
}
|
|
263
|
+
return this._spriteByUuid.get(uuid);
|
|
166
264
|
}
|
|
167
265
|
/**
|
|
168
266
|
* 添加InstancedSprite
|
|
169
|
-
* @
|
|
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
|
-
|
|
201
|
-
for (const
|
|
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
|
|
223
|
-
|
|
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
|
|
235
|
-
|
|
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;
|