babylon.quarks 0.18.0 → 0.19.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/README.md +16 -16
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.ts +1835 -1171
- package/package.json +67 -67
- package/src/BatchedRenderer.ts +213 -14
- package/src/MeshSurfaceEmitter.ts +25 -20
- package/src/ParticleEmitter.ts +34 -10
- package/src/ParticleSystem.ts +467 -203
- package/src/QuarksLoader.ts +68 -15
- package/src/QuarksPrefab.ts +8 -11
- package/src/QuarksUtil.ts +1 -3
- package/src/SpriteBatch.ts +447 -96
- package/src/TrailBatch.ts +361 -164
- package/src/VFXBatch.ts +20 -4
- package/src/envAtlas.ts +136 -0
- package/src/index.ts +34 -29
- package/src/materials/ParticleMaterials.ts +1 -1
- package/src/shaders/index.ts +4 -4
- package/src/shaders/local_particle_physics_vert.wgsl.ts +81 -0
- package/src/shaders/particle_frag.wgsl.ts +64 -0
- package/src/shaders/particle_physics_frag.glsl.ts +56 -1
- package/src/shaders/particle_physics_frag.wgsl.ts +125 -0
- package/src/shaders/particle_vert.wgsl.ts +87 -0
- package/src/shaders/shaderLanguageSupport.ts +54 -0
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +88 -0
- package/src/shaders/trail_frag.wgsl.ts +23 -0
- package/src/shaders/trail_vert.wgsl.ts +63 -0
package/src/SpriteBatch.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
|
-
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
2
|
-
import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
3
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
4
|
-
import {Effect} from '@babylonjs/core/Materials/effect';
|
|
5
|
-
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
6
|
-
import {Scene} from '@babylonjs/core/scene';
|
|
7
2
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
3
|
+
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
4
|
+
import {RawTexture} from '@babylonjs/core/Materials/Textures/rawTexture';
|
|
8
5
|
import {Vector2 as BVector2, Vector3 as BVector3, Vector4 as BVector4} from '@babylonjs/core/Maths/math.vector';
|
|
6
|
+
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
7
|
+
import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData';
|
|
8
|
+
import {Scene} from '@babylonjs/core/scene';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
Vector4,
|
|
12
|
-
Quaternion,
|
|
10
|
+
IParticleSystem,
|
|
13
11
|
Matrix3,
|
|
12
|
+
ParticleStore,
|
|
13
|
+
Quaternion,
|
|
14
14
|
SpriteParticle,
|
|
15
15
|
StretchedBillBoardSettings,
|
|
16
|
+
Vector3,
|
|
16
17
|
} from 'quarks.core';
|
|
17
|
-
import {VFXBatch, RenderMode} from './VFXBatch';
|
|
18
18
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
19
|
-
import
|
|
19
|
+
import local_particle_physics_vert from './shaders/local_particle_physics_vert.glsl';
|
|
20
|
+
import local_particle_physics_vert_wgsl from './shaders/local_particle_physics_vert.wgsl';
|
|
20
21
|
import particle_frag from './shaders/particle_frag.glsl';
|
|
22
|
+
import particle_frag_wgsl from './shaders/particle_frag.wgsl';
|
|
21
23
|
import particle_physics_frag from './shaders/particle_physics_frag.glsl';
|
|
24
|
+
import particle_physics_frag_wgsl from './shaders/particle_physics_frag.wgsl';
|
|
25
|
+
import particle_vert from './shaders/particle_vert.glsl';
|
|
26
|
+
import particle_vert_wgsl from './shaders/particle_vert.wgsl';
|
|
27
|
+
import {registerShaders, shaderLanguageFor, ShaderSources} from './shaders/shaderLanguageSupport';
|
|
22
28
|
import stretched_bb_particle_vert from './shaders/stretched_bb_particle_vert.glsl';
|
|
23
|
-
import
|
|
29
|
+
import stretched_bb_particle_vert_wgsl from './shaders/stretched_bb_particle_vert.wgsl';
|
|
30
|
+
import {RenderMode, VFXBatch} from './VFXBatch';
|
|
24
31
|
|
|
25
32
|
export class SpriteBatch extends VFXBatch {
|
|
33
|
+
private static whiteTextureByScene = new WeakMap<Scene, RawTexture>();
|
|
34
|
+
|
|
35
|
+
/** 1×1 white map so mesh batches always have a sampler2D (iOS WebKit). */
|
|
36
|
+
private static whiteTexture(scene: Scene): RawTexture {
|
|
37
|
+
let texture = SpriteBatch.whiteTextureByScene.get(scene);
|
|
38
|
+
if (!texture) {
|
|
39
|
+
texture = RawTexture.CreateRGBATexture(new Uint8Array([255, 255, 255, 255]), 1, 1, scene);
|
|
40
|
+
texture.name = 'quarksMeshWhite';
|
|
41
|
+
SpriteBatch.whiteTextureByScene.set(scene, texture);
|
|
42
|
+
}
|
|
43
|
+
return texture;
|
|
44
|
+
}
|
|
26
45
|
private offsetBuffer!: Float32Array;
|
|
27
46
|
private rotationBuffer!: Float32Array;
|
|
28
47
|
private sizeBuffer!: Float32Array;
|
|
@@ -30,6 +49,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
30
49
|
private uvTileBuffer!: Float32Array;
|
|
31
50
|
private velocityBuffer?: Float32Array;
|
|
32
51
|
|
|
52
|
+
/** Concretely typed handle on the batch material, see syncStretchedSpeedFactor. */
|
|
53
|
+
private shaderMaterial: ShaderMaterial | null = null;
|
|
54
|
+
|
|
33
55
|
private offsetVB!: VertexBuffer;
|
|
34
56
|
private rotationVB!: VertexBuffer;
|
|
35
57
|
private sizeVB!: VertexBuffer;
|
|
@@ -52,8 +74,14 @@ export class SpriteBatch extends VFXBatch {
|
|
|
52
74
|
const vertexData = new VertexData();
|
|
53
75
|
vertexData.positions = this.settings.instancingGeometry;
|
|
54
76
|
vertexData.indices = this.settings.instancingIndices;
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
const vertexCount = this.settings.instancingGeometry.length / 3;
|
|
78
|
+
const uvs = this.settings.instancingUVs;
|
|
79
|
+
// WebGL (especially iOS) raises INVALID_OPERATION if the shader reads
|
|
80
|
+
// `attribute vec2 uv` but the buffer is missing or shorter than vertices.
|
|
81
|
+
if (uvs && uvs.length >= vertexCount * 2) {
|
|
82
|
+
vertexData.uvs = uvs;
|
|
83
|
+
} else {
|
|
84
|
+
vertexData.uvs = new Float32Array(vertexCount * 2);
|
|
57
85
|
}
|
|
58
86
|
if (this.settings.instancingNormals) {
|
|
59
87
|
vertexData.normals = this.settings.instancingNormals;
|
|
@@ -63,6 +91,18 @@ export class SpriteBatch extends VFXBatch {
|
|
|
63
91
|
this.buildExpandableBuffers();
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
/** Releases the instance vertex buffers so a resize does not leak GPU memory. */
|
|
95
|
+
private disposeExpandableBuffers(): void {
|
|
96
|
+
this.offsetVB?.dispose();
|
|
97
|
+
this.colorVB?.dispose();
|
|
98
|
+
this.sizeVB?.dispose();
|
|
99
|
+
this.uvTileVB?.dispose();
|
|
100
|
+
this.rotationVB?.dispose();
|
|
101
|
+
this.velocityVB?.dispose();
|
|
102
|
+
this.velocityVB = undefined;
|
|
103
|
+
this.velocityBuffer = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
66
106
|
private buildExpandableBuffers(): void {
|
|
67
107
|
const engine = this.scene.getEngine();
|
|
68
108
|
|
|
@@ -105,30 +145,51 @@ export class SpriteBatch extends VFXBatch {
|
|
|
105
145
|
while (target >= this.maxParticles) {
|
|
106
146
|
this.maxParticles *= 2;
|
|
107
147
|
}
|
|
148
|
+
this.disposeExpandableBuffers();
|
|
108
149
|
this.buildExpandableBuffers();
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
rebuildMaterial(): void {
|
|
112
|
-
|
|
153
|
+
// The shader sources only depend on the render mode, so the store entry is
|
|
154
|
+
// registered once per mode. A unique name per rebuild would leak entries in
|
|
155
|
+
// Effect.ShadersStore and defeat Babylon's compiled-effect cache.
|
|
156
|
+
const shaderName = `quarksParticle_${this.settings.renderMode}`;
|
|
113
157
|
this.lastStretchedSpeedFactor = Number.NaN;
|
|
114
|
-
let vertexShader: string;
|
|
115
|
-
let fragmentShader: string;
|
|
116
158
|
const defines: string[] = [];
|
|
117
159
|
|
|
160
|
+
let vertexShader: ShaderSources;
|
|
161
|
+
let fragmentShader: ShaderSources;
|
|
118
162
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
119
|
-
vertexShader = local_particle_physics_vert;
|
|
120
|
-
fragmentShader = particle_physics_frag;
|
|
163
|
+
vertexShader = {glsl: local_particle_physics_vert, wgsl: local_particle_physics_vert_wgsl};
|
|
164
|
+
fragmentShader = {glsl: particle_physics_frag, wgsl: particle_physics_frag_wgsl};
|
|
121
165
|
} else if (this.settings.renderMode === RenderMode.StretchedBillBoard) {
|
|
122
|
-
vertexShader = stretched_bb_particle_vert;
|
|
123
|
-
fragmentShader = particle_frag;
|
|
166
|
+
vertexShader = {glsl: stretched_bb_particle_vert, wgsl: stretched_bb_particle_vert_wgsl};
|
|
167
|
+
fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
|
|
124
168
|
} else {
|
|
125
|
-
vertexShader = particle_vert;
|
|
126
|
-
fragmentShader = particle_frag;
|
|
169
|
+
vertexShader = {glsl: particle_vert, wgsl: particle_vert_wgsl};
|
|
170
|
+
fragmentShader = {glsl: particle_frag, wgsl: particle_frag_wgsl};
|
|
127
171
|
}
|
|
128
172
|
|
|
129
|
-
|
|
173
|
+
// Mesh batches without a diffuse map still need a sampler2D on iOS WebKit —
|
|
174
|
+
// a zero-sampler particle mesh effect often never becomes drawable there.
|
|
175
|
+
const mapTexture =
|
|
176
|
+
this.settings.texture ??
|
|
177
|
+
(this.settings.renderMode === RenderMode.Mesh ? SpriteBatch.whiteTexture(this.scene) : null);
|
|
178
|
+
if (mapTexture) {
|
|
130
179
|
defines.push('USE_MAP');
|
|
131
180
|
}
|
|
181
|
+
const atlas = this.settings.reflectionAtlas;
|
|
182
|
+
const atlasPending = this.settings.renderMode === RenderMode.Mesh && !!atlas && !atlas.isReady();
|
|
183
|
+
if (atlasPending && atlas) {
|
|
184
|
+
const onLoad = (atlas as {onLoadObservable?: {addOnce: (cb: () => void) => void}}).onLoadObservable;
|
|
185
|
+
onLoad?.addOnce(() => {
|
|
186
|
+
this.rebuildMaterial();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const useEnvAtlas = this.settings.renderMode === RenderMode.Mesh && !!atlas && atlas.isReady();
|
|
190
|
+
if (useEnvAtlas) {
|
|
191
|
+
defines.push('USE_ENVMAP_ATLAS');
|
|
192
|
+
}
|
|
132
193
|
if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
|
|
133
194
|
defines.push('UV_TILE');
|
|
134
195
|
}
|
|
@@ -148,8 +209,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
148
209
|
defines.push('HORIZONTAL');
|
|
149
210
|
}
|
|
150
211
|
|
|
151
|
-
|
|
152
|
-
|
|
212
|
+
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
213
|
+
registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
|
|
153
214
|
|
|
154
215
|
const attributes = ['position', 'uv', 'offset', 'color', 'size', 'rotation', 'uvTile'];
|
|
155
216
|
if (this.settings.renderMode === RenderMode.Mesh) {
|
|
@@ -166,9 +227,14 @@ export class SpriteBatch extends VFXBatch {
|
|
|
166
227
|
uniforms.push('tileCountX');
|
|
167
228
|
uniforms.push('tileCountY');
|
|
168
229
|
}
|
|
169
|
-
if (
|
|
230
|
+
if (mapTexture) {
|
|
170
231
|
samplers.push('map');
|
|
171
232
|
}
|
|
233
|
+
if (useEnvAtlas) {
|
|
234
|
+
samplers.push('envAtlas');
|
|
235
|
+
uniforms.push('eyePosition');
|
|
236
|
+
uniforms.push('reflectionLevel');
|
|
237
|
+
}
|
|
172
238
|
if (this.settings.renderMode === RenderMode.StretchedBillBoard) {
|
|
173
239
|
uniforms.push('speedFactor');
|
|
174
240
|
}
|
|
@@ -186,7 +252,9 @@ export class SpriteBatch extends VFXBatch {
|
|
|
186
252
|
uniforms.push('ambientColor');
|
|
187
253
|
}
|
|
188
254
|
|
|
189
|
-
const mat = new ShaderMaterial(
|
|
255
|
+
const mat = new ShaderMaterial(
|
|
256
|
+
shaderName,
|
|
257
|
+
this.scene,
|
|
190
258
|
{vertex: shaderName, fragment: shaderName},
|
|
191
259
|
{
|
|
192
260
|
attributes,
|
|
@@ -194,11 +262,25 @@ export class SpriteBatch extends VFXBatch {
|
|
|
194
262
|
samplers,
|
|
195
263
|
defines,
|
|
196
264
|
needAlphaBlending: this.settings.materialTransparent,
|
|
265
|
+
shaderLanguage,
|
|
197
266
|
}
|
|
198
267
|
);
|
|
199
268
|
|
|
200
|
-
if (
|
|
201
|
-
mat.setTexture('map',
|
|
269
|
+
if (mapTexture) {
|
|
270
|
+
mat.setTexture('map', mapTexture);
|
|
271
|
+
}
|
|
272
|
+
if (useEnvAtlas && atlas) {
|
|
273
|
+
mat.setTexture('envAtlas', atlas);
|
|
274
|
+
mat.setFloat('reflectionLevel', this.settings.reflectionLevel);
|
|
275
|
+
const eyePosition = new BVector3();
|
|
276
|
+
mat.onBindObservable.add(() => {
|
|
277
|
+
const camera = this.scene.activeCamera;
|
|
278
|
+
if (!camera) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
eyePosition.copyFrom(camera.globalPosition);
|
|
282
|
+
mat.setVector3('eyePosition', eyePosition);
|
|
283
|
+
});
|
|
202
284
|
}
|
|
203
285
|
if (this.settings.uTileCount > 1 || this.settings.vTileCount > 1) {
|
|
204
286
|
mat.setFloat('tileCountX', this.settings.uTileCount);
|
|
@@ -208,11 +290,21 @@ export class SpriteBatch extends VFXBatch {
|
|
|
208
290
|
mat.setFloat('speedFactor', this.settings.softNearFade > 0 ? this.settings.softNearFade : 1.0);
|
|
209
291
|
}
|
|
210
292
|
if (this.settings.softParticles) {
|
|
211
|
-
mat.setVector2(
|
|
293
|
+
mat.setVector2(
|
|
294
|
+
'softParams',
|
|
295
|
+
new BVector2(
|
|
296
|
+
this.settings.softNearFade,
|
|
297
|
+
1.0 / Math.max(this.settings.softFarFade - this.settings.softNearFade, 0.0001)
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
// Reused across binds: this runs on every draw call.
|
|
301
|
+
const projParams = new BVector4(0, 0, 0, 0);
|
|
212
302
|
mat.onBindObservable.add(() => {
|
|
213
303
|
const camera = this.scene.activeCamera;
|
|
214
304
|
if (camera) {
|
|
215
|
-
|
|
305
|
+
projParams.x = camera.minZ;
|
|
306
|
+
projParams.y = camera.maxZ;
|
|
307
|
+
mat.setVector4('projParams', projParams);
|
|
216
308
|
}
|
|
217
309
|
});
|
|
218
310
|
}
|
|
@@ -237,6 +329,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
237
329
|
mat.disableDepthWrite = !this.settings.materialDepthWrite;
|
|
238
330
|
|
|
239
331
|
this.mesh.material = mat;
|
|
332
|
+
this.shaderMaterial = mat;
|
|
240
333
|
}
|
|
241
334
|
|
|
242
335
|
private vector_ = new Vector3();
|
|
@@ -245,6 +338,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
245
338
|
private quaternion_ = new Quaternion();
|
|
246
339
|
private quaternion2_ = new Quaternion();
|
|
247
340
|
private quaternion3_ = new Quaternion();
|
|
341
|
+
private quaternion4_ = new Quaternion();
|
|
342
|
+
private quaternion5_ = new Quaternion();
|
|
248
343
|
private rotationMat_ = new Matrix3();
|
|
249
344
|
private rotationMat2_ = new Matrix3();
|
|
250
345
|
private lastStretchedSpeedFactor = Number.NaN;
|
|
@@ -257,33 +352,158 @@ export class SpriteBatch extends VFXBatch {
|
|
|
257
352
|
const isStretchedRender = renderMode === RenderMode.StretchedBillBoard;
|
|
258
353
|
|
|
259
354
|
const visibleSystems = this.getVisibleSystems();
|
|
260
|
-
for (
|
|
261
|
-
particleCount +=
|
|
355
|
+
for (let i = 0; i < visibleSystems.length; i++) {
|
|
356
|
+
particleCount += visibleSystems[i].particleNum;
|
|
262
357
|
}
|
|
263
358
|
if (particleCount > this.maxParticles) {
|
|
264
359
|
this.expandBuffers(particleCount);
|
|
265
360
|
}
|
|
266
361
|
|
|
267
|
-
for (
|
|
362
|
+
for (let s = 0; s < visibleSystems.length; s++) {
|
|
363
|
+
const system = visibleSystems[s];
|
|
268
364
|
const particles = system.particles;
|
|
269
365
|
const particleNum = system.particleNum;
|
|
270
366
|
const rotation = this.quaternion2_;
|
|
271
367
|
const translation = this.vector2_;
|
|
272
368
|
const scale = this.vector3_;
|
|
273
369
|
const systemWorldSpace = system.worldSpace;
|
|
274
|
-
system.emitter.matrixWorld
|
|
275
|
-
|
|
370
|
+
const emitterMatrix = system.emitter.matrixWorld;
|
|
371
|
+
emitterMatrix.decompose(translation, rotation, scale);
|
|
372
|
+
this.rotationMat_.setFromMatrix4(emitterMatrix);
|
|
276
373
|
const absScaleX = Math.abs(scale.x);
|
|
277
374
|
const absScaleY = Math.abs(scale.y);
|
|
278
375
|
const absScaleZ = Math.abs(scale.z);
|
|
376
|
+
// Stretched-billboard settings are per system, not per particle.
|
|
377
|
+
const stretchedSettings = isStretchedRender
|
|
378
|
+
? (system.rendererEmitterSettings as StretchedBillBoardSettings)
|
|
379
|
+
: undefined;
|
|
380
|
+
const speedFactor =
|
|
381
|
+
stretchedSettings === undefined || stretchedSettings.speedFactor === 0
|
|
382
|
+
? 0.001
|
|
383
|
+
: stretchedSettings.speedFactor;
|
|
384
|
+
const lengthFactor = stretchedSettings?.lengthFactor ?? 0;
|
|
385
|
+
|
|
386
|
+
// Live particles occupy store rows [0, particleNum) in the same order
|
|
387
|
+
// this loop writes them, so attributes that need no per-particle
|
|
388
|
+
// transform can be copied as one range instead of gathered.
|
|
389
|
+
const store = system.store;
|
|
390
|
+
const copiedColor = store !== undefined;
|
|
391
|
+
// Particles only carry their own parent transform when the system
|
|
392
|
+
// emits through another one; otherwise the whole range shares the
|
|
393
|
+
// emitter's matrix and can be transformed in place after the copy.
|
|
394
|
+
const sharedTransform = store !== undefined && system.onlyUsedByOther !== true;
|
|
395
|
+
// The simulation runs on a fixed step that the display does not
|
|
396
|
+
// share, so the last step is usually in the past by the time the
|
|
397
|
+
// frame is drawn. Continuing the motion that step produced puts each
|
|
398
|
+
// particle where it belongs now; without it they visibly stutter on
|
|
399
|
+
// any display that is not exactly 60Hz and in phase.
|
|
400
|
+
//
|
|
401
|
+
// The motion is measured, not predicted from velocity: plenty of
|
|
402
|
+
// behaviors move a particle by writing its position — orbits, noise,
|
|
403
|
+
// turbulence, anything a plugin does — and a velocity based guess
|
|
404
|
+
// leaves every one of those stuttering exactly as before.
|
|
405
|
+
const residual = system.simulationResidual ?? 0;
|
|
406
|
+
const stepFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
|
|
407
|
+
if (copiedColor) {
|
|
408
|
+
if (stepFraction !== 0 && particleNum > 0) {
|
|
409
|
+
SpriteBatch.continueColor(this.colorBuffer, index * 4, store!, particleNum, stepFraction);
|
|
410
|
+
} else {
|
|
411
|
+
this.colorBuffer.set(store!.color.subarray(0, particleNum * 4), index * 4);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
let copiedPositionAndSize = false;
|
|
415
|
+
if (store !== undefined && systemWorldSpace) {
|
|
416
|
+
if (stepFraction !== 0 && particleNum > 0) {
|
|
417
|
+
SpriteBatch.extrapolate(this.offsetBuffer, index * 3, store, particleNum, stepFraction);
|
|
418
|
+
} else {
|
|
419
|
+
this.offsetBuffer.set(store.position.subarray(0, particleNum * 3), index * 3);
|
|
420
|
+
}
|
|
421
|
+
if (stepFraction !== 0 && particleNum > 0) {
|
|
422
|
+
SpriteBatch.continueVector3(
|
|
423
|
+
this.sizeBuffer,
|
|
424
|
+
index * 3,
|
|
425
|
+
store.size,
|
|
426
|
+
store.previousSize,
|
|
427
|
+
particleNum,
|
|
428
|
+
stepFraction
|
|
429
|
+
);
|
|
430
|
+
} else {
|
|
431
|
+
this.sizeBuffer.set(store.size.subarray(0, particleNum * 3), index * 3);
|
|
432
|
+
}
|
|
433
|
+
copiedPositionAndSize = true;
|
|
434
|
+
} else if (sharedTransform && particleNum > 0) {
|
|
435
|
+
const base = index * 3;
|
|
436
|
+
const end = base + particleNum * 3;
|
|
437
|
+
const offsets = this.offsetBuffer;
|
|
438
|
+
if (stepFraction !== 0) {
|
|
439
|
+
SpriteBatch.extrapolate(offsets, base, store!, particleNum, stepFraction);
|
|
440
|
+
} else {
|
|
441
|
+
offsets.set(store!.position.subarray(0, particleNum * 3), base);
|
|
442
|
+
}
|
|
443
|
+
const me = emitterMatrix.elements;
|
|
444
|
+
const m00 = me[0],
|
|
445
|
+
m01 = me[1],
|
|
446
|
+
m02 = me[2],
|
|
447
|
+
m03 = me[3];
|
|
448
|
+
const m10 = me[4],
|
|
449
|
+
m11 = me[5],
|
|
450
|
+
m12 = me[6],
|
|
451
|
+
m13 = me[7];
|
|
452
|
+
const m20 = me[8],
|
|
453
|
+
m21 = me[9],
|
|
454
|
+
m22 = me[10],
|
|
455
|
+
m23 = me[11];
|
|
456
|
+
const m30 = me[12],
|
|
457
|
+
m31 = me[13],
|
|
458
|
+
m32 = me[14],
|
|
459
|
+
m33 = me[15];
|
|
460
|
+
for (let o = base; o < end; o += 3) {
|
|
461
|
+
const px = offsets[o];
|
|
462
|
+
const py = offsets[o + 1];
|
|
463
|
+
const pz = offsets[o + 2];
|
|
464
|
+
const w = 1 / (m03 * px + m13 * py + m23 * pz + m33);
|
|
465
|
+
offsets[o] = (m00 * px + m10 * py + m20 * pz + m30) * w;
|
|
466
|
+
offsets[o + 1] = (m01 * px + m11 * py + m21 * pz + m31) * w;
|
|
467
|
+
offsets[o + 2] = (m02 * px + m12 * py + m22 * pz + m32) * w;
|
|
468
|
+
}
|
|
469
|
+
const sizes = this.sizeBuffer;
|
|
470
|
+
if (stepFraction !== 0) {
|
|
471
|
+
SpriteBatch.continueVector3(
|
|
472
|
+
sizes,
|
|
473
|
+
base,
|
|
474
|
+
store!.size,
|
|
475
|
+
store!.previousSize,
|
|
476
|
+
particleNum,
|
|
477
|
+
stepFraction
|
|
478
|
+
);
|
|
479
|
+
} else {
|
|
480
|
+
sizes.set(store!.size.subarray(0, particleNum * 3), base);
|
|
481
|
+
}
|
|
482
|
+
for (let o = base; o < end; o += 3) {
|
|
483
|
+
sizes[o] *= absScaleX;
|
|
484
|
+
sizes[o + 1] *= absScaleY;
|
|
485
|
+
sizes[o + 2] *= absScaleZ;
|
|
486
|
+
}
|
|
487
|
+
copiedPositionAndSize = true;
|
|
488
|
+
}
|
|
279
489
|
|
|
280
490
|
for (let j = 0; j < particleNum; j++, index++) {
|
|
281
491
|
const particle = particles[j] as SpriteParticle;
|
|
282
492
|
|
|
283
493
|
if (isMeshRender) {
|
|
494
|
+
// Turning behaviors leave the last step's turn on the
|
|
495
|
+
// particle; a fraction of it carries the mesh the rest of
|
|
496
|
+
// the way to now.
|
|
497
|
+
let own = particle.rotation as Quaternion;
|
|
498
|
+
const step = particle.angularVelocity;
|
|
499
|
+
if (stepFraction !== 0 && step instanceof Quaternion) {
|
|
500
|
+
own = this.quaternion4_
|
|
501
|
+
.copy(own)
|
|
502
|
+
.multiply(SpriteBatch.partialTurn(step, stepFraction, this.quaternion5_));
|
|
503
|
+
}
|
|
284
504
|
let q: Quaternion;
|
|
285
505
|
if (systemWorldSpace) {
|
|
286
|
-
q =
|
|
506
|
+
q = own;
|
|
287
507
|
} else {
|
|
288
508
|
let parentQ: Quaternion;
|
|
289
509
|
if (particle.parentMatrix) {
|
|
@@ -292,7 +512,7 @@ export class SpriteBatch extends VFXBatch {
|
|
|
292
512
|
parentQ = rotation;
|
|
293
513
|
}
|
|
294
514
|
q = this.quaternion_;
|
|
295
|
-
q.copy(parentQ).multiply(
|
|
515
|
+
q.copy(parentQ).multiply(own);
|
|
296
516
|
}
|
|
297
517
|
const ri = index * 4;
|
|
298
518
|
this.rotationBuffer[ri] = q.x;
|
|
@@ -300,65 +520,77 @@ export class SpriteBatch extends VFXBatch {
|
|
|
300
520
|
this.rotationBuffer[ri + 2] = q.z;
|
|
301
521
|
this.rotationBuffer[ri + 3] = q.w;
|
|
302
522
|
} else {
|
|
303
|
-
|
|
523
|
+
const spin = particle.angularVelocity;
|
|
524
|
+
this.rotationBuffer[index] =
|
|
525
|
+
(particle.rotation as number) + (typeof spin === 'number' ? spin * residual : 0);
|
|
304
526
|
}
|
|
305
527
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
528
|
+
if (!copiedPositionAndSize) {
|
|
529
|
+
const position = particle.position;
|
|
530
|
+
const previous = particle.previousPosition;
|
|
531
|
+
let px = position.x + (position.x - previous.x) * stepFraction;
|
|
532
|
+
let py = position.y + (position.y - previous.y) * stepFraction;
|
|
533
|
+
let pz = position.z + (position.z - previous.z) * stepFraction;
|
|
534
|
+
if (!systemWorldSpace) {
|
|
535
|
+
// Inlined point transform: this is the default (local space)
|
|
536
|
+
// path and runs for every particle every frame.
|
|
537
|
+
const me = (particle.parentMatrix ?? emitterMatrix).elements;
|
|
538
|
+
const w = 1 / (me[3] * px + me[7] * py + me[11] * pz + me[15]);
|
|
539
|
+
const tx = (me[0] * px + me[4] * py + me[8] * pz + me[12]) * w;
|
|
540
|
+
const ty = (me[1] * px + me[5] * py + me[9] * pz + me[13]) * w;
|
|
541
|
+
pz = (me[2] * px + me[6] * py + me[10] * pz + me[14]) * w;
|
|
542
|
+
px = tx;
|
|
543
|
+
py = ty;
|
|
315
544
|
}
|
|
316
|
-
}
|
|
317
545
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.sizeBuffer[si] = particle.size.x;
|
|
332
|
-
this.sizeBuffer[si + 1] = particle.size.y;
|
|
333
|
-
this.sizeBuffer[si + 2] = particle.size.z;
|
|
334
|
-
} else {
|
|
335
|
-
if (particle.parentMatrix) {
|
|
336
|
-
this.sizeBuffer[si] = particle.size.x;
|
|
337
|
-
this.sizeBuffer[si + 1] = particle.size.y;
|
|
338
|
-
this.sizeBuffer[si + 2] = particle.size.z;
|
|
546
|
+
const oi = index * 3;
|
|
547
|
+
this.offsetBuffer[oi] = px;
|
|
548
|
+
this.offsetBuffer[oi + 1] = py;
|
|
549
|
+
this.offsetBuffer[oi + 2] = pz;
|
|
550
|
+
|
|
551
|
+
const size = particle.size;
|
|
552
|
+
const si = index * 3;
|
|
553
|
+
// Particle size is already in world units when the system is in world
|
|
554
|
+
// space or the particle carries its own parent transform.
|
|
555
|
+
if (systemWorldSpace || particle.parentMatrix) {
|
|
556
|
+
this.sizeBuffer[si] = size.x;
|
|
557
|
+
this.sizeBuffer[si + 1] = size.y;
|
|
558
|
+
this.sizeBuffer[si + 2] = size.z;
|
|
339
559
|
} else {
|
|
340
|
-
this.sizeBuffer[si] =
|
|
341
|
-
this.sizeBuffer[si + 1] =
|
|
342
|
-
this.sizeBuffer[si + 2] =
|
|
560
|
+
this.sizeBuffer[si] = size.x * absScaleX;
|
|
561
|
+
this.sizeBuffer[si + 1] = size.y * absScaleY;
|
|
562
|
+
this.sizeBuffer[si + 2] = size.z * absScaleZ;
|
|
343
563
|
}
|
|
344
564
|
}
|
|
345
565
|
|
|
566
|
+
if (!copiedColor) {
|
|
567
|
+
const color = particle.color;
|
|
568
|
+
const ci = index * 4;
|
|
569
|
+
this.colorBuffer[ci] = color.x;
|
|
570
|
+
this.colorBuffer[ci + 1] = color.y;
|
|
571
|
+
this.colorBuffer[ci + 2] = color.z;
|
|
572
|
+
this.colorBuffer[ci + 3] = color.w;
|
|
573
|
+
}
|
|
574
|
+
|
|
346
575
|
this.uvTileBuffer[index] = particle.uvTile;
|
|
347
576
|
|
|
348
577
|
if (isStretchedRender && this.velocityBuffer) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
578
|
+
// The streak points along the velocity and is as long as it,
|
|
579
|
+
// so a velocity frozen between steps makes the sprite's
|
|
580
|
+
// outline pop at the step rate while its position glides.
|
|
581
|
+
const velocity = particle.velocity;
|
|
582
|
+
const previousVelocity = particle.previousVelocity;
|
|
583
|
+
const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
|
|
584
|
+
const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
|
|
585
|
+
const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
|
|
586
|
+
let vel: Vector3 = this.vector_;
|
|
587
|
+
vel.set(vx, vy, vz);
|
|
588
|
+
if (!systemWorldSpace) {
|
|
357
589
|
if (particle.parentMatrix) {
|
|
358
590
|
this.rotationMat2_.setFromMatrix4(particle.parentMatrix);
|
|
359
|
-
vel.
|
|
591
|
+
vel.applyMatrix3(this.rotationMat2_);
|
|
360
592
|
} else {
|
|
361
|
-
vel.
|
|
593
|
+
vel.applyMatrix3(this.rotationMat_);
|
|
362
594
|
}
|
|
363
595
|
}
|
|
364
596
|
const vi = index * 4;
|
|
@@ -370,13 +602,8 @@ export class SpriteBatch extends VFXBatch {
|
|
|
370
602
|
}
|
|
371
603
|
}
|
|
372
604
|
|
|
373
|
-
if (isStretchedRender &&
|
|
374
|
-
|
|
375
|
-
const clampedSpeedFactor = speedFactor === 0 ? 0.001 : speedFactor;
|
|
376
|
-
if (clampedSpeedFactor !== this.lastStretchedSpeedFactor) {
|
|
377
|
-
this.mesh.material.setFloat('speedFactor', clampedSpeedFactor);
|
|
378
|
-
this.lastStretchedSpeedFactor = clampedSpeedFactor;
|
|
379
|
-
}
|
|
605
|
+
if (isStretchedRender && visibleSystems.length > 0) {
|
|
606
|
+
this.syncStretchedSpeedFactor(visibleSystems[0]);
|
|
380
607
|
}
|
|
381
608
|
|
|
382
609
|
// Keep the mesh enabled even when count is 0 — toggling visibility off for a
|
|
@@ -388,13 +615,137 @@ export class SpriteBatch extends VFXBatch {
|
|
|
388
615
|
return;
|
|
389
616
|
}
|
|
390
617
|
|
|
391
|
-
this
|
|
392
|
-
|
|
393
|
-
this.
|
|
394
|
-
this.
|
|
395
|
-
this.
|
|
618
|
+
// Upload only the slots actually filled this frame: the buffers are sized
|
|
619
|
+
// for the high-water mark and are usually far larger than the live count.
|
|
620
|
+
this.offsetVB.update(this.offsetBuffer.subarray(0, index * 3));
|
|
621
|
+
this.sizeVB.update(this.sizeBuffer.subarray(0, index * 3));
|
|
622
|
+
this.colorVB.update(this.colorBuffer.subarray(0, index * 4));
|
|
623
|
+
this.uvTileVB.update(this.uvTileBuffer.subarray(0, index));
|
|
624
|
+
this.rotationVB.update(this.rotationBuffer.subarray(0, isMeshRender ? index * 4 : index));
|
|
396
625
|
if (isStretchedRender && this.velocityVB && this.velocityBuffer) {
|
|
397
|
-
this.velocityVB.update(this.velocityBuffer);
|
|
626
|
+
this.velocityVB.update(this.velocityBuffer.subarray(0, index * 4));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Pushes the stretched-billboard speed factor to the shader when it changed.
|
|
632
|
+
*
|
|
633
|
+
* Deliberately kept out of `update`: touching the loosely typed
|
|
634
|
+
* `mesh.material` from inside that function gives V8 a generic property
|
|
635
|
+
* access it has no feedback for, which knocks the whole particle loop out of
|
|
636
|
+
* optimised code on every frame.
|
|
637
|
+
*/
|
|
638
|
+
private syncStretchedSpeedFactor(system: IParticleSystem): void {
|
|
639
|
+
const material = this.shaderMaterial;
|
|
640
|
+
if (material === null) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const speedFactor = (system.rendererEmitterSettings as StretchedBillBoardSettings).speedFactor ?? 1.0;
|
|
644
|
+
const clampedSpeedFactor = speedFactor === 0 ? 0.001 : speedFactor;
|
|
645
|
+
if (clampedSpeedFactor !== this.lastStretchedSpeedFactor) {
|
|
646
|
+
material.setFloat('speedFactor', clampedSpeedFactor);
|
|
647
|
+
this.lastStretchedSpeedFactor = clampedSpeedFactor;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* The fraction `f` of a turn, as a normalised lerp from no rotation toward
|
|
653
|
+
* `step`, written into `out`.
|
|
654
|
+
*
|
|
655
|
+
* A true slerp would be exact, but `step` is one simulation step of turning
|
|
656
|
+
* and `f` is below 1, so the arc being split is small enough that the lerp
|
|
657
|
+
* is within a rounding error of it and costs a fraction as much.
|
|
658
|
+
*/
|
|
659
|
+
private static partialTurn(step: Quaternion, f: number, out: Quaternion): Quaternion {
|
|
660
|
+
const x = step.x * f;
|
|
661
|
+
const y = step.y * f;
|
|
662
|
+
const z = step.z * f;
|
|
663
|
+
const w = 1 + (step.w - 1) * f;
|
|
664
|
+
const inverseLength = 1 / Math.sqrt(x * x + y * y + z * z + w * w);
|
|
665
|
+
out.x = x * inverseLength;
|
|
666
|
+
out.y = y * inverseLength;
|
|
667
|
+
out.z = z * inverseLength;
|
|
668
|
+
out.w = w * inverseLength;
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Continues a three-component column by `fraction` of the step it just took,
|
|
674
|
+
* never below zero.
|
|
675
|
+
*
|
|
676
|
+
* Size fades to nothing at the end of a life, so the last step before death
|
|
677
|
+
* can point past zero; a negative size turns a sprite inside out.
|
|
678
|
+
*/
|
|
679
|
+
private static continueVector3(
|
|
680
|
+
target: Float32Array,
|
|
681
|
+
base: number,
|
|
682
|
+
current: Float32Array,
|
|
683
|
+
previous: Float32Array,
|
|
684
|
+
count: number,
|
|
685
|
+
fraction: number
|
|
686
|
+
): void {
|
|
687
|
+
for (let i = 0, o = 0; i < count; i++, o += 3) {
|
|
688
|
+
const x = current[o] + (current[o] - previous[o]) * fraction;
|
|
689
|
+
const y = current[o + 1] + (current[o + 1] - previous[o + 1]) * fraction;
|
|
690
|
+
const z = current[o + 2] + (current[o + 2] - previous[o + 2]) * fraction;
|
|
691
|
+
target[base + o] = x > 0 ? x : 0;
|
|
692
|
+
target[base + o + 1] = y > 0 ? y : 0;
|
|
693
|
+
target[base + o + 2] = z > 0 ? z : 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** The same for colour, whose alpha runs out at the end of a life too. */
|
|
698
|
+
private static continueColor(
|
|
699
|
+
target: Float32Array,
|
|
700
|
+
base: number,
|
|
701
|
+
store: ParticleStore,
|
|
702
|
+
count: number,
|
|
703
|
+
fraction: number
|
|
704
|
+
): void {
|
|
705
|
+
const current = store.color;
|
|
706
|
+
const previous = store.previousColor;
|
|
707
|
+
for (let i = 0, o = 0; i < count; i++, o += 4) {
|
|
708
|
+
const r = current[o] + (current[o] - previous[o]) * fraction;
|
|
709
|
+
const g = current[o + 1] + (current[o + 1] - previous[o + 1]) * fraction;
|
|
710
|
+
const b = current[o + 2] + (current[o + 2] - previous[o + 2]) * fraction;
|
|
711
|
+
const a = current[o + 3] + (current[o + 3] - previous[o + 3]) * fraction;
|
|
712
|
+
target[base + o] = r > 0 ? r : 0;
|
|
713
|
+
target[base + o + 1] = g > 0 ? g : 0;
|
|
714
|
+
target[base + o + 2] = b > 0 ? b : 0;
|
|
715
|
+
target[base + o + 3] = a > 0 ? a : 0;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Writes rows `[0, count)` of the store's positions into `target` at `base`,
|
|
721
|
+
* each carried on by `fraction` of the step it just took.
|
|
722
|
+
*
|
|
723
|
+
* Continuing the measured last step covers every way a particle can move —
|
|
724
|
+
* velocity integration, an orbit, noise, a plugin's own behavior — because
|
|
725
|
+
* it reads what happened rather than predicting from one term of it. Over
|
|
726
|
+
* less than a step it is a straight line through a path that may curve,
|
|
727
|
+
* which for a sixtieth of a second is well under a pixel.
|
|
728
|
+
*
|
|
729
|
+
* A particle born during the step has the same position on both sides and
|
|
730
|
+
* so does not move at all, which is what puts it at the emitter rather than
|
|
731
|
+
* a step's travel away from it.
|
|
732
|
+
*/
|
|
733
|
+
private static extrapolate(
|
|
734
|
+
target: Float32Array,
|
|
735
|
+
base: number,
|
|
736
|
+
store: ParticleStore,
|
|
737
|
+
count: number,
|
|
738
|
+
fraction: number
|
|
739
|
+
): void {
|
|
740
|
+
const positions = store.position;
|
|
741
|
+
const previous = store.previousPosition;
|
|
742
|
+
for (let i = 0, o = 0; i < count; i++, o += 3) {
|
|
743
|
+
const x = positions[o];
|
|
744
|
+
const y = positions[o + 1];
|
|
745
|
+
const z = positions[o + 2];
|
|
746
|
+
target[base + o] = x + (x - previous[o]) * fraction;
|
|
747
|
+
target[base + o + 1] = y + (y - previous[o + 1]) * fraction;
|
|
748
|
+
target[base + o + 2] = z + (z - previous[o + 2]) * fraction;
|
|
398
749
|
}
|
|
399
750
|
}
|
|
400
751
|
|