reze-engine 0.15.0 → 0.15.1
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/README.md +3 -3
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/physics/body.d.ts +2 -0
- package/dist/physics/body.d.ts.map +1 -1
- package/dist/physics/body.js +30 -0
- package/dist/physics/constraint.d.ts +13 -0
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +13 -0
- package/dist/physics/contact.d.ts +28 -0
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +27 -32
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +2 -4
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -0
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +436 -293
- package/package.json +1 -1
- package/src/physics/body.ts +29 -0
- package/src/physics/constraint.ts +30 -0
- package/src/physics/contact.ts +43 -30
- package/src/physics/physics.ts +2 -4
- package/src/physics/solver.ts +431 -284
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reze Engine
|
|
2
2
|
|
|
3
|
-
**Zero-runtime-dependency** WebGPU engine for real-time MMD/PMX rendering.
|
|
3
|
+
**Zero-runtime-dependency** WebGPU engine for real-time MMD/PMX rendering. Renderer, animation, IK, and physics all in TypeScript.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -306,7 +306,7 @@ Note the asymmetry: rotation uses `rotateBones({ name, q }, 0)` (the tween-based
|
|
|
306
306
|
|
|
307
307
|
## Physics
|
|
308
308
|
|
|
309
|
-
|
|
309
|
+
In-house sequential-impulse rigid-body solver, no external physics dependency. Targets PMX rigs (sphere / box / capsule colliders, 6DOF spring joints) at quality comparable to Bullet's defaults in ~1.5k lines of TypeScript.
|
|
310
310
|
|
|
311
311
|
### Pipeline
|
|
312
312
|
|
|
@@ -317,7 +317,7 @@ predict velocities → broadphase + narrowphase → solve constraints (10 iter
|
|
|
317
317
|
→ split-impulse position correction → integrate transforms
|
|
318
318
|
```
|
|
319
319
|
|
|
320
|
-
A fixed-timestep accumulator runs the substep at a constant **
|
|
320
|
+
A fixed-timestep accumulator runs the substep at a constant **60 Hz** regardless of render rate, with up to 6 substeps per render frame. Constant `dt` keeps spring impulse, damping, and integration deterministic — without it, coupled cloth chains never reach steady state.
|
|
321
321
|
|
|
322
322
|
### Implementation notes
|
|
323
323
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class GpuTimestamp {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
readonly supported: boolean;
|
|
4
|
+
private device;
|
|
5
|
+
private querySet;
|
|
6
|
+
private resolveBuffer;
|
|
7
|
+
private readbackBuffer;
|
|
8
|
+
private nextSlot;
|
|
9
|
+
private frameLabels;
|
|
10
|
+
private inFlight;
|
|
11
|
+
private pendingLabels;
|
|
12
|
+
constructor(device: GPUDevice);
|
|
13
|
+
private ensureCreated;
|
|
14
|
+
beginFrame(): void;
|
|
15
|
+
wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
|
|
16
|
+
endFrame(encoder: GPUCommandEncoder): void;
|
|
17
|
+
afterSubmit(): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=gpu-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// WebGPU timestamp-query profiling. Records pass durations into the same
|
|
2
|
+
// profiler module as CPU phases (so the snapshot table mixes both with a
|
|
3
|
+
// "gpu." prefix). Read-only — no effect on rendering output.
|
|
4
|
+
//
|
|
5
|
+
// Pattern (per spec):
|
|
6
|
+
// 1. Each render pass declares timestampWrites { begin, end } slot indices.
|
|
7
|
+
// 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
|
|
8
|
+
// into a QUERY_RESOLVE buffer.
|
|
9
|
+
// 3. We copy the resolve buffer into a MAP_READ readback buffer.
|
|
10
|
+
// 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
|
|
11
|
+
// and record them.
|
|
12
|
+
//
|
|
13
|
+
// One readback buffer with an in-flight gate: if mapAsync hasn't resolved
|
|
14
|
+
// from a previous frame, we skip the copy this frame (no contention, just
|
|
15
|
+
// one frame of dropped sampling).
|
|
16
|
+
import { profiler } from "./physics/profile";
|
|
17
|
+
const MAX_PASSES_PER_FRAME = 64;
|
|
18
|
+
export class GpuTimestamp {
|
|
19
|
+
constructor(device) {
|
|
20
|
+
this.enabled = false;
|
|
21
|
+
this.querySet = null;
|
|
22
|
+
this.resolveBuffer = null;
|
|
23
|
+
this.readbackBuffer = null;
|
|
24
|
+
this.nextSlot = 0;
|
|
25
|
+
this.frameLabels = [];
|
|
26
|
+
this.inFlight = false;
|
|
27
|
+
// Set inside endFrame() when a copy was queued this frame; consumed by
|
|
28
|
+
// afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
|
|
29
|
+
// calling it before submit puts the buffer into "mapping pending" state,
|
|
30
|
+
// which makes the copy in the encoder illegal.
|
|
31
|
+
this.pendingLabels = null;
|
|
32
|
+
this.device = device;
|
|
33
|
+
this.supported = device.features.has("timestamp-query");
|
|
34
|
+
}
|
|
35
|
+
ensureCreated() {
|
|
36
|
+
if (this.querySet || !this.supported)
|
|
37
|
+
return;
|
|
38
|
+
const slots = MAX_PASSES_PER_FRAME * 2;
|
|
39
|
+
this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
|
|
40
|
+
this.resolveBuffer = this.device.createBuffer({
|
|
41
|
+
label: "gpu-timestamp-resolve",
|
|
42
|
+
size: slots * 8,
|
|
43
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
44
|
+
});
|
|
45
|
+
this.readbackBuffer = this.device.createBuffer({
|
|
46
|
+
label: "gpu-timestamp-readback",
|
|
47
|
+
size: slots * 8,
|
|
48
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Called once at the top of each render() before any pass wrap.
|
|
52
|
+
beginFrame() {
|
|
53
|
+
if (!this.enabled || !this.supported)
|
|
54
|
+
return;
|
|
55
|
+
this.ensureCreated();
|
|
56
|
+
this.nextSlot = 0;
|
|
57
|
+
this.frameLabels = [];
|
|
58
|
+
}
|
|
59
|
+
// Wrap a render-pass descriptor with timestampWrites for `label`. If
|
|
60
|
+
// profiling is off / unsupported / out of slots, returns desc unchanged.
|
|
61
|
+
// The same label called multiple times (e.g. each bloom mip) is fine —
|
|
62
|
+
// the profiler accumulates by label.
|
|
63
|
+
wrap(desc, label) {
|
|
64
|
+
if (!this.enabled || !this.querySet)
|
|
65
|
+
return desc;
|
|
66
|
+
if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
|
|
67
|
+
return desc;
|
|
68
|
+
const begin = this.nextSlot;
|
|
69
|
+
const end = this.nextSlot + 1;
|
|
70
|
+
this.nextSlot += 2;
|
|
71
|
+
this.frameLabels.push(label);
|
|
72
|
+
return {
|
|
73
|
+
...desc,
|
|
74
|
+
timestampWrites: {
|
|
75
|
+
querySet: this.querySet,
|
|
76
|
+
beginningOfPassWriteIndex: begin,
|
|
77
|
+
endOfPassWriteIndex: end,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Call on the encoder after all passes, BEFORE encoder.finish(). Adds
|
|
82
|
+
// resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
|
|
83
|
+
// afterSubmit() — calling it before submit transitions the readback buffer
|
|
84
|
+
// into "mapping pending" state, which makes the queued copy illegal.
|
|
85
|
+
endFrame(encoder) {
|
|
86
|
+
if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const count = this.frameLabels.length * 2;
|
|
89
|
+
encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
|
|
90
|
+
if (this.inFlight)
|
|
91
|
+
return;
|
|
92
|
+
encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
|
|
93
|
+
this.pendingLabels = this.frameLabels.slice();
|
|
94
|
+
}
|
|
95
|
+
// Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
|
|
96
|
+
// this frame; the .then() reads durations and unmaps.
|
|
97
|
+
afterSubmit() {
|
|
98
|
+
const labels = this.pendingLabels;
|
|
99
|
+
if (!labels)
|
|
100
|
+
return;
|
|
101
|
+
this.pendingLabels = null;
|
|
102
|
+
this.inFlight = true;
|
|
103
|
+
const readback = this.readbackBuffer;
|
|
104
|
+
void readback.mapAsync(GPUMapMode.READ).then(() => {
|
|
105
|
+
const arr = new BigInt64Array(readback.getMappedRange());
|
|
106
|
+
for (let i = 0; i < labels.length; i++) {
|
|
107
|
+
const start = arr[i * 2];
|
|
108
|
+
const end = arr[i * 2 + 1];
|
|
109
|
+
// Per spec, timestamps are nanoseconds.
|
|
110
|
+
const ms = Number(end - start) / 1e6;
|
|
111
|
+
if (ms >= 0 && ms < 1000)
|
|
112
|
+
profiler.record("gpu." + labels[i], ms);
|
|
113
|
+
}
|
|
114
|
+
readback.unmap();
|
|
115
|
+
this.inFlight = false;
|
|
116
|
+
}, () => {
|
|
117
|
+
this.inFlight = false;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/physics/body.d.ts
CHANGED
|
@@ -22,9 +22,11 @@ export declare class RigidBodyStore {
|
|
|
22
22
|
readonly bodyOffsetMatrix: Float32Array;
|
|
23
23
|
readonly bodyOffsetInverse: Float32Array;
|
|
24
24
|
private boneOffsetsReady;
|
|
25
|
+
private collisionPairs;
|
|
25
26
|
constructor(rigidbodies: Rigidbody[]);
|
|
26
27
|
updateAabbs(margin?: number): void;
|
|
27
28
|
computeBoneOffsets(boneInverseBindMatrices: Float32Array): void;
|
|
28
29
|
isBoneOffsetsReady(): boolean;
|
|
30
|
+
getCollisionPairs(): Uint16Array;
|
|
29
31
|
}
|
|
30
32
|
//# sourceMappingURL=body.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/physics/body.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AAIvE,qBAAa,cAAc;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IAEtB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,QAAQ,CAAC,gBAAgB,EAAE,YAAY,CAAA;IACvC,QAAQ,CAAC,iBAAiB,EAAE,YAAY,CAAA;IAExC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAI9B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAA;IACjC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAA;IACpC,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAA;IAIlC,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAA;IACpC,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAA;IAErC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAE3B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAI9B,QAAQ,CAAC,gBAAgB,EAAE,YAAY,CAAA;IACvC,QAAQ,CAAC,iBAAiB,EAAE,YAAY,CAAA;IACxC,OAAO,CAAC,gBAAgB,CAAQ;
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/physics/body.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AAIvE,qBAAa,cAAc;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IAEtB,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,QAAQ,CAAC,gBAAgB,EAAE,YAAY,CAAA;IACvC,QAAQ,CAAC,iBAAiB,EAAE,YAAY,CAAA;IAExC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAI9B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAA;IACjC,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAA;IACpC,QAAQ,CAAC,cAAc,EAAE,YAAY,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAA;IAIlC,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAA;IACpC,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAA;IAErC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAE3B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAI9B,QAAQ,CAAC,gBAAgB,EAAE,YAAY,CAAA;IACvC,QAAQ,CAAC,iBAAiB,EAAE,YAAY,CAAA;IACxC,OAAO,CAAC,gBAAgB,CAAQ;IAMhC,OAAO,CAAC,cAAc,CAA2B;gBAErC,WAAW,EAAE,SAAS,EAAE;IA4DpC,WAAW,CAAC,MAAM,SAAM,GAAG,IAAI;IA2F/B,kBAAkB,CAAC,uBAAuB,EAAE,YAAY,GAAG,IAAI;IAqD/D,kBAAkB,IAAI,OAAO;IAM7B,iBAAiB,IAAI,WAAW;CAoBjC"}
|
package/dist/physics/body.js
CHANGED
|
@@ -5,6 +5,11 @@ import { RigidbodyType, RigidbodyShape } from "./types";
|
|
|
5
5
|
export class RigidBodyStore {
|
|
6
6
|
constructor(rigidbodies) {
|
|
7
7
|
this.boneOffsetsReady = false;
|
|
8
|
+
// Flat list of (i, j) pairs that survive the static-static + group/mask
|
|
9
|
+
// filter. None of those inputs change after construction, so building this
|
|
10
|
+
// once collapses 60k pair tests/step (349 bodies) down to a few thousand.
|
|
11
|
+
// Built lazily on first access.
|
|
12
|
+
this.collisionPairs = null;
|
|
8
13
|
const N = rigidbodies.length;
|
|
9
14
|
this.count = N;
|
|
10
15
|
this.positions = new Float32Array(N * 3);
|
|
@@ -159,6 +164,31 @@ export class RigidBodyStore {
|
|
|
159
164
|
isBoneOffsetsReady() {
|
|
160
165
|
return this.boneOffsetsReady;
|
|
161
166
|
}
|
|
167
|
+
// Pair-filter inputs (invMass, group, mask) are immutable post-construction,
|
|
168
|
+
// so build the candidate-pair list once and reuse every step.
|
|
169
|
+
getCollisionPairs() {
|
|
170
|
+
if (this.collisionPairs !== null)
|
|
171
|
+
return this.collisionPairs;
|
|
172
|
+
const N = this.count;
|
|
173
|
+
const invMass = this.invMass;
|
|
174
|
+
const group = this.collisionGroup;
|
|
175
|
+
const mask = this.willCollideMask;
|
|
176
|
+
const buf = [];
|
|
177
|
+
for (let i = 0; i < N; i++) {
|
|
178
|
+
const gi = group[i];
|
|
179
|
+
const mi = mask[i];
|
|
180
|
+
const dynA = invMass[i] > 0;
|
|
181
|
+
for (let j = i + 1; j < N; j++) {
|
|
182
|
+
if (!dynA && invMass[j] === 0)
|
|
183
|
+
continue;
|
|
184
|
+
if ((mi & group[j]) === 0 || (mask[j] & gi) === 0)
|
|
185
|
+
continue;
|
|
186
|
+
buf.push(i, j);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
this.collisionPairs = new Uint16Array(buf);
|
|
190
|
+
return this.collisionPairs;
|
|
191
|
+
}
|
|
162
192
|
}
|
|
163
193
|
const _scratchA = new Float32Array(16);
|
|
164
194
|
const _scratchB = new Float32Array(16);
|
|
@@ -11,6 +11,19 @@ export interface SixDofSpringConstraint {
|
|
|
11
11
|
springEnabled: Uint8Array;
|
|
12
12
|
springStiffness: Float32Array;
|
|
13
13
|
equilibriumPoint: Float32Array;
|
|
14
|
+
cacheSkip: boolean;
|
|
15
|
+
cacheLeverA: Float32Array;
|
|
16
|
+
cacheLeverB: Float32Array;
|
|
17
|
+
cacheLinAxes: Float32Array;
|
|
18
|
+
cacheLinCrossA: Float32Array;
|
|
19
|
+
cacheLinCrossB: Float32Array;
|
|
20
|
+
cacheLinJacInv: Float32Array;
|
|
21
|
+
cacheLinTargetVel: Float32Array;
|
|
22
|
+
cacheLinActive: Uint8Array;
|
|
23
|
+
cacheAngAxes: Float32Array;
|
|
24
|
+
cacheAngTargetVel: Float32Array;
|
|
25
|
+
cacheAngActive: Uint8Array;
|
|
26
|
+
cacheAngJacInv: number;
|
|
14
27
|
}
|
|
15
28
|
export declare const STOP_ERP = 0.475;
|
|
16
29
|
export declare function buildConstraints(rigidbodies: Rigidbody[], joints: Joint[]): SixDofSpringConstraint[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constraint.d.ts","sourceRoot":"","sources":["../../src/physics/constraint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAW/C,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,YAAY,CAAA;IAGpB,SAAS,EAAE,YAAY,CAAA;IACvB,SAAS,EAAE,YAAY,CAAA;IACvB,UAAU,EAAE,YAAY,CAAA;IACxB,UAAU,EAAE,YAAY,CAAA;IAExB,aAAa,EAAE,UAAU,CAAA;IACzB,eAAe,EAAE,YAAY,CAAA;IAC7B,gBAAgB,EAAE,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"constraint.d.ts","sourceRoot":"","sources":["../../src/physics/constraint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAW/C,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,YAAY,CAAA;IAGpB,SAAS,EAAE,YAAY,CAAA;IACvB,SAAS,EAAE,YAAY,CAAA;IACvB,UAAU,EAAE,YAAY,CAAA;IACxB,UAAU,EAAE,YAAY,CAAA;IAExB,aAAa,EAAE,UAAU,CAAA;IACzB,eAAe,EAAE,YAAY,CAAA;IAC7B,gBAAgB,EAAE,YAAY,CAAA;IAK9B,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,YAAY,CAAA;IACzB,WAAW,EAAE,YAAY,CAAA;IACzB,YAAY,EAAE,YAAY,CAAA;IAC1B,cAAc,EAAE,YAAY,CAAA;IAC5B,cAAc,EAAE,YAAY,CAAA;IAC5B,cAAc,EAAE,YAAY,CAAA;IAC5B,iBAAiB,EAAE,YAAY,CAAA;IAC/B,cAAc,EAAE,UAAU,CAAA;IAC1B,YAAY,EAAE,YAAY,CAAA;IAC1B,iBAAiB,EAAE,YAAY,CAAA;IAC/B,cAAc,EAAE,UAAU,CAAA;IAC1B,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,QAAQ,QAAQ,CAAA;AAM7B,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,SAAS,EAAE,EACxB,MAAM,EAAE,KAAK,EAAE,GACd,sBAAsB,EAAE,CAkF1B"}
|
|
@@ -64,6 +64,19 @@ export function buildConstraints(rigidbodies, joints) {
|
|
|
64
64
|
springEnabled,
|
|
65
65
|
springStiffness,
|
|
66
66
|
equilibriumPoint: new Float32Array(6),
|
|
67
|
+
cacheSkip: false,
|
|
68
|
+
cacheLeverA: new Float32Array(3),
|
|
69
|
+
cacheLeverB: new Float32Array(3),
|
|
70
|
+
cacheLinAxes: new Float32Array(9),
|
|
71
|
+
cacheLinCrossA: new Float32Array(9),
|
|
72
|
+
cacheLinCrossB: new Float32Array(9),
|
|
73
|
+
cacheLinJacInv: new Float32Array(3),
|
|
74
|
+
cacheLinTargetVel: new Float32Array(3),
|
|
75
|
+
cacheLinActive: new Uint8Array(3),
|
|
76
|
+
cacheAngAxes: new Float32Array(9),
|
|
77
|
+
cacheAngTargetVel: new Float32Array(3),
|
|
78
|
+
cacheAngActive: new Uint8Array(3),
|
|
79
|
+
cacheAngJacInv: 0,
|
|
67
80
|
});
|
|
68
81
|
}
|
|
69
82
|
return out;
|
|
@@ -18,6 +18,34 @@ export interface Contact {
|
|
|
18
18
|
appliedNormalImpulse: number;
|
|
19
19
|
appliedFrictionImpulse1: number;
|
|
20
20
|
appliedFrictionImpulse2: number;
|
|
21
|
+
cAxN: number;
|
|
22
|
+
cAyN: number;
|
|
23
|
+
cAzN: number;
|
|
24
|
+
cBxN: number;
|
|
25
|
+
cByN: number;
|
|
26
|
+
cBzN: number;
|
|
27
|
+
jacInvN: number;
|
|
28
|
+
bounceVel: number;
|
|
29
|
+
t1x: number;
|
|
30
|
+
t1y: number;
|
|
31
|
+
t1z: number;
|
|
32
|
+
cAxT1: number;
|
|
33
|
+
cAyT1: number;
|
|
34
|
+
cAzT1: number;
|
|
35
|
+
cBxT1: number;
|
|
36
|
+
cByT1: number;
|
|
37
|
+
cBzT1: number;
|
|
38
|
+
jacInvT1: number;
|
|
39
|
+
t2x: number;
|
|
40
|
+
t2y: number;
|
|
41
|
+
t2z: number;
|
|
42
|
+
cAxT2: number;
|
|
43
|
+
cAyT2: number;
|
|
44
|
+
cAzT2: number;
|
|
45
|
+
cBxT2: number;
|
|
46
|
+
cByT2: number;
|
|
47
|
+
cBzT2: number;
|
|
48
|
+
jacInvT2: number;
|
|
21
49
|
}
|
|
22
50
|
export declare class ContactPool {
|
|
23
51
|
private pool;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAO5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAO5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAI/B,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IAEjB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAEhB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AA8BD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,KAAK,SAAI;IAET,OAAO,IAAI,OAAO;IAelB,KAAK,IAAI,IAAI;IAGb,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;CAGxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAahF;AAkxBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuCrG;AA4BD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAS3E"}
|
package/dist/physics/contact.js
CHANGED
|
@@ -14,23 +14,28 @@ import { RigidbodyShape } from "./types";
|
|
|
14
14
|
export const CONTACT_MARGIN = 0.04;
|
|
15
15
|
function makeContact() {
|
|
16
16
|
return {
|
|
17
|
-
bodyA: 0,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
rAz: 0,
|
|
22
|
-
rBx: 0,
|
|
23
|
-
rBy: 0,
|
|
24
|
-
rBz: 0,
|
|
25
|
-
nx: 0,
|
|
26
|
-
ny: 0,
|
|
27
|
-
nz: 0,
|
|
17
|
+
bodyA: 0, bodyB: 0,
|
|
18
|
+
rAx: 0, rAy: 0, rAz: 0,
|
|
19
|
+
rBx: 0, rBy: 0, rBz: 0,
|
|
20
|
+
nx: 0, ny: 0, nz: 0,
|
|
28
21
|
depth: 0,
|
|
29
22
|
friction: 0,
|
|
30
23
|
restitution: 0,
|
|
31
24
|
appliedNormalImpulse: 0,
|
|
32
25
|
appliedFrictionImpulse1: 0,
|
|
33
26
|
appliedFrictionImpulse2: 0,
|
|
27
|
+
cAxN: 0, cAyN: 0, cAzN: 0,
|
|
28
|
+
cBxN: 0, cByN: 0, cBzN: 0,
|
|
29
|
+
jacInvN: 0,
|
|
30
|
+
bounceVel: 0,
|
|
31
|
+
t1x: 0, t1y: 0, t1z: 0,
|
|
32
|
+
cAxT1: 0, cAyT1: 0, cAzT1: 0,
|
|
33
|
+
cBxT1: 0, cByT1: 0, cBzT1: 0,
|
|
34
|
+
jacInvT1: 0,
|
|
35
|
+
t2x: 0, t2y: 0, t2z: 0,
|
|
36
|
+
cAxT2: 0, cAyT2: 0, cAzT2: 0,
|
|
37
|
+
cBxT2: 0, cByT2: 0, cBzT2: 0,
|
|
38
|
+
jacInvT2: 0,
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
// Pool of reusable Contact objects.
|
|
@@ -701,28 +706,18 @@ function flipLastNormal(pool) {
|
|
|
701
706
|
c.ny = -c.ny;
|
|
702
707
|
c.nz = -c.nz;
|
|
703
708
|
}
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
709
|
+
// Iterate the prebuilt candidate-pair list and AABB-test each pair. The
|
|
710
|
+
// static-static and group/mask filters were applied once at construction —
|
|
711
|
+
// see RigidBodyStore.getCollisionPairs. SAP / dynamic AABB tree pay off
|
|
712
|
+
// above ~500 bodies; below that this flat sweep wins on cache locality.
|
|
708
713
|
export function findContacts(store, pool) {
|
|
709
714
|
store.updateAabbs();
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const dynA = invMass[i] > 0;
|
|
718
|
-
for (let j = i + 1; j < N; j++) {
|
|
719
|
-
if (!dynA && invMass[j] === 0)
|
|
720
|
-
continue; // both static — no resolution
|
|
721
|
-
if ((mi & group[j]) === 0 || (mask[j] & gi) === 0)
|
|
722
|
-
continue;
|
|
723
|
-
if (!aabbOverlap(store, i, j))
|
|
724
|
-
continue;
|
|
725
|
-
generateContacts(store, i, j, pool);
|
|
726
|
-
}
|
|
715
|
+
const pairs = store.getCollisionPairs();
|
|
716
|
+
for (let p = 0; p < pairs.length; p += 2) {
|
|
717
|
+
const i = pairs[p];
|
|
718
|
+
const j = pairs[p + 1];
|
|
719
|
+
if (!aabbOverlap(store, i, j))
|
|
720
|
+
continue;
|
|
721
|
+
generateContacts(store, i, j, pool);
|
|
727
722
|
}
|
|
728
723
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAYvC,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;
|
|
1
|
+
{"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../../src/physics/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAYvC,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;gBAEpB,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;IAS1D,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAG/B,UAAU,IAAI,IAAI;IAGlB,cAAc,IAAI,SAAS,EAAE;IAG7B,SAAS,IAAI,KAAK,EAAE;IAGpB,QAAQ,IAAI,cAAc;IAI1B,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IAiBnE,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAKtC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IA6BxF,OAAO,CAAC,iBAAiB;IAuCzB,OAAO,CAAC,aAAa;IA6ErB,OAAO,CAAC,oBAAoB;CAiC7B"}
|
package/dist/physics/physics.js
CHANGED
|
@@ -13,11 +13,9 @@ const _scratchQuat = new Quat(0, 0, 0, 1);
|
|
|
13
13
|
export class RezePhysics {
|
|
14
14
|
constructor(rigidbodies, joints = []) {
|
|
15
15
|
this.firstFrame = true;
|
|
16
|
-
// Fixed-timestep accumulator: physics runs at 75 Hz regardless of render
|
|
17
|
-
// rate, so spring impulse, damping, and integration are deterministic.
|
|
18
16
|
this.timeAccum = 0;
|
|
19
|
-
this.fixedTimeStep = 1 /
|
|
20
|
-
this.maxSubSteps =
|
|
17
|
+
this.fixedTimeStep = 1 / 60;
|
|
18
|
+
this.maxSubSteps = 6;
|
|
21
19
|
this.rigidbodies = rigidbodies;
|
|
22
20
|
this.joints = joints;
|
|
23
21
|
this.store = new RigidBodyStore(rigidbodies);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare class PhysicsProfiler {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
private sums;
|
|
4
|
+
private counts;
|
|
5
|
+
begin(): number;
|
|
6
|
+
end(label: string, t0: number): void;
|
|
7
|
+
record(label: string, ms: number): void;
|
|
8
|
+
snapshot(): Record<string, {
|
|
9
|
+
avgMs: number;
|
|
10
|
+
calls: number;
|
|
11
|
+
totalMs: number;
|
|
12
|
+
}>;
|
|
13
|
+
reset(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const profiler: PhysicsProfiler;
|
|
16
|
+
export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
|
|
2
|
+
// begin()/end() short-circuit so production has zero overhead. Toggle via
|
|
3
|
+
// `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
|
|
4
|
+
class PhysicsProfiler {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.enabled = false;
|
|
7
|
+
this.sums = Object.create(null);
|
|
8
|
+
this.counts = Object.create(null);
|
|
9
|
+
}
|
|
10
|
+
begin() {
|
|
11
|
+
return this.enabled ? performance.now() : 0;
|
|
12
|
+
}
|
|
13
|
+
end(label, t0) {
|
|
14
|
+
if (!this.enabled)
|
|
15
|
+
return;
|
|
16
|
+
const dt = performance.now() - t0;
|
|
17
|
+
this.sums[label] = (this.sums[label] ?? 0) + dt;
|
|
18
|
+
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
19
|
+
}
|
|
20
|
+
// Direct accumulator for durations not measured via begin()/end() — used by
|
|
21
|
+
// GPU timestamp readback, which arrives async after the frame has ended.
|
|
22
|
+
record(label, ms) {
|
|
23
|
+
if (!this.enabled)
|
|
24
|
+
return;
|
|
25
|
+
this.sums[label] = (this.sums[label] ?? 0) + ms;
|
|
26
|
+
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
// Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
|
|
29
|
+
// window cost — divide by elapsed wall time to get a "% of frame budget".
|
|
30
|
+
snapshot() {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const k in this.sums) {
|
|
33
|
+
const total = this.sums[k];
|
|
34
|
+
const calls = this.counts[k];
|
|
35
|
+
out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
reset() {
|
|
40
|
+
this.sums = Object.create(null);
|
|
41
|
+
this.counts = Object.create(null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const profiler = new PhysicsProfiler();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAWrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CAyBN"}
|