@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.
- package/API.md +207 -11
- package/README.md +28 -7
- package/index.js +1 -0
- package/package.json +16 -9
- 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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
283
|
-
if (required
|
|
346
|
+
_ensureCapacity(required) {
|
|
347
|
+
if (required <= this._maxCapacity) return;
|
|
284
348
|
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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.
|
|
442
|
+
this._ensureCapacity(requiredCapacity);
|
|
373
443
|
|
|
374
444
|
if (removeInstancedSprites.length > 0) {
|
|
375
|
-
//
|
|
445
|
+
// 末位换入(swap-and-pop)移除:把最后一个 sprite 换到被移除的位置,O(k) 而非 O(n) 全量重写
|
|
446
|
+
const movedSprites = [];
|
|
376
447
|
for (const sprite of removeInstancedSprites) {
|
|
377
|
-
const index =
|
|
378
|
-
if (index
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
459
|
+
sprite._instanceIndex = -1;
|
|
460
|
+
sprite._mesh = undefined;
|
|
385
461
|
}
|
|
386
462
|
removeInstancedSprites.length = 0;
|
|
387
463
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
//
|
|
88
|
-
const scaleFactor = sizeAttenuation.lessThan(0.5).and(cameraProjectionMatrix.element(
|
|
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