babylon.quarks 0.25.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 +854 -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 +2210 -613
- package/package.json +12 -6
- package/src/BatchedRenderer.ts +215 -106
- package/src/EffectPool.ts +4 -1
- package/src/MeshSurfaceEmitter.ts +29 -4
- package/src/ParticleEmitter.ts +45 -15
- package/src/ParticleSystem.ts +1012 -1962
- package/src/QuarksLoader.ts +463 -75
- package/src/QuarksPrefab.ts +91 -4
- package/src/QuarksSerializer.ts +484 -0
- package/src/QuarksUtil.ts +15 -0
- package/src/SpriteBatch.ts +149 -238
- package/src/TrailBatch.ts +586 -286
- package/src/VFXBatch.ts +517 -153
- package/src/batchKey.ts +237 -0
- package/src/effectResources.ts +79 -0
- package/src/envAtlas.ts +119 -17
- package/src/index.ts +51 -35
- 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 +132 -42
- package/src/shaders/local_particle_physics_vert.glsl.ts +28 -27
- package/src/shaders/local_particle_physics_vert.wgsl.ts +30 -28
- package/src/shaders/particle_frag.glsl.ts +6 -22
- package/src/shaders/particle_frag.wgsl.ts +6 -23
- package/src/shaders/particle_physics_frag.glsl.ts +22 -29
- package/src/shaders/particle_physics_frag.wgsl.ts +22 -30
- package/src/shaders/particle_vert.glsl.ts +11 -26
- package/src/shaders/particle_vert.wgsl.ts +11 -25
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +21 -39
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +22 -40
- package/src/shaders/trail_frag.glsl.ts +14 -1
- package/src/shaders/trail_frag.wgsl.ts +14 -0
- package/src/shaders/trail_vert.glsl.ts +53 -36
- package/src/shaders/trail_vert.wgsl.ts +54 -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,24 +1,110 @@
|
|
|
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;
|
|
20
104
|
private nextBuffer!: Float32Array;
|
|
21
105
|
private uvBuffer!: Float32Array;
|
|
106
|
+
/** UV channel 1: along the trail and across it, without the sheet's cell; only when a texture reads it. */
|
|
107
|
+
private uv1Buffer: Float32Array | null = null;
|
|
22
108
|
private sideBuffer!: Float32Array;
|
|
23
109
|
private widthBuffer!: Float32Array;
|
|
24
110
|
private colorBuffer!: Float32Array;
|
|
@@ -30,16 +116,17 @@ export class TrailBatch extends VFXBatch {
|
|
|
30
116
|
private sideVB!: VertexBuffer;
|
|
31
117
|
private widthVB!: VertexBuffer;
|
|
32
118
|
|
|
33
|
-
constructor(settings: VFXBatchSettings, scene: Scene) {
|
|
34
|
-
super(settings, scene);
|
|
35
|
-
//
|
|
36
|
-
// per-batch allocation in the library, so start small and let
|
|
37
|
-
//
|
|
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.
|
|
38
124
|
this.maxParticles = 1024;
|
|
39
125
|
this.setupBuffers();
|
|
40
126
|
this.rebuildMaterial();
|
|
41
127
|
}
|
|
42
128
|
|
|
129
|
+
/** @internal */
|
|
43
130
|
setupBuffers(): void {
|
|
44
131
|
this.mesh.dispose();
|
|
45
132
|
this.mesh = new Mesh('trailBatch', this.scene);
|
|
@@ -48,14 +135,17 @@ export class TrailBatch extends VFXBatch {
|
|
|
48
135
|
}
|
|
49
136
|
|
|
50
137
|
private buildGeometryBuffers(): void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
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);
|
|
59
149
|
// Vertices come in pairs, one per side of the ribbon, so the side never
|
|
60
150
|
// changes: written once here and never uploaded again.
|
|
61
151
|
for (let i = 0; i < this.sideBuffer.length; i += 2) {
|
|
@@ -86,6 +176,9 @@ export class TrailBatch extends VFXBatch {
|
|
|
86
176
|
// Set standard vertex data with full buffer capacity (updatable)
|
|
87
177
|
this.mesh.setVerticesData(VertexBuffer.PositionKind, this.positionBuffer, true);
|
|
88
178
|
this.mesh.setVerticesData(VertexBuffer.UVKind, this.uvBuffer, true);
|
|
179
|
+
if (this.uv1Buffer) {
|
|
180
|
+
this.mesh.setVerticesData(VertexBuffer.UV2Kind, this.uv1Buffer, true);
|
|
181
|
+
}
|
|
89
182
|
this.mesh.setVerticesData(VertexBuffer.ColorKind, this.colorBuffer, true, 4);
|
|
90
183
|
// The whole array, not just the dummy triangle: this call is what sizes
|
|
91
184
|
// the GPU index buffer, and updateIndices later writes into it without
|
|
@@ -117,12 +210,13 @@ export class TrailBatch extends VFXBatch {
|
|
|
117
210
|
}
|
|
118
211
|
}
|
|
119
212
|
|
|
213
|
+
/** Grows the buffers to hold more than `target` vertices. @internal */
|
|
120
214
|
expandBuffers(target: number): void {
|
|
121
215
|
while (target >= this.maxParticles) {
|
|
122
216
|
this.maxParticles *= 2;
|
|
123
217
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
218
|
+
// `Geometry.setVerticesBuffer` disposes the buffer it replaces; these custom attributes
|
|
219
|
+
// are not in the geometry's map, so they are released here.
|
|
126
220
|
this.previousVB?.dispose();
|
|
127
221
|
this.nextVB?.dispose();
|
|
128
222
|
this.sideVB?.dispose();
|
|
@@ -130,42 +224,15 @@ export class TrailBatch extends VFXBatch {
|
|
|
130
224
|
this.buildGeometryBuffers();
|
|
131
225
|
}
|
|
132
226
|
|
|
227
|
+
/** @internal */
|
|
133
228
|
rebuildMaterial(): void {
|
|
229
|
+
const program = shaderProgram(this.settings, {depth: this.depth, envAtlasReady: false, meshUV2: false});
|
|
230
|
+
const {defines, attributes, uniforms, samplers} = program;
|
|
134
231
|
// Stable name: the trail shader source never varies, so registering it once
|
|
135
232
|
// keeps Effect.ShadersStore bounded and lets Babylon reuse compiled effects.
|
|
136
|
-
const shaderName =
|
|
137
|
-
const defines: string[] = this.fogDefines();
|
|
138
|
-
|
|
139
|
-
if (this.settings.texture) {
|
|
140
|
-
defines.push('USE_MAP');
|
|
141
|
-
}
|
|
142
|
-
defines.push(...this.surfaceDefines());
|
|
143
|
-
|
|
233
|
+
const shaderName = program.name;
|
|
144
234
|
const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
|
|
145
|
-
registerShaders(
|
|
146
|
-
shaderName,
|
|
147
|
-
{glsl: trail_vert, wgsl: trail_vert_wgsl},
|
|
148
|
-
{glsl: trail_frag, wgsl: trail_frag_wgsl},
|
|
149
|
-
shaderLanguage
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const attributes = ['position', 'previous', 'next', 'side', 'width', 'uv', 'color'];
|
|
153
|
-
const uniforms = [
|
|
154
|
-
'world',
|
|
155
|
-
'view',
|
|
156
|
-
'projection',
|
|
157
|
-
'worldViewProjection',
|
|
158
|
-
'lineWidth',
|
|
159
|
-
'resolution',
|
|
160
|
-
'sizeAttenuation',
|
|
161
|
-
...VFXBatch.FOG_UNIFORMS,
|
|
162
|
-
];
|
|
163
|
-
const samplers: string[] = [];
|
|
164
|
-
|
|
165
|
-
if (this.settings.texture) {
|
|
166
|
-
samplers.push('map');
|
|
167
|
-
}
|
|
168
|
-
this.surfaceUniforms(uniforms, samplers);
|
|
235
|
+
registerShaders(shaderName, program.vertex, program.fragment, shaderLanguage);
|
|
169
236
|
|
|
170
237
|
const mat = new ShaderMaterial(
|
|
171
238
|
shaderName,
|
|
@@ -184,24 +251,8 @@ export class TrailBatch extends VFXBatch {
|
|
|
184
251
|
if (this.settings.texture) {
|
|
185
252
|
mat.setTexture('map', this.settings.texture);
|
|
186
253
|
}
|
|
187
|
-
const engine = this.scene.getEngine();
|
|
188
|
-
mat.setFloat('lineWidth', 1);
|
|
189
|
-
// Reused across binds, and only re-uploaded when the render target resizes.
|
|
190
|
-
const resolution = new BVector2(engine.getRenderWidth(), engine.getRenderHeight());
|
|
191
|
-
mat.setVector2('resolution', resolution);
|
|
192
|
-
mat.setFloat('sizeAttenuation', 1);
|
|
193
|
-
mat.onBindObservable.add(() => {
|
|
194
|
-
const renderEngine = this.scene.getEngine();
|
|
195
|
-
const width = renderEngine.getRenderWidth();
|
|
196
|
-
const height = renderEngine.getRenderHeight();
|
|
197
|
-
if (resolution.x !== width || resolution.y !== height) {
|
|
198
|
-
resolution.x = width;
|
|
199
|
-
resolution.y = height;
|
|
200
|
-
mat.setVector2('resolution', resolution);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
254
|
this.bindFog(mat);
|
|
255
|
+
this.bindSoft(mat);
|
|
205
256
|
this.bindSurface(mat);
|
|
206
257
|
mat.backFaceCulling = false;
|
|
207
258
|
|
|
@@ -210,18 +261,222 @@ export class TrailBatch extends VFXBatch {
|
|
|
210
261
|
} else {
|
|
211
262
|
mat.alphaMode = Constants.ALPHA_DISABLE;
|
|
212
263
|
}
|
|
213
|
-
|
|
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;
|
|
214
267
|
mat.forceDepthWrite = this.settings.materialDepthWrite;
|
|
215
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;
|
|
216
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);
|
|
217
275
|
this.mesh.material = mat;
|
|
218
276
|
}
|
|
219
277
|
|
|
220
|
-
private vector_ = new Vector3();
|
|
221
278
|
private vector2_ = new Vector3();
|
|
222
279
|
private vector3_ = new Vector3();
|
|
223
280
|
private quaternion_ = new Quaternion();
|
|
224
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
|
+
|
|
290
|
+
/**
|
|
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.
|
|
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
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Writes a swept ribbon's vertex pair at `pi`: the sample at (x, y, z)
|
|
458
|
+
* pushed `half` widths along its side (sx, sy, sz) one way for the first
|
|
459
|
+
* vertex and the other way for the second.
|
|
460
|
+
*/
|
|
461
|
+
private writeSweptPair(
|
|
462
|
+
pi: number,
|
|
463
|
+
x: number,
|
|
464
|
+
y: number,
|
|
465
|
+
z: number,
|
|
466
|
+
sx: number,
|
|
467
|
+
sy: number,
|
|
468
|
+
sz: number,
|
|
469
|
+
half: number
|
|
470
|
+
): void {
|
|
471
|
+
const positions = this.positionBuffer;
|
|
472
|
+
positions[pi] = x + sx * half;
|
|
473
|
+
positions[pi + 1] = y + sy * half;
|
|
474
|
+
positions[pi + 2] = z + sz * half;
|
|
475
|
+
positions[pi + 3] = x - sx * half;
|
|
476
|
+
positions[pi + 4] = y - sy * half;
|
|
477
|
+
positions[pi + 5] = z - sz * half;
|
|
478
|
+
}
|
|
479
|
+
|
|
225
480
|
/**
|
|
226
481
|
* Brings the emitter's world matrix up to date when its transform changed.
|
|
227
482
|
*
|
|
@@ -241,6 +496,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
241
496
|
}
|
|
242
497
|
}
|
|
243
498
|
|
|
499
|
+
/** @internal */
|
|
244
500
|
update(): void {
|
|
245
501
|
let index = 0;
|
|
246
502
|
let triangles = 0;
|
|
@@ -250,14 +506,16 @@ export class TrailBatch extends VFXBatch {
|
|
|
250
506
|
for (let s = 0; s < visibleSystems.length; s++) {
|
|
251
507
|
const system = visibleSystems[s];
|
|
252
508
|
const particles = system.particles;
|
|
509
|
+
const tessellation = trailTessellation(system.rendererEmitterSettings as TrailSettings);
|
|
253
510
|
for (let j = 0; j < system.particleNum; j++) {
|
|
254
511
|
const historyCount = (particles[j] as TrailParticle).historyCount;
|
|
255
512
|
if (historyCount === 0) {
|
|
256
513
|
continue;
|
|
257
514
|
}
|
|
258
|
-
// +
|
|
259
|
-
//
|
|
260
|
-
|
|
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;
|
|
261
519
|
}
|
|
262
520
|
}
|
|
263
521
|
if (particleCount > this.maxParticles) {
|
|
@@ -296,15 +554,38 @@ export class TrailBatch extends VFXBatch {
|
|
|
296
554
|
// rather than by the particle's velocity, so it is left alone.
|
|
297
555
|
const trailSettings = system.rendererEmitterSettings as TrailSettings;
|
|
298
556
|
const uvFromHead = Boolean(trailSettings.uvFromHead);
|
|
557
|
+
const uv1 = this.uv1Buffer;
|
|
299
558
|
const colorFromParticle = Boolean(trailSettings.colorFromParticle);
|
|
300
559
|
// Recorded samples already sit in world space; the live tip still comes
|
|
301
560
|
// from the particle's own (local) position.
|
|
302
561
|
const samplesInWorld = systemWorldSpace || Boolean(trailSettings.recordWorldSpace);
|
|
562
|
+
// A swept trail spans each sample's recorded side instead of facing
|
|
563
|
+
// the camera: its vertices are laid out here, and a zero width
|
|
564
|
+
// keeps the shader from pushing them.
|
|
565
|
+
const sideAxis = trailSettings.sideAxis;
|
|
303
566
|
const residual = trailSettings.followLocalOrigin ? 0 : (system.simulationResidual ?? 0);
|
|
304
567
|
// Measured from the step the particle just took, not from its
|
|
305
568
|
// velocity: a trail pulled along by an orbit or by a plugin's own
|
|
306
569
|
// behavior has motion no velocity term describes.
|
|
307
|
-
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;
|
|
308
589
|
|
|
309
590
|
for (let j = 0; j < particleNum; j++) {
|
|
310
591
|
const particle = particles[j] as TrailParticle;
|
|
@@ -315,133 +596,239 @@ export class TrailBatch extends VFXBatch {
|
|
|
315
596
|
const historyCapacity = particle.historyCapacity;
|
|
316
597
|
const historyPositions = particle.historyPositions;
|
|
317
598
|
const historySizes = particle.historySizes;
|
|
599
|
+
const historyDistances = particle.historyDistances;
|
|
600
|
+
const historySides = particle.historySides;
|
|
601
|
+
const swept = sideAxis !== undefined && historySides.length >= historyCapacity * 3;
|
|
318
602
|
// Every sample takes its color from one place: its own recorded slot, or
|
|
319
603
|
// the particle's current row in the store.
|
|
320
604
|
const colors = colorFromParticle ? particle.store.color : particle.historyColors;
|
|
321
605
|
const particleColorIndex = particle.storeIndex * 4;
|
|
322
|
-
|
|
323
|
-
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);
|
|
324
608
|
const particleMatrix = particle.parentMatrix as unknown as Matrix4 | undefined;
|
|
325
609
|
const me = (particleMatrix ?? emitterMatrix).elements;
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// the one after that.
|
|
352
|
-
let currentSlot = particle.getHistoryIndex(0);
|
|
353
|
-
let currentX = historyPositions[currentSlot * 3];
|
|
354
|
-
let currentY = historyPositions[currentSlot * 3 + 1];
|
|
355
|
-
let currentZ = historyPositions[currentSlot * 3 + 2];
|
|
356
|
-
if (!samplesInWorld) {
|
|
357
|
-
const w = 1 / (m03 * currentX + m13 * currentY + m23 * currentZ + m33);
|
|
358
|
-
const tx = (m00 * currentX + m10 * currentY + m20 * currentZ + m30) * w;
|
|
359
|
-
const ty = (m01 * currentX + m11 * currentY + m21 * currentZ + m31) * w;
|
|
360
|
-
currentZ = (m02 * currentX + m12 * currentY + m22 * currentZ + m32) * w;
|
|
361
|
-
currentX = tx;
|
|
362
|
-
currentY = ty;
|
|
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);
|
|
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;
|
|
363
635
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
let
|
|
372
|
-
let
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
636
|
+
|
|
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);
|
|
662
|
+
if (swept) {
|
|
663
|
+
// The side as the emitter holds it now.
|
|
664
|
+
samples.writeSide(tip, sideAxis!.x, sideAxis!.y, sideAxis!.z, me);
|
|
387
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
|
+
}
|
|
388
671
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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];
|
|
395
750
|
|
|
396
751
|
const pi = index * 3;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
this.previousBuffer[pi] = previousX;
|
|
405
|
-
this.previousBuffer[pi + 1] = previousY;
|
|
406
|
-
this.previousBuffer[pi + 2] = previousZ;
|
|
407
|
-
this.previousBuffer[pi + 3] = previousX;
|
|
408
|
-
this.previousBuffer[pi + 4] = previousY;
|
|
409
|
-
this.previousBuffer[pi + 5] = previousZ;
|
|
410
|
-
|
|
411
|
-
this.nextBuffer[pi] = nextX;
|
|
412
|
-
this.nextBuffer[pi + 1] = nextY;
|
|
413
|
-
this.nextBuffer[pi + 2] = nextZ;
|
|
414
|
-
this.nextBuffer[pi + 3] = nextX;
|
|
415
|
-
this.nextBuffer[pi + 4] = nextY;
|
|
416
|
-
this.nextBuffer[pi + 5] = nextZ;
|
|
417
|
-
|
|
418
|
-
if (systemWorldSpace || particle.parentMatrix) {
|
|
419
|
-
this.widthBuffer[index] = currentSize;
|
|
420
|
-
this.widthBuffer[index + 1] = currentSize;
|
|
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.
|
|
756
|
+
this.widthBuffer[index] = 0;
|
|
757
|
+
this.widthBuffer[index + 1] = 0;
|
|
421
758
|
} else {
|
|
422
|
-
this.
|
|
423
|
-
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;
|
|
424
767
|
}
|
|
425
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
|
+
|
|
426
783
|
const ui = index * 2;
|
|
427
|
-
|
|
428
|
-
|
|
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;
|
|
429
808
|
this.uvBuffer[ui] = u;
|
|
430
|
-
this.uvBuffer[ui + 1] =
|
|
809
|
+
this.uvBuffer[ui + 1] = vTop;
|
|
431
810
|
this.uvBuffer[ui + 2] = u;
|
|
432
|
-
this.uvBuffer[ui + 3] =
|
|
811
|
+
this.uvBuffer[ui + 3] = vBottom;
|
|
812
|
+
if (uv1) {
|
|
813
|
+
uv1[ui] = u1;
|
|
814
|
+
uv1[ui + 1] = 0;
|
|
815
|
+
uv1[ui + 2] = u1;
|
|
816
|
+
uv1[ui + 3] = 1;
|
|
817
|
+
}
|
|
433
818
|
|
|
819
|
+
const dc = drawn.color;
|
|
820
|
+
const c = k * 4;
|
|
434
821
|
const cci = index * 4;
|
|
435
|
-
this.colorBuffer[cci] =
|
|
436
|
-
this.colorBuffer[cci + 1] =
|
|
437
|
-
this.colorBuffer[cci + 2] =
|
|
438
|
-
this.colorBuffer[cci + 3] =
|
|
439
|
-
this.colorBuffer[cci + 4] =
|
|
440
|
-
this.colorBuffer[cci + 5] =
|
|
441
|
-
this.colorBuffer[cci + 6] =
|
|
442
|
-
this.colorBuffer[cci + 7] =
|
|
443
|
-
|
|
444
|
-
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) {
|
|
445
832
|
this.indexBuffer[triangles * 3] = index;
|
|
446
833
|
this.indexBuffer[triangles * 3 + 1] = index + 1;
|
|
447
834
|
this.indexBuffer[triangles * 3 + 2] = index + 2;
|
|
@@ -451,108 +838,17 @@ export class TrailBatch extends VFXBatch {
|
|
|
451
838
|
this.indexBuffer[triangles * 3 + 2] = index + 3;
|
|
452
839
|
triangles++;
|
|
453
840
|
}
|
|
454
|
-
|
|
455
|
-
previousX = currentX;
|
|
456
|
-
previousY = currentY;
|
|
457
|
-
previousZ = currentZ;
|
|
458
|
-
currentX = nextX;
|
|
459
|
-
currentY = nextY;
|
|
460
|
-
currentZ = nextZ;
|
|
461
|
-
currentSlot = nextSlot;
|
|
462
841
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const headX = this.positionBuffer[headIndex * 3];
|
|
474
|
-
const headY = this.positionBuffer[headIndex * 3 + 1];
|
|
475
|
-
const headZ = this.positionBuffer[headIndex * 3 + 2];
|
|
476
|
-
|
|
477
|
-
if (!systemWorldSpace) {
|
|
478
|
-
const w = 1 / (m03 * tipX + m13 * tipY + m23 * tipZ + m33);
|
|
479
|
-
const tx = (m00 * tipX + m10 * tipY + m20 * tipZ + m30) * w;
|
|
480
|
-
const ty = (m01 * tipX + m11 * tipY + m21 * tipZ + m31) * w;
|
|
481
|
-
tipZ = (m02 * tipX + m12 * tipY + m22 * tipZ + m32) * w;
|
|
482
|
-
tipX = tx;
|
|
483
|
-
tipY = ty;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const tipPi = index * 3;
|
|
487
|
-
this.positionBuffer[tipPi] = tipX;
|
|
488
|
-
this.positionBuffer[tipPi + 1] = tipY;
|
|
489
|
-
this.positionBuffer[tipPi + 2] = tipZ;
|
|
490
|
-
this.positionBuffer[tipPi + 3] = tipX;
|
|
491
|
-
this.positionBuffer[tipPi + 4] = tipY;
|
|
492
|
-
this.positionBuffer[tipPi + 5] = tipZ;
|
|
493
|
-
|
|
494
|
-
this.previousBuffer[tipPi] = headX;
|
|
495
|
-
this.previousBuffer[tipPi + 1] = headY;
|
|
496
|
-
this.previousBuffer[tipPi + 2] = headZ;
|
|
497
|
-
this.previousBuffer[tipPi + 3] = headX;
|
|
498
|
-
this.previousBuffer[tipPi + 4] = headY;
|
|
499
|
-
this.previousBuffer[tipPi + 5] = headZ;
|
|
500
|
-
|
|
501
|
-
this.nextBuffer[tipPi] = tipX;
|
|
502
|
-
this.nextBuffer[tipPi + 1] = tipY;
|
|
503
|
-
this.nextBuffer[tipPi + 2] = tipZ;
|
|
504
|
-
this.nextBuffer[tipPi + 3] = tipX;
|
|
505
|
-
this.nextBuffer[tipPi + 4] = tipY;
|
|
506
|
-
this.nextBuffer[tipPi + 5] = tipZ;
|
|
507
|
-
|
|
508
|
-
// The recorded head's `next` was itself; point it at the tip
|
|
509
|
-
// so the shader builds the live segment from one version of
|
|
510
|
-
// each endpoint.
|
|
511
|
-
const headPi = headIndex * 3;
|
|
512
|
-
this.nextBuffer[headPi] = tipX;
|
|
513
|
-
this.nextBuffer[headPi + 1] = tipY;
|
|
514
|
-
this.nextBuffer[headPi + 2] = tipZ;
|
|
515
|
-
this.nextBuffer[headPi + 3] = tipX;
|
|
516
|
-
this.nextBuffer[headPi + 4] = tipY;
|
|
517
|
-
this.nextBuffer[headPi + 5] = tipZ;
|
|
518
|
-
|
|
519
|
-
const tipSize = particle.size.x;
|
|
520
|
-
if (systemWorldSpace || particle.parentMatrix) {
|
|
521
|
-
this.widthBuffer[index] = tipSize;
|
|
522
|
-
this.widthBuffer[index + 1] = tipSize;
|
|
523
|
-
} else {
|
|
524
|
-
this.widthBuffer[index] = tipSize * objectScale;
|
|
525
|
-
this.widthBuffer[index + 1] = tipSize * objectScale;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const tipUi = index * 2;
|
|
529
|
-
const tipU = ((uvFromHead ? 0 : 1) + col) * tileWidth;
|
|
530
|
-
this.uvBuffer[tipUi] = tipU;
|
|
531
|
-
this.uvBuffer[tipUi + 1] = (vTileCount - row - 1) * tileHeight;
|
|
532
|
-
this.uvBuffer[tipUi + 2] = tipU;
|
|
533
|
-
this.uvBuffer[tipUi + 3] = (vTileCount - row) * tileHeight;
|
|
534
|
-
|
|
535
|
-
const tipColor = particle.color;
|
|
536
|
-
const tipCi = index * 4;
|
|
537
|
-
this.colorBuffer[tipCi] = tipColor.x;
|
|
538
|
-
this.colorBuffer[tipCi + 1] = tipColor.y;
|
|
539
|
-
this.colorBuffer[tipCi + 2] = tipColor.z;
|
|
540
|
-
this.colorBuffer[tipCi + 3] = tipColor.w;
|
|
541
|
-
this.colorBuffer[tipCi + 4] = tipColor.x;
|
|
542
|
-
this.colorBuffer[tipCi + 5] = tipColor.y;
|
|
543
|
-
this.colorBuffer[tipCi + 6] = tipColor.z;
|
|
544
|
-
this.colorBuffer[tipCi + 7] = tipColor.w;
|
|
545
|
-
|
|
546
|
-
this.indexBuffer[triangles * 3] = headIndex;
|
|
547
|
-
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
548
|
-
this.indexBuffer[triangles * 3 + 2] = index;
|
|
549
|
-
triangles++;
|
|
550
|
-
this.indexBuffer[triangles * 3] = index;
|
|
551
|
-
this.indexBuffer[triangles * 3 + 1] = headIndex + 1;
|
|
552
|
-
this.indexBuffer[triangles * 3 + 2] = index + 1;
|
|
553
|
-
triangles++;
|
|
554
|
-
|
|
555
|
-
index += 2;
|
|
842
|
+
}
|
|
843
|
+
const tint = (system as ParticleSystem).tint;
|
|
844
|
+
if (tint !== undefined) {
|
|
845
|
+
// As SpriteBatch does: the system's tint over its ribbons' vertices.
|
|
846
|
+
const colors = this.colorBuffer;
|
|
847
|
+
for (let o = firstVertex * 4, end = index * 4; o < end; o += 4) {
|
|
848
|
+
colors[o] *= tint.x;
|
|
849
|
+
colors[o + 1] *= tint.y;
|
|
850
|
+
colors[o + 2] *= tint.z;
|
|
851
|
+
colors[o + 3] *= tint.w;
|
|
556
852
|
}
|
|
557
853
|
}
|
|
558
854
|
this.measureSystemBounds(system as ParticleSystem, firstVertex, index);
|
|
@@ -578,8 +874,8 @@ export class TrailBatch extends VFXBatch {
|
|
|
578
874
|
let maxX = -Infinity;
|
|
579
875
|
let maxY = -Infinity;
|
|
580
876
|
let maxZ = -Infinity;
|
|
581
|
-
//
|
|
582
|
-
for (let o = fromVertex * 3, end = toVertex * 3; o < end; o +=
|
|
877
|
+
// Both of each pair: a swept ribbon's edges lie apart, a facing one's coincide.
|
|
878
|
+
for (let o = fromVertex * 3, end = toVertex * 3; o < end; o += 3) {
|
|
583
879
|
const x = positions[o];
|
|
584
880
|
const y = positions[o + 1];
|
|
585
881
|
const z = positions[o + 2];
|
|
@@ -614,6 +910,9 @@ export class TrailBatch extends VFXBatch {
|
|
|
614
910
|
// the high-water mark of every system in the batch.
|
|
615
911
|
this.mesh.updateVerticesData(VertexBuffer.PositionKind, this.positionBuffer.subarray(0, index * 3));
|
|
616
912
|
this.mesh.updateVerticesData(VertexBuffer.UVKind, this.uvBuffer.subarray(0, index * 2));
|
|
913
|
+
if (this.uv1Buffer) {
|
|
914
|
+
this.mesh.updateVerticesData(VertexBuffer.UV2Kind, this.uv1Buffer.subarray(0, index * 2));
|
|
915
|
+
}
|
|
617
916
|
// Straight to the GPU buffer. Going through `mesh.updateIndices` with a
|
|
618
917
|
// range whose length changes every frame makes Babylon copy the
|
|
619
918
|
// indices, rebuild the sub mesh and recompute its bounds from the
|
|
@@ -674,6 +973,7 @@ export class TrailBatch extends VFXBatch {
|
|
|
674
973
|
}
|
|
675
974
|
}
|
|
676
975
|
|
|
976
|
+
/** @internal */
|
|
677
977
|
dispose(): void {
|
|
678
978
|
if (this.mesh.material) {
|
|
679
979
|
this.mesh.material.dispose();
|