babylon.quarks 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +769 -8
  2. package/README.md +10 -8
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +6683 -0
  10. package/dist/types/index.d.ts +1959 -588
  11. package/package.json +12 -6
  12. package/src/BatchedRenderer.ts +204 -101
  13. package/src/EffectPool.ts +4 -1
  14. package/src/MeshSurfaceEmitter.ts +29 -4
  15. package/src/ParticleEmitter.ts +45 -15
  16. package/src/ParticleSystem.ts +909 -2061
  17. package/src/QuarksLoader.ts +421 -67
  18. package/src/QuarksPrefab.ts +91 -4
  19. package/src/QuarksSerializer.ts +484 -0
  20. package/src/QuarksUtil.ts +15 -0
  21. package/src/SpriteBatch.ts +106 -246
  22. package/src/TrailBatch.ts +533 -342
  23. package/src/VFXBatch.ts +236 -136
  24. package/src/batchKey.ts +237 -0
  25. package/src/effectResources.ts +79 -0
  26. package/src/envAtlas.ts +119 -17
  27. package/src/index.ts +32 -33
  28. package/src/nodeId.ts +13 -0
  29. package/src/shaderProgram.ts +325 -0
  30. package/src/shaderVariants.ts +225 -0
  31. package/src/shaders/chunks/fog.ts +4 -2
  32. package/src/shaders/chunks/sheet.ts +111 -0
  33. package/src/shaders/chunks/soft.ts +84 -0
  34. package/src/shaders/chunks/surface.ts +10 -0
  35. package/src/shaders/local_particle_physics_vert.glsl.ts +14 -28
  36. package/src/shaders/local_particle_physics_vert.wgsl.ts +16 -29
  37. package/src/shaders/particle_frag.glsl.ts +3 -22
  38. package/src/shaders/particle_frag.wgsl.ts +3 -23
  39. package/src/shaders/particle_physics_frag.glsl.ts +12 -25
  40. package/src/shaders/particle_physics_frag.wgsl.ts +12 -26
  41. package/src/shaders/particle_vert.glsl.ts +5 -27
  42. package/src/shaders/particle_vert.wgsl.ts +5 -26
  43. package/src/shaders/stretched_bb_particle_vert.glsl.ts +7 -33
  44. package/src/shaders/stretched_bb_particle_vert.wgsl.ts +7 -33
  45. package/src/shaders/trail_frag.glsl.ts +11 -1
  46. package/src/shaders/trail_frag.wgsl.ts +11 -0
  47. package/src/shaders/trail_vert.glsl.ts +45 -36
  48. package/src/shaders/trail_vert.wgsl.ts +46 -40
  49. package/src/softDepth.ts +136 -0
  50. package/src/system/BehaviorPlan.ts +85 -0
  51. package/src/system/BoundsTracker.ts +181 -0
  52. package/src/system/EmissionController.ts +360 -0
  53. package/src/system/EventDispatcher.ts +44 -0
  54. package/src/system/ParticleSpawner.ts +274 -0
  55. package/src/system/SimulationClock.ts +130 -0
  56. package/src/system/geometry.ts +111 -0
  57. package/src/system/integrate.ts +428 -0
  58. package/src/system/materialResolve.ts +116 -0
  59. package/src/system/normalize.ts +95 -0
  60. package/src/system/serialization.ts +401 -0
  61. package/src/system/settingsSchema.ts +317 -0
  62. package/src/system/types.ts +410 -0
  63. package/src/textureSource.ts +30 -0
  64. package/src/trailSettings.ts +51 -0
  65. package/src/BatchedParticleRenderer.ts +0 -3
  66. package/src/materials/ParticleMaterials.ts +0 -23
  67. package/src/materials/index.ts +0 -1
  68. package/src/shaders/chunks/index.ts +0 -1
  69. package/src/shaders/chunks/register-shader-chunks.ts +0 -32
  70. package/src/shaders/chunks/soft_fragment.glsl.ts +0 -13
  71. package/src/shaders/chunks/soft_pars_fragment.glsl.ts +0 -20
  72. package/src/shaders/chunks/soft_pars_vertex.glsl.ts +0 -6
  73. package/src/shaders/chunks/soft_vertex.glsl.ts +0 -6
  74. package/src/shaders/chunks/tile_fragment.glsl.ts +0 -9
  75. package/src/shaders/chunks/tile_pars_fragment.glsl.ts +0 -9
  76. package/src/shaders/chunks/tile_pars_vertex.glsl.ts +0 -28
  77. package/src/shaders/chunks/tile_vertex.glsl.ts +0 -18
  78. package/src/shaders/index.ts +0 -9
  79. package/src/shaders/local_particle_vert.glsl.ts +0 -50
@@ -0,0 +1,274 @@
1
+ import {
2
+ EmissionState,
3
+ FunctionValueGenerator,
4
+ Matrix3,
5
+ Matrix4,
6
+ Particle,
7
+ Quaternion,
8
+ random,
9
+ RotationGenerator,
10
+ SpriteParticle,
11
+ SubEmitterInheritMask,
12
+ TrailParticle,
13
+ TrailSettings,
14
+ Vector3,
15
+ Vector3Generator,
16
+ } from 'quarks.core';
17
+ import type {ParticleSystem} from '../ParticleSystem';
18
+ import {RenderMode} from '../VFXBatch';
19
+
20
+ /** Particle backed by a `ParticleStore` row, as a system always creates. */
21
+ type StoreBackedParticle = Particle & {storeIndex: number};
22
+
23
+ const UP = new Vector3(0, 0, 1);
24
+ const IDENTITY_QUATERNION = new Quaternion();
25
+ const tempQ = new Quaternion();
26
+ const tempV = new Vector3();
27
+ const tempV2 = new Vector3();
28
+
29
+ /**
30
+ * Gives a system its newborn particles: grows the pool and the store they live in, rolls each
31
+ * particle's start values on the system's generators, places it with the emitter shape and lets
32
+ * the behaviors claim their slots. One per system; `EmissionController` decides how many to
33
+ * spawn and when, this decides what each one starts as.
34
+ */
35
+ export class ParticleSpawner {
36
+ /**
37
+ * The emitter matrix's normal matrix, for turning a world-space newborn's velocity. The
38
+ * emission controller refreshes it before it spawns from a world-space system.
39
+ */
40
+ readonly normalMatrix = new Matrix3();
41
+
42
+ constructor(private readonly system: ParticleSystem) {}
43
+
44
+ /** Points every particle at the store's columns again, after any were replaced. */
45
+ rebind(): void {
46
+ const particles = this.system.particles;
47
+ for (let i = 0; i < particles.length; i++) {
48
+ (particles[i] as SpriteParticle | TrailParticle).rebind();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Appends one particle to the pool, giving it the next row of the store. Growing the store
54
+ * replaces its arrays, so every particle already bound to it is re-pointed at the new ones.
55
+ */
56
+ private grow(isTrailMode: boolean): void {
57
+ const system = this.system;
58
+ const index = system.particles.length;
59
+ if (system.store.ensureCapacity(index + 1)) {
60
+ this.rebind();
61
+ }
62
+ system.particles.push(
63
+ isTrailMode ? new TrailParticle(system.store, index) : new SpriteParticle(system.store, index)
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Spawns `count` particles at `matrix` (the emitter's, or a parent particle's for a sub
69
+ * emission), each rolled on the system's start generators at `emissionState`'s normalised
70
+ * time. A full system (see `maxParticles`) spawns only what it has room for.
71
+ */
72
+ spawn(count: number, emissionState: EmissionState, matrix: Matrix4): void {
73
+ const system = this.system;
74
+ const translation = tempV;
75
+ const scale = tempV2;
76
+ // decompose writes tempQ, so no separate setFromRotationMatrix is needed.
77
+ matrix.decompose(translation, tempQ, scale);
78
+ const behaviors = system.behaviors;
79
+ const behaviorCount = behaviors.length;
80
+ const renderMode = system.rendererSettings.renderMode;
81
+ const isTrailMode = renderMode === RenderMode.Trail;
82
+ const isSpriteMode = !isTrailMode;
83
+ const isMeshMode = renderMode === RenderMode.Mesh;
84
+ const startRotation = system.startRotation;
85
+ const startSize = system.startSize;
86
+ const startRotationIsRotation = startRotation.type === 'rotation';
87
+ const startSizeIsVector3 = startSize.type === 'vec3function';
88
+ const timeRatio = emissionState.time / system.duration;
89
+ const trailSettings = isTrailMode ? (system.rendererEmitterSettings as TrailSettings) : undefined;
90
+ const followLocalOrigin = Boolean(trailSettings?.followLocalOrigin);
91
+ const worldSpace = system.worldSpace;
92
+ const hierarchy = system.scalingMode === 'hierarchy';
93
+ const particles = system.particles;
94
+ if (system.maxParticles !== undefined) {
95
+ // Like Unity, a full system skips the particles it has no room for rather than queueing them.
96
+ count = Math.min(count, Math.max(0, system.maxParticles - system.particleNum));
97
+ }
98
+
99
+ for (let i = 0; i < count; i++) {
100
+ emissionState.burstParticleIndex = i;
101
+ system.particleNum++;
102
+ if (particles.length < system.particleNum) {
103
+ this.grow(isTrailMode);
104
+ }
105
+ const particle = particles[system.particleNum - 1];
106
+ particle.reset();
107
+ particle.serial = (particle.serial ?? 0) + 1;
108
+ particle.speedModifier = 1;
109
+ // Parent EmitSub can refill an onlyUsedByOther system after it had
110
+ // already finished empty; clear the flag so update does not treat it
111
+ // as done while it has live particles again.
112
+ system.finishedEventFired = false;
113
+ system.startColor.startGen(particle.memory);
114
+ system.startColor.genColor(particle.memory, particle.startColor, timeRatio);
115
+ particle.color.copy(particle.startColor);
116
+ system.startSpeed.startGen(particle.memory);
117
+ particle.startSpeed = system.startSpeed.genValue(particle.memory, timeRatio);
118
+ system.startLife.startGen(particle.memory);
119
+ particle.life = system.startLife.genValue(particle.memory, timeRatio);
120
+ particle.age = 0;
121
+ startSize.startGen(particle.memory);
122
+ if (startSizeIsVector3) {
123
+ (startSize as Vector3Generator).genValue(particle.memory, particle.startSize, timeRatio);
124
+ } else {
125
+ const size = (startSize as FunctionValueGenerator).genValue(particle.memory, timeRatio);
126
+ particle.startSize.set(size, size, size);
127
+ }
128
+ system.startTileIndex.startGen(particle.memory);
129
+ particle.uvTile = system.startTileIndex.genValue(particle.memory);
130
+ particle.size.copy(particle.startSize);
131
+
132
+ if (isSpriteMode) {
133
+ const sprite = particle as SpriteParticle;
134
+ startRotation.startGen(particle.memory);
135
+ if (isMeshMode) {
136
+ if (!(sprite.rotation instanceof Quaternion)) {
137
+ // Assigning a quaternion gives the particle a row in the
138
+ // store's quaternion column; the value is overwritten below.
139
+ sprite.rotation = IDENTITY_QUATERNION;
140
+ }
141
+ // A recycled particle can still carry the turn of whatever
142
+ // it was last time; the renderer would draw a fraction of it
143
+ // before any behavior has had a chance to say otherwise.
144
+ if (sprite.angularVelocity instanceof Quaternion) {
145
+ sprite.angularVelocity.set(0, 0, 0, 1);
146
+ }
147
+ if (startRotationIsRotation) {
148
+ (startRotation as RotationGenerator).genValue(
149
+ particle.memory,
150
+ sprite.rotation as Quaternion,
151
+ 1,
152
+ timeRatio
153
+ );
154
+ } else {
155
+ (sprite.rotation as Quaternion).setFromAxisAngle(
156
+ UP,
157
+ (startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio)
158
+ );
159
+ }
160
+ } else {
161
+ if (startRotationIsRotation) {
162
+ sprite.rotation = 0;
163
+ } else {
164
+ sprite.rotation = (startRotation as FunctionValueGenerator).genValue(sprite.memory, timeRatio);
165
+ }
166
+ // Set until a turning behavior says otherwise, so the
167
+ // renderer never carries a recycled particle's old spin.
168
+ sprite.angularVelocity = 0;
169
+ const flip = system.flip;
170
+ sprite.flip = flip === undefined ? 0 : (random() < flip.x ? 1 : 0) | (random() < flip.y ? 2 : 0);
171
+ }
172
+ } else if (trailSettings) {
173
+ const trail = particle as TrailParticle;
174
+ trailSettings.startLength.startGen(trail.memory);
175
+ trail.length = trailSettings.startLength.genValue(trail.memory, timeRatio);
176
+ trail.ensureHistoryCapacity(Math.max(1, Math.ceil(trail.length)));
177
+ trail.resetHistory();
178
+ }
179
+
180
+ system.emitterShape.initialize(particle, emissionState);
181
+
182
+ if (system.alignToDirection && isSpriteMode) {
183
+ alignParticleToDirection(particle as SpriteParticle, isMeshMode);
184
+ }
185
+
186
+ const parent = emissionState.particle;
187
+ const inherit = emissionState.inherit;
188
+ if (parent !== undefined && inherit !== undefined && inherit !== 0) {
189
+ inheritFromParent(particle, parent, inherit, isSpriteMode);
190
+ }
191
+
192
+ if (isTrailMode && followLocalOrigin) {
193
+ const trail = particle as TrailParticle;
194
+ if (!trail.localPosition) {
195
+ trail.localPosition = new Vector3();
196
+ }
197
+ trail.localPosition.copy(trail.position);
198
+ }
199
+
200
+ if (worldSpace) {
201
+ particle.position.applyMatrix4(matrix);
202
+ particle.startSize.multiply(scale).abs();
203
+ particle.size.copy(particle.startSize);
204
+ if (hierarchy) {
205
+ // Scaled per axis by the emitter's world scale, then turned: the speed scales with it.
206
+ particle.velocity.multiply(system.hierarchyScale).applyQuaternion(tempQ);
207
+ } else {
208
+ particle.velocity.multiply(scale).applyMatrix3(this.normalMatrix);
209
+ }
210
+ if (particle.rotation && particle.rotation instanceof Quaternion) {
211
+ particle.rotation.multiplyQuaternions(tempQ, particle.rotation);
212
+ }
213
+ } else {
214
+ if (system.onlyUsedByOther) {
215
+ particle.parentMatrix = matrix;
216
+ }
217
+ }
218
+
219
+ for (let j = 0; j < behaviorCount; j++) {
220
+ behaviors[j].initialize(particle, system);
221
+ }
222
+ // Every generator has claimed its slot by now; give the compiled
223
+ // behavior passes a column copy of the samples.
224
+ system.store.recordMemory((particle as StoreBackedParticle).storeIndex, particle.memory);
225
+ }
226
+ }
227
+ }
228
+
229
+ /** Turns a newborn so that it faces the way its shape sent it; see `ParticleSystem.alignToDirection`. */
230
+ function alignParticleToDirection(sprite: SpriteParticle, isMeshMode: boolean): void {
231
+ const velocity = sprite.velocity;
232
+ const speedSq = velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z;
233
+ if (speedSq === 0) {
234
+ return;
235
+ }
236
+ if (isMeshMode) {
237
+ // +Z along the velocity, with the start rotation applied first.
238
+ const rotation = sprite.rotation as Quaternion;
239
+ tempV.copy(velocity).divideScalar(Math.sqrt(speedSq));
240
+ tempQ.setFromUnitVectors(UP, tempV);
241
+ rotation.multiplyQuaternions(tempQ, rotation);
242
+ } else {
243
+ // The quad's top (its +Y) toward the velocity in the emitter's XY plane.
244
+ if (velocity.x !== 0 || velocity.y !== 0) {
245
+ sprite.rotation = (sprite.rotation as number) + Math.atan2(-velocity.x, velocity.y);
246
+ }
247
+ }
248
+ }
249
+
250
+ /** Applies the parent particle's inherited properties to a sub emitted newborn; see `EmitSubParticleSystem.inherit`. */
251
+ function inheritFromParent(particle: Particle, parent: Particle, inherit: number, isSpriteMode: boolean): void {
252
+ if (inherit & SubEmitterInheritMask.color) {
253
+ particle.startColor.multiply(parent.color);
254
+ particle.color.copy(particle.startColor);
255
+ }
256
+ if (inherit & SubEmitterInheritMask.size) {
257
+ particle.startSize.multiply(parent.size);
258
+ particle.size.copy(particle.startSize);
259
+ }
260
+ if (inherit & SubEmitterInheritMask.rotation && isSpriteMode) {
261
+ const sprite = particle as SpriteParticle;
262
+ const parentRotation = parent.rotation;
263
+ if (typeof sprite.rotation === 'number') {
264
+ if (typeof parentRotation === 'number') {
265
+ sprite.rotation += parentRotation;
266
+ }
267
+ } else if (parentRotation instanceof Quaternion) {
268
+ (sprite.rotation as Quaternion).multiplyQuaternions(parentRotation, sprite.rotation as Quaternion);
269
+ }
270
+ }
271
+ if (inherit & SubEmitterInheritMask.lifetime && parent.life > 0) {
272
+ particle.life *= Math.max(0, 1 - parent.age / parent.life);
273
+ }
274
+ }
@@ -0,0 +1,130 @@
1
+ /** Length of one simulation step, in seconds: the simulation runs at a fixed 60 Hz. */
2
+ export const SIMULATION_STEP = 1 / 60;
3
+
4
+ /** Most steps one host frame runs at playback speed 1; a faster speed raises the cap in proportion. */
5
+ export const MAX_STEPS_PER_FRAME = 8;
6
+
7
+ /** Most host time one frame simulates, in seconds, so a stall or a tab in the background is not replayed. */
8
+ export const MAX_FRAME_SECONDS = 0.1;
9
+
10
+ /** Most off-screen time a `'pauseAndCatchUp'` system replays on coming back into view, in seconds. */
11
+ export const MAX_CATCH_UP_SECONDS = 2;
12
+
13
+ /**
14
+ * Math.round() breaks an exact tie (the accumulator sitting at precisely half a step) by rounding
15
+ * up. At exactly 120 Hz that tie is not a corner case, it is every frame, and rounding it up would
16
+ * run a step on every one of them instead of every other one. Nudging the tie down costs nothing on
17
+ * a real display (real frame times essentially never land on the tie exactly) and keeps the
18
+ * well-tested "half a step is no step yet" behaviour.
19
+ */
20
+ const ROUND_TIE_BIAS = 1e-9;
21
+
22
+ /**
23
+ * The fixed-step clock of a particle system: turns the host's uneven frames into whole 60 Hz
24
+ * steps, and keeps the start delay and the time a culled system still owes. Pure numbers, so the
25
+ * frame-rate rules can be tested without a scene.
26
+ *
27
+ * The steps a frame runs are chosen by *rounding* the accumulated time to the nearest step, not
28
+ * flooring it. Flooring always leaves the accumulator positive, and when the real frame rate sits
29
+ * very close to 60 Hz (as most displays do) that leftover creeps toward a full step for many frames
30
+ * before tipping over into a frame that runs two steps at once and resets it to near zero: a
31
+ * visible lurch, every particle at once, with a perfectly steady frame rate. Rounding lets the
32
+ * leftover go negative instead, which keeps it centred near zero. The renderer closes the gap by
33
+ * drawing each particle advanced along its velocity by {@link residual}.
34
+ */
35
+ export class SimulationClock {
36
+ /** Simulated time not yet covered by a whole step, in seconds; negative when a step ran early. */
37
+ private accumulator = 0;
38
+ /** Off-screen time a `'pauseAndCatchUp'` system has not simulated yet, in seconds. */
39
+ private pendingCatchUp = 0;
40
+ /** Start delay still to wait out, in seconds. */
41
+ private delayLeft = 0;
42
+
43
+ /** Back to the start of a play: nothing accumulated, nothing owed. The start delay is set apart. */
44
+ reset(): void {
45
+ this.accumulator = 0;
46
+ this.pendingCatchUp = 0;
47
+ }
48
+
49
+ /** Sets the start delay to wait out before the first step; a negative or NaN delay is none. */
50
+ setStartDelay(seconds: number): void {
51
+ this.delayLeft = seconds > 0 ? seconds : 0;
52
+ }
53
+
54
+ /** Whether a start delay is still being waited out. */
55
+ get delaying(): boolean {
56
+ return this.delayLeft > 0;
57
+ }
58
+
59
+ /**
60
+ * The simulated seconds a host frame of `hostDelta` seconds stands for at `playbackSpeed`. The
61
+ * host time is clamped to {@link MAX_FRAME_SECONDS} first, so a fast playback speed advances
62
+ * its full share of a normal frame (speed 10 at 60 fps simulates 10 seconds per second) while a
63
+ * stalled frame is still not replayed. A negative or NaN frame is no time at all.
64
+ */
65
+ static frameSeconds(hostDelta: number, playbackSpeed: number): number {
66
+ if (!(hostDelta > 0)) return 0;
67
+ const clamped = hostDelta > MAX_FRAME_SECONDS ? MAX_FRAME_SECONDS : hostDelta;
68
+ return clamped * playbackSpeed;
69
+ }
70
+
71
+ /**
72
+ * Takes `delta` seconds off the start delay and returns what is left over for the simulation:
73
+ * 0 while the delay runs, the remainder of the frame that ends it (at most one frame's worth, so
74
+ * a long frame does not burst the first steps), and `delta` itself once there is no delay.
75
+ */
76
+ consumeStartDelay(delta: number): number {
77
+ if (this.delayLeft <= 0) {
78
+ return delta;
79
+ }
80
+ this.delayLeft -= delta;
81
+ if (this.delayLeft >= 0) {
82
+ return 0;
83
+ }
84
+ const left = Math.min(-this.delayLeft, MAX_FRAME_SECONDS);
85
+ this.delayLeft = 0;
86
+ return left;
87
+ }
88
+
89
+ /**
90
+ * Adds `delta` simulated seconds and returns how many whole steps to run for them, at most
91
+ * {@link MAX_STEPS_PER_FRAME} scaled by `playbackSpeed` above 1. The steps' time is taken off
92
+ * the accumulator whether or not the caller runs them all, so a frame that cannot afford its
93
+ * steps drops their time rather than carrying it into the next frame.
94
+ */
95
+ accrue(delta: number, playbackSpeed = 1): number {
96
+ this.accumulator += delta;
97
+ const cap = playbackSpeed > 1 ? Math.ceil(MAX_STEPS_PER_FRAME * playbackSpeed) : MAX_STEPS_PER_FRAME;
98
+ const steps = Math.min(cap, Math.max(0, Math.round(this.accumulator / SIMULATION_STEP - ROUND_TIE_BIAS)));
99
+ this.accumulator -= steps * SIMULATION_STEP;
100
+ return steps;
101
+ }
102
+
103
+ /**
104
+ * Time the display is ahead of the simulation (positive) or behind it (negative, the simulation
105
+ * ran a hair early), in seconds. Its magnitude never exceeds one step, even when the per-frame
106
+ * step cap left more than that accumulated.
107
+ */
108
+ get residual(): number {
109
+ if (this.accumulator > SIMULATION_STEP) return SIMULATION_STEP;
110
+ if (this.accumulator < -SIMULATION_STEP) return -SIMULATION_STEP;
111
+ return this.accumulator;
112
+ }
113
+
114
+ /** Remembers `delta` simulated seconds a culled system missed, up to {@link MAX_CATCH_UP_SECONDS}. */
115
+ deferCatchUp(delta: number): void {
116
+ this.pendingCatchUp = Math.min(MAX_CATCH_UP_SECONDS, this.pendingCatchUp + delta);
117
+ }
118
+
119
+ /** Whether a culled system still owes steps. */
120
+ get owesCatchUp(): boolean {
121
+ return this.pendingCatchUp > 0;
122
+ }
123
+
124
+ /** The whole steps a system owes from its time off screen, and forgets them. */
125
+ takeCatchUpSteps(): number {
126
+ const steps = Math.floor(this.pendingCatchUp / SIMULATION_STEP);
127
+ this.pendingCatchUp = 0;
128
+ return steps;
129
+ }
130
+ }
@@ -0,0 +1,111 @@
1
+ import {ensureTriangleIndices} from '../geometryUtil';
2
+
3
+ /** Positions of the unit quad billboards are drawn with, and the default mesh. */
4
+ export const DEFAULT_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]);
5
+ /** UVs of the default quad. */
6
+ export const DEFAULT_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
7
+ /** Indices of the default quad's two triangles. */
8
+ export const DEFAULT_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
9
+
10
+ /** Zero UVs sized to a position buffer — missing/mismatched uv attrs → GL_INVALID_OPERATION on iOS. */
11
+ const fallbackUVs = new WeakMap<Float32Array, Float32Array>();
12
+ const fallbackNormals = new WeakMap<Float32Array, Float32Array>();
13
+
14
+ /**
15
+ * Zero UVs for a geometry given without any, one array per geometry: batches
16
+ * are keyed by the UV array too, so systems sharing a geometry share it.
17
+ */
18
+ export function fallbackUVsForPositions(positions: Float32Array): Float32Array {
19
+ let uvs = fallbackUVs.get(positions);
20
+ if (uvs === undefined || uvs.length !== (positions.length / 3) * 2) {
21
+ uvs = new Float32Array((positions.length / 3) * 2);
22
+ fallbackUVs.set(positions, uvs);
23
+ }
24
+ return uvs;
25
+ }
26
+
27
+ /** Whether `uvs` are the ones {@link fallbackUVsForPositions} made for `positions`. */
28
+ export function isFallbackUVs(uvs: Float32Array | undefined, positions: Float32Array): boolean {
29
+ return uvs !== undefined && uvs === fallbackUVs.get(positions);
30
+ }
31
+
32
+ /** Normals facing +Z, one array per geometry so systems sharing it still batch together. */
33
+ export function fallbackNormalsForPositions(positions: Float32Array): Float32Array {
34
+ let normals = fallbackNormals.get(positions);
35
+ if (!normals) {
36
+ normals = new Float32Array(positions.length);
37
+ for (let i = 2; i < normals.length; i += 3) {
38
+ normals[i] = 1;
39
+ }
40
+ fallbackNormals.set(positions, normals);
41
+ }
42
+ return normals;
43
+ }
44
+
45
+ /** Whether `normals` are the ones {@link fallbackNormalsForPositions} made for `positions`. */
46
+ export function isFallbackNormals(normals: Float32Array | undefined, positions: Float32Array): boolean {
47
+ return normals !== undefined && normals === fallbackNormals.get(positions);
48
+ }
49
+
50
+ /**
51
+ * Typed arrays from a geometry entry of an effect document: quarks' own `{positions, indices, uvs,
52
+ * uvs2, normals}`, or a three.js `BufferGeometry` JSON. Anything else gives the default quad.
53
+ */
54
+ export function resolveGeometryData(entry: any): {
55
+ positions: Float32Array;
56
+ indices: Uint32Array | Uint16Array;
57
+ uvs?: Float32Array;
58
+ uvs2?: Float32Array;
59
+ normals?: Float32Array;
60
+ } {
61
+ if (entry?.positions) {
62
+ const positions = entry.positions instanceof Float32Array ? entry.positions : new Float32Array(entry.positions);
63
+ const rawIndices =
64
+ entry.indices instanceof Uint16Array || entry.indices instanceof Uint32Array
65
+ ? entry.indices
66
+ : entry.indices
67
+ ? new Uint32Array(entry.indices)
68
+ : undefined;
69
+ return {
70
+ positions,
71
+ indices: ensureTriangleIndices(positions, rawIndices),
72
+ uvs: entry.uvs ? (entry.uvs instanceof Float32Array ? entry.uvs : new Float32Array(entry.uvs)) : undefined,
73
+ uvs2: entry.uvs2
74
+ ? entry.uvs2 instanceof Float32Array
75
+ ? entry.uvs2
76
+ : new Float32Array(entry.uvs2)
77
+ : undefined,
78
+ normals: entry.normals
79
+ ? entry.normals instanceof Float32Array
80
+ ? entry.normals
81
+ : new Float32Array(entry.normals)
82
+ : undefined,
83
+ };
84
+ }
85
+
86
+ const data = entry?.data ?? entry;
87
+ if (data?.attributes?.position?.array) {
88
+ const positions = new Float32Array(data.attributes.position.array);
89
+ let indices: Uint32Array | Uint16Array | undefined;
90
+ if (data.index?.array) {
91
+ const indexType = data.index.type === 'Uint16Array' ? Uint16Array : Uint32Array;
92
+ indices = new indexType(data.index.array);
93
+ }
94
+ return {
95
+ positions,
96
+ indices: ensureTriangleIndices(positions, indices),
97
+ uvs: data.attributes.uv?.array ? new Float32Array(data.attributes.uv.array) : undefined,
98
+ // three.js names the second set `uv1` (r151 on), `uv2` before.
99
+ uvs2: (data.attributes.uv1 ?? data.attributes.uv2)?.array
100
+ ? new Float32Array((data.attributes.uv1 ?? data.attributes.uv2).array)
101
+ : undefined,
102
+ normals: data.attributes.normal?.array ? new Float32Array(data.attributes.normal.array) : undefined,
103
+ };
104
+ }
105
+
106
+ return {
107
+ positions: DEFAULT_POSITIONS,
108
+ indices: DEFAULT_INDICES,
109
+ uvs: DEFAULT_UVS,
110
+ };
111
+ }