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