babylon.quarks 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +22 -0
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.ts +324 -13
- package/package.json +1 -1
- package/src/BatchedRenderer.ts +16 -2
- package/src/MeshSurfaceEmitter.ts +10 -4
- package/src/ParticleSystem.ts +124 -1
- package/src/QuarksLoader.ts +150 -4
- package/src/SpriteBatch.ts +75 -4
- package/src/TrailBatch.ts +24 -13
- package/src/VFXBatch.ts +4 -1
- package/src/shaders/particle_vert.glsl.ts +20 -0
- package/src/shaders/particle_vert.wgsl.ts +21 -1
- package/src/shaders/stretched_bb_particle_vert.glsl.ts +18 -5
- package/src/shaders/stretched_bb_particle_vert.wgsl.ts +19 -6
package/src/BatchedRenderer.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture';
|
|
|
2
2
|
import {Texture} from '@babylonjs/core/Materials/Textures/texture';
|
|
3
3
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
4
4
|
import {Scene} from '@babylonjs/core/scene';
|
|
5
|
-
import {EmitSubParticleSystem, IParticleSystem} from 'quarks.core';
|
|
5
|
+
import {EmitSubParticleSystem, IParticleSystem, Vector3} from 'quarks.core';
|
|
6
6
|
import {ParticleSystem} from './ParticleSystem';
|
|
7
7
|
import {SpriteBatch} from './SpriteBatch';
|
|
8
8
|
import {TrailBatch} from './TrailBatch';
|
|
@@ -47,6 +47,14 @@ export interface VFXBatchSettings {
|
|
|
47
47
|
*/
|
|
48
48
|
reflectionAtlas: BaseTexture | null;
|
|
49
49
|
layerMask: number;
|
|
50
|
+
/**
|
|
51
|
+
* Offset of billboard quads from their particle, in particle-size units
|
|
52
|
+
* (Unity's renderer Pivot). On billboards x/y move the quad in its own plane
|
|
53
|
+
* before it is rotated and z moves it toward the camera (up for horizontal
|
|
54
|
+
* billboards). On stretched billboards x runs from the head to the tail and
|
|
55
|
+
* y across, and z is unused. Absent or zero: no offset. Meshes ignore it.
|
|
56
|
+
*/
|
|
57
|
+
pivot?: Vector3;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
export interface AdaptivePerformanceOptions {
|
|
@@ -57,6 +65,11 @@ export interface AdaptivePerformanceOptions {
|
|
|
57
65
|
increaseStep: number;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
/** True when both pivots put the quad in the same place; a missing pivot is zero. */
|
|
69
|
+
function pivotEqual(a: Vector3 | undefined, b: Vector3 | undefined): boolean {
|
|
70
|
+
return (a?.x ?? 0) === (b?.x ?? 0) && (a?.y ?? 0) === (b?.y ?? 0) && (a?.z ?? 0) === (b?.z ?? 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/** True when both face lists are the same six texture references (or both empty). */
|
|
61
74
|
function reflectionFacesEqual(a: BaseTexture[] | null | undefined, b: BaseTexture[] | null | undefined): boolean {
|
|
62
75
|
if (a === b) {
|
|
@@ -147,7 +160,8 @@ export class BatchedRenderer extends TransformNode {
|
|
|
147
160
|
a.vTileCount === b.vTileCount &&
|
|
148
161
|
a.instancingGeometry === b.instancingGeometry &&
|
|
149
162
|
a.renderOrder === b.renderOrder &&
|
|
150
|
-
a.layerMask === b.layerMask
|
|
163
|
+
a.layerMask === b.layerMask &&
|
|
164
|
+
pivotEqual(a.pivot, b.pivot)
|
|
151
165
|
);
|
|
152
166
|
}
|
|
153
167
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {VertexBuffer} from '@babylonjs/core/Buffers/buffer';
|
|
2
2
|
import {Mesh} from '@babylonjs/core/Meshes/mesh';
|
|
3
|
-
import {EmitterShape, IParticleSystem, Particle, Plugin, ShapeJSON, Vector3} from 'quarks.core';
|
|
3
|
+
import {EmitterShape, IParticleSystem, Particle, Plugin, ShapeJSON, ShapeTransform, Vector3} from 'quarks.core';
|
|
4
4
|
|
|
5
5
|
export class MeshSurfaceEmitter implements EmitterShape {
|
|
6
6
|
type = 'mesh_surface';
|
|
@@ -10,6 +10,8 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
10
10
|
private _positions?: Float32Array;
|
|
11
11
|
private _indices?: Uint32Array | Int32Array;
|
|
12
12
|
private _meshReferenceId?: string;
|
|
13
|
+
/** Offset, rotation and scale of the mesh within the particle system. */
|
|
14
|
+
transform?: ShapeTransform;
|
|
13
15
|
|
|
14
16
|
get mesh() {
|
|
15
17
|
return this._mesh;
|
|
@@ -53,8 +55,9 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
constructor(mesh?: Mesh, meshReferenceId?: string) {
|
|
58
|
+
constructor(mesh?: Mesh, meshReferenceId?: string, transform?: ShapeTransform) {
|
|
57
59
|
this._meshReferenceId = meshReferenceId;
|
|
60
|
+
this.transform = transform;
|
|
58
61
|
if (mesh) this.mesh = mesh;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -62,6 +65,7 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
62
65
|
if (!this._positions || !this._indices) {
|
|
63
66
|
p.position.set(0, 0, 0);
|
|
64
67
|
p.velocity.set(0, 0, 1).multiplyScalar(p.startSpeed);
|
|
68
|
+
this.transform?.apply(p);
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -107,21 +111,23 @@ export class MeshSurfaceEmitter implements EmitterShape {
|
|
|
107
111
|
const nz = bx * cy - by * cx;
|
|
108
112
|
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
109
113
|
p.velocity.set(nx / len, ny / len, nz / len).multiplyScalar(p.startSpeed);
|
|
114
|
+
this.transform?.apply(p);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
toJSON(): ShapeJSON {
|
|
113
118
|
return {
|
|
114
119
|
type: 'mesh_surface',
|
|
115
120
|
mesh: this._mesh ? (this._mesh as any).uniqueId?.toString() : (this._meshReferenceId ?? ''),
|
|
121
|
+
...ShapeTransform.toShapeJSON(this.transform),
|
|
116
122
|
};
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
static fromJSON(json: any): MeshSurfaceEmitter {
|
|
120
|
-
return new MeshSurfaceEmitter(undefined, json?.mesh);
|
|
126
|
+
return new MeshSurfaceEmitter(undefined, json?.mesh, ShapeTransform.fromJSON(json?.transform));
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
clone(): EmitterShape {
|
|
124
|
-
return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId);
|
|
130
|
+
return new MeshSurfaceEmitter(this._mesh, this._meshReferenceId, this.transform?.clone());
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
update(_system: IParticleSystem, _delta: number): void {}
|
package/src/ParticleSystem.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
TrailSettings,
|
|
39
39
|
ValueGenerator,
|
|
40
40
|
ValueGeneratorFromJSON,
|
|
41
|
+
Vector2,
|
|
41
42
|
Vector3,
|
|
42
43
|
Vector3Generator,
|
|
43
44
|
Vector4,
|
|
@@ -83,6 +84,16 @@ function fallbackUVsForPositions(positions: Float32Array): Float32Array {
|
|
|
83
84
|
}
|
|
84
85
|
const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
85
86
|
|
|
87
|
+
/** The optional trail flags that are switched on, so JSON and clones only carry the ones in use. */
|
|
88
|
+
function trailFlags(settings: TrailSettings | undefined): Partial<TrailSettings> {
|
|
89
|
+
const flags: Partial<TrailSettings> = {};
|
|
90
|
+
if (settings?.uvFromHead) flags.uvFromHead = true;
|
|
91
|
+
if (settings?.colorFromParticle) flags.colorFromParticle = true;
|
|
92
|
+
if (settings?.dieWithParticle) flags.dieWithParticle = true;
|
|
93
|
+
if (settings?.recordWorldSpace) flags.recordWorldSpace = true;
|
|
94
|
+
return flags;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
export interface ParticleSystemParameters {
|
|
87
98
|
autoDestroy?: boolean;
|
|
88
99
|
looping?: boolean;
|
|
@@ -99,6 +110,8 @@ export interface ParticleSystemParameters {
|
|
|
99
110
|
emissionOverTime?: ValueGenerator | FunctionValueGenerator;
|
|
100
111
|
emissionOverDistance?: ValueGenerator | FunctionValueGenerator;
|
|
101
112
|
emissionBursts?: Array<BurstParameters>;
|
|
113
|
+
/** Most particles alive at once; emission past it is dropped. Leave unset for no limit. */
|
|
114
|
+
maxParticles?: number;
|
|
102
115
|
onlyUsedByOther?: boolean;
|
|
103
116
|
behaviors?: Array<Behavior>;
|
|
104
117
|
instancingGeometry?: Float32Array;
|
|
@@ -125,6 +138,10 @@ export interface ParticleSystemParameters {
|
|
|
125
138
|
depthWrite?: boolean;
|
|
126
139
|
alphaTest?: number;
|
|
127
140
|
layerMask?: number;
|
|
141
|
+
/** Billboard pivot in particle-size units, see {@link VFXBatchSettings.pivot}. */
|
|
142
|
+
pivot?: Vector3;
|
|
143
|
+
/** Fraction of billboard particles mirrored along U (x) and V (y), see {@link ParticleSystem.flip}. */
|
|
144
|
+
flip?: Vector2;
|
|
128
145
|
scene?: Scene;
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -152,6 +169,7 @@ export interface ParticleSystemJSONParameters {
|
|
|
152
169
|
emissionOverTime: FunctionJSON;
|
|
153
170
|
emissionOverDistance: FunctionJSON;
|
|
154
171
|
emissionBursts?: Array<BurstParametersJSON>;
|
|
172
|
+
maxParticles?: number;
|
|
155
173
|
onlyUsedByOther: boolean;
|
|
156
174
|
rendererEmitterSettings: RendererEmitterSettings;
|
|
157
175
|
instancingGeometry?: any;
|
|
@@ -175,6 +193,10 @@ export interface ParticleSystemJSONParameters {
|
|
|
175
193
|
alphaTest?: number;
|
|
176
194
|
behaviors: Array<any>;
|
|
177
195
|
worldSpace: boolean;
|
|
196
|
+
/** `[x, y, z]`; written only when non-zero. */
|
|
197
|
+
pivot?: number[];
|
|
198
|
+
/** `[u, v]`; written only when non-zero. */
|
|
199
|
+
flip?: number[];
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
export interface BabylonMetaData {
|
|
@@ -203,9 +225,17 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
203
225
|
startColor: ColorGenerator | FunctionColorGenerator;
|
|
204
226
|
startTileIndex: ValueGenerator;
|
|
205
227
|
rendererEmitterSettings: RendererEmitterSettings;
|
|
228
|
+
/**
|
|
229
|
+
* Fraction of billboard particles mirrored along U (x) and V (y), each
|
|
230
|
+
* picked at random when the particle is born — Unity's renderer Flip.
|
|
231
|
+
* Undefined: nothing is mirrored. Meshes ignore it.
|
|
232
|
+
*/
|
|
233
|
+
flip?: Vector2;
|
|
206
234
|
emissionOverTime: ValueGenerator | FunctionValueGenerator;
|
|
207
235
|
emissionOverDistance: ValueGenerator | FunctionValueGenerator;
|
|
208
236
|
emissionBursts: Array<BurstParameters>;
|
|
237
|
+
/** Most particles alive at once (Unity's Max Particles); emission past it is dropped. Undefined means no limit. */
|
|
238
|
+
maxParticles?: number;
|
|
209
239
|
onlyUsedByOther: boolean;
|
|
210
240
|
worldSpace: boolean;
|
|
211
241
|
particleNum: number;
|
|
@@ -356,6 +386,28 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
356
386
|
this.neededToUpdateRender = true;
|
|
357
387
|
}
|
|
358
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Billboard pivot in particle-size units (Unity's renderer Pivot), see
|
|
391
|
+
* {@link VFXBatchSettings.pivot}. Assign a new vector to change it: editing
|
|
392
|
+
* the returned one in place does not reach the renderer.
|
|
393
|
+
*/
|
|
394
|
+
get pivot(): Vector3 | undefined {
|
|
395
|
+
return this.rendererSettings.pivot;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
set pivot(v: Vector3 | undefined) {
|
|
399
|
+
this.rendererSettings.pivot = ParticleSystem.nonZeroPivot(v);
|
|
400
|
+
this.neededToUpdateRender = true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private static nonZeroPivot(v: Vector3 | undefined): Vector3 | undefined {
|
|
404
|
+
return v && (v.x !== 0 || v.y !== 0 || v.z !== 0) ? v.clone() : undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private static nonZeroFlip(v: Vector2 | undefined): Vector2 | undefined {
|
|
408
|
+
return v && (v.x > 0 || v.y > 0) ? v.clone() : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
359
411
|
get softNearFade() {
|
|
360
412
|
return this.rendererSettings.softNearFade;
|
|
361
413
|
}
|
|
@@ -468,6 +520,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
468
520
|
this.emissionOverTime = parameters.emissionOverTime ?? new ConstantValue(10);
|
|
469
521
|
this.emissionOverDistance = parameters.emissionOverDistance ?? new ConstantValue(0);
|
|
470
522
|
this.emissionBursts = parameters.emissionBursts ?? [];
|
|
523
|
+
this.maxParticles = parameters.maxParticles;
|
|
471
524
|
this.onlyUsedByOther = parameters.onlyUsedByOther ?? false;
|
|
472
525
|
this.emitterShape = parameters.shape ?? new SphereEmitter();
|
|
473
526
|
this.behaviors = parameters.behaviors ?? [];
|
|
@@ -509,7 +562,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
509
562
|
reflectionFaces: null,
|
|
510
563
|
reflectionAtlas: null,
|
|
511
564
|
layerMask: parameters.layerMask ?? 0x0fffffff,
|
|
565
|
+
pivot: ParticleSystem.nonZeroPivot(parameters.pivot),
|
|
512
566
|
};
|
|
567
|
+
this.flip = ParticleSystem.nonZeroFlip(parameters.flip);
|
|
513
568
|
if (this.rendererSettings.renderMode === RenderMode.Mesh && !this.rendererSettings.instancingNormals) {
|
|
514
569
|
this.rendererSettings.instancingNormals = ParticleSystem.createFallbackNormals(
|
|
515
570
|
this.rendererSettings.instancingGeometry
|
|
@@ -724,6 +779,10 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
724
779
|
const timeRatio = emissionState.time / this.duration;
|
|
725
780
|
const trailSettings = isTrailMode ? (this.rendererEmitterSettings as TrailSettings) : undefined;
|
|
726
781
|
const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
|
|
782
|
+
if (this.maxParticles !== undefined) {
|
|
783
|
+
// Like Unity, a full system skips the particles it has no room for rather than queueing them.
|
|
784
|
+
count = Math.min(count, Math.max(0, this.maxParticles - this.particleNum));
|
|
785
|
+
}
|
|
727
786
|
|
|
728
787
|
for (let i = 0; i < count; i++) {
|
|
729
788
|
emissionState.burstParticleIndex = i;
|
|
@@ -795,6 +854,9 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
795
854
|
// Set until a turning behavior says otherwise, so the
|
|
796
855
|
// renderer never carries a recycled particle's old spin.
|
|
797
856
|
sprite.angularVelocity = 0;
|
|
857
|
+
const flip = this.flip;
|
|
858
|
+
sprite.flip =
|
|
859
|
+
flip === undefined ? 0 : (Math.random() < flip.x ? 1 : 0) | (Math.random() < flip.y ? 2 : 0);
|
|
798
860
|
}
|
|
799
861
|
} else if (trailSettings) {
|
|
800
862
|
const trail = particle as TrailParticle;
|
|
@@ -1084,6 +1146,33 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1084
1146
|
}
|
|
1085
1147
|
}
|
|
1086
1148
|
|
|
1149
|
+
// Newborns were snapshotted above with spawn size/color (startSize /
|
|
1150
|
+
// startColor) before SizeOverLife / ColorOverLife ran. That false jump
|
|
1151
|
+
// is then extrapolated by the renderer when simulationResidual is
|
|
1152
|
+
// negative (common with rounded fixed-step) and flashes the start
|
|
1153
|
+
// appearance at the emitter. Collapse it so birth matches t=0 on the
|
|
1154
|
+
// curves. Age is still 0 here — integration below ages them.
|
|
1155
|
+
if (particleCount > 0) {
|
|
1156
|
+
const prevSize = columns.previousSize;
|
|
1157
|
+
const curSize = columns.size;
|
|
1158
|
+
const prevColor = columns.previousColor;
|
|
1159
|
+
const curColor = columns.color;
|
|
1160
|
+
const scalars = columns.scalars;
|
|
1161
|
+
const stride = ParticleStore.SCALAR_STRIDE;
|
|
1162
|
+
for (let i = 0, o3 = 0, o4 = 0, s = 0; i < particleCount; i++, o3 += 3, o4 += 4, s += stride) {
|
|
1163
|
+
if (scalars[s + ParticleStore.AGE] !== 0) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
prevSize[o3] = curSize[o3];
|
|
1167
|
+
prevSize[o3 + 1] = curSize[o3 + 1];
|
|
1168
|
+
prevSize[o3 + 2] = curSize[o3 + 2];
|
|
1169
|
+
prevColor[o4] = curColor[o4];
|
|
1170
|
+
prevColor[o4 + 1] = curColor[o4 + 1];
|
|
1171
|
+
prevColor[o4 + 2] = curColor[o4 + 2];
|
|
1172
|
+
prevColor[o4 + 3] = curColor[o4 + 3];
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1087
1176
|
const followLocalOrigin = isTrailMode && (this.rendererEmitterSettings as TrailSettings).followLocalOrigin;
|
|
1088
1177
|
if (followLocalOrigin) {
|
|
1089
1178
|
const emitterMatrix = this.emitter.matrixWorld;
|
|
@@ -1114,8 +1203,30 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1114
1203
|
}
|
|
1115
1204
|
|
|
1116
1205
|
if (isTrailMode) {
|
|
1206
|
+
const trailSettings = this.rendererEmitterSettings as TrailSettings;
|
|
1207
|
+
const dieWithParticle = trailSettings.dieWithParticle;
|
|
1208
|
+
// A world-space system records world positions already.
|
|
1209
|
+
const recordWorldSpace = trailSettings.recordWorldSpace && !this.worldSpace;
|
|
1210
|
+
const emitterMatrix = this.emitter.matrixWorld;
|
|
1117
1211
|
for (let i = 0; i < particleCount; i++) {
|
|
1118
|
-
|
|
1212
|
+
const trail = particles[i] as TrailParticle;
|
|
1213
|
+
// Emptying the history lets the removal pass below take the
|
|
1214
|
+
// particle this same step.
|
|
1215
|
+
if (dieWithParticle && trail.age >= trail.life) {
|
|
1216
|
+
trail.resetHistory();
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
trail.update();
|
|
1220
|
+
// update() wrote a sample only while the particle is alive.
|
|
1221
|
+
if (recordWorldSpace && trail.age <= trail.life) {
|
|
1222
|
+
const offset = ((trail.historyHead + trail.historyCapacity - 1) % trail.historyCapacity) * 3;
|
|
1223
|
+
const positions = trail.historyPositions;
|
|
1224
|
+
tempV.set(positions[offset], positions[offset + 1], positions[offset + 2]);
|
|
1225
|
+
tempV.applyMatrix4(trail.parentMatrix ?? emitterMatrix);
|
|
1226
|
+
positions[offset] = tempV.x;
|
|
1227
|
+
positions[offset + 1] = tempV.y;
|
|
1228
|
+
positions[offset + 2] = tempV.z;
|
|
1229
|
+
}
|
|
1119
1230
|
}
|
|
1120
1231
|
}
|
|
1121
1232
|
|
|
@@ -1281,6 +1392,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1281
1392
|
interval: burst.interval,
|
|
1282
1393
|
cycle: burst.cycle,
|
|
1283
1394
|
})),
|
|
1395
|
+
...(this.maxParticles !== undefined ? {maxParticles: this.maxParticles} : {}),
|
|
1284
1396
|
onlyUsedByOther: this.onlyUsedByOther,
|
|
1285
1397
|
instancingGeometry: geometryUUID,
|
|
1286
1398
|
renderMode: this.renderMode,
|
|
@@ -1302,6 +1414,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1302
1414
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1303
1415
|
behaviors: this.behaviors.map((b) => b.toJSON()),
|
|
1304
1416
|
worldSpace: this.worldSpace,
|
|
1417
|
+
...(this.pivot ? {pivot: [this.pivot.x, this.pivot.y, this.pivot.z]} : {}),
|
|
1418
|
+
...(this.flip ? {flip: [this.flip.x, this.flip.y]} : {}),
|
|
1305
1419
|
};
|
|
1306
1420
|
}
|
|
1307
1421
|
|
|
@@ -1321,6 +1435,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1321
1435
|
? ValueGeneratorFromJSON(trailSettings.startLength)
|
|
1322
1436
|
: new ConstantValue(30),
|
|
1323
1437
|
followLocalOrigin: trailSettings?.followLocalOrigin ?? false,
|
|
1438
|
+
...trailFlags(trailSettings),
|
|
1324
1439
|
};
|
|
1325
1440
|
} else if (json.renderMode === RenderMode.StretchedBillBoard) {
|
|
1326
1441
|
rendererEmitterSettings = {...(json.rendererEmitterSettings ?? {})};
|
|
@@ -1371,6 +1486,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1371
1486
|
interval: burst.interval ?? 0.1,
|
|
1372
1487
|
cycle: burst.cycle ?? burst.cycleCount ?? 1,
|
|
1373
1488
|
})),
|
|
1489
|
+
maxParticles: typeof json.maxParticles === 'number' ? json.maxParticles : undefined,
|
|
1374
1490
|
onlyUsedByOther: json.onlyUsedByOther,
|
|
1375
1491
|
instancingGeometry: resolvedGeometry.positions,
|
|
1376
1492
|
instancingIndices: resolvedGeometry.indices,
|
|
@@ -1399,6 +1515,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1399
1515
|
behaviors: [],
|
|
1400
1516
|
worldSpace: json.worldSpace,
|
|
1401
1517
|
layerMask: json.layers,
|
|
1518
|
+
pivot: json.pivot ? new Vector3(json.pivot[0] ?? 0, json.pivot[1] ?? 0, json.pivot[2] ?? 0) : undefined,
|
|
1519
|
+
flip: json.flip ? new Vector2(json.flip[0] ?? 0, json.flip[1] ?? 0) : undefined,
|
|
1402
1520
|
});
|
|
1403
1521
|
ps.behaviors = (json.behaviors ?? [])
|
|
1404
1522
|
.map((behaviorJson) => {
|
|
@@ -1432,6 +1550,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1432
1550
|
return {
|
|
1433
1551
|
startLength: (this.rendererEmitterSettings as TrailSettings).startLength.toJSON(),
|
|
1434
1552
|
followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
|
|
1553
|
+
...trailFlags(this.rendererEmitterSettings as TrailSettings),
|
|
1435
1554
|
};
|
|
1436
1555
|
}
|
|
1437
1556
|
if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
@@ -1594,6 +1713,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1594
1713
|
rendererEmitterSettings = {
|
|
1595
1714
|
startLength: (this.rendererEmitterSettings as TrailSettings).startLength.clone(),
|
|
1596
1715
|
followLocalOrigin: (this.rendererEmitterSettings as TrailSettings).followLocalOrigin,
|
|
1716
|
+
...trailFlags(this.rendererEmitterSettings as TrailSettings),
|
|
1597
1717
|
};
|
|
1598
1718
|
} else if (this.renderMode === RenderMode.StretchedBillBoard) {
|
|
1599
1719
|
rendererEmitterSettings = {
|
|
@@ -1619,6 +1739,7 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1619
1739
|
emissionOverTime: this.emissionOverTime.clone(),
|
|
1620
1740
|
emissionOverDistance: this.emissionOverDistance.clone(),
|
|
1621
1741
|
emissionBursts: this.emissionBursts.map((b) => ({...b})),
|
|
1742
|
+
maxParticles: this.maxParticles,
|
|
1622
1743
|
onlyUsedByOther: this.onlyUsedByOther,
|
|
1623
1744
|
instancingGeometry: this.rendererSettings.instancingGeometry,
|
|
1624
1745
|
instancingIndices: this.rendererSettings.instancingIndices,
|
|
@@ -1644,6 +1765,8 @@ export class ParticleSystem implements IParticleSystem {
|
|
|
1644
1765
|
depthWrite: this.rendererSettings.materialDepthWrite,
|
|
1645
1766
|
alphaTest: this.rendererSettings.materialAlphaTest,
|
|
1646
1767
|
layerMask: this.rendererSettings.layerMask,
|
|
1768
|
+
pivot: this.pivot,
|
|
1769
|
+
flip: this.flip,
|
|
1647
1770
|
scene: this.emitter.getScene(),
|
|
1648
1771
|
});
|
|
1649
1772
|
}
|
package/src/QuarksLoader.ts
CHANGED
|
@@ -14,6 +14,15 @@ import {ensureTriangleIndices} from './geometryUtil';
|
|
|
14
14
|
|
|
15
15
|
export interface QuarksLoaderOptions {
|
|
16
16
|
baseUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Reuse one Texture per scene for every effect that references the same image with the same
|
|
19
|
+
* sampling, and one copy of the data for identical mesh geometry. The renderer only batches
|
|
20
|
+
* systems that share the texture (and geometry) object, so without this every loaded effect
|
|
21
|
+
* gets batches — draw calls and buffer uploads — of its own even when its images are the same
|
|
22
|
+
* as another effect's. Default true; turn off to give each effect private textures, e.g. to
|
|
23
|
+
* change one effect's texture settings without touching the others.
|
|
24
|
+
*/
|
|
25
|
+
shareResources?: boolean;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
interface ParsedGeometry {
|
|
@@ -23,6 +32,45 @@ interface ParsedGeometry {
|
|
|
23
32
|
normals?: Float32Array;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/** Textures and geometries reused across loads, per scene. See QuarksLoaderOptions.shareResources. */
|
|
36
|
+
const sharedTextures = new WeakMap<Scene, Map<string, Texture>>();
|
|
37
|
+
const sharedGeometries = new WeakMap<Scene, Map<string, ParsedGeometry>>();
|
|
38
|
+
|
|
39
|
+
function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<string, T> {
|
|
40
|
+
let map = maps.get(scene);
|
|
41
|
+
if (map === undefined) {
|
|
42
|
+
map = new Map();
|
|
43
|
+
maps.set(scene, map);
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Content key for a geometry: two independent 32-bit hashes over every array's bytes, plus lengths. */
|
|
49
|
+
function geometryKey(geometry: ParsedGeometry): string {
|
|
50
|
+
let h1 = 0x811c9dc5;
|
|
51
|
+
let h2 = 0x9e3779b9;
|
|
52
|
+
const parts: Array<ArrayBufferView | undefined> = [
|
|
53
|
+
geometry.positions,
|
|
54
|
+
geometry.indices,
|
|
55
|
+
geometry.uvs,
|
|
56
|
+
geometry.normals,
|
|
57
|
+
];
|
|
58
|
+
let lengths = '';
|
|
59
|
+
for (const part of parts) {
|
|
60
|
+
if (part === undefined) {
|
|
61
|
+
lengths += '-,';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const bytes = new Uint8Array(part.buffer, part.byteOffset, part.byteLength);
|
|
65
|
+
lengths += bytes.length + ',';
|
|
66
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
67
|
+
h1 = Math.imul(h1 ^ bytes[i], 0x01000193);
|
|
68
|
+
h2 = Math.imul(h2 ^ bytes[i], 0x5bd1e995) ^ (h2 >>> 15);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return `${lengths}${(h1 >>> 0).toString(16)}${(h2 >>> 0).toString(16)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
interface LoadedMeta {
|
|
27
75
|
textures: {[uuid: string]: Texture | null};
|
|
28
76
|
geometries: {[uuid: string]: ParsedGeometry};
|
|
@@ -42,9 +90,45 @@ export class QuarksLoader {
|
|
|
42
90
|
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
43
91
|
const response = await fetch(url);
|
|
44
92
|
const json = await response.json();
|
|
93
|
+
await QuarksLoader.resolveExternalGeometries(json, async (geometryUrl) => {
|
|
94
|
+
const geometryResponse = await fetch(QuarksLoader.resolveUrl(baseUrl, geometryUrl));
|
|
95
|
+
if (!geometryResponse.ok) {
|
|
96
|
+
throw new Error(`HTTP ${geometryResponse.status}`);
|
|
97
|
+
}
|
|
98
|
+
return geometryResponse.json();
|
|
99
|
+
});
|
|
45
100
|
return this.parse(json, baseUrl);
|
|
46
101
|
}
|
|
47
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Inlines geometries that an export wrote to their own file (`{uuid, type: 'QuarksGeometry',
|
|
105
|
+
* url}` — e.g. the Unity exporter's "geometries as files"): each `url` is passed to
|
|
106
|
+
* `loadGeometry`, and the `positions / indices / uvs / normals` it returns replace the url in
|
|
107
|
+
* place, so `parse()` (which is synchronous) sees an ordinary inline geometry. `load()` does
|
|
108
|
+
* this with `fetch`; call it yourself before `parse()` when the files come from elsewhere.
|
|
109
|
+
* A geometry that fails to load is left as is and warned about.
|
|
110
|
+
*/
|
|
111
|
+
static async resolveExternalGeometries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void> {
|
|
112
|
+
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
113
|
+
await Promise.all(
|
|
114
|
+
geometries
|
|
115
|
+
.filter((geom) => typeof geom?.url === 'string' && !geom.positions)
|
|
116
|
+
.map(async (geom) => {
|
|
117
|
+
try {
|
|
118
|
+
const data = await loadGeometry(geom.url);
|
|
119
|
+
for (const key of ['positions', 'indices', 'uvs', 'normals']) {
|
|
120
|
+
if (data?.[key] !== undefined) {
|
|
121
|
+
geom[key] = data[key];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
delete geom.url;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
48
132
|
parse(json: any, baseUrl: string = ''): TransformNode {
|
|
49
133
|
const meta: LoadedMeta = {
|
|
50
134
|
textures: {},
|
|
@@ -89,16 +173,47 @@ export class QuarksLoader {
|
|
|
89
173
|
const parsed = this.parseBufferGeometry(geom.data);
|
|
90
174
|
meta.geometries[geom.uuid] = parsed;
|
|
91
175
|
} else if (geom.type === 'QuarksGeometry') {
|
|
92
|
-
|
|
176
|
+
if (typeof geom.url === 'string' && !geom.positions) {
|
|
177
|
+
console.warn(
|
|
178
|
+
`[QuarksLoader] Geometry "${geom.url}" is an external file; use load() or ` +
|
|
179
|
+
'QuarksLoader.resolveExternalGeometries() before parse().'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
meta.geometries[geom.uuid] = this.share({
|
|
93
183
|
positions: new Float32Array(geom.positions ?? []),
|
|
94
184
|
indices: geom.indices ? new Uint32Array(geom.indices) : new Uint32Array(0),
|
|
95
185
|
uvs: geom.uvs ? new Float32Array(geom.uvs) : undefined,
|
|
96
186
|
normals: geom.normals ? new Float32Array(geom.normals) : undefined,
|
|
97
|
-
};
|
|
187
|
+
});
|
|
98
188
|
}
|
|
99
189
|
}
|
|
100
190
|
}
|
|
101
191
|
|
|
192
|
+
/** The scene's copy of an identical geometry when resources are shared, else `geometry` itself. */
|
|
193
|
+
private share(geometry: ParsedGeometry): ParsedGeometry {
|
|
194
|
+
if (this.options.shareResources === false) {
|
|
195
|
+
return geometry;
|
|
196
|
+
}
|
|
197
|
+
const geometries = sceneMap(sharedGeometries, this.scene);
|
|
198
|
+
const key = geometryKey(geometry);
|
|
199
|
+
const existing = geometries.get(key);
|
|
200
|
+
if (existing !== undefined) {
|
|
201
|
+
return existing;
|
|
202
|
+
}
|
|
203
|
+
geometries.set(key, geometry);
|
|
204
|
+
return geometry;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Forgets the textures and geometries shared between loads into `scene` (see
|
|
209
|
+
* {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep theirs; the next
|
|
210
|
+
* load creates new ones. Textures are not disposed — they may still be in use.
|
|
211
|
+
*/
|
|
212
|
+
static clearSharedResources(scene: Scene): void {
|
|
213
|
+
sharedTextures.delete(scene);
|
|
214
|
+
sharedGeometries.delete(scene);
|
|
215
|
+
}
|
|
216
|
+
|
|
102
217
|
private parseSphereGeometry(geom: any): ParsedGeometry {
|
|
103
218
|
const radius = geom.radius ?? 0.5;
|
|
104
219
|
const widthSegments = Math.max(3, Math.floor(geom.widthSegments ?? 16));
|
|
@@ -257,7 +372,7 @@ export class QuarksLoader {
|
|
|
257
372
|
if (img.url) {
|
|
258
373
|
QuarksLoader.warnIfNotAnImageUrl(img.url);
|
|
259
374
|
}
|
|
260
|
-
(this as any)._images[img.uuid] = img.url ?
|
|
375
|
+
(this as any)._images[img.uuid] = img.url ? QuarksLoader.resolveUrl(baseUrl, img.url) : null;
|
|
261
376
|
}
|
|
262
377
|
}
|
|
263
378
|
|
|
@@ -286,7 +401,7 @@ export class QuarksLoader {
|
|
|
286
401
|
* babylon.quarks-editor). Prepending baseUrl to those turns e.g.
|
|
287
402
|
* "data:image/png;base64,AAAA" into an invalid path like "/some/dir/data:image/png;base64,AAAA".
|
|
288
403
|
*/
|
|
289
|
-
private
|
|
404
|
+
private static resolveUrl(baseUrl: string, url: string): string {
|
|
290
405
|
if (/^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/')) {
|
|
291
406
|
return url;
|
|
292
407
|
}
|
|
@@ -300,6 +415,28 @@ export class QuarksLoader {
|
|
|
300
415
|
if (imageUrl) {
|
|
301
416
|
const invertY = texDef.invertY !== undefined ? !!texDef.invertY : true;
|
|
302
417
|
const noMipmap = !!texDef.noMipmap;
|
|
418
|
+
// Everything below that ends up on the Texture object is part of the key, so a
|
|
419
|
+
// shared texture always looks the way each of its effects asked for.
|
|
420
|
+
const share = this.options.shareResources !== false;
|
|
421
|
+
const key = share
|
|
422
|
+
? [
|
|
423
|
+
invertY,
|
|
424
|
+
noMipmap,
|
|
425
|
+
texDef.samplingMode,
|
|
426
|
+
texDef.wrap,
|
|
427
|
+
texDef.repeat,
|
|
428
|
+
texDef.offset,
|
|
429
|
+
texDef.rotation,
|
|
430
|
+
].join('|') +
|
|
431
|
+
'|' +
|
|
432
|
+
imageUrl
|
|
433
|
+
: '';
|
|
434
|
+
const cache = share ? sceneMap(sharedTextures, this.scene) : undefined;
|
|
435
|
+
const cached = cache?.get(key);
|
|
436
|
+
if (cached !== undefined) {
|
|
437
|
+
meta.textures[texDef.uuid] = cached;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
303
440
|
const texture = new Texture(imageUrl, this.scene, {
|
|
304
441
|
noMipmap,
|
|
305
442
|
invertY,
|
|
@@ -320,6 +457,15 @@ export class QuarksLoader {
|
|
|
320
457
|
if (typeof texDef.rotation === 'number') {
|
|
321
458
|
texture.wAng = texDef.rotation;
|
|
322
459
|
}
|
|
460
|
+
if (cache !== undefined) {
|
|
461
|
+
cache.set(key, texture);
|
|
462
|
+
// Something outside the library may dispose it; never hand out a dead texture.
|
|
463
|
+
texture.onDisposeObservable.addOnce(() => {
|
|
464
|
+
if (cache.get(key) === texture) {
|
|
465
|
+
cache.delete(key);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
323
469
|
meta.textures[texDef.uuid] = texture;
|
|
324
470
|
} else {
|
|
325
471
|
meta.textures[texDef.uuid] = null;
|