@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.
@@ -0,0 +1,406 @@
1
+ import Check from "@lijuhong1981/jscheck/src/Check.js";
2
+ import { BufferAttribute, InstancedBufferAttribute, InstancedBufferGeometry, Mesh, Raycaster, Texture } from "three";
3
+ import InstancedSprite from "./InstancedSprite.js";
4
+ import InstancedSpriteMaterial from "./InstancedSpriteMaterial.js";
5
+ import InstancedSpriteNodeMaterial from "./InstancedSpriteNodeMaterial.js";
6
+
7
+ /**
8
+ * @import InstancedSpriteCollection from "./InstancedSpriteCollection.js";
9
+ */
10
+
11
+ /**
12
+ * 顶点数据,定义了一个单位正方形的四个顶点位置和UV坐标,以及组成两个三角形的索引
13
+ * @ignore
14
+ */
15
+ const positions = new Float32Array([
16
+ -0.5, -0.5, 0,
17
+ 0.5, -0.5, 0,
18
+ 0.5, 0.5, 0,
19
+ -0.5, 0.5, 0,
20
+ ]);
21
+ const uvs = new Float32Array([
22
+ 0, 0,
23
+ 1, 0,
24
+ 1, 1,
25
+ 0, 1,
26
+ ]);
27
+ const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
28
+ const geometry = new InstancedBufferGeometry();
29
+ geometry.setAttribute("position", new BufferAttribute(positions, 3));
30
+ geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
31
+ geometry.setIndex(new BufferAttribute(indices, 1));
32
+ // geometry.computeVertexNormals();
33
+
34
+ // --- 定义InstancedSpriteMaterial使用的attribute名称常量 ---
35
+ const aPositionAndShow = 'aPositionAndShow';
36
+ const aCenterAndSize = 'aCenterAndSize';
37
+ const aScaleAndRotationAndSizeAttenuation = 'aScaleAndRotationAndSizeAttenuation';
38
+ const aColorAndOpacity = 'aColorAndOpacity';
39
+ /**
40
+ * InstancedSpriteMaterial使用的attribute名称常量与itemSize大小,InstancedSpriteMesh会根据名称和itemSize创建InstancedBufferAttribute
41
+ * @type {Object<string, number>}
42
+ * @readonly
43
+ * @ignore
44
+ */
45
+ const AttributesItemSize = Object.freeze({
46
+ aPositionAndShow: 4,
47
+ aCenterAndSize: 4,
48
+ aScaleAndRotationAndSizeAttenuation: 3,
49
+ aColorAndOpacity: 4,
50
+ });
51
+ /**
52
+ * 检查并更新一个InstancedSprite实例的数据到对应的InstancedBufferAttribute位置
53
+ * @param {InstancedSpriteMesh} mesh
54
+ * @param {InstancedSprite} sprite
55
+ * @ignore
56
+ */
57
+ function checkAndUpdateInstancedSprite(mesh, sprite) {
58
+ const attributesData = mesh._attributesData;
59
+ const index = sprite._instanceIndex;
60
+
61
+ const positionChanged = (sprite._position.equals(sprite.position) === false);
62
+ const showChanged = (sprite._show !== sprite.show);
63
+ const centerChanged = (sprite._center.equals(sprite.center) === false);
64
+ const imageSizeChanged = (sprite._imageSize.equals(sprite.imageSize) === false);
65
+ const scaleChanged = (sprite._scale !== sprite.scale);
66
+ const rotationChanged = (sprite._rotation !== sprite.rotation);
67
+ const sizeAttenuationChanged = (sprite._sizeAttenuation !== sprite.sizeAttenuation);
68
+ const colorChanged = (sprite._color.equals(sprite.color) === false);
69
+ const opacityChanged = (sprite._opacity !== sprite.opacity);
70
+
71
+ let idx = 0;
72
+
73
+ if (positionChanged || showChanged) {
74
+ idx = index * AttributesItemSize.aPositionAndShow;
75
+ if (positionChanged) {
76
+ sprite._position.copy(sprite.position);
77
+ attributesData.aPositionAndShow[idx] = sprite._position.x;
78
+ attributesData.aPositionAndShow[idx + 1] = sprite._position.y;
79
+ attributesData.aPositionAndShow[idx + 2] = sprite._position.z;
80
+ }
81
+ if (showChanged) {
82
+ sprite._show = sprite.show;
83
+ attributesData.aPositionAndShow[idx + 3] = sprite._show ? 1 : 0;
84
+ }
85
+ mesh.geometry.attributes.aPositionAndShow.needsUpdate = true;
86
+ }
87
+
88
+ if (centerChanged || imageSizeChanged) {
89
+ idx = index * AttributesItemSize.aCenterAndSize;
90
+ if (centerChanged) {
91
+ sprite._center.copy(sprite.center);
92
+ attributesData.aCenterAndSize[idx] = sprite._center.x;
93
+ attributesData.aCenterAndSize[idx + 1] = sprite._center.y;
94
+ }
95
+ if (imageSizeChanged) {
96
+ sprite._imageSize.copy(sprite.imageSize);
97
+ attributesData.aCenterAndSize[idx + 2] = sprite._imageSize.x;
98
+ attributesData.aCenterAndSize[idx + 3] = sprite._imageSize.y;
99
+ }
100
+ mesh.geometry.attributes.aCenterAndSize.needsUpdate = true;
101
+ }
102
+
103
+ if (scaleChanged || rotationChanged || sizeAttenuationChanged) {
104
+ idx = index * AttributesItemSize.aScaleAndRotationAndSizeAttenuation;
105
+ if (scaleChanged) {
106
+ sprite._scale = sprite.scale;
107
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx] = sprite._scale;
108
+ }
109
+ if (rotationChanged) {
110
+ sprite._rotation = sprite.rotation;
111
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx + 1] = sprite._rotation;
112
+ }
113
+ if (sizeAttenuationChanged) {
114
+ sprite._sizeAttenuation = sprite.sizeAttenuation;
115
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx + 2] = sprite._sizeAttenuation ? 1 : 0;
116
+ }
117
+ mesh.geometry.attributes.aScaleAndRotationAndSizeAttenuation.needsUpdate = true;
118
+ }
119
+
120
+ if (colorChanged || opacityChanged) {
121
+ idx = index * AttributesItemSize.aColorAndOpacity;
122
+ if (colorChanged) {
123
+ sprite._color.copy(sprite.color);
124
+ attributesData.aColorAndOpacity[idx] = sprite._color.r;
125
+ attributesData.aColorAndOpacity[idx + 1] = sprite._color.g;
126
+ attributesData.aColorAndOpacity[idx + 2] = sprite._color.b;
127
+ }
128
+ if (opacityChanged) {
129
+ sprite._opacity = sprite.opacity;
130
+ attributesData.aColorAndOpacity[idx + 3] = sprite._opacity;
131
+ }
132
+ mesh.geometry.attributes.aColorAndOpacity.needsUpdate = true;
133
+ }
134
+ };
135
+ /**
136
+ * 添加一个InstancedSprite实例的数据到InstancedBufferAttribute中对应的位置
137
+ * @param {InstancedSpriteMesh} mesh
138
+ * @param {InstancedSprite} sprite
139
+ * @param {number} index
140
+ * @ignore
141
+ */
142
+ function addInstancedSprite(mesh, sprite, index) {
143
+ const attributesData = mesh._attributesData;
144
+ sprite._instanceIndex = index;
145
+ let idx = 0;
146
+
147
+ idx = index * AttributesItemSize.aPositionAndShow;
148
+ sprite._position.copy(sprite.position);
149
+ attributesData.aPositionAndShow[idx] = sprite._position.x;
150
+ attributesData.aPositionAndShow[idx + 1] = sprite._position.y;
151
+ attributesData.aPositionAndShow[idx + 2] = sprite._position.z;
152
+ sprite._show = sprite.show;
153
+ attributesData.aPositionAndShow[idx + 3] = sprite._show ? 1 : 0;
154
+ mesh.geometry.attributes.aPositionAndShow.needsUpdate = true;
155
+
156
+ idx = index * AttributesItemSize.aCenterAndSize;
157
+ sprite._center.copy(sprite.center);
158
+ attributesData.aCenterAndSize[idx] = sprite._center.x;
159
+ attributesData.aCenterAndSize[idx + 1] = sprite._center.y;
160
+ sprite._imageSize.copy(sprite.imageSize);
161
+ attributesData.aCenterAndSize[idx + 2] = sprite._imageSize.x;
162
+ attributesData.aCenterAndSize[idx + 3] = sprite._imageSize.y;
163
+ mesh.geometry.attributes.aCenterAndSize.needsUpdate = true;
164
+
165
+ idx = index * AttributesItemSize.aScaleAndRotationAndSizeAttenuation;
166
+ sprite._scale = sprite.scale;
167
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx] = sprite._scale;
168
+ sprite._rotation = sprite.rotation;
169
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx + 1] = sprite._rotation;
170
+ sprite._sizeAttenuation = sprite.sizeAttenuation;
171
+ attributesData.aScaleAndRotationAndSizeAttenuation[idx + 2] = sprite._sizeAttenuation ? 1 : 0;
172
+ mesh.geometry.attributes.aScaleAndRotationAndSizeAttenuation.needsUpdate = true;
173
+
174
+ idx = index * AttributesItemSize.aColorAndOpacity;
175
+ sprite._color.copy(sprite.color);
176
+ attributesData.aColorAndOpacity[idx] = sprite._color.r;
177
+ attributesData.aColorAndOpacity[idx + 1] = sprite._color.g;
178
+ attributesData.aColorAndOpacity[idx + 2] = sprite._color.b;
179
+ sprite._opacity = sprite.opacity;
180
+ attributesData.aColorAndOpacity[idx + 3] = sprite._opacity;
181
+ mesh.geometry.attributes.aColorAndOpacity.needsUpdate = true;
182
+ };
183
+
184
+ /**
185
+ * InstancedSpriteMesh类,基于InstancedBufferGeometry实现的高性能InstancedSprite渲染组件
186
+ *
187
+ * @extends {Mesh}
188
+ */
189
+ class InstancedSpriteMesh extends Mesh {
190
+ /**
191
+ * @param {InstancedSpriteCollection} collection - 所属的InstancedSpriteCollection实例,必填
192
+ * @param {Texture} texture - 材质图像纹理,必填
193
+ * @constructor
194
+ */
195
+ constructor(collection, texture) {
196
+ Check.defined('collection', collection);
197
+ Check.defined('texture', texture);
198
+ super(geometry.clone(), collection.useNodeMaterial ? new InstancedSpriteNodeMaterial() : new InstancedSpriteMaterial());
199
+ this._collection = collection; //所属的InstancedSpriteCollection实例
200
+ this.material.texture = texture;
201
+ /**
202
+ * 对象类型标识
203
+ * @type {string}
204
+ * @readonly
205
+ */
206
+ this.type = "InstancedSpriteMesh";
207
+ /**
208
+ * @type {Array<InstancedSprite>}
209
+ * @ignore
210
+ */
211
+ this._instancedSprites = [];
212
+ /**
213
+ * @type {Array<InstancedSprite>}
214
+ * @ignore
215
+ */
216
+ this._addInstancedSprites = [];
217
+ /**
218
+ * @type {Array<InstancedSprite>}
219
+ * @ignore
220
+ */
221
+ this._removeInstancedSprites = [];
222
+ /**
223
+ * InstancedBufferAttribute数据存储对象,key为属性名称,value为对应的Float32Array数组
224
+ * @type {Object<string, Float32Array>}
225
+ * @ignore
226
+ */
227
+ this._attributesData = {};
228
+ /**
229
+ * 当前能容纳的最大InstancedSprite数量,初始为0,InstancedSpriteMesh会根据需要动态扩容
230
+ * @type {number}
231
+ * @ignore
232
+ */
233
+ this._maxCapacity = 0;
234
+ this._enaureCapacity(64);
235
+ }
236
+ /**
237
+ * InstancedSpriteMesh对象标识
238
+ * @type {boolean}
239
+ * @readonly
240
+ */
241
+ get isInstancedSpriteMesh() { return true; }
242
+ /**
243
+ * 深度测试开关,默认为true,开启后会进行深度测试以正确处理遮挡关系,但可能会有性能影响;如果关闭则所有InstancedSprite都会被渲染在最前面,适合需要始终显示的UI元素等场景
244
+ * @type {boolean}
245
+ * @default true
246
+ */
247
+ get depthTest() { return this.material.depthTest; }
248
+ set depthTest(value) {
249
+ Check.typeOf.boolean('depthTest', value);
250
+ this.material.depthTest = value;
251
+ }
252
+ /**
253
+ * InstancedSprite对象数组
254
+ * @type {Array<InstancedSprite>}
255
+ * @readonly
256
+ */
257
+ get instancedSprites() { return this._instancedSprites; }
258
+ /**
259
+ * 确保InstancedBufferAttribute的大小能够容纳指定数量的InstancedSprite实例,如果当前最大容量不足,则进行扩容
260
+ * @param {number} required - 需要容纳的InstancedSprite数量
261
+ * @private
262
+ */
263
+ _enaureCapacity(required) {
264
+ if (required < this._maxCapacity) return;
265
+
266
+ const newCapacity = Math.max(required, this._maxCapacity + 64);
267
+
268
+ for (const [name, itemSize] of Object.entries(AttributesItemSize)) {
269
+ const newArray = new Float32Array(newCapacity * itemSize);
270
+ const oldArray = this._attributesData[name];
271
+ if (oldArray) {
272
+ const copyLen = Math.min(oldArray.length, newArray.length);
273
+ newArray.set(oldArray.subarray(0, copyLen));
274
+ }
275
+ this._attributesData[name] = newArray;
276
+ }
277
+
278
+ // 创建 InstancedBufferAttribute 对象
279
+ for (const [name, itemSize] of Object.entries(AttributesItemSize)) {
280
+ const attr = new InstancedBufferAttribute(this._attributesData[name], itemSize);
281
+ this.geometry.setAttribute(name, attr);
282
+ }
283
+
284
+ this.geometry.instanceCount = required;
285
+ this._maxCapacity = newCapacity;
286
+ }
287
+ /**
288
+ * 添加一个InstancedSprite
289
+ * @param {InstancedSprite} sprite - 要添加的InstancedSprite实例
290
+ * @returns {InstancedSpriteMesh}
291
+ */
292
+ add(sprite) {
293
+ sprite._mesh = this;
294
+ this._addInstancedSprites.push(sprite);
295
+ return this;
296
+ }
297
+ /**
298
+ * 移除一个InstancedSprite
299
+ * @param {InstancedSprite} sprite - 要移除的InstancedSprite实例
300
+ * @returns {InstancedSpriteMesh}
301
+ */
302
+ remove(sprite) {
303
+ if (this._instancedSprites.includes(sprite) && !this._removeInstancedSprites.includes(sprite))
304
+ this._removeInstancedSprites.push(sprite);
305
+ else
306
+ console.warn('InstancedSprite not found in InstancedSpriteMesh or already marked for removal');
307
+ return this;
308
+ }
309
+ /**
310
+ * 移除所有InstancedSprite
311
+ * @returns {InstancedSpriteMesh}
312
+ */
313
+ clear() {
314
+ this._addInstancedSprites.length = 0;
315
+ this._removeInstancedSprites.length = 0;
316
+ this._instancedSprites.length = 0;
317
+ this.geometry.instanceCount = 0;
318
+ return this;
319
+ }
320
+ /**
321
+ * Computes intersection points between a casted ray and this sprite.
322
+ *
323
+ * @param {Raycaster} raycaster - The raycaster.
324
+ * @param {Array<Object>} intersects - The target array that holds the intersection points.
325
+ */
326
+ raycast(raycaster, intersects) {
327
+
328
+ if (raycaster.camera === null) {
329
+
330
+ console.error('InstancedSpriteMesh: "Raycaster.camera" needs to be set in order to raycast against sprites.');
331
+ return;
332
+
333
+ }
334
+
335
+ const sprites = this._instancedSprites;
336
+ for (const sprite of sprites) {
337
+ sprite.raycast(raycaster, intersects, raycaster.camera.matrixWorldInverse);
338
+ }
339
+ }
340
+ /**
341
+ * 每帧更新
342
+ * @private
343
+ */
344
+ update() {
345
+ if (!this.visible) return;
346
+
347
+ const removeInstancedSprites = this._removeInstancedSprites;
348
+ const addInstancedSprites = this._addInstancedSprites;
349
+ const instancedSprites = this._instancedSprites;
350
+
351
+ // 计算最终需要的容量
352
+ const requiredCapacity = instancedSprites.length + addInstancedSprites.length - removeInstancedSprites.length;
353
+ this._enaureCapacity(requiredCapacity);
354
+
355
+ if (removeInstancedSprites.length > 0) {
356
+ // 如果有需要移除的InstancedSprite,先从instancedSprites数组中移除
357
+ for (const sprite of removeInstancedSprites) {
358
+ const index = instancedSprites.indexOf(sprite);
359
+ if (index !== -1) {
360
+ instancedSprites.splice(index, 1); //从instancedSprites数组中移除
361
+ sprite._mesh = undefined;
362
+ sprite._instanceIndex = -1;
363
+ }
364
+ // else
365
+ // console.warn('InstancedSprite not found in InstancedSpriteMesh during removal');
366
+ }
367
+ removeInstancedSprites.length = 0;
368
+
369
+ // 如果有需要添加的InstancedSprite,则加入instancedSprites数组中
370
+ if (addInstancedSprites.length > 0) {
371
+ instancedSprites.push(...addInstancedSprites);
372
+ addInstancedSprites.length = 0;
373
+ }
374
+
375
+ // 重新写入所有InstancedSprite的属性数据到InstancedBufferAttribute中
376
+ const length = instancedSprites.length;
377
+ for (let i = 0; i < length; i++) {
378
+ const sprite = instancedSprites[i];
379
+ addInstancedSprite(this, sprite, i);
380
+ }
381
+ } else {
382
+ // 检查并更新属性值有变化的InstancedSprite
383
+ for (const sprite of instancedSprites) {
384
+ checkAndUpdateInstancedSprite(this, sprite);
385
+ }
386
+
387
+ // 添加InstancedSprite
388
+ while (addInstancedSprites.length > 0) {
389
+ const sprite = addInstancedSprites.shift();
390
+ addInstancedSprite(this, sprite, instancedSprites.length);
391
+ instancedSprites.push(sprite);
392
+ }
393
+ }
394
+
395
+ this.geometry.instanceCount = instancedSprites.length; //更新instanceCount
396
+ }
397
+ dispose() {
398
+ this.clear();
399
+ this.geometry.dispose();
400
+ this.material.dispose();
401
+ }
402
+ };
403
+
404
+ export default InstancedSpriteMesh;
405
+ export { InstancedSpriteMesh };
406
+
@@ -0,0 +1,148 @@
1
+ import { attribute, cameraProjectionMatrix, cos, Discard, float, Fn, If, modelViewMatrix, positionGeometry, sin, texture, uniformTexture, uv, varying, vec2, vec4, } from 'three/tsl';
2
+ import { NodeMaterial, Texture } from 'three/webgpu';
3
+
4
+ /**
5
+ * InstancedSpriteNodeMaterial 材质类,基于 Three.js TSL (Three Shading Language) 语法实现
6
+ *
7
+ * 功能与 InstancedSpriteMaterial (ShaderMaterial) 完全相同,但使用 TSL 节点系统构建,
8
+ * 可更好地与 Three.js 的 NodeMaterial 管线集成(自动处理色调映射、色彩空间转换等)。
9
+ *
10
+ * **注意**:使用此类需要 Three.js 的 WebGPU/TSL 构建(`three/webgpu`),非标准 `three` 构建。
11
+ *
12
+ * @extends NodeMaterial
13
+ */
14
+ class InstancedSpriteNodeMaterial extends NodeMaterial {
15
+ constructor() {
16
+ super();
17
+
18
+ // --- 材质属性 ---
19
+ this.transparent = true;
20
+ this.depthTest = true;
21
+ this.depthWrite = true;
22
+
23
+ // 关闭 NodeMaterial 默认的雾效/光照,与原始 ShaderMaterial 行为一致
24
+ this.fog = false;
25
+ this.lights = false;
26
+
27
+ // 纹理 Uniform 节点(可延迟赋值,使用空纹理保证节点类型稳定)
28
+ this._textureNode = uniformTexture();
29
+
30
+ this._setupShader();
31
+ }
32
+
33
+ /**
34
+ * 使用 TSL 节点系统构建顶点/片元着色器
35
+ * @private
36
+ */
37
+ _setupShader() {
38
+ // ============================================================
39
+ // 实例属性 (InstancedBufferAttribute) 节点
40
+ // ============================================================
41
+ const aPositionAndShow = attribute('aPositionAndShow', 'vec4');
42
+ const aCenterAndSize = attribute('aCenterAndSize', 'vec4');
43
+ const aScaleAndRotationAndSizeAttenuation = attribute('aScaleAndRotationAndSizeAttenuation', 'vec3');
44
+ const aColorAndOpacity = attribute('aColorAndOpacity', 'vec4');
45
+
46
+ // ============================================================
47
+ // Varying 变量 — 从顶点着色器传递到片元着色器
48
+ // ============================================================
49
+ const vShow = varying(float(1));
50
+ const vColorAndOpacity = varying(vec4(1));
51
+ const vUv = varying(vec2(1));
52
+
53
+ // ============================================================
54
+ // 顶点着色器 (vertexNode)
55
+ // ============================================================
56
+ this.vertexNode = Fn(() => {
57
+ // --- 1. 解包实例属性 ---
58
+ const show = aPositionAndShow.w;
59
+ const scale = aScaleAndRotationAndSizeAttenuation.x;
60
+ const rotation = aScaleAndRotationAndSizeAttenuation.y;
61
+ const sizeAttenuation = aScaleAndRotationAndSizeAttenuation.z;
62
+ const center = aCenterAndSize.xy;
63
+ const imageSize = aCenterAndSize.zw;
64
+
65
+ // --- 2. 输出 varying ---
66
+ vShow.assign(show);
67
+ vColorAndOpacity.assign(aColorAndOpacity);
68
+ vUv.assign(uv());
69
+
70
+ // --- 3. 计算模型视图位置 ---
71
+ const mvPosition = modelViewMatrix.mul(vec4(aPositionAndShow.xyz, 1));
72
+ const depth = mvPosition.z.negate();
73
+
74
+ // 默认把顶点放到裁剪空间外;只有有效实例才覆盖为真实位置。
75
+ // 避免在 TSL 的 If callback 中依赖 JS return,也避免 imageSize.y 为 0 时继续做除法。
76
+ const clipPosition = vec4(0, 0, 2, 1).toVar('clipPosition');
77
+ const isVisible = show.greaterThanEqual(0.5)
78
+ .and(imageSize.x.greaterThan(0))
79
+ .and(imageSize.y.greaterThan(0))
80
+ .and(depth.greaterThan(0));
81
+
82
+ If(isVisible, () => {
83
+ // --- 4. sizeAttenuation:大小跟随相机深度 ---
84
+ // 检查是否为透视投影: 透视投影矩阵中 m[2][3] (elements[11]) == -1
85
+ const scaleFactor = sizeAttenuation.lessThan(0.5).and(cameraProjectionMatrix.element(11).equal(-1))
86
+ .select(scale.mul(depth), scale);
87
+
88
+ // --- 5. 坐标对齐 ---
89
+ // 原始GLSL: (position.xy - (center - vec2(0.5))) * aspectRatio * scale
90
+ const aspectRatio = imageSize.x.div(imageSize.y);
91
+ const alignedPosition = positionGeometry.xy
92
+ .sub(center.sub(vec2(0.5)))
93
+ .mul(aspectRatio)
94
+ .mul(scaleFactor);
95
+
96
+ // --- 6. 计算旋转 ---
97
+ // 原始GLSL: cos(rotation) * x - sin(rotation) * y
98
+ const cosR = cos(rotation);
99
+ const sinR = sin(rotation);
100
+ const rotatedX = cosR.mul(alignedPosition.x).sub(sinR.mul(alignedPosition.y));
101
+ const rotatedY = sinR.mul(alignedPosition.x).add(cosR.mul(alignedPosition.y));
102
+
103
+ // --- 7. 将旋转偏移叠加到视图空间位置 ---
104
+ const finalMvPosition = vec4(
105
+ mvPosition.x.add(rotatedX),
106
+ mvPosition.y.add(rotatedY),
107
+ mvPosition.z,
108
+ mvPosition.w
109
+ );
110
+
111
+ // --- 8. 投影变换(返回 clip-space 坐标)---
112
+ // 使用 cameraProjectionMatrix 替代原始GLSL中的 projectionMatrix
113
+ clipPosition.assign(cameraProjectionMatrix.mul(finalMvPosition));
114
+ }).Else(() => {
115
+ vShow.assign(float(0));
116
+ });
117
+
118
+ return clipPosition;
119
+ }).once();
120
+
121
+ // ============================================================
122
+ // 片元着色器 (fragmentNode)
123
+ // ============================================================
124
+ this.fragmentNode = Fn(() => {
125
+ // 采样纹理
126
+ const texColor = texture(this._textureNode, vUv);
127
+ const diffuseColor = texColor.mul(vColorAndOpacity);
128
+
129
+ // 丢弃完全透明或隐藏的像素
130
+ Discard(diffuseColor.a.lessThan(0.005).or(vShow.lessThan(0.5)));
131
+
132
+ return diffuseColor;
133
+ }).once();
134
+ }
135
+ /**
136
+ * 图片纹理
137
+ * @type {Texture|null}
138
+ */
139
+ get texture() {
140
+ return this._textureNode.value;
141
+ }
142
+ set texture(value) {
143
+ this._textureNode.value = value;
144
+ }
145
+ }
146
+
147
+ export default InstancedSpriteNodeMaterial;
148
+ export { InstancedSpriteNodeMaterial };