@lijuhong1981/three.instancedsprite 1.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/.claude/settings.local.json +7 -0
- package/API.md +514 -0
- package/README.md +55 -0
- package/index.js +5 -0
- package/package.json +41 -0
- package/src/InstancedSprite.js +345 -0
- package/src/InstancedSpriteCollection.js +226 -0
- package/src/InstancedSpriteMaterial.js +134 -0
- package/src/InstancedSpriteMesh.js +406 -0
- package/src/InstancedSpriteNodeMaterial.js +148 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import Check from "@lijuhong1981/jscheck/src/Check.js";
|
|
2
|
+
import ImageLoader from "@lijuhong1981/jsload/src/ImageLoader.js";
|
|
3
|
+
import { Loader } from "@lijuhong1981/jsload/src/Loader.js";
|
|
4
|
+
import setValues from "@lijuhong1981/three.utils/src/setValues.js";
|
|
5
|
+
import { Color, InstancedBufferGeometry, MathUtils, Matrix4, Triangle, Vector2, Vector3 } from "three";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @import InstancedSpriteCollection from "./InstancedSpriteCollection.js";
|
|
9
|
+
* @import InstancedSpriteMesh from "./InstancedSpriteMesh.js";
|
|
10
|
+
* @import InstancedSpriteMaterial from "./InstancedSpriteMaterial.js";
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const imageLoader = new ImageLoader({ cacheType: Loader.CacheType.ALONE, });
|
|
14
|
+
|
|
15
|
+
const _intersectPoint = /*@__PURE__*/ new Vector3();
|
|
16
|
+
const _worldScale = /*@__PURE__*/ new Vector3();
|
|
17
|
+
const _mvPosition = /*@__PURE__*/ new Vector3();
|
|
18
|
+
|
|
19
|
+
const _alignedPosition = /*@__PURE__*/ new Vector2();
|
|
20
|
+
const _rotatedPosition = /*@__PURE__*/ new Vector2();
|
|
21
|
+
const _viewWorldMatrix = /*@__PURE__*/ new Matrix4();
|
|
22
|
+
|
|
23
|
+
const _vA = /*@__PURE__*/ new Vector3();
|
|
24
|
+
const _vB = /*@__PURE__*/ new Vector3();
|
|
25
|
+
const _vC = /*@__PURE__*/ new Vector3();
|
|
26
|
+
|
|
27
|
+
const _uvA = /*@__PURE__*/ new Vector2();
|
|
28
|
+
const _uvB = /*@__PURE__*/ new Vector2();
|
|
29
|
+
const _uvC = /*@__PURE__*/ new Vector2();
|
|
30
|
+
|
|
31
|
+
function transformVertex(vertexPosition, mvPosition, center, scale, sin, cos) {
|
|
32
|
+
|
|
33
|
+
// compute position in camera space
|
|
34
|
+
_alignedPosition.subVectors(vertexPosition, center).addScalar(0.5).multiply(scale);
|
|
35
|
+
|
|
36
|
+
// to check if rotation is not zero
|
|
37
|
+
if (sin !== undefined) {
|
|
38
|
+
|
|
39
|
+
_rotatedPosition.x = (cos * _alignedPosition.x) - (sin * _alignedPosition.y);
|
|
40
|
+
_rotatedPosition.y = (sin * _alignedPosition.x) + (cos * _alignedPosition.y);
|
|
41
|
+
|
|
42
|
+
} else {
|
|
43
|
+
|
|
44
|
+
_rotatedPosition.copy(_alignedPosition);
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
vertexPosition.copy(mvPosition);
|
|
50
|
+
vertexPosition.x += _rotatedPosition.x;
|
|
51
|
+
vertexPosition.y += _rotatedPosition.y;
|
|
52
|
+
|
|
53
|
+
// transform to world space
|
|
54
|
+
vertexPosition.applyMatrix4(_viewWorldMatrix);
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* InstancedSprite 对象配置项选项
|
|
60
|
+
* @typedef {object} InstancedSpriteOptions
|
|
61
|
+
* @property {boolean} [show=true] 是否显示
|
|
62
|
+
* @property {Vector3|Array<number>} [position=(0,0,0)] 位置(世界空间)
|
|
63
|
+
* @property {number} [scale=1] 缩放
|
|
64
|
+
* @property {number} [rotation=0] 旋转(弧度)
|
|
65
|
+
* @property {boolean} [sizeAttenuation=true] 尺寸是否跟随相机深度变化
|
|
66
|
+
* @property {Vector2|Array<number>} [center=(0.5,0.5)] 锚点中心(0-1)
|
|
67
|
+
* @property {Color|number|string} [color=0xffffff] 颜色
|
|
68
|
+
* @property {number} [opacity=1] 不透明度(0-1)
|
|
69
|
+
* @property {string|HTMLImageElement|HTMLCanvasElement} [image] 图片资源
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* InstancedSprite 数据模型类,用于存储单个 InstancedSprite 的所有属性
|
|
74
|
+
*
|
|
75
|
+
* * InstancedSprite跟Sprite不同,它只是一个数据集合对象,并不继承自Object3D,不能直接加入到Scene中
|
|
76
|
+
* * 但它可以作为属性Module使用,修改InstancedSprite的属性会自动更新至GPU中
|
|
77
|
+
*/
|
|
78
|
+
class InstancedSprite {
|
|
79
|
+
/**
|
|
80
|
+
* @param {InstancedSpriteOptions} options - 初始化配置项
|
|
81
|
+
* @param {InstancedSpriteCollection} collection - 所属InstancedSpriteCollection实例,必填
|
|
82
|
+
* @constructor
|
|
83
|
+
*/
|
|
84
|
+
constructor(options = {}, collection) {
|
|
85
|
+
Check.defined('collection', collection);
|
|
86
|
+
/**
|
|
87
|
+
* 所属集合,内部设置
|
|
88
|
+
* @type {InstancedSpriteCollection}
|
|
89
|
+
* @ignore
|
|
90
|
+
*/
|
|
91
|
+
this._collection = collection;
|
|
92
|
+
/**
|
|
93
|
+
* 所属Mesh,内部设置
|
|
94
|
+
* @type {InstancedSpriteMesh}
|
|
95
|
+
* @ignore
|
|
96
|
+
*/
|
|
97
|
+
this._mesh = undefined;
|
|
98
|
+
/**
|
|
99
|
+
* 在 InstancedSpriteMesh 中的实例索引,由 InstancedSpriteMesh 内部设置
|
|
100
|
+
* @type {number}
|
|
101
|
+
* @ignore
|
|
102
|
+
*/
|
|
103
|
+
this._instanceIndex = -1;
|
|
104
|
+
/**
|
|
105
|
+
* 对象类型标识
|
|
106
|
+
* @type {string}
|
|
107
|
+
* @readonly
|
|
108
|
+
*/
|
|
109
|
+
this.type = "InstancedSprite";
|
|
110
|
+
/**
|
|
111
|
+
* 当前InstancedSprite的唯一标识id
|
|
112
|
+
* @type {string}
|
|
113
|
+
* @readonly
|
|
114
|
+
*/
|
|
115
|
+
this.uuid = MathUtils.generateUUID();
|
|
116
|
+
/**
|
|
117
|
+
* 是否显示
|
|
118
|
+
* @type {boolean}
|
|
119
|
+
*/
|
|
120
|
+
this.show = true;
|
|
121
|
+
this._show = true;
|
|
122
|
+
/**
|
|
123
|
+
* 世界坐标位置
|
|
124
|
+
* @type {Vector3}
|
|
125
|
+
*/
|
|
126
|
+
this.position = new Vector3(0, 0, 0);
|
|
127
|
+
this._position = new Vector3(0, 0, 0);
|
|
128
|
+
/**
|
|
129
|
+
* 缩放
|
|
130
|
+
* @type {number}
|
|
131
|
+
*/
|
|
132
|
+
this.scale = 1;
|
|
133
|
+
this._scale = 1;
|
|
134
|
+
/**
|
|
135
|
+
* 旋转(弧度)
|
|
136
|
+
* @type {number}
|
|
137
|
+
*/
|
|
138
|
+
this.rotation = 0;
|
|
139
|
+
this._rotation = 0;
|
|
140
|
+
/**
|
|
141
|
+
* 尺寸是否跟随相机深度变化
|
|
142
|
+
* @type {boolean}
|
|
143
|
+
*/
|
|
144
|
+
this.sizeAttenuation = true;
|
|
145
|
+
this._sizeAttenuation = true;
|
|
146
|
+
/**
|
|
147
|
+
* 锚点中心(0-1)
|
|
148
|
+
* @type {Vector2}
|
|
149
|
+
*/
|
|
150
|
+
this.center = new Vector2(0.5, 0.5);
|
|
151
|
+
this._center = new Vector2(0.5, 0.5);
|
|
152
|
+
/**
|
|
153
|
+
* 颜色
|
|
154
|
+
* @type {Color}
|
|
155
|
+
*/
|
|
156
|
+
this.color = new Color(0xffffff);
|
|
157
|
+
this._color = new Color(0xffffff);
|
|
158
|
+
/**
|
|
159
|
+
* 不透明度(0-1)
|
|
160
|
+
* @type {number}
|
|
161
|
+
*/
|
|
162
|
+
this.opacity = 1;
|
|
163
|
+
this._opacity = 1;
|
|
164
|
+
/**
|
|
165
|
+
* 图片尺寸(像素),图片加载完成后可用
|
|
166
|
+
* @type {Vector2}
|
|
167
|
+
* @readonly
|
|
168
|
+
*/
|
|
169
|
+
this.imageSize = new Vector2(0, 0);
|
|
170
|
+
this._imageSize = new Vector2(0, 0);
|
|
171
|
+
this._image = null;
|
|
172
|
+
|
|
173
|
+
this.setValues(options);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* InstancedSprite对象标识
|
|
177
|
+
* @type {boolean}
|
|
178
|
+
* @readonly
|
|
179
|
+
*/
|
|
180
|
+
get isInstancedSprite() { return true; }
|
|
181
|
+
/**
|
|
182
|
+
* 设置参数
|
|
183
|
+
* @param {InstancedSpriteOptions} options
|
|
184
|
+
* @returns {InstancedSprite}
|
|
185
|
+
*/
|
|
186
|
+
setValues(options) {
|
|
187
|
+
setValues(this, options);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 是否可见,等同于show
|
|
192
|
+
* @type {boolean}
|
|
193
|
+
*/
|
|
194
|
+
get visible() {
|
|
195
|
+
return this.show;
|
|
196
|
+
}
|
|
197
|
+
set visible(value) { this.show = value; }
|
|
198
|
+
/**
|
|
199
|
+
* 旋转(角度)
|
|
200
|
+
* @type {number}
|
|
201
|
+
*/
|
|
202
|
+
get rotationDegrees() { return MathUtils.radToDeg(this.rotation); }
|
|
203
|
+
set rotationDegrees(value) { this.rotation = MathUtils.degToRad(value); }
|
|
204
|
+
/**
|
|
205
|
+
* 图片资源,可以是URL字符串、HTMLImageElement或HTMLCanvasElement
|
|
206
|
+
* @type {string|HTMLImageElement|HTMLCanvasElement}
|
|
207
|
+
*/
|
|
208
|
+
get image() { return this._image; }
|
|
209
|
+
set image(value) {
|
|
210
|
+
if (this._image) {
|
|
211
|
+
if (typeof value === 'string') {
|
|
212
|
+
if (this._image.src === value)
|
|
213
|
+
return;
|
|
214
|
+
} else if (value instanceof HTMLImageElement || value instanceof HTMLCanvasElement) {
|
|
215
|
+
if (this._image === value)
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this._collection._setImage(value, this);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 图片ID,图片加载完成后可用,通常为图片URL或生成的UUID
|
|
223
|
+
* @type {string}
|
|
224
|
+
* @readonly
|
|
225
|
+
*/
|
|
226
|
+
get imageId() { return this._image && this._image.src; }
|
|
227
|
+
/**
|
|
228
|
+
* 图片像素宽度,图片加载完成后可用
|
|
229
|
+
* @type {number}
|
|
230
|
+
* @readonly
|
|
231
|
+
*/
|
|
232
|
+
get imageWidth() { return this.imageSize.x; }
|
|
233
|
+
/**
|
|
234
|
+
* 图片像素高度,图片加载完成后可用
|
|
235
|
+
* @type {number}
|
|
236
|
+
* @readonly
|
|
237
|
+
*/
|
|
238
|
+
get imageHeight() { return this.imageSize.y; }
|
|
239
|
+
_remove() {
|
|
240
|
+
this._mesh && this._mesh.remove(this);
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* InstancedSprite对象的几何体属性
|
|
245
|
+
* @type {InstancedBufferGeometry}
|
|
246
|
+
* @readonly
|
|
247
|
+
*/
|
|
248
|
+
get geometry() {
|
|
249
|
+
return this._mesh ? this._mesh.geometry : undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* InstancedSprite对象的材质属性
|
|
253
|
+
* @type {InstancedSpriteMaterial}
|
|
254
|
+
* @readonly
|
|
255
|
+
*/
|
|
256
|
+
get material() {
|
|
257
|
+
return this._mesh ? this._mesh.material : undefined;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 从Mesh中移除该InstancedSprite对象
|
|
261
|
+
* @return {InstancedSprite}
|
|
262
|
+
*/
|
|
263
|
+
remove() {
|
|
264
|
+
this._collection && this._collection.remove(this);
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Computes intersection points between a casted ray and this sprite.
|
|
269
|
+
*
|
|
270
|
+
* @param {Raycaster} raycaster - The raycaster.
|
|
271
|
+
* @param {Array<Object>} intersects - The target array that holds the intersection points.
|
|
272
|
+
* @param {Matrix4} modelViewMatrix
|
|
273
|
+
*/
|
|
274
|
+
raycast(raycaster, intersects, modelViewMatrix) {
|
|
275
|
+
|
|
276
|
+
if (this.show === false || this.imageSize.x <= 0 || this.imageSize.y <= 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_viewWorldMatrix.copy(raycaster.camera.matrixWorld);
|
|
281
|
+
_mvPosition.copy(this.position).applyMatrix4(modelViewMatrix);
|
|
282
|
+
|
|
283
|
+
const depth = -_mvPosition.z;
|
|
284
|
+
if (depth <= 0) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let scale = this.scale;
|
|
289
|
+
if (raycaster.camera.isPerspectiveCamera && this.sizeAttenuation === false) {
|
|
290
|
+
scale *= depth;
|
|
291
|
+
}
|
|
292
|
+
const aspectRatio = this.imageSize.x / this.imageSize.y;
|
|
293
|
+
_worldScale.set(1, 1, 1).multiplyScalar(scale * aspectRatio);
|
|
294
|
+
|
|
295
|
+
const rotation = this.rotation;
|
|
296
|
+
let sin, cos;
|
|
297
|
+
|
|
298
|
+
if (rotation !== 0) {
|
|
299
|
+
cos = Math.cos(rotation);
|
|
300
|
+
sin = Math.sin(rotation);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const center = this.center;
|
|
304
|
+
|
|
305
|
+
transformVertex(_vA.set(-0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
|
306
|
+
transformVertex(_vB.set(0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
|
307
|
+
transformVertex(_vC.set(0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
|
308
|
+
|
|
309
|
+
_uvA.set(0, 0);
|
|
310
|
+
_uvB.set(1, 0);
|
|
311
|
+
_uvC.set(1, 1);
|
|
312
|
+
|
|
313
|
+
// check first triangle
|
|
314
|
+
let intersect = raycaster.ray.intersectTriangle(_vA, _vB, _vC, false, _intersectPoint);
|
|
315
|
+
|
|
316
|
+
if (intersect === null) {
|
|
317
|
+
|
|
318
|
+
// check second triangle
|
|
319
|
+
transformVertex(_vB.set(-0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
|
320
|
+
_uvB.set(0, 1);
|
|
321
|
+
|
|
322
|
+
intersect = raycaster.ray.intersectTriangle(_vA, _vC, _vB, false, _intersectPoint);
|
|
323
|
+
if (intersect === null) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const distance = raycaster.ray.origin.distanceTo(_intersectPoint);
|
|
330
|
+
|
|
331
|
+
if (distance < raycaster.near || distance > raycaster.far) return;
|
|
332
|
+
|
|
333
|
+
intersects.push({
|
|
334
|
+
distance: distance,
|
|
335
|
+
point: _intersectPoint.clone(),
|
|
336
|
+
uv: Triangle.getInterpolation(_intersectPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2()),
|
|
337
|
+
face: null,
|
|
338
|
+
object: this,
|
|
339
|
+
instanceId: this._instanceIndex,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export default InstancedSprite;
|
|
345
|
+
export { InstancedSprite };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import Check from "@lijuhong1981/jscheck/src/Check.js";
|
|
2
|
+
import {
|
|
3
|
+
MathUtils,
|
|
4
|
+
Object3D,
|
|
5
|
+
Texture
|
|
6
|
+
} from "three";
|
|
7
|
+
import InstancedSprite from "./InstancedSprite.js";
|
|
8
|
+
import InstancedSpriteMesh from "./InstancedSpriteMesh.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import InstancedSpriteOptions from "./InstancedSprite.js";
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @type {Map<string, HTMLImageElement|HTMLCanvasElement>}
|
|
16
|
+
* @ignore
|
|
17
|
+
*/
|
|
18
|
+
const imageCache = new Map();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* InstancedSpriteCollection类,批量管理InstancedSprite实例
|
|
22
|
+
*
|
|
23
|
+
* * 继承自Object3D
|
|
24
|
+
* * 根据InstancedSprite的图像属性生成InstancedSpriteMaterial和InstancedSpriteMesh,
|
|
25
|
+
* * 管理InstancedSprite与InstancedSpriteMesh实例
|
|
26
|
+
* @extends Object3D
|
|
27
|
+
*/
|
|
28
|
+
class InstancedSpriteCollection extends Object3D {
|
|
29
|
+
/**
|
|
30
|
+
* @param {boolean} [useNodeMaterial=false] - 是否使用TSL的NodeMaterial,默认false
|
|
31
|
+
* @constructor
|
|
32
|
+
*/
|
|
33
|
+
constructor(useNodeMaterial = false) {
|
|
34
|
+
super();
|
|
35
|
+
/**
|
|
36
|
+
* 是否使用TSL的NodeMaterial,默认false
|
|
37
|
+
* @type {boolean}
|
|
38
|
+
* @readonly
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
this.useNodeMaterial = useNodeMaterial;
|
|
42
|
+
/**
|
|
43
|
+
* 对象类型标识
|
|
44
|
+
* @type {string}
|
|
45
|
+
* @readonly
|
|
46
|
+
*/
|
|
47
|
+
this.type = "InstancedSpriteCollection";
|
|
48
|
+
// this.frustumCulled = false;
|
|
49
|
+
/**
|
|
50
|
+
* @type {Array<InstancedSprite>}
|
|
51
|
+
* @ignore
|
|
52
|
+
*/
|
|
53
|
+
this._instancedSprites = [];
|
|
54
|
+
/**
|
|
55
|
+
* @type {Map<HTMLImageElement|HTMLCanvasElement, InstancedSpriteMesh>}
|
|
56
|
+
* @ignore
|
|
57
|
+
*/
|
|
58
|
+
this._meshes = new Map();
|
|
59
|
+
this._depthTest = true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* InstancedSpriteCollection对象标识
|
|
63
|
+
* @type {boolean}
|
|
64
|
+
* @readonly
|
|
65
|
+
*/
|
|
66
|
+
get isInstancedSpriteCollection() { return true; }
|
|
67
|
+
/**
|
|
68
|
+
* 深度测试开关,默认为true,开启后会进行深度测试以正确处理遮挡关系,但可能会有性能影响;如果关闭则所有InstancedSprite都会被渲染在最前面,适合需要始终显示的UI元素等场景
|
|
69
|
+
* @type {boolean}
|
|
70
|
+
* @default true
|
|
71
|
+
*/
|
|
72
|
+
get depthTest() { return this._depthTest; }
|
|
73
|
+
set depthTest(value) {
|
|
74
|
+
Check.typeOf.boolean(value, 'depthTest');
|
|
75
|
+
this._depthTest = value;
|
|
76
|
+
const meshes = this._meshes.values();
|
|
77
|
+
for (const mesh of meshes) {
|
|
78
|
+
mesh.depthTest = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @param {string|HTMLImageElement|HTMLCanvasElement} source
|
|
83
|
+
* @param {InstancedSprite} sprite
|
|
84
|
+
*/
|
|
85
|
+
_setImage(source, sprite) {
|
|
86
|
+
if (source instanceof HTMLImageElement || source instanceof HTMLCanvasElement) {
|
|
87
|
+
if (!source.src)
|
|
88
|
+
source.src = MathUtils.generateUUID();
|
|
89
|
+
if (!imageCache.has(source.src))
|
|
90
|
+
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
|
+
}
|
|
120
|
+
} else if (typeof source === 'string') {
|
|
121
|
+
let image = imageCache.get(source);
|
|
122
|
+
if (!image) {
|
|
123
|
+
image = new Image();
|
|
124
|
+
image.src = source;
|
|
125
|
+
imageCache.set(source, image);
|
|
126
|
+
}
|
|
127
|
+
this._setImage(image, sprite);
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error('Invalid image source type ' + source);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* InstancedSprite实例数组
|
|
134
|
+
* @type {Array<InstancedSprite>}
|
|
135
|
+
* @readonly
|
|
136
|
+
*/
|
|
137
|
+
get instancedSprites() {
|
|
138
|
+
return this._instancedSprites;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* InstancedSprite实例数量
|
|
142
|
+
* @type {number}
|
|
143
|
+
* @readonly
|
|
144
|
+
*/
|
|
145
|
+
get size() {
|
|
146
|
+
return this._instancedSprites.length;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 根据索引获取InstancedSprite实例
|
|
150
|
+
* @param {number} index
|
|
151
|
+
* @returns {InstancedSprite|undefined}
|
|
152
|
+
*/
|
|
153
|
+
get(index) {
|
|
154
|
+
return this._instancedSprites[index];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 根据uuid获取InstancedSprite实例
|
|
158
|
+
* @param {string} uuid
|
|
159
|
+
* @returns {InstancedSprite|undefined}
|
|
160
|
+
*/
|
|
161
|
+
getByUuid(uuid) {
|
|
162
|
+
for (const sprite of this._instancedSprites) {
|
|
163
|
+
if (sprite.uuid === uuid)
|
|
164
|
+
return sprite;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 添加InstancedSprite
|
|
169
|
+
* @type {InstancedSpriteConstructorOptions} options
|
|
170
|
+
* @returns {InstancedSprite}
|
|
171
|
+
*/
|
|
172
|
+
add(options = {}) {
|
|
173
|
+
const sprite = new InstancedSprite(options, this);
|
|
174
|
+
this._instancedSprites.push(sprite);
|
|
175
|
+
return sprite;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 移除InstancedSprite
|
|
179
|
+
* @param {InstancedSprite} sprite
|
|
180
|
+
* @returns {InstancedSpriteCollection}
|
|
181
|
+
*/
|
|
182
|
+
remove(sprite) {
|
|
183
|
+
const index = this._instancedSprites.indexOf(sprite);
|
|
184
|
+
if (index !== -1) {
|
|
185
|
+
this._instancedSprites.splice(index, 1);
|
|
186
|
+
sprite._remove();
|
|
187
|
+
}
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 遍历所有的InstancedSprite
|
|
192
|
+
* @param {Function} callback
|
|
193
|
+
* @returns {InstancedSpriteCollection}
|
|
194
|
+
*/
|
|
195
|
+
forEach(callback) {
|
|
196
|
+
this._instancedSprites.forEach(callback);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Computes intersection points between a casted ray and this sprite.
|
|
201
|
+
*
|
|
202
|
+
* @param {Raycaster} raycaster - The raycaster.
|
|
203
|
+
* @param {Array<Object>} intersects - The target array that holds the intersection points.
|
|
204
|
+
*/
|
|
205
|
+
raycast(raycaster, intersects) {
|
|
206
|
+
const meshes = this._meshes.values();
|
|
207
|
+
for (const mesh of meshes) {
|
|
208
|
+
mesh.raycast(raycaster, intersects);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 每帧更新
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
update() {
|
|
216
|
+
if (!this.visible) return;
|
|
217
|
+
|
|
218
|
+
const meshes = this._meshes.values();
|
|
219
|
+
for (const mesh of meshes) {
|
|
220
|
+
mesh.update();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default InstancedSpriteCollection;
|
|
226
|
+
export { InstancedSpriteCollection };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ShaderMaterial, Texture } from "three";
|
|
2
|
+
|
|
3
|
+
const vertexShader = `
|
|
4
|
+
// Three.js ShaderMaterial专用,自动注入uv、position、modelViewMatrix、projectionMatrix
|
|
5
|
+
precision highp float;
|
|
6
|
+
precision highp int;
|
|
7
|
+
|
|
8
|
+
#include <common>
|
|
9
|
+
#include <logdepthbuf_pars_vertex>
|
|
10
|
+
|
|
11
|
+
// 基础属性
|
|
12
|
+
attribute vec4 aPositionAndShow; // xyz: InstancedSprite位置(世界空间),w:显示标志(0/1)
|
|
13
|
+
attribute vec4 aCenterAndSize; // xy: 锚点中心(0-1), zw: 图片宽高(像素/米)
|
|
14
|
+
attribute vec3 aScaleAndRotationAndSizeAttenuation; // x: 缩放, y: 旋转(弧度), z: 大小跟随相机深度(0/1)
|
|
15
|
+
attribute vec4 aColorAndOpacity; // RGBA颜色
|
|
16
|
+
|
|
17
|
+
varying float vShow;
|
|
18
|
+
varying vec2 vUv;
|
|
19
|
+
varying vec4 vColorAndOpacity;
|
|
20
|
+
|
|
21
|
+
void main() {
|
|
22
|
+
// --- 1. 提前解包属性(GPU会自动优化,无性能损失)---
|
|
23
|
+
float show = aPositionAndShow.w;
|
|
24
|
+
float scale = aScaleAndRotationAndSizeAttenuation.x;
|
|
25
|
+
float rotation = aScaleAndRotationAndSizeAttenuation.y;
|
|
26
|
+
float sizeAttenuation = aScaleAndRotationAndSizeAttenuation.z;
|
|
27
|
+
vec2 center = aCenterAndSize.xy;
|
|
28
|
+
vec2 imageSize = aCenterAndSize.zw;
|
|
29
|
+
|
|
30
|
+
// --- 2. 输出varying变量 ---
|
|
31
|
+
vShow = show;
|
|
32
|
+
vUv = uv;
|
|
33
|
+
vColorAndOpacity = aColorAndOpacity;
|
|
34
|
+
|
|
35
|
+
// --- 3. 提前隐藏不可见物体(顶点级丢弃,性能最优)---
|
|
36
|
+
if (show < 0.5 || imageSize.x <= 0.0 || imageSize.y <= 0.0) {
|
|
37
|
+
vShow = 0.0;
|
|
38
|
+
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- 4. 计算模型视图位置 ---
|
|
43
|
+
vec4 mvPosition = modelViewMatrix * vec4(aPositionAndShow.xyz, 1.0);
|
|
44
|
+
|
|
45
|
+
// --- 5. 深度检查:隐藏相机后面的物体,避免尺寸翻转 ---
|
|
46
|
+
float depth = -mvPosition.z;
|
|
47
|
+
if (depth <= 0.0) {
|
|
48
|
+
vShow = 0.0;
|
|
49
|
+
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- 6. sizeAttenuation检查:大小跟随深度
|
|
54
|
+
if (sizeAttenuation < 0.5) {
|
|
55
|
+
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
|
|
56
|
+
if ( isPerspective ) scale *= depth;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- 7. 坐标对齐
|
|
60
|
+
float aspectRatio = imageSize.x / imageSize.y;
|
|
61
|
+
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * aspectRatio * scale;
|
|
62
|
+
|
|
63
|
+
// --- 8. 计算旋转
|
|
64
|
+
vec2 rotatedPosition;
|
|
65
|
+
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
|
|
66
|
+
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
|
|
67
|
+
mvPosition.xy += rotatedPosition;
|
|
68
|
+
|
|
69
|
+
// --- 9. 投影变换 ---
|
|
70
|
+
gl_Position = projectionMatrix * mvPosition;
|
|
71
|
+
|
|
72
|
+
#include <logdepthbuf_vertex>
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const fragmentShader = `
|
|
77
|
+
uniform sampler2D uTexture;
|
|
78
|
+
|
|
79
|
+
#include <common>
|
|
80
|
+
#include <logdepthbuf_pars_fragment>
|
|
81
|
+
|
|
82
|
+
varying float vShow;
|
|
83
|
+
varying vec2 vUv;
|
|
84
|
+
varying vec4 vColorAndOpacity;
|
|
85
|
+
|
|
86
|
+
void main() {
|
|
87
|
+
vec4 texColor = texture2D(uTexture, vUv);
|
|
88
|
+
vec4 diffuseColor = texColor * vColorAndOpacity;
|
|
89
|
+
|
|
90
|
+
// 丢弃完全透明的像素
|
|
91
|
+
if (diffuseColor.a < 0.005 || vShow < 0.5) {
|
|
92
|
+
discard;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
gl_FragColor = diffuseColor;
|
|
96
|
+
|
|
97
|
+
#include <logdepthbuf_fragment>
|
|
98
|
+
#include <tonemapping_fragment>
|
|
99
|
+
#include <colorspace_fragment>
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* InstancedSprite 材质类,以attribute形式传入InstancedSprite对象实例属性
|
|
105
|
+
*
|
|
106
|
+
* @extends ShaderMaterial
|
|
107
|
+
*/
|
|
108
|
+
class InstancedSpriteMaterial extends ShaderMaterial {
|
|
109
|
+
constructor() {
|
|
110
|
+
super({
|
|
111
|
+
vertexShader,
|
|
112
|
+
fragmentShader,
|
|
113
|
+
uniforms: {
|
|
114
|
+
uTexture: { value: null },
|
|
115
|
+
},
|
|
116
|
+
transparent: true,
|
|
117
|
+
depthTest: true,
|
|
118
|
+
depthWrite: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 图片纹理
|
|
123
|
+
* @type {Texture}
|
|
124
|
+
*/
|
|
125
|
+
set texture(value) {
|
|
126
|
+
this.uniforms.uTexture.value = value;
|
|
127
|
+
}
|
|
128
|
+
get texture() {
|
|
129
|
+
return this.uniforms.uTexture.value;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default InstancedSpriteMaterial;
|
|
134
|
+
export { InstancedSpriteMaterial };
|