babylon.quarks 0.26.0 → 0.28.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 +1194 -8
- package/README.md +13 -9
- 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 +10108 -0
- package/dist/types/index.d.ts +5794 -998
- package/package.json +18 -7
- package/src/BatchedRenderer.ts +274 -156
- package/src/EffectPool.ts +94 -12
- package/src/MeshSurfaceEmitter.ts +50 -14
- package/src/ParticleEmitter.ts +75 -17
- package/src/ParticleSystem.ts +981 -2066
- package/src/QuarksLoader.ts +666 -194
- package/src/QuarksPrefab.ts +367 -73
- package/src/QuarksSerializer.ts +512 -0
- package/src/QuarksUtil.ts +77 -7
- package/src/SpriteBatch.ts +127 -252
- package/src/TrailBatch.ts +545 -348
- package/src/VFXBatch.ts +238 -596
- package/src/batchKey.ts +239 -0
- package/src/batchSettings.ts +546 -0
- package/src/effectDocumentJSON.ts +261 -0
- package/src/effectResources.ts +76 -0
- package/src/envAtlas.ts +126 -18
- package/src/geometryUtil.ts +1 -1
- package/src/index.ts +69 -35
- package/src/nodeId.ts +36 -0
- package/src/shaderProgram.ts +419 -0
- package/src/shaderVariants.ts +245 -0
- package/src/shaders/chunks/blend.ts +31 -0
- package/src/shaders/chunks/fog.ts +12 -5
- package/src/shaders/chunks/sheet.ts +111 -0
- package/src/shaders/chunks/soft.ts +85 -0
- package/src/shaders/chunks/surface.ts +12 -2
- 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 +5 -22
- package/src/shaders/particle_frag.wgsl.ts +5 -23
- package/src/shaders/particle_physics_frag.glsl.ts +16 -26
- package/src/shaders/particle_physics_frag.wgsl.ts +14 -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 +13 -0
- package/src/shaders/trail_frag.wgsl.ts +13 -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 +358 -0
- package/src/system/EventDispatcher.ts +60 -0
- package/src/system/ParticleSpawner.ts +274 -0
- package/src/system/SimulationClock.ts +130 -0
- package/src/system/geometry.ts +104 -0
- package/src/system/integrate.ts +428 -0
- package/src/system/materialResolve.ts +165 -0
- package/src/system/normalize.ts +100 -0
- package/src/system/serialization.ts +422 -0
- package/src/system/settingsSchema.ts +339 -0
- package/src/system/types.ts +449 -0
- package/src/textureSource.ts +30 -0
- package/src/trailSettings.ts +54 -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,33 +116,36 @@ 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
|
-
//
|
|
40
|
-
this.maxParticles =
|
|
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.
|
|
124
|
+
this.maxParticles = this.initialCapacity;
|
|
41
125
|
this.setupBuffers();
|
|
42
126
|
this.rebuildMaterial();
|
|
43
127
|
}
|
|
44
128
|
|
|
129
|
+
/** @internal */
|
|
45
130
|
setupBuffers(): void {
|
|
46
|
-
this.mesh
|
|
131
|
+
this.mesh?.dispose();
|
|
47
132
|
this.mesh = new Mesh('trailBatch', this.scene);
|
|
48
133
|
this.configureMesh();
|
|
49
134
|
this.buildGeometryBuffers();
|
|
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,21 @@ export class TrailBatch extends VFXBatch {
|
|
|
123
210
|
}
|
|
124
211
|
}
|
|
125
212
|
|
|
213
|
+
/** @internal */
|
|
214
|
+
protected readonly initialCapacity = 1024;
|
|
215
|
+
|
|
216
|
+
/** Grows the buffers to hold more than `target` vertices. @internal */
|
|
126
217
|
expandBuffers(target: number): void {
|
|
127
218
|
while (target >= this.maxParticles) {
|
|
128
219
|
this.maxParticles *= 2;
|
|
129
220
|
}
|
|
130
|
-
|
|
131
|
-
|
|
221
|
+
this.resizeBuffers();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @internal */
|
|
225
|
+
protected resizeBuffers(): void {
|
|
226
|
+
// `Geometry.setVerticesBuffer` disposes the buffer it replaces; these custom attributes
|
|
227
|
+
// are not in the geometry's map, so they are released here.
|
|
132
228
|
this.previousVB?.dispose();
|
|
133
229
|
this.nextVB?.dispose();
|
|
134
230
|
this.sideVB?.dispose();
|
|
@@ -136,45 +232,15 @@ export class TrailBatch extends VFXBatch {
|
|
|
136
232
|
this.buildGeometryBuffers();
|
|
137
233
|
}
|
|
138
234
|
|
|
235
|
+
/** @internal */
|
|
139
236
|
rebuildMaterial(): void {
|
|
237
|
+
const program = shaderProgram(this.settings, {depth: this.depth, envAtlasReady: false, meshUV2: false});
|
|
238
|
+
const {defines, attributes, uniforms, samplers} = program;
|
|
140
239
|
// Stable name: the trail shader source never varies, so registering it once
|
|
141
240
|
// 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
|
-
|
|
241
|
+
const shaderName = program.name;
|
|
150
242
|
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);
|
|
243
|
+
registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
|
|
178
244
|
|
|
179
245
|
const mat = new ShaderMaterial(
|
|
180
246
|
shaderName,
|
|
@@ -193,24 +259,8 @@ export class TrailBatch extends VFXBatch {
|
|
|
193
259
|
if (this.settings.texture) {
|
|
194
260
|
mat.setTexture('map', this.settings.texture);
|
|
195
261
|
}
|
|
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
262
|
this.bindFog(mat);
|
|
263
|
+
this.bindSoft(mat);
|
|
214
264
|
this.bindSurface(mat);
|
|
215
265
|
mat.backFaceCulling = false;
|
|
216
266
|
|
|
@@ -219,27 +269,198 @@ export class TrailBatch extends VFXBatch {
|
|
|
219
269
|
} else {
|
|
220
270
|
mat.alphaMode = Constants.ALPHA_DISABLE;
|
|
221
271
|
}
|
|
222
|
-
|
|
272
|
+
// A transparent surface that writes depth is drawn into the depth buffer first, so its own
|
|
273
|
+
// overlapping pieces do not fight; an opaque one writes depth as it draws.
|
|
274
|
+
mat.needDepthPrePass = this.settings.materialDepthWrite && this.settings.materialTransparent;
|
|
223
275
|
mat.forceDepthWrite = this.settings.materialDepthWrite;
|
|
224
276
|
mat.disableDepthWrite = !this.settings.materialDepthWrite;
|
|
277
|
+
// 0 keeps the engine's depth function (LEQUAL); ALWAYS draws over everything, like ZTest Always.
|
|
278
|
+
mat.depthFunction = this.settings.materialDepthTest ? 0 : Constants.ALWAYS;
|
|
225
279
|
|
|
280
|
+
// A rebuild (a depth texture attached or removed) replaces the material and its bind
|
|
281
|
+
// observers. Textures belong to the settings, so they stay.
|
|
282
|
+
(this.mesh.material as ShaderMaterial | null)?.dispose(false, false);
|
|
226
283
|
this.mesh.material = mat;
|
|
227
284
|
}
|
|
228
285
|
|
|
229
|
-
private vector_ = new Vector3();
|
|
230
286
|
private vector2_ = new Vector3();
|
|
231
287
|
private vector3_ = new Vector3();
|
|
232
288
|
private quaternion_ = new Quaternion();
|
|
233
289
|
|
|
290
|
+
private readonly samples_ = new TrailSamples();
|
|
291
|
+
private readonly curve_ = new TrailSamples();
|
|
292
|
+
|
|
293
|
+
/** The scratch a trail's samples are gathered into, room for `count`. */
|
|
294
|
+
private trailSamples(count: number): TrailSamples {
|
|
295
|
+
return this.samples_.reserve(count);
|
|
296
|
+
}
|
|
297
|
+
|
|
234
298
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* Babylon only recomputes a world matrix once per render id, so a frame that
|
|
238
|
-
* moves the emitter after that (or a step driven without rendering) would
|
|
239
|
-
* draw the ribbon against the old one. Recomputing unconditionally every
|
|
240
|
-
* frame costs a matrix compose per system; checking the transform cache
|
|
241
|
-
* first makes a still emitter free.
|
|
299
|
+
* Shortens the last segment of a drawn ribbon to `keep` of its length, along the points already
|
|
300
|
+
* drawn for it, so it stays a prefix of that same curve. Every channel follows.
|
|
242
301
|
*/
|
|
302
|
+
private cutLastSegment(drawn: TrailSamples, count: number, perSegment: number, keep: number, swept: boolean): void {
|
|
303
|
+
const first = count - 1 - perSegment;
|
|
304
|
+
const scratch = this.cutScratch_.reserve(perSegment + 1);
|
|
305
|
+
scratch.position.set(drawn.position.subarray(first * 3, count * 3));
|
|
306
|
+
scratch.side.set(drawn.side.subarray(first * 3, count * 3));
|
|
307
|
+
scratch.size.set(drawn.size.subarray(first, count));
|
|
308
|
+
scratch.color.set(drawn.color.subarray(first * 4, count * 4));
|
|
309
|
+
scratch.distance.set(drawn.distance.subarray(first, count));
|
|
310
|
+
for (let j = 1; j <= perSegment; j++) {
|
|
311
|
+
const at = keep * j;
|
|
312
|
+
const i = Math.min(perSegment - 1, Math.floor(at));
|
|
313
|
+
const t = at - i;
|
|
314
|
+
const d = first + j;
|
|
315
|
+
for (let c = 0; c < 3; c++) {
|
|
316
|
+
drawn.position[d * 3 + c] = scratch.position[i * 3 + c] * (1 - t) + scratch.position[i * 3 + 3 + c] * t;
|
|
317
|
+
if (swept) {
|
|
318
|
+
drawn.side[d * 3 + c] = scratch.side[i * 3 + c] * (1 - t) + scratch.side[i * 3 + 3 + c] * t;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
drawn.size[d] = scratch.size[i] * (1 - t) + scratch.size[i + 1] * t;
|
|
322
|
+
drawn.distance[d] = scratch.distance[i] * (1 - t) + scratch.distance[i + 1] * t;
|
|
323
|
+
for (let c = 0; c < 4; c++) {
|
|
324
|
+
drawn.color[d * 4 + c] = scratch.color[i * 4 + c] * (1 - t) + scratch.color[i * 4 + 4 + c] * t;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private readonly cutScratch_ = new TrailSamples();
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The curve through a trail's `count` samples, each segment drawn as
|
|
333
|
+
* `steps + 1`: through the first `curved` (the recorded samples) a
|
|
334
|
+
* centripetal cardinal spline (Catmull-Rom at tension 0, straight at 1),
|
|
335
|
+
* the same through the sides of a swept trail with each side's length
|
|
336
|
+
* eased between its ends'; past them (the live tip) a straight segment,
|
|
337
|
+
* so the recorded part does not move as the tip does. Widths, colors and
|
|
338
|
+
* distances are eased along. The curve passes through every sample, so
|
|
339
|
+
* the trail keeps its shape and only loses its corners; centripetal
|
|
340
|
+
* spacing keeps it from overshooting where samples lie unevenly apart.
|
|
341
|
+
*/
|
|
342
|
+
private tessellate(
|
|
343
|
+
samples: TrailSamples,
|
|
344
|
+
curved: number,
|
|
345
|
+
count: number,
|
|
346
|
+
steps: number,
|
|
347
|
+
tension: number,
|
|
348
|
+
swept: boolean
|
|
349
|
+
): TrailSamples {
|
|
350
|
+
const out = this.curve_.reserve((count - 1) * (steps + 1) + 1);
|
|
351
|
+
const p = samples.position;
|
|
352
|
+
const sd = samples.side;
|
|
353
|
+
const op = out.position;
|
|
354
|
+
const os = out.side;
|
|
355
|
+
const tangent = 1 - tension;
|
|
356
|
+
let k = 0;
|
|
357
|
+
for (let i = 0; i + 1 < count; i++) {
|
|
358
|
+
const a = i * 3;
|
|
359
|
+
const b = a + 3;
|
|
360
|
+
// Each end's tangent, as weights of the neighbouring differences
|
|
361
|
+
// (Yuksel et al., parameterization of Catmull-Rom curves).
|
|
362
|
+
let w1a = 0;
|
|
363
|
+
let w1b = 0;
|
|
364
|
+
let w1c = 0;
|
|
365
|
+
let w2a = 0;
|
|
366
|
+
let w2b = 0;
|
|
367
|
+
let w2c = 0;
|
|
368
|
+
let before = a;
|
|
369
|
+
let after = b;
|
|
370
|
+
if (i + 1 < curved) {
|
|
371
|
+
before = (i > 0 ? i - 1 : i) * 3;
|
|
372
|
+
after = (i + 2 < curved ? i + 2 : i + 1) * 3;
|
|
373
|
+
const t12 = knot(p, a, b);
|
|
374
|
+
if (t12 > 1e-6) {
|
|
375
|
+
const t01 = knot(p, before, a);
|
|
376
|
+
const t23 = knot(p, b, after);
|
|
377
|
+
w1c = tangent;
|
|
378
|
+
w2a = tangent;
|
|
379
|
+
if (t01 > 1e-6) {
|
|
380
|
+
w1a = (tangent * t12) / t01;
|
|
381
|
+
w1b = (-tangent * t12) / (t01 + t12);
|
|
382
|
+
}
|
|
383
|
+
if (t23 > 1e-6) {
|
|
384
|
+
w2b = (-tangent * t12) / (t12 + t23);
|
|
385
|
+
w2c = (tangent * t12) / t23;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// m1 = w1a (A - Before) + w1b (B - Before) + w1c (B - A);
|
|
390
|
+
// m2 = w2a (B - A) + w2b (After - A) + w2c (After - B).
|
|
391
|
+
// With no tangents (the tip's segment, or tension 1) the curve is the straight segment
|
|
392
|
+
// itself: walk it evenly, as the widths, colors and distances are, rather than easing.
|
|
393
|
+
const straight = w1a === 0 && w1b === 0 && w1c === 0 && w2a === 0 && w2b === 0 && w2c === 0;
|
|
394
|
+
const la = swept ? Math.sqrt(sd[a] * sd[a] + sd[a + 1] * sd[a + 1] + sd[a + 2] * sd[a + 2]) : 0;
|
|
395
|
+
const lb = swept ? Math.sqrt(sd[b] * sd[b] + sd[b + 1] * sd[b + 1] + sd[b + 2] * sd[b + 2]) : 0;
|
|
396
|
+
for (let step = 0; step <= steps; step++, k++) {
|
|
397
|
+
const t = step / (steps + 1);
|
|
398
|
+
const t2 = t * t;
|
|
399
|
+
const t3 = t2 * t;
|
|
400
|
+
const h00 = straight ? 1 - t : 2 * t3 - 3 * t2 + 1;
|
|
401
|
+
const h10 = t3 - 2 * t2 + t;
|
|
402
|
+
const h01 = straight ? t : -2 * t3 + 3 * t2;
|
|
403
|
+
const h11 = t3 - t2;
|
|
404
|
+
const o = k * 3;
|
|
405
|
+
for (let c = 0; c < 3; c++) {
|
|
406
|
+
const A = p[a + c];
|
|
407
|
+
const B = p[b + c];
|
|
408
|
+
const m1 = w1a * (A - p[before + c]) + w1b * (B - p[before + c]) + w1c * (B - A);
|
|
409
|
+
const m2 = w2a * (B - A) + w2b * (p[after + c] - A) + w2c * (p[after + c] - B);
|
|
410
|
+
op[o + c] = h00 * A + h10 * m1 + h01 * B + h11 * m2;
|
|
411
|
+
}
|
|
412
|
+
if (swept) {
|
|
413
|
+
let x = 0;
|
|
414
|
+
let y = 0;
|
|
415
|
+
let z = 0;
|
|
416
|
+
for (let c = 0; c < 3; c++) {
|
|
417
|
+
const A = sd[a + c];
|
|
418
|
+
const B = sd[b + c];
|
|
419
|
+
const m1 = w1a * (A - sd[before + c]) + w1b * (B - sd[before + c]) + w1c * (B - A);
|
|
420
|
+
const m2 = w2a * (B - A) + w2b * (sd[after + c] - A) + w2c * (sd[after + c] - B);
|
|
421
|
+
const v = h00 * A + h10 * m1 + h01 * B + h11 * m2;
|
|
422
|
+
if (c === 0) x = v;
|
|
423
|
+
else if (c === 1) y = v;
|
|
424
|
+
else z = v;
|
|
425
|
+
}
|
|
426
|
+
// A side turning with the blade keeps its length on the way round.
|
|
427
|
+
let length = Math.sqrt(x * x + y * y + z * z);
|
|
428
|
+
if (length <= 1e-6 * Math.max(la, lb, 1e-6)) {
|
|
429
|
+
// Turned half round in one step: no way round to follow, so
|
|
430
|
+
// hold the nearer end's side rather than pinch to nothing.
|
|
431
|
+
const from = t < 0.5 ? a : b;
|
|
432
|
+
x = sd[from];
|
|
433
|
+
y = sd[from + 1];
|
|
434
|
+
z = sd[from + 2];
|
|
435
|
+
length = t < 0.5 ? la : lb;
|
|
436
|
+
}
|
|
437
|
+
const fit = length > 0 ? (la + (lb - la) * t) / length : 0;
|
|
438
|
+
os[o] = x * fit;
|
|
439
|
+
os[o + 1] = y * fit;
|
|
440
|
+
os[o + 2] = z * fit;
|
|
441
|
+
}
|
|
442
|
+
out.size[k] = samples.size[i] + (samples.size[i + 1] - samples.size[i]) * t;
|
|
443
|
+
out.distance[k] = samples.distance[i] + (samples.distance[i + 1] - samples.distance[i]) * t;
|
|
444
|
+
const ca = i * 4;
|
|
445
|
+
const co = k * 4;
|
|
446
|
+
for (let c = 0; c < 4; c++) {
|
|
447
|
+
out.color[co + c] = samples.color[ca + c] + (samples.color[ca + 4 + c] - samples.color[ca + c]) * t;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// The last sample, as it is.
|
|
452
|
+
const last = (count - 1) * 3;
|
|
453
|
+
const o = k * 3;
|
|
454
|
+
for (let c = 0; c < 3; c++) {
|
|
455
|
+
op[o + c] = p[last + c];
|
|
456
|
+
os[o + c] = sd[last + c];
|
|
457
|
+
}
|
|
458
|
+
out.size[k] = samples.size[count - 1];
|
|
459
|
+
out.distance[k] = samples.distance[count - 1];
|
|
460
|
+
for (let c = 0; c < 4; c++) out.color[k * 4 + c] = samples.color[(count - 1) * 4 + c];
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
243
464
|
/**
|
|
244
465
|
* Writes a swept ribbon's vertex pair at `pi`: the sample at (x, y, z)
|
|
245
466
|
* pushed `half` widths along its side (sx, sy, sz) one way for the first
|
|
@@ -264,6 +485,15 @@ export class TrailBatch extends VFXBatch {
|
|
|
264
485
|
positions[pi + 5] = z - sz * half;
|
|
265
486
|
}
|
|
266
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Brings the emitter's world matrix up to date when its transform changed.
|
|
490
|
+
*
|
|
491
|
+
* Babylon only recomputes a world matrix once per render id, so a frame that
|
|
492
|
+
* moves the emitter after that (or a step driven without rendering) would
|
|
493
|
+
* draw the ribbon against the old one. Recomputing unconditionally every
|
|
494
|
+
* frame costs a matrix compose per system; checking the transform cache
|
|
495
|
+
* first makes a still emitter free.
|
|
496
|
+
*/
|
|
267
497
|
private static ensureWorldMatrix(system: IParticleSystem): void {
|
|
268
498
|
const emitter = system.emitter as {
|
|
269
499
|
computeWorldMatrix?: (force: boolean) => void;
|
|
@@ -274,6 +504,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
274
504
|
}
|
|
275
505
|
}
|
|
276
506
|
|
|
507
|
+
/** @internal */
|
|
277
508
|
update(): void {
|
|
278
509
|
let index = 0;
|
|
279
510
|
let triangles = 0;
|
|
@@ -283,19 +514,19 @@ export class TrailBatch extends VFXBatch {
|
|
|
283
514
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
284
515
|
const system = visibleSystems[s];
|
|
285
516
|
const particles = system.particles;
|
|
517
|
+
const tessellation = trailTessellation(system.rendererEmitterSettings as TrailSettings);
|
|
286
518
|
for (let j = 0; j < system.particleNum; j++) {
|
|
287
519
|
const historyCount = (particles[j] as TrailParticle).historyCount;
|
|
288
520
|
if (historyCount === 0) {
|
|
289
521
|
continue;
|
|
290
522
|
}
|
|
291
|
-
// +
|
|
292
|
-
//
|
|
293
|
-
|
|
523
|
+
// +1 reserves the live tip that frames between simulation
|
|
524
|
+
// steps append ahead of the recorded head; a tessellated trail
|
|
525
|
+
// draws each segment as several.
|
|
526
|
+
particleCount += (historyCount * (tessellation + 1) + 1) * 2;
|
|
294
527
|
}
|
|
295
528
|
}
|
|
296
|
-
|
|
297
|
-
this.expandBuffers(particleCount);
|
|
298
|
-
}
|
|
529
|
+
this.fitBuffers(particleCount);
|
|
299
530
|
|
|
300
531
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
301
532
|
const system = visibleSystems[s];
|
|
@@ -342,7 +573,25 @@ export class TrailBatch extends VFXBatch {
|
|
|
342
573
|
// Measured from the step the particle just took, not from its
|
|
343
574
|
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
344
575
|
// behavior has motion no velocity term describes.
|
|
345
|
-
const
|
|
576
|
+
const rawFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
|
|
577
|
+
// A few ten-thousandths of a step are float noise left in the accumulator, not time: a
|
|
578
|
+
// tip there would sit on the head as a segment of no length.
|
|
579
|
+
const stepFraction = Math.abs(rawFraction) < 1e-4 ? 0 : rawFraction;
|
|
580
|
+
|
|
581
|
+
const tessellation = trailTessellation(trailSettings);
|
|
582
|
+
const curveTension = trailCurveTension(trailSettings);
|
|
583
|
+
const textureModeName = trailTextureMode(trailSettings);
|
|
584
|
+
const textureMode =
|
|
585
|
+
textureModeName === 'stretch'
|
|
586
|
+
? TextureMode.Stretch
|
|
587
|
+
: textureModeName === 'tile'
|
|
588
|
+
? TextureMode.Tile
|
|
589
|
+
: textureModeName === 'repeatPerSegment'
|
|
590
|
+
? TextureMode.RepeatPerSegment
|
|
591
|
+
: TextureMode.DistributePerSegment;
|
|
592
|
+
const textureScale = trailTextureScale(trailSettings);
|
|
593
|
+
// With a minimum vertex distance the last recorded segment is not one step of travel.
|
|
594
|
+
const samplesFollow = trailMinVertexDistance(trailSettings) > 0;
|
|
346
595
|
|
|
347
596
|
for (let j = 0; j < particleNum; j++) {
|
|
348
597
|
const particle = particles[j] as TrailParticle;
|
|
@@ -353,160 +602,239 @@ export class TrailBatch extends VFXBatch {
|
|
|
353
602
|
const historyCapacity = particle.historyCapacity;
|
|
354
603
|
const historyPositions = particle.historyPositions;
|
|
355
604
|
const historySizes = particle.historySizes;
|
|
605
|
+
const historyDistances = particle.historyDistances;
|
|
356
606
|
const historySides = particle.historySides;
|
|
357
|
-
const swept = sideAxis !== undefined && historySides.length
|
|
607
|
+
const swept = sideAxis !== undefined && historySides.length >= historyCapacity * 3;
|
|
358
608
|
// Every sample takes its color from one place: its own recorded slot, or
|
|
359
609
|
// the particle's current row in the store.
|
|
360
610
|
const colors = colorFromParticle ? particle.store.color : particle.historyColors;
|
|
361
611
|
const particleColorIndex = particle.storeIndex * 4;
|
|
362
|
-
|
|
363
|
-
const row =
|
|
612
|
+
// The same cell as the sprite shader, wrapped into the sheet the same way.
|
|
613
|
+
const {column: col, row} = sheetCell(particle.uvTile, uTileCount, vTileCount);
|
|
364
614
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
365
615
|
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
|
-
}
|
|
616
|
+
const widthScale = systemWorldSpace || particle.parentMatrix ? 1 : objectScale;
|
|
617
|
+
// Recorded distances are world lengths once the samples are, a local trail's scale with the emitter.
|
|
618
|
+
const distanceScale = samplesInWorld || particle.parentMatrix ? 1 : objectScale;
|
|
619
|
+
// A particle at the end of its life records nothing more (TrailParticle.update), so
|
|
620
|
+
// its trail ends at the last sample: a tip would follow the particle on as a growing spike.
|
|
621
|
+
const recording = particle.age < particle.life;
|
|
622
|
+
// The display runs ahead of the last step: draw a tip that far past the head.
|
|
623
|
+
const tipAhead = recording && stepFraction > 0;
|
|
624
|
+
const sampleCount = particleHistoryLength + (tipAhead ? 1 : 0);
|
|
625
|
+
const samples = this.trailSamples(sampleCount);
|
|
626
|
+
|
|
627
|
+
// The recorded samples, oldest first, brought into world space once.
|
|
628
|
+
let slot = particle.getHistoryIndex(0);
|
|
629
|
+
for (let i = 0; i < particleHistoryLength; i++) {
|
|
630
|
+
const p = historyPositions;
|
|
631
|
+
samples.writePoint(i, p[slot * 3], p[slot * 3 + 1], p[slot * 3 + 2], samplesInWorld ? null : me);
|
|
632
|
+
if (swept) {
|
|
633
|
+
const s = historySides;
|
|
634
|
+
samples.writeSide(i, s[slot * 3], s[slot * 3 + 1], s[slot * 3 + 2], samplesInWorld ? null : me);
|
|
427
635
|
}
|
|
636
|
+
samples.size[i] = historySizes[slot];
|
|
637
|
+
samples.distance[i] = historyDistances[slot];
|
|
638
|
+
const ci = colorFromParticle ? particleColorIndex : slot * 4;
|
|
639
|
+
samples.writeColor(i, colors[ci], colors[ci + 1], colors[ci + 2], colors[ci + 3]);
|
|
640
|
+
slot = slot + 1 === historyCapacity ? 0 : slot + 1;
|
|
641
|
+
}
|
|
428
642
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
643
|
+
// Live tip: same place a sprite would be drawn this frame, past
|
|
644
|
+
// the last recorded sample. The recorded ribbon stays put.
|
|
645
|
+
if (tipAhead) {
|
|
646
|
+
const previousHead = particle.previousPosition;
|
|
647
|
+
const position = particle.position;
|
|
648
|
+
const tip = particleHistoryLength;
|
|
649
|
+
let tx = position.x + (position.x - previousHead.x) * stepFraction;
|
|
650
|
+
let ty = position.y + (position.y - previousHead.y) * stepFraction;
|
|
651
|
+
let tz = position.z + (position.z - previousHead.z) * stepFraction;
|
|
652
|
+
samples.writePoint(tip, tx, ty, tz, systemWorldSpace ? null : me);
|
|
653
|
+
// The tip's distance along the trail, measured in the space the samples are
|
|
654
|
+
// recorded in, as theirs are.
|
|
655
|
+
if (samplesInWorld && !systemWorldSpace) {
|
|
656
|
+
const w = 1 / (me[3] * tx + me[7] * ty + me[11] * tz + me[15]);
|
|
657
|
+
const wx = (me[0] * tx + me[4] * ty + me[8] * tz + me[12]) * w;
|
|
658
|
+
const wy = (me[1] * tx + me[5] * ty + me[9] * tz + me[13]) * w;
|
|
659
|
+
tz = (me[2] * tx + me[6] * ty + me[10] * tz + me[14]) * w;
|
|
660
|
+
tx = wx;
|
|
661
|
+
ty = wy;
|
|
662
|
+
}
|
|
663
|
+
const headSlot = particle.getHistoryIndex(particleHistoryLength - 1) * 3;
|
|
664
|
+
const hx = tx - historyPositions[headSlot];
|
|
665
|
+
const hy = ty - historyPositions[headSlot + 1];
|
|
666
|
+
const hz = tz - historyPositions[headSlot + 2];
|
|
667
|
+
samples.distance[tip] = samples.distance[tip - 1] + Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
437
668
|
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;
|
|
669
|
+
// The side as the emitter holds it now.
|
|
670
|
+
samples.writeSide(tip, sideAxis!.x, sideAxis!.y, sideAxis!.z, me);
|
|
457
671
|
}
|
|
672
|
+
// As wide as the recorded head, which WidthOverLength has already scaled.
|
|
673
|
+
samples.size[tip] = samples.size[tip - 1];
|
|
674
|
+
const tipColor = particle.color;
|
|
675
|
+
samples.writeColor(tip, tipColor.x, tipColor.y, tipColor.z, tipColor.w);
|
|
676
|
+
}
|
|
458
677
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
678
|
+
// Curved through the samples (see TrailSettings.tessellation),
|
|
679
|
+
// or drawn through them as they are.
|
|
680
|
+
const drawn =
|
|
681
|
+
tessellation > 0
|
|
682
|
+
? this.tessellate(
|
|
683
|
+
samples,
|
|
684
|
+
particleHistoryLength,
|
|
685
|
+
sampleCount,
|
|
686
|
+
tessellation,
|
|
687
|
+
curveTension,
|
|
688
|
+
swept
|
|
689
|
+
)
|
|
690
|
+
: samples;
|
|
691
|
+
const drawnCount = tessellation > 0 ? (sampleCount - 1) * (tessellation + 1) + 1 : sampleCount;
|
|
692
|
+
const perSegment = tessellation + 1;
|
|
693
|
+
// The last step ran ahead of the display: the head is drawn where it was at display
|
|
694
|
+
// time, back along the drawn last segment, rather than a tip folding back over it.
|
|
695
|
+
// Cut from the drawn curve, so the rest of the ribbon is the same as on any frame.
|
|
696
|
+
const pullBack = recording && stepFraction < 0 && particleHistoryLength > 1;
|
|
697
|
+
// How much of the last drawn segment is kept: all but the step's share that ran
|
|
698
|
+
// ahead. The segment is one step of travel, unless the head has been following the
|
|
699
|
+
// particle (minVertexDistance), when it is that step's share of its length.
|
|
700
|
+
let keep = 1 + stepFraction;
|
|
701
|
+
if (pullBack && samplesFollow) {
|
|
702
|
+
const dd = drawn.distance;
|
|
703
|
+
const segmentLength = dd[drawnCount - 1] - dd[drawnCount - 1 - perSegment];
|
|
704
|
+
if (segmentLength > 1e-9) {
|
|
705
|
+
const previousHead = particle.previousPosition;
|
|
706
|
+
const position = particle.position;
|
|
707
|
+
let sx = position.x - previousHead.x;
|
|
708
|
+
let sy = position.y - previousHead.y;
|
|
709
|
+
let sz = position.z - previousHead.z;
|
|
710
|
+
if (samplesInWorld && !systemWorldSpace) {
|
|
711
|
+
const wx = me[0] * sx + me[4] * sy + me[8] * sz;
|
|
712
|
+
const wy = me[1] * sx + me[5] * sy + me[9] * sz;
|
|
713
|
+
sz = me[2] * sx + me[6] * sy + me[10] * sz;
|
|
714
|
+
sx = wx;
|
|
715
|
+
sy = wy;
|
|
716
|
+
}
|
|
717
|
+
const stepTravel = Math.sqrt(sx * sx + sy * sy + sz * sz);
|
|
718
|
+
keep = 1 + stepFraction * Math.min(1, stepTravel / segmentLength);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (pullBack) {
|
|
722
|
+
this.cutLastSegment(drawn, drawnCount, perSegment, keep, swept);
|
|
723
|
+
}
|
|
724
|
+
// The per-segment texture modes span the ribbon in samples: the recorded ones, plus
|
|
725
|
+
// the part of a step the tip runs ahead or the head was drawn back, so a sample's U
|
|
726
|
+
// changes smoothly from frame to frame whichever way the residual falls. The others
|
|
727
|
+
// read each point's distance along the trail, tile mode in world units.
|
|
728
|
+
const spanFraction = tipAhead ? stepFraction : pullBack ? keep - 1 : 0;
|
|
729
|
+
const spanSamples = particleHistoryLength - 1 + spanFraction;
|
|
730
|
+
const invLength = 1 / Math.max(1e-3, spanSamples);
|
|
731
|
+
const lastSegment = (sampleCount - 2) * perSegment;
|
|
732
|
+
const dp = drawn.position;
|
|
733
|
+
const dd = drawn.distance;
|
|
734
|
+
const tailDistance = dd[0];
|
|
735
|
+
const headDistance = dd[drawnCount - 1];
|
|
736
|
+
// A ribbon that has not gone anywhere has no distance to stretch over: lay the
|
|
737
|
+
// texture out per segment instead, which at least keeps its ends where they belong.
|
|
738
|
+
const stretch = textureMode === TextureMode.Stretch && headDistance - tailDistance > 1e-6;
|
|
739
|
+
const invSpanDistance = 1 / Math.max(1e-6, headDistance - tailDistance);
|
|
740
|
+
// Tile mode: U in repeats of the texture along the trail, anchored where the particle
|
|
741
|
+
// was born, less the whole repeats behind the tail so U stays small (a texture that
|
|
742
|
+
// wraps cannot tell) however far the particle has travelled.
|
|
743
|
+
const tileScale = distanceScale * textureScale;
|
|
744
|
+
const tileOffset = Math.floor(tailDistance * tileScale);
|
|
745
|
+
const vTop = (vTileCount - row - 1) * tileHeight;
|
|
746
|
+
const vBottom = (vTileCount - row) * tileHeight;
|
|
747
|
+
|
|
748
|
+
for (let k = 0; k < drawnCount; k++, index += 2) {
|
|
749
|
+
const o = k * 3;
|
|
750
|
+
const prev = k > 0 ? o - 3 : o;
|
|
751
|
+
const next = k + 1 < drawnCount ? o + 3 : o;
|
|
752
|
+
const x = dp[o];
|
|
753
|
+
const y = dp[o + 1];
|
|
754
|
+
const z = dp[o + 2];
|
|
755
|
+
const size = drawn.size[k];
|
|
472
756
|
|
|
757
|
+
const pi = index * 3;
|
|
473
758
|
if (swept) {
|
|
759
|
+
const ds = drawn.side;
|
|
760
|
+
this.writeSweptPair(pi, x, y, z, ds[o], ds[o + 1], ds[o + 2], size * 0.5);
|
|
761
|
+
// A zero width keeps the shader from pushing the pair.
|
|
474
762
|
this.widthBuffer[index] = 0;
|
|
475
763
|
this.widthBuffer[index + 1] = 0;
|
|
476
|
-
} else if (systemWorldSpace || particle.parentMatrix) {
|
|
477
|
-
this.widthBuffer[index] = currentSize;
|
|
478
|
-
this.widthBuffer[index + 1] = currentSize;
|
|
479
764
|
} else {
|
|
480
|
-
this.
|
|
481
|
-
this.
|
|
765
|
+
this.positionBuffer[pi] = x;
|
|
766
|
+
this.positionBuffer[pi + 1] = y;
|
|
767
|
+
this.positionBuffer[pi + 2] = z;
|
|
768
|
+
this.positionBuffer[pi + 3] = x;
|
|
769
|
+
this.positionBuffer[pi + 4] = y;
|
|
770
|
+
this.positionBuffer[pi + 5] = z;
|
|
771
|
+
this.widthBuffer[index] = size * widthScale;
|
|
772
|
+
this.widthBuffer[index + 1] = size * widthScale;
|
|
482
773
|
}
|
|
483
774
|
|
|
775
|
+
this.previousBuffer[pi] = dp[prev];
|
|
776
|
+
this.previousBuffer[pi + 1] = dp[prev + 1];
|
|
777
|
+
this.previousBuffer[pi + 2] = dp[prev + 2];
|
|
778
|
+
this.previousBuffer[pi + 3] = dp[prev];
|
|
779
|
+
this.previousBuffer[pi + 4] = dp[prev + 1];
|
|
780
|
+
this.previousBuffer[pi + 5] = dp[prev + 2];
|
|
781
|
+
|
|
782
|
+
this.nextBuffer[pi] = dp[next];
|
|
783
|
+
this.nextBuffer[pi + 1] = dp[next + 1];
|
|
784
|
+
this.nextBuffer[pi + 2] = dp[next + 2];
|
|
785
|
+
this.nextBuffer[pi + 3] = dp[next];
|
|
786
|
+
this.nextBuffer[pi + 4] = dp[next + 1];
|
|
787
|
+
this.nextBuffer[pi + 5] = dp[next + 2];
|
|
788
|
+
|
|
484
789
|
const ui = index * 2;
|
|
485
|
-
|
|
486
|
-
|
|
790
|
+
let u1: number;
|
|
791
|
+
if (stretch) {
|
|
792
|
+
const along = (dd[k] - tailDistance) * invSpanDistance;
|
|
793
|
+
u1 = (uvFromHead ? 1 - along : along) * textureScale;
|
|
794
|
+
} else if (textureMode === TextureMode.Tile) {
|
|
795
|
+
// A sample's U holds while it lives; from the particle back with uvFromHead.
|
|
796
|
+
u1 = uvFromHead ? (headDistance - dd[k]) * tileScale : dd[k] * tileScale - tileOffset;
|
|
797
|
+
} else {
|
|
798
|
+
// Where this point sits along the ribbon, in samples.
|
|
799
|
+
let at = k / perSegment;
|
|
800
|
+
if (k > lastSegment && (tipAhead || pullBack)) {
|
|
801
|
+
const into = (k - lastSegment) / perSegment;
|
|
802
|
+
at = tipAhead
|
|
803
|
+
? particleHistoryLength - 1 + stepFraction * into
|
|
804
|
+
: particleHistoryLength - 2 + keep * into;
|
|
805
|
+
}
|
|
806
|
+
if (textureMode !== TextureMode.RepeatPerSegment) {
|
|
807
|
+
const along = Math.min(1, at * invLength);
|
|
808
|
+
u1 = (uvFromHead ? 1 - along : along) * textureScale;
|
|
809
|
+
} else {
|
|
810
|
+
u1 = (uvFromHead ? spanSamples - at : at) * textureScale;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const u = (u1 + col) * tileWidth;
|
|
487
814
|
this.uvBuffer[ui] = u;
|
|
488
|
-
this.uvBuffer[ui + 1] =
|
|
815
|
+
this.uvBuffer[ui + 1] = vTop;
|
|
489
816
|
this.uvBuffer[ui + 2] = u;
|
|
490
|
-
this.uvBuffer[ui + 3] =
|
|
817
|
+
this.uvBuffer[ui + 3] = vBottom;
|
|
491
818
|
if (uv1) {
|
|
492
|
-
const u1 = uvFromHead ? 1 - along : along;
|
|
493
819
|
uv1[ui] = u1;
|
|
494
820
|
uv1[ui + 1] = 0;
|
|
495
821
|
uv1[ui + 2] = u1;
|
|
496
822
|
uv1[ui + 3] = 1;
|
|
497
823
|
}
|
|
498
824
|
|
|
825
|
+
const dc = drawn.color;
|
|
826
|
+
const c = k * 4;
|
|
499
827
|
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 (
|
|
828
|
+
this.colorBuffer[cci] = dc[c];
|
|
829
|
+
this.colorBuffer[cci + 1] = dc[c + 1];
|
|
830
|
+
this.colorBuffer[cci + 2] = dc[c + 2];
|
|
831
|
+
this.colorBuffer[cci + 3] = dc[c + 3];
|
|
832
|
+
this.colorBuffer[cci + 4] = dc[c];
|
|
833
|
+
this.colorBuffer[cci + 5] = dc[c + 1];
|
|
834
|
+
this.colorBuffer[cci + 6] = dc[c + 2];
|
|
835
|
+
this.colorBuffer[cci + 7] = dc[c + 3];
|
|
836
|
+
|
|
837
|
+
if (k + 1 < drawnCount) {
|
|
510
838
|
this.indexBuffer[triangles * 3] = index;
|
|
511
839
|
this.indexBuffer[triangles * 3 + 1] = index + 1;
|
|
512
840
|
this.indexBuffer[triangles * 3 + 2] = index + 2;
|
|
@@ -516,138 +844,6 @@ export class TrailBatch extends VFXBatch {
|
|
|
516
844
|
this.indexBuffer[triangles * 3 + 2] = index + 3;
|
|
517
845
|
triangles++;
|
|
518
846
|
}
|
|
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
847
|
}
|
|
652
848
|
}
|
|
653
849
|
const tint = (system as ParticleSystem).tint;
|
|
@@ -779,10 +975,11 @@ export class TrailBatch extends VFXBatch {
|
|
|
779
975
|
private adoptMeshBounds(): void {
|
|
780
976
|
const meshBoundingInfo = this.mesh.getBoundingInfo();
|
|
781
977
|
for (const subMesh of this.mesh.subMeshes) {
|
|
782
|
-
|
|
978
|
+
subMesh.setBoundingInfo(meshBoundingInfo);
|
|
783
979
|
}
|
|
784
980
|
}
|
|
785
981
|
|
|
982
|
+
/** @internal */
|
|
786
983
|
dispose(): void {
|
|
787
984
|
if (this.mesh.material) {
|
|
788
985
|
this.mesh.material.dispose();
|