babylon.quarks 0.26.0 → 0.27.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 +769 -8
- package/README.md +10 -8
- 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.cts +6683 -0
- package/dist/types/index.d.ts +1959 -588
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +204 -101
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +909 -2061
- package/src/QuarksLoader.ts +421 -67
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +106 -246
- package/src/TrailBatch.ts +533 -342
- package/src/VFXBatch.ts +236 -136
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +32 -33
- package/src/nodeId.ts +13 -0
- package/src/shaderProgram.ts +325 -0
- package/src/shaderVariants.ts +225 -0
- package/src/shaders/chunks/fog.ts +4 -2
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +84 -0
- package/src/shaders/chunks/surface.ts +10 -0
- package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
- package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
- package/src/shaders/particle_frag.glsl.ts +3 -22
- package/src/shaders/particle_frag.wgsl.ts +3 -23
- package/src/shaders/particle_physics_frag.glsl.ts +12 -25
- package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
- package/src/shaders/particle_vert.glsl.ts +5 -27
- package/src/shaders/particle_vert.wgsl.ts +5 -26
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
- package/src/shaders/trail_frag.glsl.ts +11 -1
- package/src/shaders/trail_frag.wgsl.ts +11 -0
- package/src/shaders/trail_vert.glsl.ts +45 -36
- package/src/shaders/trail_vert.wgsl.ts +46 -40
- package/src/softDepth.ts +136 -0
- package/src/system/BehaviorPlan.ts +85 -0
- package/src/system/BoundsTracker.ts +181 -0
- package/src/system/EmissionController.ts +360 -0
- package/src/system/EventDispatcher.ts +44 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +111 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +116 -0
- package/src/system/normalize.ts +95 -0
- package/src/system/serialization.ts +401 -0
- package/src/system/settingsSchema.ts +317 -0
- package/src/system/types.ts +410 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +51 -0
- package/src/BatchedParticleRenderer.ts +0 -3
- package/src/materials/ParticleMaterials.ts +0 -23
- package/src/materials/index.ts +0 -1
- package/src/shaders/chunks/index.ts +0 -1
- package/src/shaders/chunks/register-shader-chunks.ts +0 -32
- package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
- package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
- package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
- package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
- package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
- package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
- package/src/shaders/index.ts +0 -9
- package/src/shaders/local_particle_vert.glsl.ts +0 -50
package/src/TrailBatch.ts
CHANGED
|
@@ -1,19 +1,103 @@
|
|
|
1
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
2
2
|
import {Constants} from '@babylonjs/core/Engines/constants';
|
|
3
3
|
import {ShaderMaterial} from '@babylonjs/core/Materials/shaderMaterial';
|
|
4
|
-
import {Vector2 as BVector2} from '@babylonjs/core/Maths/math.vector';
|
|
5
4
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
6
5
|
import {Scene} from '@babylonjs/core/scene';
|
|
7
6
|
import {IParticleSystem, Matrix4, Quaternion, TrailParticle, TrailSettings, Vector3} from 'quarks.core';
|
|
8
7
|
import {VFXBatchSettings} from './BatchedRenderer';
|
|
9
8
|
import type {ParticleSystem} from './ParticleSystem';
|
|
9
|
+
import {shaderProgram} from './shaderProgram';
|
|
10
|
+
import {sheetCell} from './shaders/chunks/sheet';
|
|
10
11
|
import {registerShaders, shaderLanguageFor} from './shaders/shaderLanguageSupport';
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import type {DepthBinding} from './softDepth';
|
|
13
|
+
import {
|
|
14
|
+
trailCurveTension,
|
|
15
|
+
trailMinVertexDistance,
|
|
16
|
+
trailTessellation,
|
|
17
|
+
trailTextureMode,
|
|
18
|
+
trailTextureScale,
|
|
19
|
+
} from './trailSettings';
|
|
15
20
|
import {VFXBatch} from './VFXBatch';
|
|
16
21
|
|
|
22
|
+
/** The texture modes as the vertex loop tells them apart, see `TrailSettings.textureMode`. */
|
|
23
|
+
const enum TextureMode {
|
|
24
|
+
Stretch,
|
|
25
|
+
Tile,
|
|
26
|
+
DistributePerSegment,
|
|
27
|
+
RepeatPerSegment,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One trail's points for a frame, in world space: where each lies, its side
|
|
32
|
+
* when the trail is swept, its width, its color and its distance along the
|
|
33
|
+
* trail. Grown as needed and kept, so drawing a trail allocates nothing.
|
|
34
|
+
*/
|
|
35
|
+
class TrailSamples {
|
|
36
|
+
position = new Float32Array(0);
|
|
37
|
+
side = new Float32Array(0);
|
|
38
|
+
size = new Float32Array(0);
|
|
39
|
+
color = new Float32Array(0);
|
|
40
|
+
/** Distance along the trail, as recorded (`TrailParticle.historyDistances`). */
|
|
41
|
+
distance = new Float32Array(0);
|
|
42
|
+
|
|
43
|
+
reserve(count: number): this {
|
|
44
|
+
if (count > this.size.length) {
|
|
45
|
+
const capacity = Math.max(count, this.size.length * 2, 64);
|
|
46
|
+
this.position = new Float32Array(capacity * 3);
|
|
47
|
+
this.side = new Float32Array(capacity * 3);
|
|
48
|
+
this.size = new Float32Array(capacity);
|
|
49
|
+
this.color = new Float32Array(capacity * 4);
|
|
50
|
+
this.distance = new Float32Array(capacity);
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Sample `i` at the point, carried by the matrix `m` (column-major elements) when given. */
|
|
56
|
+
writePoint(i: number, x: number, y: number, z: number, m: ArrayLike<number> | null): void {
|
|
57
|
+
const o = i * 3;
|
|
58
|
+
if (m) {
|
|
59
|
+
const w = 1 / (m[3] * x + m[7] * y + m[11] * z + m[15]);
|
|
60
|
+
this.position[o] = (m[0] * x + m[4] * y + m[8] * z + m[12]) * w;
|
|
61
|
+
this.position[o + 1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) * w;
|
|
62
|
+
this.position[o + 2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) * w;
|
|
63
|
+
} else {
|
|
64
|
+
this.position[o] = x;
|
|
65
|
+
this.position[o + 1] = y;
|
|
66
|
+
this.position[o + 2] = z;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Sample `i`'s side, a direction turned (and scaled) by `m` when given. */
|
|
71
|
+
writeSide(i: number, x: number, y: number, z: number, m: ArrayLike<number> | null): void {
|
|
72
|
+
const o = i * 3;
|
|
73
|
+
if (m) {
|
|
74
|
+
this.side[o] = m[0] * x + m[4] * y + m[8] * z;
|
|
75
|
+
this.side[o + 1] = m[1] * x + m[5] * y + m[9] * z;
|
|
76
|
+
this.side[o + 2] = m[2] * x + m[6] * y + m[10] * z;
|
|
77
|
+
} else {
|
|
78
|
+
this.side[o] = x;
|
|
79
|
+
this.side[o + 1] = y;
|
|
80
|
+
this.side[o + 2] = z;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeColor(i: number, r: number, g: number, b: number, a: number): void {
|
|
85
|
+
const o = i * 4;
|
|
86
|
+
this.color[o] = r;
|
|
87
|
+
this.color[o + 1] = g;
|
|
88
|
+
this.color[o + 2] = b;
|
|
89
|
+
this.color[o + 3] = a;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Knot spacing of a centripetal spline: the square root of the length between two points of `p`. */
|
|
94
|
+
function knot(p: Float32Array, from: number, to: number): number {
|
|
95
|
+
return Math.sqrt(
|
|
96
|
+
Math.sqrt((p[to] - p[from]) ** 2 + (p[to + 1] - p[from + 1]) ** 2 + (p[to + 2] - p[from + 2]) ** 2)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The batch of trail particles. @internal */
|
|
17
101
|
export class TrailBatch extends VFXBatch {
|
|
18
102
|
private positionBuffer!: Float32Array;
|
|
19
103
|
private previousBuffer!: Float32Array;
|
|
@@ -32,16 +116,17 @@ export class TrailBatch extends VFXBatch {
|
|
|
32
116
|
private sideVB!: VertexBuffer;
|
|
33
117
|
private widthVB!: VertexBuffer;
|
|
34
118
|
|
|
35
|
-
constructor(settings: VFXBatchSettings, scene: Scene) {
|
|
36
|
-
super(settings, scene);
|
|
37
|
-
//
|
|
38
|
-
// per-batch allocation in the library, so start small and let
|
|
39
|
-
//
|
|
119
|
+
constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
|
|
120
|
+
super(settings, scene, depth);
|
|
121
|
+
// For a trail batch `maxParticles` counts vertices, two per drawn point: the buffers are
|
|
122
|
+
// the largest per-batch allocation in the library, so start small and let expandBuffers
|
|
123
|
+
// grow to whatever the batch actually needs.
|
|
40
124
|
this.maxParticles = 1024;
|
|
41
125
|
this.setupBuffers();
|
|
42
126
|
this.rebuildMaterial();
|
|
43
127
|
}
|
|
44
128
|
|
|
129
|
+
/** @internal */
|
|
45
130
|
setupBuffers(): void {
|
|
46
131
|
this.mesh.dispose();
|
|
47
132
|
this.mesh = new Mesh('trailBatch', this.scene);
|
|
@@ -50,15 +135,17 @@ export class TrailBatch extends VFXBatch {
|
|
|
50
135
|
}
|
|
51
136
|
|
|
52
137
|
private buildGeometryBuffers(): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
138
|
+
// `maxParticles` vertices; a ribbon of n points is 2n vertices and 6(n - 1) indices.
|
|
139
|
+
const vertices = this.maxParticles;
|
|
140
|
+
this.positionBuffer = new Float32Array(vertices * 3);
|
|
141
|
+
this.previousBuffer = new Float32Array(vertices * 3);
|
|
142
|
+
this.nextBuffer = new Float32Array(vertices * 3);
|
|
143
|
+
this.uvBuffer = new Float32Array(vertices * 2);
|
|
144
|
+
this.uv1Buffer = this.usesUV1() ? new Float32Array(vertices * 2) : null;
|
|
145
|
+
this.sideBuffer = new Float32Array(vertices);
|
|
146
|
+
this.widthBuffer = new Float32Array(vertices);
|
|
147
|
+
this.colorBuffer = new Float32Array(vertices * 4);
|
|
148
|
+
this.indexBuffer = new Uint32Array(vertices * 3);
|
|
62
149
|
// Vertices come in pairs, one per side of the ribbon, so the side never
|
|
63
150
|
// changes: written once here and never uploaded again.
|
|
64
151
|
for (let i = 0; i < this.sideBuffer.length; i += 2) {
|
|
@@ -123,12 +210,13 @@ export class TrailBatch extends VFXBatch {
|
|
|
123
210
|
}
|
|
124
211
|
}
|
|
125
212
|
|
|
213
|
+
/** Grows the buffers to hold more than `target` vertices. @internal */
|
|
126
214
|
expandBuffers(target: number): void {
|
|
127
215
|
while (target >= this.maxParticles) {
|
|
128
216
|
this.maxParticles *= 2;
|
|
129
217
|
}
|
|
130
|
-
//
|
|
131
|
-
//
|
|
218
|
+
// `Geometry.setVerticesBuffer` disposes the buffer it replaces; these custom attributes
|
|
219
|
+
// are not in the geometry's map, so they are released here.
|
|
132
220
|
this.previousVB?.dispose();
|
|
133
221
|
this.nextVB?.dispose();
|
|
134
222
|
this.sideVB?.dispose();
|
|
@@ -136,45 +224,15 @@ export class TrailBatch extends VFXBatch {
|
|
|
136
224
|
this.buildGeometryBuffers();
|
|
137
225
|
}
|
|
138
226
|
|
|
227
|
+
/** @internal */
|
|
139
228
|
rebuildMaterial(): void {
|
|
229
|
+
const program = shaderProgram(this.settings, {depth: this.depth, envAtlasReady: false, meshUV2: false});
|
|
230
|
+
const {defines, attributes, uniforms, samplers} = program;
|
|
140
231
|
// Stable name: the trail shader source never varies, so registering it once
|
|
141
232
|
// keeps Effect.ShadersStore bounded and lets Babylon reuse compiled effects.
|
|
142
|
-
const shaderName =
|
|
143
|
-
const defines: string[] = this.fogDefines();
|
|
144
|
-
|
|
145
|
-
if (this.settings.texture) {
|
|
146
|
-
defines.push('USE_MAP');
|
|
147
|
-
}
|
|
148
|
-
defines.push(...this.surfaceDefines());
|
|
149
|
-
|
|
233
|
+
const shaderName = program.name;
|
|
150
234
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
151
|
-
registerShaders(
|
|
152
|
-
shaderName,
|
|
153
|
-
{glsl: trail_vert, wgsl: trail_vert_wgsl},
|
|
154
|
-
{glsl: trail_frag, wgsl: trail_frag_wgsl},
|
|
155
|
-
shaderLanguage
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const attributes = ['position', 'previous', 'next', 'side', 'width', 'uv', 'color'];
|
|
159
|
-
if (this.uv1Buffer) {
|
|
160
|
-
attributes.push('uv2');
|
|
161
|
-
}
|
|
162
|
-
const uniforms = [
|
|
163
|
-
'world',
|
|
164
|
-
'view',
|
|
165
|
-
'projection',
|
|
166
|
-
'worldViewProjection',
|
|
167
|
-
'lineWidth',
|
|
168
|
-
'resolution',
|
|
169
|
-
'sizeAttenuation',
|
|
170
|
-
...VFXBatch.FOG_UNIFORMS,
|
|
171
|
-
];
|
|
172
|
-
const samplers: string[] = [];
|
|
173
|
-
|
|
174
|
-
if (this.settings.texture) {
|
|
175
|
-
samplers.push('map');
|
|
176
|
-
}
|
|
177
|
-
this.surfaceUniforms(uniforms, samplers);
|
|
235
|
+
registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
|
|
178
236
|
|
|
179
237
|
const mat = new ShaderMaterial(
|
|
180
238
|
shaderName,
|
|
@@ -193,24 +251,8 @@ export class TrailBatch extends VFXBatch {
|
|
|
193
251
|
if (this.settings.texture) {
|
|
194
252
|
mat.setTexture('map', this.settings.texture);
|
|
195
253
|
}
|
|
196
|
-
const engine = this.scene.getEngine();
|
|
197
|
-
mat.setFloat('lineWidth', 1);
|
|
198
|
-
// Reused across binds, and only re-uploaded when the render target resizes.
|
|
199
|
-
const resolution = new BVector2(engine.getRenderWidth(), engine.getRenderHeight());
|
|
200
|
-
mat.setVector2('resolution', resolution);
|
|
201
|
-
mat.setFloat('sizeAttenuation', 1);
|
|
202
|
-
mat.onBindObservable.add(() => {
|
|
203
|
-
const renderEngine = this.scene.getEngine();
|
|
204
|
-
const width = renderEngine.getRenderWidth();
|
|
205
|
-
const height = renderEngine.getRenderHeight();
|
|
206
|
-
if (resolution.x !== width || resolution.y !== height) {
|
|
207
|
-
resolution.x = width;
|
|
208
|
-
resolution.y = height;
|
|
209
|
-
mat.setVector2('resolution', resolution);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
254
|
this.bindFog(mat);
|
|
255
|
+
this.bindSoft(mat);
|
|
214
256
|
this.bindSurface(mat);
|
|
215
257
|
mat.backFaceCulling = false;
|
|
216
258
|
|
|
@@ -219,27 +261,198 @@ export class TrailBatch extends VFXBatch {
|
|
|
219
261
|
} else {
|
|
220
262
|
mat.alphaMode = Constants.ALPHA_DISABLE;
|
|
221
263
|
}
|
|
222
|
-
|
|
264
|
+
// A transparent surface that writes depth is drawn into the depth buffer first, so its own
|
|
265
|
+
// overlapping pieces do not fight; an opaque one writes depth as it draws.
|
|
266
|
+
mat.needDepthPrePass = this.settings.materialDepthWrite && this.settings.materialTransparent;
|
|
223
267
|
mat.forceDepthWrite = this.settings.materialDepthWrite;
|
|
224
268
|
mat.disableDepthWrite = !this.settings.materialDepthWrite;
|
|
269
|
+
// 0 keeps the engine's depth function (LEQUAL); ALWAYS draws over everything, like ZTest Always.
|
|
270
|
+
mat.depthFunction = this.settings.materialDepthTest ? 0 : Constants.ALWAYS;
|
|
225
271
|
|
|
272
|
+
// A rebuild (a depth texture attached or removed) replaces the material and its bind
|
|
273
|
+
// observers. Textures belong to the settings, so they stay.
|
|
274
|
+
(this.mesh.material as ShaderMaterial | null)?.dispose(false, false);
|
|
226
275
|
this.mesh.material = mat;
|
|
227
276
|
}
|
|
228
277
|
|
|
229
|
-
private vector_ = new Vector3();
|
|
230
278
|
private vector2_ = new Vector3();
|
|
231
279
|
private vector3_ = new Vector3();
|
|
232
280
|
private quaternion_ = new Quaternion();
|
|
233
281
|
|
|
282
|
+
private readonly samples_ = new TrailSamples();
|
|
283
|
+
private readonly curve_ = new TrailSamples();
|
|
284
|
+
|
|
285
|
+
/** The scratch a trail's samples are gathered into, room for `count`. */
|
|
286
|
+
private trailSamples(count: number): TrailSamples {
|
|
287
|
+
return this.samples_.reserve(count);
|
|
288
|
+
}
|
|
289
|
+
|
|
234
290
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
291
|
+
* Shortens the last segment of a drawn ribbon to `keep` of its length, along the points already
|
|
292
|
+
* drawn for it, so it stays a prefix of that same curve. Every channel follows.
|
|
293
|
+
*/
|
|
294
|
+
private cutLastSegment(drawn: TrailSamples, count: number, perSegment: number, keep: number, swept: boolean): void {
|
|
295
|
+
const first = count - 1 - perSegment;
|
|
296
|
+
const scratch = this.cutScratch_.reserve(perSegment + 1);
|
|
297
|
+
scratch.position.set(drawn.position.subarray(first * 3, count * 3));
|
|
298
|
+
scratch.side.set(drawn.side.subarray(first * 3, count * 3));
|
|
299
|
+
scratch.size.set(drawn.size.subarray(first, count));
|
|
300
|
+
scratch.color.set(drawn.color.subarray(first * 4, count * 4));
|
|
301
|
+
scratch.distance.set(drawn.distance.subarray(first, count));
|
|
302
|
+
for (let j = 1; j <= perSegment; j++) {
|
|
303
|
+
const at = keep * j;
|
|
304
|
+
const i = Math.min(perSegment - 1, Math.floor(at));
|
|
305
|
+
const t = at - i;
|
|
306
|
+
const d = first + j;
|
|
307
|
+
for (let c = 0; c < 3; c++) {
|
|
308
|
+
drawn.position[d * 3 + c] = scratch.position[i * 3 + c] * (1 - t) + scratch.position[i * 3 + 3 + c] * t;
|
|
309
|
+
if (swept) {
|
|
310
|
+
drawn.side[d * 3 + c] = scratch.side[i * 3 + c] * (1 - t) + scratch.side[i * 3 + 3 + c] * t;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
drawn.size[d] = scratch.size[i] * (1 - t) + scratch.size[i + 1] * t;
|
|
314
|
+
drawn.distance[d] = scratch.distance[i] * (1 - t) + scratch.distance[i + 1] * t;
|
|
315
|
+
for (let c = 0; c < 4; c++) {
|
|
316
|
+
drawn.color[d * 4 + c] = scratch.color[i * 4 + c] * (1 - t) + scratch.color[i * 4 + 4 + c] * t;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private readonly cutScratch_ = new TrailSamples();
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The curve through a trail's `count` samples, each segment drawn as
|
|
325
|
+
* `steps + 1`: through the first `curved` (the recorded samples) a
|
|
326
|
+
* centripetal cardinal spline (Catmull-Rom at tension 0, straight at 1),
|
|
327
|
+
* the same through the sides of a swept trail with each side's length
|
|
328
|
+
* eased between its ends'; past them (the live tip) a straight segment,
|
|
329
|
+
* so the recorded part does not move as the tip does. Widths, colors and
|
|
330
|
+
* distances are eased along. The curve passes through every sample, so
|
|
331
|
+
* the trail keeps its shape and only loses its corners; centripetal
|
|
332
|
+
* spacing keeps it from overshooting where samples lie unevenly apart.
|
|
242
333
|
*/
|
|
334
|
+
private tessellate(
|
|
335
|
+
samples: TrailSamples,
|
|
336
|
+
curved: number,
|
|
337
|
+
count: number,
|
|
338
|
+
steps: number,
|
|
339
|
+
tension: number,
|
|
340
|
+
swept: boolean
|
|
341
|
+
): TrailSamples {
|
|
342
|
+
const out = this.curve_.reserve((count - 1) * (steps + 1) + 1);
|
|
343
|
+
const p = samples.position;
|
|
344
|
+
const sd = samples.side;
|
|
345
|
+
const op = out.position;
|
|
346
|
+
const os = out.side;
|
|
347
|
+
const tangent = 1 - tension;
|
|
348
|
+
let k = 0;
|
|
349
|
+
for (let i = 0; i + 1 < count; i++) {
|
|
350
|
+
const a = i * 3;
|
|
351
|
+
const b = a + 3;
|
|
352
|
+
// Each end's tangent, as weights of the neighbouring differences
|
|
353
|
+
// (Yuksel et al., parameterization of Catmull-Rom curves).
|
|
354
|
+
let w1a = 0;
|
|
355
|
+
let w1b = 0;
|
|
356
|
+
let w1c = 0;
|
|
357
|
+
let w2a = 0;
|
|
358
|
+
let w2b = 0;
|
|
359
|
+
let w2c = 0;
|
|
360
|
+
let before = a;
|
|
361
|
+
let after = b;
|
|
362
|
+
if (i + 1 < curved) {
|
|
363
|
+
before = (i > 0 ? i - 1 : i) * 3;
|
|
364
|
+
after = (i + 2 < curved ? i + 2 : i + 1) * 3;
|
|
365
|
+
const t12 = knot(p, a, b);
|
|
366
|
+
if (t12 > 1e-6) {
|
|
367
|
+
const t01 = knot(p, before, a);
|
|
368
|
+
const t23 = knot(p, b, after);
|
|
369
|
+
w1c = tangent;
|
|
370
|
+
w2a = tangent;
|
|
371
|
+
if (t01 > 1e-6) {
|
|
372
|
+
w1a = (tangent * t12) / t01;
|
|
373
|
+
w1b = (-tangent * t12) / (t01 + t12);
|
|
374
|
+
}
|
|
375
|
+
if (t23 > 1e-6) {
|
|
376
|
+
w2b = (-tangent * t12) / (t12 + t23);
|
|
377
|
+
w2c = (tangent * t12) / t23;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// m1 = w1a (A - Before) + w1b (B - Before) + w1c (B - A);
|
|
382
|
+
// m2 = w2a (B - A) + w2b (After - A) + w2c (After - B).
|
|
383
|
+
// With no tangents (the tip's segment, or tension 1) the curve is the straight segment
|
|
384
|
+
// itself: walk it evenly, as the widths, colors and distances are, rather than easing.
|
|
385
|
+
const straight = w1a === 0 && w1b === 0 && w1c === 0 && w2a === 0 && w2b === 0 && w2c === 0;
|
|
386
|
+
const la = swept ? Math.sqrt(sd[a] * sd[a] + sd[a + 1] * sd[a + 1] + sd[a + 2] * sd[a + 2]) : 0;
|
|
387
|
+
const lb = swept ? Math.sqrt(sd[b] * sd[b] + sd[b + 1] * sd[b + 1] + sd[b + 2] * sd[b + 2]) : 0;
|
|
388
|
+
for (let step = 0; step <= steps; step++, k++) {
|
|
389
|
+
const t = step / (steps + 1);
|
|
390
|
+
const t2 = t * t;
|
|
391
|
+
const t3 = t2 * t;
|
|
392
|
+
const h00 = straight ? 1 - t : 2 * t3 - 3 * t2 + 1;
|
|
393
|
+
const h10 = t3 - 2 * t2 + t;
|
|
394
|
+
const h01 = straight ? t : -2 * t3 + 3 * t2;
|
|
395
|
+
const h11 = t3 - t2;
|
|
396
|
+
const o = k * 3;
|
|
397
|
+
for (let c = 0; c < 3; c++) {
|
|
398
|
+
const A = p[a + c];
|
|
399
|
+
const B = p[b + c];
|
|
400
|
+
const m1 = w1a * (A - p[before + c]) + w1b * (B - p[before + c]) + w1c * (B - A);
|
|
401
|
+
const m2 = w2a * (B - A) + w2b * (p[after + c] - A) + w2c * (p[after + c] - B);
|
|
402
|
+
op[o + c] = h00 * A + h10 * m1 + h01 * B + h11 * m2;
|
|
403
|
+
}
|
|
404
|
+
if (swept) {
|
|
405
|
+
let x = 0;
|
|
406
|
+
let y = 0;
|
|
407
|
+
let z = 0;
|
|
408
|
+
for (let c = 0; c < 3; c++) {
|
|
409
|
+
const A = sd[a + c];
|
|
410
|
+
const B = sd[b + c];
|
|
411
|
+
const m1 = w1a * (A - sd[before + c]) + w1b * (B - sd[before + c]) + w1c * (B - A);
|
|
412
|
+
const m2 = w2a * (B - A) + w2b * (sd[after + c] - A) + w2c * (sd[after + c] - B);
|
|
413
|
+
const v = h00 * A + h10 * m1 + h01 * B + h11 * m2;
|
|
414
|
+
if (c === 0) x = v;
|
|
415
|
+
else if (c === 1) y = v;
|
|
416
|
+
else z = v;
|
|
417
|
+
}
|
|
418
|
+
// A side turning with the blade keeps its length on the way round.
|
|
419
|
+
let length = Math.sqrt(x * x + y * y + z * z);
|
|
420
|
+
if (length <= 1e-6 * Math.max(la, lb, 1e-6)) {
|
|
421
|
+
// Turned half round in one step: no way round to follow, so
|
|
422
|
+
// hold the nearer end's side rather than pinch to nothing.
|
|
423
|
+
const from = t < 0.5 ? a : b;
|
|
424
|
+
x = sd[from];
|
|
425
|
+
y = sd[from + 1];
|
|
426
|
+
z = sd[from + 2];
|
|
427
|
+
length = t < 0.5 ? la : lb;
|
|
428
|
+
}
|
|
429
|
+
const fit = length > 0 ? (la + (lb - la) * t) / length : 0;
|
|
430
|
+
os[o] = x * fit;
|
|
431
|
+
os[o + 1] = y * fit;
|
|
432
|
+
os[o + 2] = z * fit;
|
|
433
|
+
}
|
|
434
|
+
out.size[k] = samples.size[i] + (samples.size[i + 1] - samples.size[i]) * t;
|
|
435
|
+
out.distance[k] = samples.distance[i] + (samples.distance[i + 1] - samples.distance[i]) * t;
|
|
436
|
+
const ca = i * 4;
|
|
437
|
+
const co = k * 4;
|
|
438
|
+
for (let c = 0; c < 4; c++) {
|
|
439
|
+
out.color[co + c] = samples.color[ca + c] + (samples.color[ca + 4 + c] - samples.color[ca + c]) * t;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// The last sample, as it is.
|
|
444
|
+
const last = (count - 1) * 3;
|
|
445
|
+
const o = k * 3;
|
|
446
|
+
for (let c = 0; c < 3; c++) {
|
|
447
|
+
op[o + c] = p[last + c];
|
|
448
|
+
os[o + c] = sd[last + c];
|
|
449
|
+
}
|
|
450
|
+
out.size[k] = samples.size[count - 1];
|
|
451
|
+
out.distance[k] = samples.distance[count - 1];
|
|
452
|
+
for (let c = 0; c < 4; c++) out.color[k * 4 + c] = samples.color[(count - 1) * 4 + c];
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
243
456
|
/**
|
|
244
457
|
* Writes a swept ribbon's vertex pair at `pi`: the sample at (x, y, z)
|
|
245
458
|
* pushed `half` widths along its side (sx, sy, sz) one way for the first
|
|
@@ -264,6 +477,15 @@ export class TrailBatch extends VFXBatch {
|
|
|
264
477
|
positions[pi + 5] = z - sz * half;
|
|
265
478
|
}
|
|
266
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Brings the emitter's world matrix up to date when its transform changed.
|
|
482
|
+
*
|
|
483
|
+
* Babylon only recomputes a world matrix once per render id, so a frame that
|
|
484
|
+
* moves the emitter after that (or a step driven without rendering) would
|
|
485
|
+
* draw the ribbon against the old one. Recomputing unconditionally every
|
|
486
|
+
* frame costs a matrix compose per system; checking the transform cache
|
|
487
|
+
* first makes a still emitter free.
|
|
488
|
+
*/
|
|
267
489
|
private static ensureWorldMatrix(system: IParticleSystem): void {
|
|
268
490
|
const emitter = system.emitter as {
|
|
269
491
|
computeWorldMatrix?: (force: boolean) => void;
|
|
@@ -274,6 +496,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
274
496
|
}
|
|
275
497
|
}
|
|
276
498
|
|
|
499
|
+
/** @internal */
|
|
277
500
|
update(): void {
|
|
278
501
|
let index = 0;
|
|
279
502
|
let triangles = 0;
|
|
@@ -283,14 +506,16 @@ export class TrailBatch extends VFXBatch {
|
|
|
283
506
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
284
507
|
const system = visibleSystems[s];
|
|
285
508
|
const particles = system.particles;
|
|
509
|
+
const tessellation = trailTessellation(system.rendererEmitterSettings as TrailSettings);
|
|
286
510
|
for (let j = 0; j < system.particleNum; j++) {
|
|
287
511
|
const historyCount = (particles[j] as TrailParticle).historyCount;
|
|
288
512
|
if (historyCount === 0) {
|
|
289
513
|
continue;
|
|
290
514
|
}
|
|
291
|
-
// +
|
|
292
|
-
//
|
|
293
|
-
|
|
515
|
+
// +1 reserves the live tip that frames between simulation
|
|
516
|
+
// steps append ahead of the recorded head; a tessellated trail
|
|
517
|
+
// draws each segment as several.
|
|
518
|
+
particleCount += (historyCount * (tessellation + 1) + 1) * 2;
|
|
294
519
|
}
|
|
295
520
|
}
|
|
296
521
|
if (particleCount > this.maxParticles) {
|
|
@@ -342,7 +567,25 @@ export class TrailBatch extends VFXBatch {
|
|
|
342
567
|
// Measured from the step the particle just took, not from its
|
|
343
568
|
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
344
569
|
// behavior has motion no velocity term describes.
|
|
345
|
-
const
|
|
570
|
+
const rawFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
|
|
571
|
+
// A few ten-thousandths of a step are float noise left in the accumulator, not time: a
|
|
572
|
+
// tip there would sit on the head as a segment of no length.
|
|
573
|
+
const stepFraction = Math.abs(rawFraction) < 1e-4 ? 0 : rawFraction;
|
|
574
|
+
|
|
575
|
+
const tessellation = trailTessellation(trailSettings);
|
|
576
|
+
const curveTension = trailCurveTension(trailSettings);
|
|
577
|
+
const textureModeName = trailTextureMode(trailSettings);
|
|
578
|
+
const textureMode =
|
|
579
|
+
textureModeName === 'stretch'
|
|
580
|
+
? TextureMode.Stretch
|
|
581
|
+
: textureModeName === 'tile'
|
|
582
|
+
? TextureMode.Tile
|
|
583
|
+
: textureModeName === 'repeatPerSegment'
|
|
584
|
+
? TextureMode.RepeatPerSegment
|
|
585
|
+
: TextureMode.DistributePerSegment;
|
|
586
|
+
const textureScale = trailTextureScale(trailSettings);
|
|
587
|
+
// With a minimum vertex distance the last recorded segment is not one step of travel.
|
|
588
|
+
const samplesFollow = trailMinVertexDistance(trailSettings) > 0;
|
|
346
589
|
|
|
347
590
|
for (let j = 0; j < particleNum; j++) {
|
|
348
591
|
const particle = particles[j] as TrailParticle;
|
|
@@ -353,160 +596,239 @@ export class TrailBatch extends VFXBatch {
|
|
|
353
596
|
const historyCapacity = particle.historyCapacity;
|
|
354
597
|
const historyPositions = particle.historyPositions;
|
|
355
598
|
const historySizes = particle.historySizes;
|
|
599
|
+
const historyDistances = particle.historyDistances;
|
|
356
600
|
const historySides = particle.historySides;
|
|
357
|
-
const swept = sideAxis !== undefined && historySides.length
|
|
601
|
+
const swept = sideAxis !== undefined && historySides.length >= historyCapacity * 3;
|
|
358
602
|
// Every sample takes its color from one place: its own recorded slot, or
|
|
359
603
|
// the particle's current row in the store.
|
|
360
604
|
const colors = colorFromParticle ? particle.store.color : particle.historyColors;
|
|
361
605
|
const particleColorIndex = particle.storeIndex * 4;
|
|
362
|
-
|
|
363
|
-
const row =
|
|
606
|
+
// The same cell as the sprite shader, wrapped into the sheet the same way.
|
|
607
|
+
const {column: col, row} = sheetCell(particle.uvTile, uTileCount, vTileCount);
|
|
364
608
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
365
609
|
const me = (particleMatrix ?? emitterMatrix).elements;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
1 / (stepFraction !== 0 ? particleHistoryLength : Math.max(1, particleHistoryLength - 1));
|
|
386
|
-
|
|
387
|
-
// Walk the ring buffer oldest-first with hand-rolled wrapping so the
|
|
388
|
-
// inner loop stays free of modulo operations. Each sample is brought
|
|
389
|
-
// into world space once: the point written as `next` on one
|
|
390
|
-
// iteration is `current` on the following one and `previous` on
|
|
391
|
-
// the one after that.
|
|
392
|
-
let currentSlot = particle.getHistoryIndex(0);
|
|
393
|
-
let currentX = historyPositions[currentSlot * 3];
|
|
394
|
-
let currentY = historyPositions[currentSlot * 3 + 1];
|
|
395
|
-
let currentZ = historyPositions[currentSlot * 3 + 2];
|
|
396
|
-
if (!samplesInWorld) {
|
|
397
|
-
const w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
|
|
398
|
-
const tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
|
|
399
|
-
const ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
|
|
400
|
-
currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
|
|
401
|
-
currentX = tx;
|
|
402
|
-
currentY = ty;
|
|
403
|
-
}
|
|
404
|
-
let previousX = currentX;
|
|
405
|
-
let previousY = currentY;
|
|
406
|
-
let previousZ = currentZ;
|
|
407
|
-
|
|
408
|
-
for (let i = 0; i < particleHistoryLength; i++, index += 2) {
|
|
409
|
-
let nextX = currentX;
|
|
410
|
-
let nextY = currentY;
|
|
411
|
-
let nextZ = currentZ;
|
|
412
|
-
let nextSlot = currentSlot;
|
|
413
|
-
if (i + 1 < particleHistoryLength) {
|
|
414
|
-
nextSlot = currentSlot + 1 === historyCapacity ? 0 : currentSlot + 1;
|
|
415
|
-
const nextPosIndex = nextSlot * 3;
|
|
416
|
-
nextX = historyPositions[nextPosIndex];
|
|
417
|
-
nextY = historyPositions[nextPosIndex + 1];
|
|
418
|
-
nextZ = historyPositions[nextPosIndex + 2];
|
|
419
|
-
if (!samplesInWorld) {
|
|
420
|
-
const w = 1 / (m03 * nextX + m13 * nextY + m23 * nextZ + m33);
|
|
421
|
-
const tx = (m00 * nextX + m10 * nextY + m20 * nextZ + m30) * w;
|
|
422
|
-
const ty = (m01 * nextX + m11 * nextY + m21 * nextZ + m31) * w;
|
|
423
|
-
nextZ = (m02 * nextX + m12 * nextY + m22 * nextZ + m32) * w;
|
|
424
|
-
nextX = tx;
|
|
425
|
-
nextY = ty;
|
|
426
|
-
}
|
|
610
|
+
const widthScale = systemWorldSpace || particle.parentMatrix ? 1 : objectScale;
|
|
611
|
+
// Recorded distances are world lengths once the samples are, a local trail's scale with the emitter.
|
|
612
|
+
const distanceScale = samplesInWorld || particle.parentMatrix ? 1 : objectScale;
|
|
613
|
+
// A particle at the end of its life records nothing more (TrailParticle.update), so
|
|
614
|
+
// its trail ends at the last sample: a tip would follow the particle on as a growing spike.
|
|
615
|
+
const recording = particle.age < particle.life;
|
|
616
|
+
// The display runs ahead of the last step: draw a tip that far past the head.
|
|
617
|
+
const tipAhead = recording && stepFraction > 0;
|
|
618
|
+
const sampleCount = particleHistoryLength + (tipAhead ? 1 : 0);
|
|
619
|
+
const samples = this.trailSamples(sampleCount);
|
|
620
|
+
|
|
621
|
+
// The recorded samples, oldest first, brought into world space once.
|
|
622
|
+
let slot = particle.getHistoryIndex(0);
|
|
623
|
+
for (let i = 0; i < particleHistoryLength; i++) {
|
|
624
|
+
const p = historyPositions;
|
|
625
|
+
samples.writePoint(i, p[slot * 3], p[slot * 3 + 1], p[slot * 3 + 2], samplesInWorld ? null : me);
|
|
626
|
+
if (swept) {
|
|
627
|
+
const s = historySides;
|
|
628
|
+
samples.writeSide(i, s[slot * 3], s[slot * 3 + 1], s[slot * 3 + 2], samplesInWorld ? null : me);
|
|
427
629
|
}
|
|
630
|
+
samples.size[i] = historySizes[slot];
|
|
631
|
+
samples.distance[i] = historyDistances[slot];
|
|
632
|
+
const ci = colorFromParticle ? particleColorIndex : slot * 4;
|
|
633
|
+
samples.writeColor(i, colors[ci], colors[ci + 1], colors[ci + 2], colors[ci + 3]);
|
|
634
|
+
slot = slot + 1 === historyCapacity ? 0 : slot + 1;
|
|
635
|
+
}
|
|
428
636
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
637
|
+
// Live tip: same place a sprite would be drawn this frame, past
|
|
638
|
+
// the last recorded sample. The recorded ribbon stays put.
|
|
639
|
+
if (tipAhead) {
|
|
640
|
+
const previousHead = particle.previousPosition;
|
|
641
|
+
const position = particle.position;
|
|
642
|
+
const tip = particleHistoryLength;
|
|
643
|
+
let tx = position.x + (position.x - previousHead.x) * stepFraction;
|
|
644
|
+
let ty = position.y + (position.y - previousHead.y) * stepFraction;
|
|
645
|
+
let tz = position.z + (position.z - previousHead.z) * stepFraction;
|
|
646
|
+
samples.writePoint(tip, tx, ty, tz, systemWorldSpace ? null : me);
|
|
647
|
+
// The tip's distance along the trail, measured in the space the samples are
|
|
648
|
+
// recorded in, as theirs are.
|
|
649
|
+
if (samplesInWorld && !systemWorldSpace) {
|
|
650
|
+
const w = 1 / (me[3] * tx + me[7] * ty + me[11] * tz + me[15]);
|
|
651
|
+
const wx = (me[0] * tx + me[4] * ty + me[8] * tz + me[12]) * w;
|
|
652
|
+
const wy = (me[1] * tx + me[5] * ty + me[9] * tz + me[13]) * w;
|
|
653
|
+
tz = (me[2] * tx + me[6] * ty + me[10] * tz + me[14]) * w;
|
|
654
|
+
tx = wx;
|
|
655
|
+
ty = wy;
|
|
656
|
+
}
|
|
657
|
+
const headSlot = particle.getHistoryIndex(particleHistoryLength - 1) * 3;
|
|
658
|
+
const hx = tx - historyPositions[headSlot];
|
|
659
|
+
const hy = ty - historyPositions[headSlot + 1];
|
|
660
|
+
const hz = tz - historyPositions[headSlot + 2];
|
|
661
|
+
samples.distance[tip] = samples.distance[tip - 1] + Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
437
662
|
if (swept) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
let sy = historySides[si + 1];
|
|
441
|
-
let sz = historySides[si + 2];
|
|
442
|
-
if (!samplesInWorld) {
|
|
443
|
-
const tx = m00 * sx + m10 * sy + m20 * sz;
|
|
444
|
-
const ty = m01 * sx + m11 * sy + m21 * sz;
|
|
445
|
-
sz = m02 * sx + m12 * sy + m22 * sz;
|
|
446
|
-
sx = tx;
|
|
447
|
-
sy = ty;
|
|
448
|
-
}
|
|
449
|
-
this.writeSweptPair(pi, currentX, currentY, currentZ, sx, sy, sz, currentSize * 0.5);
|
|
450
|
-
} else {
|
|
451
|
-
this.positionBuffer[pi] = currentX;
|
|
452
|
-
this.positionBuffer[pi + 1] = currentY;
|
|
453
|
-
this.positionBuffer[pi + 2] = currentZ;
|
|
454
|
-
this.positionBuffer[pi + 3] = currentX;
|
|
455
|
-
this.positionBuffer[pi + 4] = currentY;
|
|
456
|
-
this.positionBuffer[pi + 5] = currentZ;
|
|
663
|
+
// The side as the emitter holds it now.
|
|
664
|
+
samples.writeSide(tip, sideAxis!.x, sideAxis!.y, sideAxis!.z, me);
|
|
457
665
|
}
|
|
666
|
+
// As wide as the recorded head, which WidthOverLength has already scaled.
|
|
667
|
+
samples.size[tip] = samples.size[tip - 1];
|
|
668
|
+
const tipColor = particle.color;
|
|
669
|
+
samples.writeColor(tip, tipColor.x, tipColor.y, tipColor.z, tipColor.w);
|
|
670
|
+
}
|
|
458
671
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
672
|
+
// Curved through the samples (see TrailSettings.tessellation),
|
|
673
|
+
// or drawn through them as they are.
|
|
674
|
+
const drawn =
|
|
675
|
+
tessellation > 0
|
|
676
|
+
? this.tessellate(
|
|
677
|
+
samples,
|
|
678
|
+
particleHistoryLength,
|
|
679
|
+
sampleCount,
|
|
680
|
+
tessellation,
|
|
681
|
+
curveTension,
|
|
682
|
+
swept
|
|
683
|
+
)
|
|
684
|
+
: samples;
|
|
685
|
+
const drawnCount = tessellation > 0 ? (sampleCount - 1) * (tessellation + 1) + 1 : sampleCount;
|
|
686
|
+
const perSegment = tessellation + 1;
|
|
687
|
+
// The last step ran ahead of the display: the head is drawn where it was at display
|
|
688
|
+
// time, back along the drawn last segment, rather than a tip folding back over it.
|
|
689
|
+
// Cut from the drawn curve, so the rest of the ribbon is the same as on any frame.
|
|
690
|
+
const pullBack = recording && stepFraction < 0 && particleHistoryLength > 1;
|
|
691
|
+
// How much of the last drawn segment is kept: all but the step's share that ran
|
|
692
|
+
// ahead. The segment is one step of travel, unless the head has been following the
|
|
693
|
+
// particle (minVertexDistance), when it is that step's share of its length.
|
|
694
|
+
let keep = 1 + stepFraction;
|
|
695
|
+
if (pullBack && samplesFollow) {
|
|
696
|
+
const dd = drawn.distance;
|
|
697
|
+
const segmentLength = dd[drawnCount - 1] - dd[drawnCount - 1 - perSegment];
|
|
698
|
+
if (segmentLength > 1e-9) {
|
|
699
|
+
const previousHead = particle.previousPosition;
|
|
700
|
+
const position = particle.position;
|
|
701
|
+
let sx = position.x - previousHead.x;
|
|
702
|
+
let sy = position.y - previousHead.y;
|
|
703
|
+
let sz = position.z - previousHead.z;
|
|
704
|
+
if (samplesInWorld && !systemWorldSpace) {
|
|
705
|
+
const wx = me[0] * sx + me[4] * sy + me[8] * sz;
|
|
706
|
+
const wy = me[1] * sx + me[5] * sy + me[9] * sz;
|
|
707
|
+
sz = me[2] * sx + me[6] * sy + me[10] * sz;
|
|
708
|
+
sx = wx;
|
|
709
|
+
sy = wy;
|
|
710
|
+
}
|
|
711
|
+
const stepTravel = Math.sqrt(sx * sx + sy * sy + sz * sz);
|
|
712
|
+
keep = 1 + stepFraction * Math.min(1, stepTravel / segmentLength);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (pullBack) {
|
|
716
|
+
this.cutLastSegment(drawn, drawnCount, perSegment, keep, swept);
|
|
717
|
+
}
|
|
718
|
+
// The per-segment texture modes span the ribbon in samples: the recorded ones, plus
|
|
719
|
+
// the part of a step the tip runs ahead or the head was drawn back, so a sample's U
|
|
720
|
+
// changes smoothly from frame to frame whichever way the residual falls. The others
|
|
721
|
+
// read each point's distance along the trail, tile mode in world units.
|
|
722
|
+
const spanFraction = tipAhead ? stepFraction : pullBack ? keep - 1 : 0;
|
|
723
|
+
const spanSamples = particleHistoryLength - 1 + spanFraction;
|
|
724
|
+
const invLength = 1 / Math.max(1e-3, spanSamples);
|
|
725
|
+
const lastSegment = (sampleCount - 2) * perSegment;
|
|
726
|
+
const dp = drawn.position;
|
|
727
|
+
const dd = drawn.distance;
|
|
728
|
+
const tailDistance = dd[0];
|
|
729
|
+
const headDistance = dd[drawnCount - 1];
|
|
730
|
+
// A ribbon that has not gone anywhere has no distance to stretch over: lay the
|
|
731
|
+
// texture out per segment instead, which at least keeps its ends where they belong.
|
|
732
|
+
const stretch = textureMode === TextureMode.Stretch && headDistance - tailDistance > 1e-6;
|
|
733
|
+
const invSpanDistance = 1 / Math.max(1e-6, headDistance - tailDistance);
|
|
734
|
+
// Tile mode: U in repeats of the texture along the trail, anchored where the particle
|
|
735
|
+
// was born, less the whole repeats behind the tail so U stays small (a texture that
|
|
736
|
+
// wraps cannot tell) however far the particle has travelled.
|
|
737
|
+
const tileScale = distanceScale * textureScale;
|
|
738
|
+
const tileOffset = Math.floor(tailDistance * tileScale);
|
|
739
|
+
const vTop = (vTileCount - row - 1) * tileHeight;
|
|
740
|
+
const vBottom = (vTileCount - row) * tileHeight;
|
|
741
|
+
|
|
742
|
+
for (let k = 0; k < drawnCount; k++, index += 2) {
|
|
743
|
+
const o = k * 3;
|
|
744
|
+
const prev = k > 0 ? o - 3 : o;
|
|
745
|
+
const next = k + 1 < drawnCount ? o + 3 : o;
|
|
746
|
+
const x = dp[o];
|
|
747
|
+
const y = dp[o + 1];
|
|
748
|
+
const z = dp[o + 2];
|
|
749
|
+
const size = drawn.size[k];
|
|
472
750
|
|
|
751
|
+
const pi = index * 3;
|
|
473
752
|
if (swept) {
|
|
753
|
+
const ds = drawn.side;
|
|
754
|
+
this.writeSweptPair(pi, x, y, z, ds[o], ds[o + 1], ds[o + 2], size * 0.5);
|
|
755
|
+
// A zero width keeps the shader from pushing the pair.
|
|
474
756
|
this.widthBuffer[index] = 0;
|
|
475
757
|
this.widthBuffer[index + 1] = 0;
|
|
476
|
-
} else if (systemWorldSpace || particle.parentMatrix) {
|
|
477
|
-
this.widthBuffer[index] = currentSize;
|
|
478
|
-
this.widthBuffer[index + 1] = currentSize;
|
|
479
758
|
} else {
|
|
480
|
-
this.
|
|
481
|
-
this.
|
|
759
|
+
this.positionBuffer[pi] = x;
|
|
760
|
+
this.positionBuffer[pi + 1] = y;
|
|
761
|
+
this.positionBuffer[pi + 2] = z;
|
|
762
|
+
this.positionBuffer[pi + 3] = x;
|
|
763
|
+
this.positionBuffer[pi + 4] = y;
|
|
764
|
+
this.positionBuffer[pi + 5] = z;
|
|
765
|
+
this.widthBuffer[index] = size * widthScale;
|
|
766
|
+
this.widthBuffer[index + 1] = size * widthScale;
|
|
482
767
|
}
|
|
483
768
|
|
|
769
|
+
this.previousBuffer[pi] = dp[prev];
|
|
770
|
+
this.previousBuffer[pi + 1] = dp[prev + 1];
|
|
771
|
+
this.previousBuffer[pi + 2] = dp[prev + 2];
|
|
772
|
+
this.previousBuffer[pi + 3] = dp[prev];
|
|
773
|
+
this.previousBuffer[pi + 4] = dp[prev + 1];
|
|
774
|
+
this.previousBuffer[pi + 5] = dp[prev + 2];
|
|
775
|
+
|
|
776
|
+
this.nextBuffer[pi] = dp[next];
|
|
777
|
+
this.nextBuffer[pi + 1] = dp[next + 1];
|
|
778
|
+
this.nextBuffer[pi + 2] = dp[next + 2];
|
|
779
|
+
this.nextBuffer[pi + 3] = dp[next];
|
|
780
|
+
this.nextBuffer[pi + 4] = dp[next + 1];
|
|
781
|
+
this.nextBuffer[pi + 5] = dp[next + 2];
|
|
782
|
+
|
|
484
783
|
const ui = index * 2;
|
|
485
|
-
|
|
486
|
-
|
|
784
|
+
let u1: number;
|
|
785
|
+
if (stretch) {
|
|
786
|
+
const along = (dd[k] - tailDistance) * invSpanDistance;
|
|
787
|
+
u1 = (uvFromHead ? 1 - along : along) * textureScale;
|
|
788
|
+
} else if (textureMode === TextureMode.Tile) {
|
|
789
|
+
// A sample's U holds while it lives; from the particle back with uvFromHead.
|
|
790
|
+
u1 = uvFromHead ? (headDistance - dd[k]) * tileScale : dd[k] * tileScale - tileOffset;
|
|
791
|
+
} else {
|
|
792
|
+
// Where this point sits along the ribbon, in samples.
|
|
793
|
+
let at = k / perSegment;
|
|
794
|
+
if (k > lastSegment && (tipAhead || pullBack)) {
|
|
795
|
+
const into = (k - lastSegment) / perSegment;
|
|
796
|
+
at = tipAhead
|
|
797
|
+
? particleHistoryLength - 1 + stepFraction * into
|
|
798
|
+
: particleHistoryLength - 2 + keep * into;
|
|
799
|
+
}
|
|
800
|
+
if (textureMode !== TextureMode.RepeatPerSegment) {
|
|
801
|
+
const along = Math.min(1, at * invLength);
|
|
802
|
+
u1 = (uvFromHead ? 1 - along : along) * textureScale;
|
|
803
|
+
} else {
|
|
804
|
+
u1 = (uvFromHead ? spanSamples - at : at) * textureScale;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const u = (u1 + col) * tileWidth;
|
|
487
808
|
this.uvBuffer[ui] = u;
|
|
488
|
-
this.uvBuffer[ui + 1] =
|
|
809
|
+
this.uvBuffer[ui + 1] = vTop;
|
|
489
810
|
this.uvBuffer[ui + 2] = u;
|
|
490
|
-
this.uvBuffer[ui + 3] =
|
|
811
|
+
this.uvBuffer[ui + 3] = vBottom;
|
|
491
812
|
if (uv1) {
|
|
492
|
-
const u1 = uvFromHead ? 1 - along : along;
|
|
493
813
|
uv1[ui] = u1;
|
|
494
814
|
uv1[ui + 1] = 0;
|
|
495
815
|
uv1[ui + 2] = u1;
|
|
496
816
|
uv1[ui + 3] = 1;
|
|
497
817
|
}
|
|
498
818
|
|
|
819
|
+
const dc = drawn.color;
|
|
820
|
+
const c = k * 4;
|
|
499
821
|
const cci = index * 4;
|
|
500
|
-
this.colorBuffer[cci] =
|
|
501
|
-
this.colorBuffer[cci + 1] =
|
|
502
|
-
this.colorBuffer[cci + 2] =
|
|
503
|
-
this.colorBuffer[cci + 3] =
|
|
504
|
-
this.colorBuffer[cci + 4] =
|
|
505
|
-
this.colorBuffer[cci + 5] =
|
|
506
|
-
this.colorBuffer[cci + 6] =
|
|
507
|
-
this.colorBuffer[cci + 7] =
|
|
508
|
-
|
|
509
|
-
if (
|
|
822
|
+
this.colorBuffer[cci] = dc[c];
|
|
823
|
+
this.colorBuffer[cci + 1] = dc[c + 1];
|
|
824
|
+
this.colorBuffer[cci + 2] = dc[c + 2];
|
|
825
|
+
this.colorBuffer[cci + 3] = dc[c + 3];
|
|
826
|
+
this.colorBuffer[cci + 4] = dc[c];
|
|
827
|
+
this.colorBuffer[cci + 5] = dc[c + 1];
|
|
828
|
+
this.colorBuffer[cci + 6] = dc[c + 2];
|
|
829
|
+
this.colorBuffer[cci + 7] = dc[c + 3];
|
|
830
|
+
|
|
831
|
+
if (k + 1 < drawnCount) {
|
|
510
832
|
this.indexBuffer[triangles * 3] = index;
|
|
511
833
|
this.indexBuffer[triangles * 3 + 1] = index + 1;
|
|
512
834
|
this.indexBuffer[triangles * 3 + 2] = index + 2;
|
|
@@ -516,138 +838,6 @@ export class TrailBatch extends VFXBatch {
|
|
|
516
838
|
this.indexBuffer[triangles * 3 + 2] = index + 3;
|
|
517
839
|
triangles++;
|
|
518
840
|
}
|
|
519
|
-
|
|
520
|
-
previousX = currentX;
|
|
521
|
-
previousY = currentY;
|
|
522
|
-
previousZ = currentZ;
|
|
523
|
-
currentX = nextX;
|
|
524
|
-
currentY = nextY;
|
|
525
|
-
currentZ = nextZ;
|
|
526
|
-
currentSlot = nextSlot;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Live tip: same place a sprite would be drawn this frame, past
|
|
530
|
-
// the last recorded sample. The recorded ribbon stays put.
|
|
531
|
-
if (stepFraction !== 0) {
|
|
532
|
-
const previousHead = particle.previousPosition;
|
|
533
|
-
const position = particle.position;
|
|
534
|
-
let tipX = position.x + (position.x - previousHead.x) * stepFraction;
|
|
535
|
-
let tipY = position.y + (position.y - previousHead.y) * stepFraction;
|
|
536
|
-
let tipZ = position.z + (position.z - previousHead.z) * stepFraction;
|
|
537
|
-
const headIndex = index - 2;
|
|
538
|
-
// The pair's midpoint: a swept pair lies to either side of its sample.
|
|
539
|
-
const headX = (this.positionBuffer[headIndex * 3] + this.positionBuffer[headIndex * 3 + 3]) * 0.5;
|
|
540
|
-
const headY =
|
|
541
|
-
(this.positionBuffer[headIndex * 3 + 1] + this.positionBuffer[headIndex * 3 + 4]) * 0.5;
|
|
542
|
-
const headZ =
|
|
543
|
-
(this.positionBuffer[headIndex * 3 + 2] + this.positionBuffer[headIndex * 3 + 5]) * 0.5;
|
|
544
|
-
|
|
545
|
-
if (!systemWorldSpace) {
|
|
546
|
-
const w = 1 / (m03 * tipX + m13 * tipY + m23 * tipZ + m33);
|
|
547
|
-
const tx = (m00 * tipX + m10 * tipY + m20 * tipZ + m30) * w;
|
|
548
|
-
const ty = (m01 * tipX + m11 * tipY + m21 * tipZ + m31) * w;
|
|
549
|
-
tipZ = (m02 * tipX + m12 * tipY + m22 * tipZ + m32) * w;
|
|
550
|
-
tipX = tx;
|
|
551
|
-
tipY = ty;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const tipPi = index * 3;
|
|
555
|
-
const tipSize = particle.size.x;
|
|
556
|
-
if (swept) {
|
|
557
|
-
// The side as the emitter holds it now.
|
|
558
|
-
const ax = sideAxis!.x;
|
|
559
|
-
const ay = sideAxis!.y;
|
|
560
|
-
const az = sideAxis!.z;
|
|
561
|
-
this.writeSweptPair(
|
|
562
|
-
tipPi,
|
|
563
|
-
tipX,
|
|
564
|
-
tipY,
|
|
565
|
-
tipZ,
|
|
566
|
-
m00 * ax + m10 * ay + m20 * az,
|
|
567
|
-
m01 * ax + m11 * ay + m21 * az,
|
|
568
|
-
m02 * ax + m12 * ay + m22 * az,
|
|
569
|
-
tipSize * 0.5
|
|
570
|
-
);
|
|
571
|
-
} else {
|
|
572
|
-
this.positionBuffer[tipPi] = tipX;
|
|
573
|
-
this.positionBuffer[tipPi + 1] = tipY;
|
|
574
|
-
this.positionBuffer[tipPi + 2] = tipZ;
|
|
575
|
-
this.positionBuffer[tipPi + 3] = tipX;
|
|
576
|
-
this.positionBuffer[tipPi + 4] = tipY;
|
|
577
|
-
this.positionBuffer[tipPi + 5] = tipZ;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
this.previousBuffer[tipPi] = headX;
|
|
581
|
-
this.previousBuffer[tipPi + 1] = headY;
|
|
582
|
-
this.previousBuffer[tipPi + 2] = headZ;
|
|
583
|
-
this.previousBuffer[tipPi + 3] = headX;
|
|
584
|
-
this.previousBuffer[tipPi + 4] = headY;
|
|
585
|
-
this.previousBuffer[tipPi + 5] = headZ;
|
|
586
|
-
|
|
587
|
-
this.nextBuffer[tipPi] = tipX;
|
|
588
|
-
this.nextBuffer[tipPi + 1] = tipY;
|
|
589
|
-
this.nextBuffer[tipPi + 2] = tipZ;
|
|
590
|
-
this.nextBuffer[tipPi + 3] = tipX;
|
|
591
|
-
this.nextBuffer[tipPi + 4] = tipY;
|
|
592
|
-
this.nextBuffer[tipPi + 5] = tipZ;
|
|
593
|
-
|
|
594
|
-
// The recorded head's `next` was itself; point it at the tip
|
|
595
|
-
// so the shader builds the live segment from one version of
|
|
596
|
-
// each endpoint.
|
|
597
|
-
const headPi = headIndex * 3;
|
|
598
|
-
this.nextBuffer[headPi] = tipX;
|
|
599
|
-
this.nextBuffer[headPi + 1] = tipY;
|
|
600
|
-
this.nextBuffer[headPi + 2] = tipZ;
|
|
601
|
-
this.nextBuffer[headPi + 3] = tipX;
|
|
602
|
-
this.nextBuffer[headPi + 4] = tipY;
|
|
603
|
-
this.nextBuffer[headPi + 5] = tipZ;
|
|
604
|
-
|
|
605
|
-
if (swept) {
|
|
606
|
-
this.widthBuffer[index] = 0;
|
|
607
|
-
this.widthBuffer[index + 1] = 0;
|
|
608
|
-
} else if (systemWorldSpace || particle.parentMatrix) {
|
|
609
|
-
this.widthBuffer[index] = tipSize;
|
|
610
|
-
this.widthBuffer[index + 1] = tipSize;
|
|
611
|
-
} else {
|
|
612
|
-
this.widthBuffer[index] = tipSize * objectScale;
|
|
613
|
-
this.widthBuffer[index + 1] = tipSize * objectScale;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const tipUi = index * 2;
|
|
617
|
-
const tipU = ((uvFromHead ? 0 : 1) + col) * tileWidth;
|
|
618
|
-
this.uvBuffer[tipUi] = tipU;
|
|
619
|
-
this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
|
|
620
|
-
this.uvBuffer[tipUi + 2] = tipU;
|
|
621
|
-
this.uvBuffer[tipUi + 3] = (vTileCount - row) * tileHeight;
|
|
622
|
-
if (uv1) {
|
|
623
|
-
const tipU1 = uvFromHead ? 0 : 1;
|
|
624
|
-
uv1[tipUi] = tipU1;
|
|
625
|
-
uv1[tipUi + 1] = 0;
|
|
626
|
-
uv1[tipUi + 2] = tipU1;
|
|
627
|
-
uv1[tipUi + 3] = 1;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const tipColor = particle.color;
|
|
631
|
-
const tipCi = index * 4;
|
|
632
|
-
this.colorBuffer[tipCi] = tipColor.x;
|
|
633
|
-
this.colorBuffer[tipCi + 1] = tipColor.y;
|
|
634
|
-
this.colorBuffer[tipCi + 2] = tipColor.z;
|
|
635
|
-
this.colorBuffer[tipCi + 3] = tipColor.w;
|
|
636
|
-
this.colorBuffer[tipCi + 4] = tipColor.x;
|
|
637
|
-
this.colorBuffer[tipCi + 5] = tipColor.y;
|
|
638
|
-
this.colorBuffer[tipCi + 6] = tipColor.z;
|
|
639
|
-
this.colorBuffer[tipCi + 7] = tipColor.w;
|
|
640
|
-
|
|
641
|
-
this.indexBuffer[triangles * 3] = headIndex;
|
|
642
|
-
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
643
|
-
this.indexBuffer[triangles * 3 + 2] = index;
|
|
644
|
-
triangles++;
|
|
645
|
-
this.indexBuffer[triangles * 3] = index;
|
|
646
|
-
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
647
|
-
this.indexBuffer[triangles * 3 + 2] = index + 1;
|
|
648
|
-
triangles++;
|
|
649
|
-
|
|
650
|
-
index += 2;
|
|
651
841
|
}
|
|
652
842
|
}
|
|
653
843
|
const tint = (system as ParticleSystem).tint;
|
|
@@ -783,6 +973,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
783
973
|
}
|
|
784
974
|
}
|
|
785
975
|
|
|
976
|
+
/** @internal */
|
|
786
977
|
dispose(): void {
|
|
787
978
|
if (this.mesh.material) {
|
|
788
979
|
this.mesh.material.dispose();
|