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/TrailBatch.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
2
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
3
|
-
import {Effect} from '@babylonjs/core/Materials/effect';
|
|
4
|
-
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
5
|
-
import {Scene} from '@babylonjs/core/scene';
|
|
6
2
|
import {BoundingInfo} from '@babylonjs/core/Culling/boundingInfo';
|
|
7
|
-
import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
|
|
8
|
-
import {Vector2 as BVector2} from '@babylonjs/core/Maths/math.vector';
|
|
9
3
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
4
|
+
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
5
|
+
import {Vector2 as BVector2, Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
|
|
6
|
+
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
7
|
+
import {Scene} from '@babylonjs/core/scene';
|
|
8
|
+
import {IParticleSystem, Matrix4, Quaternion, TrailParticle, TrailSettings, Vector3} from 'quarks.core';
|
|
12
9
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
13
|
-
import
|
|
10
|
+
import {registerShaders, shaderLanguageFor} from './shaders/shaderLanguageSupport';
|
|
14
11
|
import trail_frag from './shaders/trail_frag.glsl';
|
|
12
|
+
import trail_frag_wgsl from './shaders/trail_frag.wgsl';
|
|
13
|
+
import trail_vert from './shaders/trail_vert.glsl';
|
|
14
|
+
import trail_vert_wgsl from './shaders/trail_vert.wgsl';
|
|
15
|
+
import {VFXBatch} from './VFXBatch';
|
|
15
16
|
|
|
16
17
|
export class TrailBatch extends VFXBatch {
|
|
17
18
|
private positionBuffer!: Float32Array;
|
|
@@ -22,23 +23,19 @@ export class TrailBatch extends VFXBatch {
|
|
|
22
23
|
private widthBuffer!: Float32Array;
|
|
23
24
|
private colorBuffer!: Float32Array;
|
|
24
25
|
private indexBuffer!: Uint32Array;
|
|
26
|
+
/** True while the batch is parked on the degenerate draw, see uploadGeometry. */
|
|
27
|
+
private drawsNothing = false;
|
|
25
28
|
private previousVB!: VertexBuffer;
|
|
26
29
|
private nextVB!: VertexBuffer;
|
|
27
30
|
private sideVB!: VertexBuffer;
|
|
28
31
|
private widthVB!: VertexBuffer;
|
|
29
32
|
|
|
30
|
-
private getTrailHistoryCount(particle: TrailParticle): number {
|
|
31
|
-
const trailAny = particle as any;
|
|
32
|
-
const fastHistoryCount = trailAny._trailHistoryCount;
|
|
33
|
-
if (typeof fastHistoryCount === 'number') {
|
|
34
|
-
return fastHistoryCount;
|
|
35
|
-
}
|
|
36
|
-
return particle.previous.length;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
constructor(settings: VFXBatchSettings, scene: Scene) {
|
|
40
34
|
super(settings, scene);
|
|
41
|
-
|
|
35
|
+
// Trail buffers hold two vertices per recorded sample and are the largest
|
|
36
|
+
// per-batch allocation in the library, so start small and let
|
|
37
|
+
// expandBuffers grow to whatever the batch actually needs.
|
|
38
|
+
this.maxParticles = 1024;
|
|
42
39
|
this.setupBuffers();
|
|
43
40
|
this.rebuildMaterial();
|
|
44
41
|
}
|
|
@@ -61,18 +58,42 @@ export class TrailBatch extends VFXBatch {
|
|
|
61
58
|
this.indexBuffer = new Uint32Array(this.maxParticles * 6);
|
|
62
59
|
|
|
63
60
|
// Initialize with full-capacity buffers (updatable) and a dummy triangle
|
|
64
|
-
this.positionBuffer[0] = 0;
|
|
65
|
-
this.positionBuffer[
|
|
66
|
-
this.positionBuffer[
|
|
67
|
-
this.positionBuffer[
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
61
|
+
this.positionBuffer[0] = 0;
|
|
62
|
+
this.positionBuffer[1] = 0;
|
|
63
|
+
this.positionBuffer[2] = 0;
|
|
64
|
+
this.positionBuffer[3] = 1;
|
|
65
|
+
this.positionBuffer[4] = 0;
|
|
66
|
+
this.positionBuffer[5] = 0;
|
|
67
|
+
this.positionBuffer[6] = 0;
|
|
68
|
+
this.positionBuffer[7] = 1;
|
|
69
|
+
this.positionBuffer[8] = 0;
|
|
70
|
+
this.positionBuffer[9] = 1;
|
|
71
|
+
this.positionBuffer[10] = 1;
|
|
72
|
+
this.positionBuffer[11] = 0;
|
|
73
|
+
this.indexBuffer[0] = 0;
|
|
74
|
+
this.indexBuffer[1] = 1;
|
|
75
|
+
this.indexBuffer[2] = 2;
|
|
76
|
+
this.indexBuffer[3] = 1;
|
|
77
|
+
this.indexBuffer[4] = 3;
|
|
78
|
+
this.indexBuffer[5] = 2;
|
|
70
79
|
|
|
71
80
|
// Set standard vertex data with full buffer capacity (updatable)
|
|
72
81
|
this.mesh.setVerticesData(VertexBuffer.PositionKind, this.positionBuffer, true);
|
|
73
82
|
this.mesh.setVerticesData(VertexBuffer.UVKind, this.uvBuffer, true);
|
|
74
83
|
this.mesh.setVerticesData(VertexBuffer.ColorKind, this.colorBuffer, true, 4);
|
|
75
|
-
|
|
84
|
+
// The whole array, not just the dummy triangle: this call is what sizes
|
|
85
|
+
// the GPU index buffer, and updateIndices later writes into it without
|
|
86
|
+
// resizing it. Handing it six indices here left a 24 byte buffer that
|
|
87
|
+
// every later frame overran — WebGL tolerated it, WebGPU rejects the
|
|
88
|
+
// draw outright ("index range does not fit in index buffer size").
|
|
89
|
+
// The vertex buffers above are already created at full capacity; this
|
|
90
|
+
// was the one that was not.
|
|
91
|
+
this.mesh.setIndices(this.indexBuffer, null, true);
|
|
92
|
+
this.drawsNothing = false;
|
|
93
|
+
if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
|
|
94
|
+
// Until the first update, only the dummy triangle is real.
|
|
95
|
+
this.mesh.subMeshes[0].indexCount = 6;
|
|
96
|
+
}
|
|
76
97
|
const engine = this.scene.getEngine();
|
|
77
98
|
this.previousVB = new VertexBuffer(engine, this.previousBuffer, 'previous', true, false, 3, false);
|
|
78
99
|
this.nextVB = new VertexBuffer(engine, this.nextBuffer, 'next', true, false, 3, false);
|
|
@@ -99,29 +120,52 @@ export class TrailBatch extends VFXBatch {
|
|
|
99
120
|
while (target >= this.maxParticles) {
|
|
100
121
|
this.maxParticles *= 2;
|
|
101
122
|
}
|
|
123
|
+
// Release the previous custom attribute buffers; setVerticesBuffer only
|
|
124
|
+
// replaces the reference, so skipping this leaks GPU memory on every grow.
|
|
125
|
+
this.previousVB?.dispose();
|
|
126
|
+
this.nextVB?.dispose();
|
|
127
|
+
this.sideVB?.dispose();
|
|
128
|
+
this.widthVB?.dispose();
|
|
102
129
|
this.buildGeometryBuffers();
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
rebuildMaterial(): void {
|
|
106
|
-
|
|
133
|
+
// Stable name: the trail shader source never varies, so registering it once
|
|
134
|
+
// keeps Effect.ShadersStore bounded and lets Babylon reuse compiled effects.
|
|
135
|
+
const shaderName = 'quarksTrail';
|
|
107
136
|
const defines: string[] = [];
|
|
108
137
|
|
|
109
138
|
if (this.settings.texture) {
|
|
110
139
|
defines.push('USE_MAP');
|
|
111
140
|
}
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
|
|
142
|
+
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
143
|
+
registerShaders(
|
|
144
|
+
shaderName,
|
|
145
|
+
{glsl: trail_vert, wgsl: trail_vert_wgsl},
|
|
146
|
+
{glsl: trail_frag, wgsl: trail_frag_wgsl},
|
|
147
|
+
shaderLanguage
|
|
148
|
+
);
|
|
115
149
|
|
|
116
150
|
const attributes = ['position', 'previous', 'next', 'side', 'width', 'uv', 'color'];
|
|
117
|
-
const uniforms = [
|
|
151
|
+
const uniforms = [
|
|
152
|
+
'world',
|
|
153
|
+
'view',
|
|
154
|
+
'projection',
|
|
155
|
+
'worldViewProjection',
|
|
156
|
+
'lineWidth',
|
|
157
|
+
'resolution',
|
|
158
|
+
'sizeAttenuation',
|
|
159
|
+
];
|
|
118
160
|
const samplers: string[] = [];
|
|
119
161
|
|
|
120
162
|
if (this.settings.texture) {
|
|
121
163
|
samplers.push('map');
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
const mat = new ShaderMaterial(
|
|
166
|
+
const mat = new ShaderMaterial(
|
|
167
|
+
shaderName,
|
|
168
|
+
this.scene,
|
|
125
169
|
{vertex: shaderName, fragment: shaderName},
|
|
126
170
|
{
|
|
127
171
|
attributes,
|
|
@@ -129,6 +173,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
129
173
|
samplers,
|
|
130
174
|
defines,
|
|
131
175
|
needAlphaBlending: this.settings.materialTransparent,
|
|
176
|
+
shaderLanguage,
|
|
132
177
|
}
|
|
133
178
|
);
|
|
134
179
|
|
|
@@ -137,11 +182,19 @@ export class TrailBatch extends VFXBatch {
|
|
|
137
182
|
}
|
|
138
183
|
const engine = this.scene.getEngine();
|
|
139
184
|
mat.setFloat('lineWidth', 1);
|
|
140
|
-
|
|
185
|
+
// Reused across binds, and only re-uploaded when the render target resizes.
|
|
186
|
+
const resolution = new BVector2(engine.getRenderWidth(), engine.getRenderHeight());
|
|
187
|
+
mat.setVector2('resolution', resolution);
|
|
141
188
|
mat.setFloat('sizeAttenuation', 1);
|
|
142
189
|
mat.onBindObservable.add(() => {
|
|
143
190
|
const renderEngine = this.scene.getEngine();
|
|
144
|
-
|
|
191
|
+
const width = renderEngine.getRenderWidth();
|
|
192
|
+
const height = renderEngine.getRenderHeight();
|
|
193
|
+
if (resolution.x !== width || resolution.y !== height) {
|
|
194
|
+
resolution.x = width;
|
|
195
|
+
resolution.y = height;
|
|
196
|
+
mat.setVector2('resolution', resolution);
|
|
197
|
+
}
|
|
145
198
|
});
|
|
146
199
|
|
|
147
200
|
mat.backFaceCulling = false;
|
|
@@ -163,30 +216,45 @@ export class TrailBatch extends VFXBatch {
|
|
|
163
216
|
private vector3_ = new Vector3();
|
|
164
217
|
private quaternion_ = new Quaternion();
|
|
165
218
|
|
|
219
|
+
/** Forces the emitter's world matrix to be current before sampling it. */
|
|
220
|
+
private static ensureWorldMatrix(system: IParticleSystem): void {
|
|
221
|
+
const emitter = system.emitter as {computeWorldMatrix?: (force: boolean) => void};
|
|
222
|
+
if (emitter.computeWorldMatrix) {
|
|
223
|
+
emitter.computeWorldMatrix(true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
166
227
|
update(): void {
|
|
167
228
|
let index = 0;
|
|
168
229
|
let triangles = 0;
|
|
169
230
|
|
|
170
231
|
let particleCount = 0;
|
|
171
232
|
const visibleSystems = this.getVisibleSystems();
|
|
172
|
-
for (
|
|
233
|
+
for (let s = 0; s < visibleSystems.length; s++) {
|
|
234
|
+
const system = visibleSystems[s];
|
|
235
|
+
const particles = system.particles;
|
|
173
236
|
for (let j = 0; j < system.particleNum; j++) {
|
|
174
|
-
|
|
237
|
+
const historyCount = (particles[j] as TrailParticle).historyCount;
|
|
238
|
+
if (historyCount === 0) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// +2 reserves the live tip vertex pair that frames between
|
|
242
|
+
// simulation steps append ahead of the recorded head.
|
|
243
|
+
particleCount += historyCount * 2 + 2;
|
|
175
244
|
}
|
|
176
245
|
}
|
|
177
246
|
if (particleCount > this.maxParticles) {
|
|
178
247
|
this.expandBuffers(particleCount);
|
|
179
248
|
}
|
|
180
249
|
|
|
181
|
-
for (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(system.emitter as any).computeWorldMatrix(true);
|
|
185
|
-
}
|
|
250
|
+
for (let s = 0; s < visibleSystems.length; s++) {
|
|
251
|
+
const system = visibleSystems[s];
|
|
252
|
+
TrailBatch.ensureWorldMatrix(system);
|
|
186
253
|
const rotation = this.quaternion_;
|
|
187
254
|
const translation = this.vector2_;
|
|
188
255
|
const scale = this.vector3_;
|
|
189
|
-
system.emitter.matrixWorld
|
|
256
|
+
const emitterMatrix = system.emitter.matrixWorld;
|
|
257
|
+
emitterMatrix.decompose(translation, rotation, scale);
|
|
190
258
|
|
|
191
259
|
const particles = system.particles;
|
|
192
260
|
const particleNum = system.particleNum;
|
|
@@ -196,123 +264,127 @@ export class TrailBatch extends VFXBatch {
|
|
|
196
264
|
const tileHeight = 1 / vTileCount;
|
|
197
265
|
const systemWorldSpace = system.worldSpace;
|
|
198
266
|
const objectScale = (Math.abs(scale.x) + Math.abs(scale.y) + Math.abs(scale.z)) / 3;
|
|
267
|
+
// A ribbon's samples were recorded at past steps and stay where they
|
|
268
|
+
// were. The particle itself has moved on since the step that wrote
|
|
269
|
+
// the newest sample, so frames that run no step would freeze the
|
|
270
|
+
// whole trail if that head were left behind. Moving the recorded
|
|
271
|
+
// head forward to catch up stretches the last segment from one
|
|
272
|
+
// step of travel to almost two, then snaps it back when the next
|
|
273
|
+
// step lands — a rubber-band the eye reads as lost smoothness,
|
|
274
|
+
// especially at 120Hz where every other frame is between steps.
|
|
275
|
+
// Appending a live tip ahead of the recorded samples keeps the
|
|
276
|
+
// body still and the tip moving without that pulse. A trail pinned
|
|
277
|
+
// to the emitter's origin is driven by the emitter's transform
|
|
278
|
+
// rather than by the particle's velocity, so it is left alone.
|
|
279
|
+
const trailSettings = system.rendererEmitterSettings as TrailSettings;
|
|
280
|
+
const uvFromHead = Boolean(trailSettings.uvFromHead);
|
|
281
|
+
const colorFromParticle = Boolean(trailSettings.colorFromParticle);
|
|
282
|
+
// Recorded samples already sit in world space; the live tip still comes
|
|
283
|
+
// from the particle's own (local) position.
|
|
284
|
+
const samplesInWorld = systemWorldSpace || Boolean(trailSettings.recordWorldSpace);
|
|
285
|
+
const residual = trailSettings.followLocalOrigin ? 0 : (system.simulationResidual ?? 0);
|
|
286
|
+
// Measured from the step the particle just took, not from its
|
|
287
|
+
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
288
|
+
// behavior has motion no velocity term describes.
|
|
289
|
+
const stepFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
|
|
199
290
|
|
|
200
291
|
for (let j = 0; j < particleNum; j++) {
|
|
201
292
|
const particle = particles[j] as TrailParticle;
|
|
202
|
-
const
|
|
203
|
-
const fastHistoryCount = particleAny._trailHistoryCount as number | undefined;
|
|
204
|
-
const fastHistoryCapacity = particleAny._trailHistoryCapacity as number | undefined;
|
|
205
|
-
const fastHistoryHead = particleAny._trailHistoryHead as number | undefined;
|
|
206
|
-
const fastHistoryPositions = particleAny._trailHistoryPositions as Float32Array | undefined;
|
|
207
|
-
const fastHistorySizes = particleAny._trailHistorySizes as Float32Array | undefined;
|
|
208
|
-
const fastHistoryColors = particleAny._trailHistoryColors as Float32Array | undefined;
|
|
209
|
-
const hasFastHistory =
|
|
210
|
-
typeof fastHistoryCount === 'number' &&
|
|
211
|
-
fastHistoryCount > 0 &&
|
|
212
|
-
typeof fastHistoryCapacity === 'number' &&
|
|
213
|
-
typeof fastHistoryHead === 'number' &&
|
|
214
|
-
fastHistoryPositions instanceof Float32Array &&
|
|
215
|
-
fastHistorySizes instanceof Float32Array &&
|
|
216
|
-
fastHistoryColors instanceof Float32Array;
|
|
217
|
-
const historyCapacity = hasFastHistory ? (fastHistoryCapacity as number) : 0;
|
|
218
|
-
const historyHead = hasFastHistory ? (fastHistoryHead as number) : 0;
|
|
219
|
-
const historyPositions = fastHistoryPositions as Float32Array;
|
|
220
|
-
const historySizes = fastHistorySizes as Float32Array;
|
|
221
|
-
const historyColors = fastHistoryColors as Float32Array;
|
|
222
|
-
const particleHistory = particle.previous;
|
|
223
|
-
const particleHistoryLength = hasFastHistory ? fastHistoryCount : particleHistory.length;
|
|
293
|
+
const particleHistoryLength = particle.historyCount;
|
|
224
294
|
if (particleHistoryLength === 0) {
|
|
225
295
|
continue;
|
|
226
296
|
}
|
|
297
|
+
const historyCapacity = particle.historyCapacity;
|
|
298
|
+
const historyPositions = particle.historyPositions;
|
|
299
|
+
const historySizes = particle.historySizes;
|
|
300
|
+
// Every sample takes its color from one place: its own recorded slot, or
|
|
301
|
+
// the particle's current row in the store.
|
|
302
|
+
const colors = colorFromParticle ? particle.store.color : particle.historyColors;
|
|
303
|
+
const particleColorIndex = particle.storeIndex * 4;
|
|
227
304
|
const col = particle.uvTile % vTileCount;
|
|
228
305
|
const row = Math.floor(particle.uvTile / vTileCount + 0.001);
|
|
229
306
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
307
|
+
const me = (particleMatrix ?? emitterMatrix).elements;
|
|
308
|
+
const m00 = me[0],
|
|
309
|
+
m01 = me[1],
|
|
310
|
+
m02 = me[2],
|
|
311
|
+
m03 = me[3];
|
|
312
|
+
const m10 = me[4],
|
|
313
|
+
m11 = me[5],
|
|
314
|
+
m12 = me[6],
|
|
315
|
+
m13 = me[7];
|
|
316
|
+
const m20 = me[8],
|
|
317
|
+
m21 = me[9],
|
|
318
|
+
m22 = me[10],
|
|
319
|
+
m23 = me[11];
|
|
320
|
+
const m30 = me[12],
|
|
321
|
+
m31 = me[13],
|
|
322
|
+
m32 = me[14],
|
|
323
|
+
m33 = me[15];
|
|
324
|
+
// The texture spans the ribbon end to end: up to the live tip when one is
|
|
325
|
+
// drawn, otherwise up to the newest recorded sample.
|
|
326
|
+
const invHistoryLength =
|
|
327
|
+
1 / (stepFraction !== 0 ? particleHistoryLength : Math.max(1, particleHistoryLength - 1));
|
|
328
|
+
|
|
329
|
+
// Walk the ring buffer oldest-first with hand-rolled wrapping so the
|
|
330
|
+
// inner loop stays free of modulo operations.
|
|
331
|
+
let currentSlot = particle.getHistoryIndex(0);
|
|
332
|
+
let previousSlot = currentSlot;
|
|
238
333
|
|
|
239
334
|
for (let i = 0; i < particleHistoryLength; i++, index += 2) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let
|
|
252
|
-
let
|
|
253
|
-
let
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
nextX = next.position.x;
|
|
294
|
-
nextY = next.position.y;
|
|
295
|
-
nextZ = next.position.z;
|
|
296
|
-
currentSize = current.size;
|
|
297
|
-
currentColorX = current.color.x;
|
|
298
|
-
currentColorY = current.color.y;
|
|
299
|
-
currentColorZ = current.color.z;
|
|
300
|
-
currentColorW = current.color.w;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (!systemWorldSpace) {
|
|
304
|
-
this.vector_.set(currentX, currentY, currentZ).applyMatrix4(matrixToUse);
|
|
305
|
-
currentX = this.vector_.x;
|
|
306
|
-
currentY = this.vector_.y;
|
|
307
|
-
currentZ = this.vector_.z;
|
|
308
|
-
this.vector_.set(previousX, previousY, previousZ).applyMatrix4(matrixToUse);
|
|
309
|
-
previousX = this.vector_.x;
|
|
310
|
-
previousY = this.vector_.y;
|
|
311
|
-
previousZ = this.vector_.z;
|
|
312
|
-
this.vector_.set(nextX, nextY, nextZ).applyMatrix4(matrixToUse);
|
|
313
|
-
nextX = this.vector_.x;
|
|
314
|
-
nextY = this.vector_.y;
|
|
315
|
-
nextZ = this.vector_.z;
|
|
335
|
+
const nextSlot =
|
|
336
|
+
i + 1 < particleHistoryLength
|
|
337
|
+
? currentSlot + 1 === historyCapacity
|
|
338
|
+
? 0
|
|
339
|
+
: currentSlot + 1
|
|
340
|
+
: currentSlot;
|
|
341
|
+
|
|
342
|
+
const currentPosIndex = currentSlot * 3;
|
|
343
|
+
const previousPosIndex = previousSlot * 3;
|
|
344
|
+
const nextPosIndex = nextSlot * 3;
|
|
345
|
+
|
|
346
|
+
let currentX = historyPositions[currentPosIndex];
|
|
347
|
+
let currentY = historyPositions[currentPosIndex + 1];
|
|
348
|
+
let currentZ = historyPositions[currentPosIndex + 2];
|
|
349
|
+
let previousX = historyPositions[previousPosIndex];
|
|
350
|
+
let previousY = historyPositions[previousPosIndex + 1];
|
|
351
|
+
let previousZ = historyPositions[previousPosIndex + 2];
|
|
352
|
+
let nextX = historyPositions[nextPosIndex];
|
|
353
|
+
let nextY = historyPositions[nextPosIndex + 1];
|
|
354
|
+
let nextZ = historyPositions[nextPosIndex + 2];
|
|
355
|
+
|
|
356
|
+
const currentSize = historySizes[currentSlot];
|
|
357
|
+
const currentColorIndex = colorFromParticle ? particleColorIndex : currentSlot * 4;
|
|
358
|
+
const currentColorX = colors[currentColorIndex];
|
|
359
|
+
const currentColorY = colors[currentColorIndex + 1];
|
|
360
|
+
const currentColorZ = colors[currentColorIndex + 2];
|
|
361
|
+
const currentColorW = colors[currentColorIndex + 3];
|
|
362
|
+
|
|
363
|
+
previousSlot = currentSlot;
|
|
364
|
+
currentSlot = nextSlot;
|
|
365
|
+
|
|
366
|
+
if (!samplesInWorld) {
|
|
367
|
+
// Inlined point transforms — three per trail sample.
|
|
368
|
+
let w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
|
|
369
|
+
let tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
|
|
370
|
+
let ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
|
|
371
|
+
currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
|
|
372
|
+
currentX = tx;
|
|
373
|
+
currentY = ty;
|
|
374
|
+
|
|
375
|
+
w = 1 / (m03 * previousX + m13 * previousY + m23 * previousZ + m33);
|
|
376
|
+
tx = (m00 * previousX + m10 * previousY + m20 * previousZ + m30) * w;
|
|
377
|
+
ty = (m01 * previousX + m11 * previousY + m21 * previousZ + m31) * w;
|
|
378
|
+
previousZ = (m02 * previousX + m12 * previousY + m22 * previousZ + m32) * w;
|
|
379
|
+
previousX = tx;
|
|
380
|
+
previousY = ty;
|
|
381
|
+
|
|
382
|
+
w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
|
|
383
|
+
tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
|
|
384
|
+
ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
|
|
385
|
+
nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
|
|
386
|
+
nextX = tx;
|
|
387
|
+
nextY = ty;
|
|
316
388
|
}
|
|
317
389
|
|
|
318
390
|
const pi = index * 3;
|
|
@@ -349,9 +421,11 @@ export class TrailBatch extends VFXBatch {
|
|
|
349
421
|
}
|
|
350
422
|
|
|
351
423
|
const ui = index * 2;
|
|
352
|
-
|
|
424
|
+
const along = i * invHistoryLength;
|
|
425
|
+
const u = ((uvFromHead ? 1 - along : along) + col) * tileWidth;
|
|
426
|
+
this.uvBuffer[ui] = u;
|
|
353
427
|
this.uvBuffer[ui + 1] = (vTileCount - row - 1) * tileHeight;
|
|
354
|
-
this.uvBuffer[ui + 2] =
|
|
428
|
+
this.uvBuffer[ui + 2] = u;
|
|
355
429
|
this.uvBuffer[ui + 3] = (vTileCount - row) * tileHeight;
|
|
356
430
|
|
|
357
431
|
const cci = index * 4;
|
|
@@ -374,39 +448,173 @@ export class TrailBatch extends VFXBatch {
|
|
|
374
448
|
this.indexBuffer[triangles * 3 + 2] = index + 3;
|
|
375
449
|
triangles++;
|
|
376
450
|
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Live tip: same place a sprite would be drawn this frame, past
|
|
454
|
+
// the last recorded sample. The recorded ribbon stays put.
|
|
455
|
+
if (stepFraction !== 0) {
|
|
456
|
+
const previousHead = particle.previousPosition;
|
|
457
|
+
const position = particle.position;
|
|
458
|
+
let tipX = position.x + (position.x - previousHead.x) * stepFraction;
|
|
459
|
+
let tipY = position.y + (position.y - previousHead.y) * stepFraction;
|
|
460
|
+
let tipZ = position.z + (position.z - previousHead.z) * stepFraction;
|
|
461
|
+
const headIndex = index - 2;
|
|
462
|
+
const headX = this.positionBuffer[headIndex * 3];
|
|
463
|
+
const headY = this.positionBuffer[headIndex * 3 + 1];
|
|
464
|
+
const headZ = this.positionBuffer[headIndex * 3 + 2];
|
|
465
|
+
|
|
466
|
+
if (!systemWorldSpace) {
|
|
467
|
+
const w = 1 / (m03 * tipX + m13 * tipY + m23 * tipZ + m33);
|
|
468
|
+
const tx = (m00 * tipX + m10 * tipY + m20 * tipZ + m30) * w;
|
|
469
|
+
const ty = (m01 * tipX + m11 * tipY + m21 * tipZ + m31) * w;
|
|
470
|
+
tipZ = (m02 * tipX + m12 * tipY + m22 * tipZ + m32) * w;
|
|
471
|
+
tipX = tx;
|
|
472
|
+
tipY = ty;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const tipPi = index * 3;
|
|
476
|
+
this.positionBuffer[tipPi] = tipX;
|
|
477
|
+
this.positionBuffer[tipPi + 1] = tipY;
|
|
478
|
+
this.positionBuffer[tipPi + 2] = tipZ;
|
|
479
|
+
this.positionBuffer[tipPi + 3] = tipX;
|
|
480
|
+
this.positionBuffer[tipPi + 4] = tipY;
|
|
481
|
+
this.positionBuffer[tipPi + 5] = tipZ;
|
|
482
|
+
|
|
483
|
+
this.previousBuffer[tipPi] = headX;
|
|
484
|
+
this.previousBuffer[tipPi + 1] = headY;
|
|
485
|
+
this.previousBuffer[tipPi + 2] = headZ;
|
|
486
|
+
this.previousBuffer[tipPi + 3] = headX;
|
|
487
|
+
this.previousBuffer[tipPi + 4] = headY;
|
|
488
|
+
this.previousBuffer[tipPi + 5] = headZ;
|
|
489
|
+
|
|
490
|
+
this.nextBuffer[tipPi] = tipX;
|
|
491
|
+
this.nextBuffer[tipPi + 1] = tipY;
|
|
492
|
+
this.nextBuffer[tipPi + 2] = tipZ;
|
|
493
|
+
this.nextBuffer[tipPi + 3] = tipX;
|
|
494
|
+
this.nextBuffer[tipPi + 4] = tipY;
|
|
495
|
+
this.nextBuffer[tipPi + 5] = tipZ;
|
|
496
|
+
|
|
497
|
+
// The recorded head's `next` was itself; point it at the tip
|
|
498
|
+
// so the shader builds the live segment from one version of
|
|
499
|
+
// each endpoint.
|
|
500
|
+
const headPi = headIndex * 3;
|
|
501
|
+
this.nextBuffer[headPi] = tipX;
|
|
502
|
+
this.nextBuffer[headPi + 1] = tipY;
|
|
503
|
+
this.nextBuffer[headPi + 2] = tipZ;
|
|
504
|
+
this.nextBuffer[headPi + 3] = tipX;
|
|
505
|
+
this.nextBuffer[headPi + 4] = tipY;
|
|
506
|
+
this.nextBuffer[headPi + 5] = tipZ;
|
|
507
|
+
|
|
508
|
+
this.sideBuffer[index] = 1;
|
|
509
|
+
this.sideBuffer[index + 1] = -1;
|
|
377
510
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
511
|
+
const tipSize = particle.size.x;
|
|
512
|
+
if (systemWorldSpace || particle.parentMatrix) {
|
|
513
|
+
this.widthBuffer[index] = tipSize;
|
|
514
|
+
this.widthBuffer[index + 1] = tipSize;
|
|
515
|
+
} else {
|
|
516
|
+
this.widthBuffer[index] = tipSize * objectScale;
|
|
517
|
+
this.widthBuffer[index + 1] = tipSize * objectScale;
|
|
382
518
|
}
|
|
519
|
+
|
|
520
|
+
const tipUi = index * 2;
|
|
521
|
+
const tipU = ((uvFromHead ? 0 : 1) + col) * tileWidth;
|
|
522
|
+
this.uvBuffer[tipUi] = tipU;
|
|
523
|
+
this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
|
|
524
|
+
this.uvBuffer[tipUi + 2] = tipU;
|
|
525
|
+
this.uvBuffer[tipUi + 3] = (vTileCount - row) * tileHeight;
|
|
526
|
+
|
|
527
|
+
const tipColor = particle.color;
|
|
528
|
+
const tipCi = index * 4;
|
|
529
|
+
this.colorBuffer[tipCi] = tipColor.x;
|
|
530
|
+
this.colorBuffer[tipCi + 1] = tipColor.y;
|
|
531
|
+
this.colorBuffer[tipCi + 2] = tipColor.z;
|
|
532
|
+
this.colorBuffer[tipCi + 3] = tipColor.w;
|
|
533
|
+
this.colorBuffer[tipCi + 4] = tipColor.x;
|
|
534
|
+
this.colorBuffer[tipCi + 5] = tipColor.y;
|
|
535
|
+
this.colorBuffer[tipCi + 6] = tipColor.z;
|
|
536
|
+
this.colorBuffer[tipCi + 7] = tipColor.w;
|
|
537
|
+
|
|
538
|
+
this.indexBuffer[triangles * 3] = headIndex;
|
|
539
|
+
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
540
|
+
this.indexBuffer[triangles * 3 + 2] = index;
|
|
541
|
+
triangles++;
|
|
542
|
+
this.indexBuffer[triangles * 3] = index;
|
|
543
|
+
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
544
|
+
this.indexBuffer[triangles * 3 + 2] = index + 1;
|
|
545
|
+
triangles++;
|
|
546
|
+
|
|
547
|
+
index += 2;
|
|
383
548
|
}
|
|
384
549
|
}
|
|
385
550
|
}
|
|
386
551
|
|
|
552
|
+
this.uploadGeometry(index, triangles);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Uploads the vertex data written by `update` and resizes the sub mesh.
|
|
557
|
+
*
|
|
558
|
+
* Split out of `update` on purpose: the Babylon mesh/sub-mesh plumbing here
|
|
559
|
+
* runs a handful of times per frame, and folding it into the per-vertex loop
|
|
560
|
+
* costs that loop its optimised code.
|
|
561
|
+
*/
|
|
562
|
+
private uploadGeometry(index: number, triangles: number): void {
|
|
387
563
|
if (index > 0 && triangles > 0) {
|
|
388
|
-
this
|
|
389
|
-
|
|
564
|
+
// Upload only the vertices written this frame; the buffers are sized for
|
|
565
|
+
// the high-water mark of every system in the batch.
|
|
566
|
+
this.mesh.updateVerticesData(VertexBuffer.PositionKind, this.positionBuffer.subarray(0, index * 3));
|
|
567
|
+
this.mesh.updateVerticesData(VertexBuffer.UVKind, this.uvBuffer.subarray(0, index * 2));
|
|
390
568
|
this.mesh.updateIndices(this.indexBuffer.subarray(0, triangles * 3));
|
|
391
|
-
this.previousVB.update(this.previousBuffer);
|
|
392
|
-
this.nextVB.update(this.nextBuffer);
|
|
393
|
-
this.sideVB.update(this.sideBuffer);
|
|
394
|
-
this.widthVB.update(this.widthBuffer);
|
|
569
|
+
this.previousVB.update(this.previousBuffer.subarray(0, index * 3));
|
|
570
|
+
this.nextVB.update(this.nextBuffer.subarray(0, index * 3));
|
|
571
|
+
this.sideVB.update(this.sideBuffer.subarray(0, index));
|
|
572
|
+
this.widthVB.update(this.widthBuffer.subarray(0, index));
|
|
395
573
|
|
|
396
574
|
const colorVB = this.mesh.getVertexBuffer(VertexBuffer.ColorKind);
|
|
397
|
-
if (colorVB) colorVB.update(this.colorBuffer);
|
|
575
|
+
if (colorVB) colorVB.update(this.colorBuffer.subarray(0, index * 4));
|
|
398
576
|
|
|
399
577
|
if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
|
|
400
|
-
|
|
401
|
-
for (const subMesh of this.mesh.subMeshes) {
|
|
402
|
-
(subMesh as any)._boundingInfo = meshBoundingInfo;
|
|
403
|
-
}
|
|
578
|
+
this.adoptMeshBounds();
|
|
404
579
|
this.mesh.subMeshes[0].indexCount = triangles * 3;
|
|
405
580
|
this.mesh.subMeshes[0].verticesCount = index;
|
|
406
581
|
}
|
|
582
|
+
this.drawsNothing = false;
|
|
407
583
|
} else if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
|
|
408
|
-
|
|
409
|
-
|
|
584
|
+
// Nothing to draw. Leaving the count at zero still submits the draw,
|
|
585
|
+
// and WebGPU complains about it once per frame for as long as the
|
|
586
|
+
// batch stays empty — which, for a looping effect, is every gap
|
|
587
|
+
// between passes. Disabling the mesh instead costs a frame coming
|
|
588
|
+
// back, the flicker the sprite batch already works around.
|
|
589
|
+
//
|
|
590
|
+
// So draw a triangle whose corners are all the same vertex: it is a
|
|
591
|
+
// legal draw that rasterises nothing, whatever stale geometry is
|
|
592
|
+
// still in the buffers. Only written on the way into empty, not on
|
|
593
|
+
// every empty frame.
|
|
594
|
+
if (!this.drawsNothing) {
|
|
595
|
+
this.indexBuffer.fill(0, 0, 6);
|
|
596
|
+
this.mesh.updateIndices(this.indexBuffer.subarray(0, 6));
|
|
597
|
+
this.adoptMeshBounds();
|
|
598
|
+
this.drawsNothing = true;
|
|
599
|
+
}
|
|
600
|
+
this.mesh.subMeshes[0].indexCount = 6;
|
|
601
|
+
this.mesh.subMeshes[0].verticesCount = 1;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Points every sub mesh at the mesh's own bounds.
|
|
607
|
+
*
|
|
608
|
+
* Updating indices rebuilds the sub meshes, and a fresh one computes its
|
|
609
|
+
* bounds from the vertex data — which this batch keeps deliberately stale
|
|
610
|
+
* beyond the range it draws. Transparent sorting reads those bounds every
|
|
611
|
+
* frame, so they have to be replaced again after every index update, not
|
|
612
|
+
* only when the geometry is built.
|
|
613
|
+
*/
|
|
614
|
+
private adoptMeshBounds(): void {
|
|
615
|
+
const meshBoundingInfo = this.mesh.getBoundingInfo();
|
|
616
|
+
for (const subMesh of this.mesh.subMeshes) {
|
|
617
|
+
(subMesh as any)._boundingInfo = meshBoundingInfo;
|
|
410
618
|
}
|
|
411
619
|
}
|
|
412
620
|
|