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/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,117 @@ 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 residual = (system.rendererEmitterSettings as TrailSettings).followLocalOrigin
|
|
280
|
+
? 0
|
|
281
|
+
: (system.simulationResidual ?? 0);
|
|
282
|
+
// Measured from the step the particle just took, not from its
|
|
283
|
+
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
284
|
+
// behavior has motion no velocity term describes.
|
|
285
|
+
const stepFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
|
|
199
286
|
|
|
200
287
|
for (let j = 0; j < particleNum; j++) {
|
|
201
288
|
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;
|
|
289
|
+
const particleHistoryLength = particle.historyCount;
|
|
224
290
|
if (particleHistoryLength === 0) {
|
|
225
291
|
continue;
|
|
226
292
|
}
|
|
293
|
+
const historyCapacity = particle.historyCapacity;
|
|
294
|
+
const historyPositions = particle.historyPositions;
|
|
295
|
+
const historySizes = particle.historySizes;
|
|
296
|
+
const historyColors = particle.historyColors;
|
|
227
297
|
const col = particle.uvTile % vTileCount;
|
|
228
298
|
const row = Math.floor(particle.uvTile / vTileCount + 0.001);
|
|
229
299
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
300
|
+
const me = (particleMatrix ?? emitterMatrix).elements;
|
|
301
|
+
const m00 = me[0],
|
|
302
|
+
m01 = me[1],
|
|
303
|
+
m02 = me[2],
|
|
304
|
+
m03 = me[3];
|
|
305
|
+
const m10 = me[4],
|
|
306
|
+
m11 = me[5],
|
|
307
|
+
m12 = me[6],
|
|
308
|
+
m13 = me[7];
|
|
309
|
+
const m20 = me[8],
|
|
310
|
+
m21 = me[9],
|
|
311
|
+
m22 = me[10],
|
|
312
|
+
m23 = me[11];
|
|
313
|
+
const m30 = me[12],
|
|
314
|
+
m31 = me[13],
|
|
315
|
+
m32 = me[14],
|
|
316
|
+
m33 = me[15];
|
|
317
|
+
const invHistoryLength = 1 / particleHistoryLength;
|
|
318
|
+
|
|
319
|
+
// Walk the ring buffer oldest-first with hand-rolled wrapping so the
|
|
320
|
+
// inner loop stays free of modulo operations.
|
|
321
|
+
let currentSlot = particle.getHistoryIndex(0);
|
|
322
|
+
let previousSlot = currentSlot;
|
|
238
323
|
|
|
239
324
|
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
|
-
previousX = historyPositions[previousPosIndex];
|
|
271
|
-
previousY = historyPositions[previousPosIndex + 1];
|
|
272
|
-
previousZ = historyPositions[previousPosIndex + 2];
|
|
273
|
-
nextX = historyPositions[nextPosIndex];
|
|
274
|
-
nextY = historyPositions[nextPosIndex + 1];
|
|
275
|
-
nextZ = historyPositions[nextPosIndex + 2];
|
|
276
|
-
|
|
277
|
-
currentSize = historySizes[currentHistoryIndex];
|
|
278
|
-
const currentColorIndex = currentHistoryIndex * 4;
|
|
279
|
-
currentColorX = historyColors[currentColorIndex];
|
|
280
|
-
currentColorY = historyColors[currentColorIndex + 1];
|
|
281
|
-
currentColorZ = historyColors[currentColorIndex + 2];
|
|
282
|
-
currentColorW = historyColors[currentColorIndex + 3];
|
|
283
|
-
} else {
|
|
284
|
-
const previous = previousNode!.data as RecordState;
|
|
285
|
-
const current = currentNode!.data as RecordState;
|
|
286
|
-
const next = nextNode!.data as RecordState;
|
|
287
|
-
currentX = current.position.x;
|
|
288
|
-
currentY = current.position.y;
|
|
289
|
-
currentZ = current.position.z;
|
|
290
|
-
previousX = previous.position.x;
|
|
291
|
-
previousY = previous.position.y;
|
|
292
|
-
previousZ = previous.position.z;
|
|
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
|
-
}
|
|
325
|
+
const nextSlot =
|
|
326
|
+
i + 1 < particleHistoryLength
|
|
327
|
+
? currentSlot + 1 === historyCapacity
|
|
328
|
+
? 0
|
|
329
|
+
: currentSlot + 1
|
|
330
|
+
: currentSlot;
|
|
331
|
+
|
|
332
|
+
const currentPosIndex = currentSlot * 3;
|
|
333
|
+
const previousPosIndex = previousSlot * 3;
|
|
334
|
+
const nextPosIndex = nextSlot * 3;
|
|
335
|
+
|
|
336
|
+
let currentX = historyPositions[currentPosIndex];
|
|
337
|
+
let currentY = historyPositions[currentPosIndex + 1];
|
|
338
|
+
let currentZ = historyPositions[currentPosIndex + 2];
|
|
339
|
+
let previousX = historyPositions[previousPosIndex];
|
|
340
|
+
let previousY = historyPositions[previousPosIndex + 1];
|
|
341
|
+
let previousZ = historyPositions[previousPosIndex + 2];
|
|
342
|
+
let nextX = historyPositions[nextPosIndex];
|
|
343
|
+
let nextY = historyPositions[nextPosIndex + 1];
|
|
344
|
+
let nextZ = historyPositions[nextPosIndex + 2];
|
|
345
|
+
|
|
346
|
+
const currentSize = historySizes[currentSlot];
|
|
347
|
+
const currentColorIndex = currentSlot * 4;
|
|
348
|
+
const currentColorX = historyColors[currentColorIndex];
|
|
349
|
+
const currentColorY = historyColors[currentColorIndex + 1];
|
|
350
|
+
const currentColorZ = historyColors[currentColorIndex + 2];
|
|
351
|
+
const currentColorW = historyColors[currentColorIndex + 3];
|
|
352
|
+
|
|
353
|
+
previousSlot = currentSlot;
|
|
354
|
+
currentSlot = nextSlot;
|
|
302
355
|
|
|
303
356
|
if (!systemWorldSpace) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
357
|
+
// Inlined point transforms — three per trail sample.
|
|
358
|
+
let w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
|
|
359
|
+
let tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
|
|
360
|
+
let ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
|
|
361
|
+
currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
|
|
362
|
+
currentX = tx;
|
|
363
|
+
currentY = ty;
|
|
364
|
+
|
|
365
|
+
w = 1 / (m03 * previousX + m13 * previousY + m23 * previousZ + m33);
|
|
366
|
+
tx = (m00 * previousX + m10 * previousY + m20 * previousZ + m30) * w;
|
|
367
|
+
ty = (m01 * previousX + m11 * previousY + m21 * previousZ + m31) * w;
|
|
368
|
+
previousZ = (m02 * previousX + m12 * previousY + m22 * previousZ + m32) * w;
|
|
369
|
+
previousX = tx;
|
|
370
|
+
previousY = ty;
|
|
371
|
+
|
|
372
|
+
w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
|
|
373
|
+
tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
|
|
374
|
+
ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
|
|
375
|
+
nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
|
|
376
|
+
nextX = tx;
|
|
377
|
+
nextY = ty;
|
|
316
378
|
}
|
|
317
379
|
|
|
318
380
|
const pi = index * 3;
|
|
@@ -349,9 +411,10 @@ export class TrailBatch extends VFXBatch {
|
|
|
349
411
|
}
|
|
350
412
|
|
|
351
413
|
const ui = index * 2;
|
|
352
|
-
|
|
414
|
+
const u = (i * invHistoryLength + col) * tileWidth;
|
|
415
|
+
this.uvBuffer[ui] = u;
|
|
353
416
|
this.uvBuffer[ui + 1] = (vTileCount - row - 1) * tileHeight;
|
|
354
|
-
this.uvBuffer[ui + 2] =
|
|
417
|
+
this.uvBuffer[ui + 2] = u;
|
|
355
418
|
this.uvBuffer[ui + 3] = (vTileCount - row) * tileHeight;
|
|
356
419
|
|
|
357
420
|
const cci = index * 4;
|
|
@@ -374,39 +437,173 @@ export class TrailBatch extends VFXBatch {
|
|
|
374
437
|
this.indexBuffer[triangles * 3 + 2] = index + 3;
|
|
375
438
|
triangles++;
|
|
376
439
|
}
|
|
440
|
+
}
|
|
377
441
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
442
|
+
// Live tip: same place a sprite would be drawn this frame, past
|
|
443
|
+
// the last recorded sample. The recorded ribbon stays put.
|
|
444
|
+
if (stepFraction !== 0) {
|
|
445
|
+
const previousHead = particle.previousPosition;
|
|
446
|
+
const position = particle.position;
|
|
447
|
+
let tipX = position.x + (position.x - previousHead.x) * stepFraction;
|
|
448
|
+
let tipY = position.y + (position.y - previousHead.y) * stepFraction;
|
|
449
|
+
let tipZ = position.z + (position.z - previousHead.z) * stepFraction;
|
|
450
|
+
const headIndex = index - 2;
|
|
451
|
+
const headX = this.positionBuffer[headIndex * 3];
|
|
452
|
+
const headY = this.positionBuffer[headIndex * 3 + 1];
|
|
453
|
+
const headZ = this.positionBuffer[headIndex * 3 + 2];
|
|
454
|
+
|
|
455
|
+
if (!systemWorldSpace) {
|
|
456
|
+
const w = 1 / (m03 * tipX + m13 * tipY + m23 * tipZ + m33);
|
|
457
|
+
const tx = (m00 * tipX + m10 * tipY + m20 * tipZ + m30) * w;
|
|
458
|
+
const ty = (m01 * tipX + m11 * tipY + m21 * tipZ + m31) * w;
|
|
459
|
+
tipZ = (m02 * tipX + m12 * tipY + m22 * tipZ + m32) * w;
|
|
460
|
+
tipX = tx;
|
|
461
|
+
tipY = ty;
|
|
382
462
|
}
|
|
463
|
+
|
|
464
|
+
const tipPi = index * 3;
|
|
465
|
+
this.positionBuffer[tipPi] = tipX;
|
|
466
|
+
this.positionBuffer[tipPi + 1] = tipY;
|
|
467
|
+
this.positionBuffer[tipPi + 2] = tipZ;
|
|
468
|
+
this.positionBuffer[tipPi + 3] = tipX;
|
|
469
|
+
this.positionBuffer[tipPi + 4] = tipY;
|
|
470
|
+
this.positionBuffer[tipPi + 5] = tipZ;
|
|
471
|
+
|
|
472
|
+
this.previousBuffer[tipPi] = headX;
|
|
473
|
+
this.previousBuffer[tipPi + 1] = headY;
|
|
474
|
+
this.previousBuffer[tipPi + 2] = headZ;
|
|
475
|
+
this.previousBuffer[tipPi + 3] = headX;
|
|
476
|
+
this.previousBuffer[tipPi + 4] = headY;
|
|
477
|
+
this.previousBuffer[tipPi + 5] = headZ;
|
|
478
|
+
|
|
479
|
+
this.nextBuffer[tipPi] = tipX;
|
|
480
|
+
this.nextBuffer[tipPi + 1] = tipY;
|
|
481
|
+
this.nextBuffer[tipPi + 2] = tipZ;
|
|
482
|
+
this.nextBuffer[tipPi + 3] = tipX;
|
|
483
|
+
this.nextBuffer[tipPi + 4] = tipY;
|
|
484
|
+
this.nextBuffer[tipPi + 5] = tipZ;
|
|
485
|
+
|
|
486
|
+
// The recorded head's `next` was itself; point it at the tip
|
|
487
|
+
// so the shader builds the live segment from one version of
|
|
488
|
+
// each endpoint.
|
|
489
|
+
const headPi = headIndex * 3;
|
|
490
|
+
this.nextBuffer[headPi] = tipX;
|
|
491
|
+
this.nextBuffer[headPi + 1] = tipY;
|
|
492
|
+
this.nextBuffer[headPi + 2] = tipZ;
|
|
493
|
+
this.nextBuffer[headPi + 3] = tipX;
|
|
494
|
+
this.nextBuffer[headPi + 4] = tipY;
|
|
495
|
+
this.nextBuffer[headPi + 5] = tipZ;
|
|
496
|
+
|
|
497
|
+
this.sideBuffer[index] = 1;
|
|
498
|
+
this.sideBuffer[index + 1] = -1;
|
|
499
|
+
|
|
500
|
+
const tipSize = particle.size.x;
|
|
501
|
+
if (systemWorldSpace || particle.parentMatrix) {
|
|
502
|
+
this.widthBuffer[index] = tipSize;
|
|
503
|
+
this.widthBuffer[index + 1] = tipSize;
|
|
504
|
+
} else {
|
|
505
|
+
this.widthBuffer[index] = tipSize * objectScale;
|
|
506
|
+
this.widthBuffer[index + 1] = tipSize * objectScale;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const tipUi = index * 2;
|
|
510
|
+
const tipU = (1 + col) * tileWidth;
|
|
511
|
+
this.uvBuffer[tipUi] = tipU;
|
|
512
|
+
this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
|
|
513
|
+
this.uvBuffer[tipUi + 2] = tipU;
|
|
514
|
+
this.uvBuffer[tipUi + 3] = (vTileCount - row) * tileHeight;
|
|
515
|
+
|
|
516
|
+
const tipColor = particle.color;
|
|
517
|
+
const tipCi = index * 4;
|
|
518
|
+
this.colorBuffer[tipCi] = tipColor.x;
|
|
519
|
+
this.colorBuffer[tipCi + 1] = tipColor.y;
|
|
520
|
+
this.colorBuffer[tipCi + 2] = tipColor.z;
|
|
521
|
+
this.colorBuffer[tipCi + 3] = tipColor.w;
|
|
522
|
+
this.colorBuffer[tipCi + 4] = tipColor.x;
|
|
523
|
+
this.colorBuffer[tipCi + 5] = tipColor.y;
|
|
524
|
+
this.colorBuffer[tipCi + 6] = tipColor.z;
|
|
525
|
+
this.colorBuffer[tipCi + 7] = tipColor.w;
|
|
526
|
+
|
|
527
|
+
this.indexBuffer[triangles * 3] = headIndex;
|
|
528
|
+
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
529
|
+
this.indexBuffer[triangles * 3 + 2] = index;
|
|
530
|
+
triangles++;
|
|
531
|
+
this.indexBuffer[triangles * 3] = index;
|
|
532
|
+
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
533
|
+
this.indexBuffer[triangles * 3 + 2] = index + 1;
|
|
534
|
+
triangles++;
|
|
535
|
+
|
|
536
|
+
index += 2;
|
|
383
537
|
}
|
|
384
538
|
}
|
|
385
539
|
}
|
|
386
540
|
|
|
541
|
+
this.uploadGeometry(index, triangles);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Uploads the vertex data written by `update` and resizes the sub mesh.
|
|
546
|
+
*
|
|
547
|
+
* Split out of `update` on purpose: the Babylon mesh/sub-mesh plumbing here
|
|
548
|
+
* runs a handful of times per frame, and folding it into the per-vertex loop
|
|
549
|
+
* costs that loop its optimised code.
|
|
550
|
+
*/
|
|
551
|
+
private uploadGeometry(index: number, triangles: number): void {
|
|
387
552
|
if (index > 0 && triangles > 0) {
|
|
388
|
-
this
|
|
389
|
-
|
|
553
|
+
// Upload only the vertices written this frame; the buffers are sized for
|
|
554
|
+
// the high-water mark of every system in the batch.
|
|
555
|
+
this.mesh.updateVerticesData(VertexBuffer.PositionKind, this.positionBuffer.subarray(0, index * 3));
|
|
556
|
+
this.mesh.updateVerticesData(VertexBuffer.UVKind, this.uvBuffer.subarray(0, index * 2));
|
|
390
557
|
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);
|
|
558
|
+
this.previousVB.update(this.previousBuffer.subarray(0, index * 3));
|
|
559
|
+
this.nextVB.update(this.nextBuffer.subarray(0, index * 3));
|
|
560
|
+
this.sideVB.update(this.sideBuffer.subarray(0, index));
|
|
561
|
+
this.widthVB.update(this.widthBuffer.subarray(0, index));
|
|
395
562
|
|
|
396
563
|
const colorVB = this.mesh.getVertexBuffer(VertexBuffer.ColorKind);
|
|
397
|
-
if (colorVB) colorVB.update(this.colorBuffer);
|
|
564
|
+
if (colorVB) colorVB.update(this.colorBuffer.subarray(0, index * 4));
|
|
398
565
|
|
|
399
566
|
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
|
-
}
|
|
567
|
+
this.adoptMeshBounds();
|
|
404
568
|
this.mesh.subMeshes[0].indexCount = triangles * 3;
|
|
405
569
|
this.mesh.subMeshes[0].verticesCount = index;
|
|
406
570
|
}
|
|
571
|
+
this.drawsNothing = false;
|
|
407
572
|
} else if (this.mesh.subMeshes && this.mesh.subMeshes.length > 0) {
|
|
408
|
-
|
|
409
|
-
|
|
573
|
+
// Nothing to draw. Leaving the count at zero still submits the draw,
|
|
574
|
+
// and WebGPU complains about it once per frame for as long as the
|
|
575
|
+
// batch stays empty — which, for a looping effect, is every gap
|
|
576
|
+
// between passes. Disabling the mesh instead costs a frame coming
|
|
577
|
+
// back, the flicker the sprite batch already works around.
|
|
578
|
+
//
|
|
579
|
+
// So draw a triangle whose corners are all the same vertex: it is a
|
|
580
|
+
// legal draw that rasterises nothing, whatever stale geometry is
|
|
581
|
+
// still in the buffers. Only written on the way into empty, not on
|
|
582
|
+
// every empty frame.
|
|
583
|
+
if (!this.drawsNothing) {
|
|
584
|
+
this.indexBuffer.fill(0, 0, 6);
|
|
585
|
+
this.mesh.updateIndices(this.indexBuffer.subarray(0, 6));
|
|
586
|
+
this.adoptMeshBounds();
|
|
587
|
+
this.drawsNothing = true;
|
|
588
|
+
}
|
|
589
|
+
this.mesh.subMeshes[0].indexCount = 6;
|
|
590
|
+
this.mesh.subMeshes[0].verticesCount = 1;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Points every sub mesh at the mesh's own bounds.
|
|
596
|
+
*
|
|
597
|
+
* Updating indices rebuilds the sub meshes, and a fresh one computes its
|
|
598
|
+
* bounds from the vertex data — which this batch keeps deliberately stale
|
|
599
|
+
* beyond the range it draws. Transparent sorting reads those bounds every
|
|
600
|
+
* frame, so they have to be replaced again after every index update, not
|
|
601
|
+
* only when the geometry is built.
|
|
602
|
+
*/
|
|
603
|
+
private adoptMeshBounds(): void {
|
|
604
|
+
const meshBoundingInfo = this.mesh.getBoundingInfo();
|
|
605
|
+
for (const subMesh of this.mesh.subMeshes) {
|
|
606
|
+
(subMesh as any)._boundingInfo = meshBoundingInfo;
|
|
410
607
|
}
|
|
411
608
|
}
|
|
412
609
|
|