reze-engine 0.32.2 → 0.33.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 +1 -3
- package/dist/engine.d.ts +1 -11
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +2 -31
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +13 -3
- package/dist/physics/physics.d.ts +20 -0
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +44 -10
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +9 -2
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +29 -11
- package/package.json +1 -1
- package/src/engine.ts +3 -33
- package/src/physics/contact.ts +10 -3
- package/src/physics/physics.ts +35 -6
- package/src/physics/world.ts +9 -2
- package/src/pmx-loader.ts +28 -12
- package/dist/gpu-profile.d.ts +0 -19
- package/dist/gpu-profile.d.ts.map +0 -1
- package/dist/gpu-profile.js +0 -120
- package/dist/physics/physics.worker.d.ts +0 -2
- package/dist/physics/physics.worker.d.ts.map +0 -1
- package/dist/physics/physics.worker.js +0 -52
- package/dist/physics/profile.d.ts +0 -18
- package/dist/physics/profile.d.ts.map +0 -1
- package/dist/physics/profile.js +0 -44
- package/dist/physics/worker-physics.d.ts +0 -28
- package/dist/physics/worker-physics.d.ts.map +0 -1
- package/dist/physics/worker-physics.js +0 -91
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/src/physics/physics.worker.ts +0 -66
- package/src/physics/worker-physics.ts +0 -126
package/src/engine.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { VMDLoader } from "./vmd-loader"
|
|
|
7
7
|
import { CameraAnimation } from "./camera-animation"
|
|
8
8
|
import { PmxLoader } from "./pmx-loader"
|
|
9
9
|
import { RezePhysics } from "./physics"
|
|
10
|
-
import { WorkerPhysics } from "./physics/worker-physics"
|
|
11
10
|
import {
|
|
12
11
|
createFetchAssetReader,
|
|
13
12
|
createFileMapAssetReader,
|
|
@@ -347,11 +346,6 @@ export interface GizmoDragEvent {
|
|
|
347
346
|
export type GizmoDragCallback = (event: GizmoDragEvent) => void
|
|
348
347
|
|
|
349
348
|
export type EngineOptions = {
|
|
350
|
-
/** Run each model's physics on a dedicated Web Worker (default true where
|
|
351
|
-
* Workers exist). Pipelined one frame deep: main-thread physics cost drops
|
|
352
|
-
* to two small copies and multi-model scenes pay for the SLOWEST model
|
|
353
|
-
* instead of the sum. false = classic main-thread stepping. */
|
|
354
|
-
physicsWorkers?: boolean
|
|
355
349
|
world?: WorldOptions
|
|
356
350
|
sun?: SunOptions
|
|
357
351
|
camera?: CameraOptions
|
|
@@ -382,7 +376,6 @@ export interface EngineStats {
|
|
|
382
376
|
jitter: number // ms — stddev of frame intervals (pacing evenness; high = janky at any mean fps)
|
|
383
377
|
cpuAnimMs: number // ms/frame (EMA) — model updates: blending, IK, world matrices
|
|
384
378
|
cpuPhysicsMs: number // ms/frame (EMA) — physics stepping across all instances
|
|
385
|
-
cpuPhysicsWorkerMs: number // ms/frame (EMA) — the SLOWEST worker's step cost. Background-thread time (often an efficiency core), parallel to the frame: it only matters if it exceeds the frame budget.
|
|
386
379
|
cpuRenderMs: number // ms/frame (EMA) — the rest of the render thread: uniforms, encoding, submit
|
|
387
380
|
}
|
|
388
381
|
|
|
@@ -436,7 +429,7 @@ interface ModelInstance {
|
|
|
436
429
|
pickPerInstanceBindGroup: GPUBindGroup
|
|
437
430
|
pickDrawCalls: PickDrawCall[]
|
|
438
431
|
hiddenMaterials: Set<string>
|
|
439
|
-
physics: RezePhysics |
|
|
432
|
+
physics: RezePhysics | null
|
|
440
433
|
vertexBufferNeedsUpdate: boolean
|
|
441
434
|
gpuMorph: GpuMorph | null
|
|
442
435
|
// Style groups applied to this model: group id → compiled install.
|
|
@@ -837,7 +830,6 @@ export class Engine {
|
|
|
837
830
|
fps1PercentLow: 0,
|
|
838
831
|
cpuAnimMs: 0,
|
|
839
832
|
cpuPhysicsMs: 0,
|
|
840
|
-
cpuPhysicsWorkerMs: 0,
|
|
841
833
|
cpuRenderMs: 0,
|
|
842
834
|
jitter: 0,
|
|
843
835
|
}
|
|
@@ -847,7 +839,6 @@ export class Engine {
|
|
|
847
839
|
private viewTransform!: ViewTransformOptions
|
|
848
840
|
|
|
849
841
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
850
|
-
this.physicsWorkersEnabled = options?.physicsWorkers ?? true
|
|
851
842
|
this.canvas = canvas
|
|
852
843
|
const d = DEFAULT_ENGINE_OPTIONS
|
|
853
844
|
this.world = {
|
|
@@ -3040,10 +3031,7 @@ export class Engine {
|
|
|
3040
3031
|
|
|
3041
3032
|
dispose() {
|
|
3042
3033
|
this.stopRenderLoop()
|
|
3043
|
-
this.forEachInstance((inst) =>
|
|
3044
|
-
inst.model.stopAnimation()
|
|
3045
|
-
if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
|
|
3046
|
-
})
|
|
3034
|
+
this.forEachInstance((inst) => inst.model.stopAnimation())
|
|
3047
3035
|
if (Engine.instance === this) Engine.instance = null
|
|
3048
3036
|
if (this.camera) this.camera.detachControl()
|
|
3049
3037
|
|
|
@@ -3120,7 +3108,6 @@ export class Engine {
|
|
|
3120
3108
|
const inst = this.modelInstances.get(name)
|
|
3121
3109
|
if (!inst) return
|
|
3122
3110
|
inst.model.stopAnimation()
|
|
3123
|
-
if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
|
|
3124
3111
|
for (const path of inst.textureCacheKeys) {
|
|
3125
3112
|
const tex = this.textureCache.get(path)
|
|
3126
3113
|
if (!tex) continue
|
|
@@ -3294,9 +3281,6 @@ export class Engine {
|
|
|
3294
3281
|
// else on the render thread. The first question of any perf report.
|
|
3295
3282
|
private cpuAnimMs = 0
|
|
3296
3283
|
private cpuPhysicsMs = 0
|
|
3297
|
-
/** EMA of worker-side step cost (all instances) — the off-thread physics bill. */
|
|
3298
|
-
private cpuPhysicsWorkerMs = 0
|
|
3299
|
-
private physicsWorkersEnabled = true
|
|
3300
3284
|
private cpuRenderMs = 0
|
|
3301
3285
|
private frameAnimMsRaw = 0
|
|
3302
3286
|
private framePhysicsMsRaw = 0
|
|
@@ -3304,7 +3288,6 @@ export class Engine {
|
|
|
3304
3288
|
private updateInstances(deltaTime: number): void {
|
|
3305
3289
|
let animMs = 0
|
|
3306
3290
|
let physicsMs = 0
|
|
3307
|
-
let workerMs = 0
|
|
3308
3291
|
this.forEachInstance((inst) => {
|
|
3309
3292
|
const tAnim = performance.now()
|
|
3310
3293
|
const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
|
|
@@ -3330,7 +3313,6 @@ export class Engine {
|
|
|
3330
3313
|
const tPhys = performance.now()
|
|
3331
3314
|
inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
|
|
3332
3315
|
physicsMs += performance.now() - tPhys
|
|
3333
|
-
if (inst.physics instanceof WorkerPhysics) workerMs = Math.max(workerMs, inst.physics.stepMs)
|
|
3334
3316
|
}
|
|
3335
3317
|
if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
|
|
3336
3318
|
})
|
|
@@ -3339,7 +3321,6 @@ export class Engine {
|
|
|
3339
3321
|
const EMA = 0.1
|
|
3340
3322
|
this.cpuAnimMs += (animMs - this.cpuAnimMs) * EMA
|
|
3341
3323
|
this.cpuPhysicsMs += (physicsMs - this.cpuPhysicsMs) * EMA
|
|
3342
|
-
this.cpuPhysicsWorkerMs += (workerMs - this.cpuPhysicsWorkerMs) * EMA
|
|
3343
3324
|
}
|
|
3344
3325
|
|
|
3345
3326
|
private updateVertexBuffer(inst: ModelInstance): void {
|
|
@@ -3451,17 +3432,7 @@ export class Engine {
|
|
|
3451
3432
|
this.device.queue.writeBuffer(indexBuffer, 0, indices)
|
|
3452
3433
|
|
|
3453
3434
|
const rbs = model.getRigidbodies()
|
|
3454
|
-
|
|
3455
|
-
if (rbs.length > 0) {
|
|
3456
|
-
if (this.physicsWorkersEnabled && WorkerPhysics.supported()) {
|
|
3457
|
-
try {
|
|
3458
|
-
physics = await WorkerPhysics.create(rbs, model.getJoints(), model.getBoneInverseBindMatrices())
|
|
3459
|
-
} catch (e) {
|
|
3460
|
-
console.warn("[reze] physics worker unavailable — falling back to main-thread physics:", e)
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
3463
|
-
if (!physics) physics = new RezePhysics(rbs, model.getJoints())
|
|
3464
|
-
}
|
|
3435
|
+
const physics = rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null
|
|
3465
3436
|
|
|
3466
3437
|
const shadowBindGroup = this.device.createBindGroup({
|
|
3467
3438
|
label: `${name}: shadow bind`,
|
|
@@ -5515,7 +5486,6 @@ export class Engine {
|
|
|
5515
5486
|
this.stats.jitter = Math.round(stddev * 100) / 100
|
|
5516
5487
|
this.stats.cpuAnimMs = Math.round(this.cpuAnimMs * 100) / 100
|
|
5517
5488
|
this.stats.cpuPhysicsMs = Math.round(this.cpuPhysicsMs * 100) / 100
|
|
5518
|
-
this.stats.cpuPhysicsWorkerMs = Math.round(this.cpuPhysicsWorkerMs * 100) / 100
|
|
5519
5489
|
this.stats.cpuRenderMs = Math.round(this.cpuRenderMs * 100) / 100
|
|
5520
5490
|
}
|
|
5521
5491
|
}
|
package/src/physics/contact.ts
CHANGED
|
@@ -929,8 +929,13 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
929
929
|
return
|
|
930
930
|
}
|
|
931
931
|
if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Sphere) {
|
|
932
|
+
// Only flip a contact this call actually produced. The detector returns
|
|
933
|
+
// without emitting whenever the shapes are out of range, and flipping then
|
|
934
|
+
// reverses the normal of whatever unrelated contact happens to sit at the
|
|
935
|
+
// end of the pool — pushing those two bodies together instead of apart.
|
|
936
|
+
const before = pool.count
|
|
932
937
|
detectSphereCapsule(store, b, a, pool)
|
|
933
|
-
flipLastNormal(pool)
|
|
938
|
+
if (pool.count > before) flipLastNormal(pool)
|
|
934
939
|
return
|
|
935
940
|
}
|
|
936
941
|
if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Capsule) {
|
|
@@ -942,8 +947,9 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
942
947
|
return
|
|
943
948
|
}
|
|
944
949
|
if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Sphere) {
|
|
950
|
+
const before = pool.count
|
|
945
951
|
detectSphereBox(store, b, a, pool)
|
|
946
|
-
flipLastNormal(pool)
|
|
952
|
+
if (pool.count > before) flipLastNormal(pool)
|
|
947
953
|
return
|
|
948
954
|
}
|
|
949
955
|
if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Box) {
|
|
@@ -951,8 +957,9 @@ export function generateContacts(store: RigidBodyStore, a: number, b: number, po
|
|
|
951
957
|
return
|
|
952
958
|
}
|
|
953
959
|
if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Capsule) {
|
|
960
|
+
const before = pool.count
|
|
954
961
|
detectCapsuleBox(store, b, a, pool)
|
|
955
|
-
flipLastNormal(pool)
|
|
962
|
+
if (pool.count > before) flipLastNormal(pool)
|
|
956
963
|
return
|
|
957
964
|
}
|
|
958
965
|
// Box-box left unimplemented.
|
package/src/physics/physics.ts
CHANGED
|
@@ -266,10 +266,12 @@ export class RezePhysics {
|
|
|
266
266
|
}
|
|
267
267
|
if (this.timeAccum >= this.fixedTimeStep) {
|
|
268
268
|
// Substep budget exhausted mid-catchup: drop the remaining time and
|
|
269
|
-
// snap kinematic bodies the rest of the way
|
|
270
|
-
//
|
|
269
|
+
// snap kinematic bodies the rest of the way so they don't start next
|
|
270
|
+
// frame lagging behind their bones. They keep the velocity their
|
|
271
|
+
// trajectory implies — the character did move, and cloth in contact
|
|
272
|
+
// needs to know that or it stops being dragged and starts juddering.
|
|
271
273
|
this.timeAccum = 0
|
|
272
|
-
this.snapKinematicToTargets(false)
|
|
274
|
+
this.snapKinematicToTargets(false, true)
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
// MMD mode-2 bone alignment for pinned (depth-1) bodies: position
|
|
@@ -470,7 +472,27 @@ export class RezePhysics {
|
|
|
470
472
|
// Snap kinematic bodies straight to the frame target with zero velocity.
|
|
471
473
|
// Used on teleports (onlyFlagged) and when the substep budget runs out
|
|
472
474
|
// mid-catchup (all).
|
|
473
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Put kinematic bodies exactly on their targets.
|
|
477
|
+
*
|
|
478
|
+
* `keepTrajectoryVelocity` decides what the cloth is then told about how
|
|
479
|
+
* those bodies got there, and the two callers want opposite answers.
|
|
480
|
+
*
|
|
481
|
+
* After a teleport the honest answer is "nothing" — a scrub is not motion
|
|
482
|
+
* and handing the solver the implied velocity is what used to fling cloth
|
|
483
|
+
* across the discontinuity.
|
|
484
|
+
*
|
|
485
|
+
* After the substep budget is exhausted it is the opposite. The body really
|
|
486
|
+
* did travel along its animated trajectory this frame; the simulation just
|
|
487
|
+
* could not afford to walk it there in steps. Zeroing there tells every
|
|
488
|
+
* dynamic body in contact that the character stopped dead, so cloth loses
|
|
489
|
+
* its drag and is teleported by the body instead of dragged by it — it stops
|
|
490
|
+
* flowing and starts juddering. That branch only fires on a device already
|
|
491
|
+
* running under its refresh rate, which is precisely where the simulation
|
|
492
|
+
* can least afford to also look broken. The trajectory velocity is already
|
|
493
|
+
* computed every frame in kinTargetVel; it just needs to survive.
|
|
494
|
+
*/
|
|
495
|
+
private snapKinematicToTargets(onlyFlagged: boolean, keepTrajectoryVelocity = false): void {
|
|
474
496
|
const N = this.store.count
|
|
475
497
|
const positions = this.store.positions
|
|
476
498
|
const orientations = this.store.orientations
|
|
@@ -480,6 +502,8 @@ export class RezePhysics {
|
|
|
480
502
|
const boneIdx = this.store.boneIndex
|
|
481
503
|
const tp = this.kinTargetPos
|
|
482
504
|
const to = this.kinTargetOri
|
|
505
|
+
const tv = this.kinTargetVel
|
|
506
|
+
const tav = this.kinTargetAngVel
|
|
483
507
|
const flags = this.teleportFlags
|
|
484
508
|
|
|
485
509
|
for (let i = 0; i < N; i++) {
|
|
@@ -496,8 +520,13 @@ export class RezePhysics {
|
|
|
496
520
|
orientations[i4 + 1] = to[i4 + 1]
|
|
497
521
|
orientations[i4 + 2] = to[i4 + 2]
|
|
498
522
|
orientations[i4 + 3] = to[i4 + 3]
|
|
499
|
-
|
|
500
|
-
|
|
523
|
+
if (keepTrajectoryVelocity) {
|
|
524
|
+
lv[i3 + 0] = tv[i3 + 0]; lv[i3 + 1] = tv[i3 + 1]; lv[i3 + 2] = tv[i3 + 2]
|
|
525
|
+
av[i3 + 0] = tav[i3 + 0]; av[i3 + 1] = tav[i3 + 1]; av[i3 + 2] = tav[i3 + 2]
|
|
526
|
+
} else {
|
|
527
|
+
lv[i3 + 0] = 0; lv[i3 + 1] = 0; lv[i3 + 2] = 0
|
|
528
|
+
av[i3 + 0] = 0; av[i3 + 1] = 0; av[i3 + 2] = 0
|
|
529
|
+
}
|
|
501
530
|
}
|
|
502
531
|
}
|
|
503
532
|
|
package/src/physics/world.ts
CHANGED
|
@@ -93,8 +93,15 @@ export class World {
|
|
|
93
93
|
// it because it doesn't go through the velocity channel. Inverse-mass
|
|
94
94
|
// weighted so a kinematic body stays put and only the dynamic one
|
|
95
95
|
// translates.
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Softer and later than a rigid-body engine would use, deliberately. MMD
|
|
97
|
+
// cloth is a dense chain of small bodies resting on the character, and
|
|
98
|
+
// aggressive penetration recovery feeds those contacts energy that the
|
|
99
|
+
// joint springs hand straight back — the tips never settle. Measured on a
|
|
100
|
+
// 688-body model: resting peak velocity 1.79 → 1.10, and 12.9 → 6.6 under
|
|
101
|
+
// an idle animation, for a slop of two hundredths of a unit (under 2 mm at
|
|
102
|
+
// MMD scale) that no eye finds.
|
|
103
|
+
const POS_CORRECTION_FACTOR = 0.15
|
|
104
|
+
const POS_SLOP = 0.02
|
|
98
105
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
99
106
|
const c = contacts.get(ci)
|
|
100
107
|
if (c.depth <= POS_SLOP) continue
|
package/src/pmx-loader.ts
CHANGED
|
@@ -84,10 +84,19 @@ export class PmxLoader {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
private parseHeader() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
87
|
+
// The signature is four bytes. PMX 2.x writes "PMX ", but exporters in the
|
|
88
|
+
// wild also write "Pmx " — and a strict uppercase compare rejected those
|
|
89
|
+
// models outright. Compare case-insensitively and name what was actually
|
|
90
|
+
// found when it is something else, so a PMD (the older format, signature
|
|
91
|
+
// "Pmd") says so instead of "not a PMX file".
|
|
92
|
+
const signature = this.getString(4)
|
|
93
|
+
if (signature.slice(0, 3).toUpperCase() !== "PMX") {
|
|
94
|
+
const head = signature.slice(0, 3).toUpperCase()
|
|
95
|
+
if (head === "PMD") {
|
|
96
|
+
throw new Error("This is a PMD file (MMD's older format). Convert it to PMX in PMXEditor first.")
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Not a PMX file (signature "${signature.replace(/[^\x20-\x7e]/g, "?")}")`)
|
|
99
|
+
}
|
|
91
100
|
|
|
92
101
|
// PMX: version (float32)
|
|
93
102
|
const version = this.getFloat32()
|
|
@@ -1128,7 +1137,9 @@ export class PmxLoader {
|
|
|
1128
1137
|
}
|
|
1129
1138
|
|
|
1130
1139
|
private getString(len: number) {
|
|
1131
|
-
|
|
1140
|
+
// byteOffset matters: a DataView built over a slice of a larger buffer has
|
|
1141
|
+
// its own origin, and reading the raw buffer without it lands elsewhere.
|
|
1142
|
+
const bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
|
|
1132
1143
|
this.offset += len
|
|
1133
1144
|
return String.fromCharCode(...bytes)
|
|
1134
1145
|
}
|
|
@@ -1137,18 +1148,23 @@ export class PmxLoader {
|
|
|
1137
1148
|
const len = this.getInt32()
|
|
1138
1149
|
if (len <= 0) return ""
|
|
1139
1150
|
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1151
|
+
// No arbitrary length ceiling. A model's comment field carries credits and
|
|
1152
|
+
// terms of use and routinely runs past a thousand bytes — roughly five
|
|
1153
|
+
// hundred characters in UTF-16 — so a "this looks too long" heuristic
|
|
1154
|
+
// rejected perfectly ordinary models from several distributors. The bounds
|
|
1155
|
+
// check below is the real guard: a length that would read past the end of
|
|
1156
|
+
// the file is corrupt, and any length that fits is the author's business.
|
|
1145
1157
|
// Ensure we don't read beyond buffer bounds
|
|
1146
|
-
if (this.offset + len > this.view.
|
|
1158
|
+
if (this.offset + len > this.view.byteLength) {
|
|
1147
1159
|
throw new RangeError(`String length ${len} exceeds buffer bounds at offset ${this.offset - 4}`)
|
|
1148
1160
|
}
|
|
1149
1161
|
|
|
1150
|
-
|
|
1162
|
+
let bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
|
|
1151
1163
|
this.offset += len
|
|
1164
|
+
// UTF-16 needs whole code units. A stray odd length (seen from a few
|
|
1165
|
+
// exporters) made TextDecoder emit a replacement char and, worse, shifted
|
|
1166
|
+
// nothing — better to drop the orphan byte than to carry it into the name.
|
|
1167
|
+
if (this.encoding === 0 && len % 2 === 1) bytes = bytes.subarray(0, len - 1)
|
|
1152
1168
|
return this.decoder.decode(bytes)
|
|
1153
1169
|
}
|
|
1154
1170
|
|
package/dist/gpu-profile.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/gpu-profile.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"physics.worker.d.ts","sourceRoot":"","sources":["../../src/physics/physics.worker.ts"],"names":[],"mappings":""}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Physics worker: owns ONE model's RezePhysics world. The host posts the
|
|
2
|
-
// frame's bone world matrices as a transferable buffer; the worker steps the
|
|
3
|
-
// simulation (identical math to the main-thread path — same class, same code)
|
|
4
|
-
// and returns the buffer with the dynamic bones' matrices written back, plus
|
|
5
|
-
// its own step cost for the host's stats.
|
|
6
|
-
//
|
|
7
|
-
// The incoming buffer is COPIED into a persistent local pose (and back out)
|
|
8
|
-
// rather than wrapped: transferred ArrayBuffers get a fresh identity on every
|
|
9
|
-
// hop, so wrappers could never be cached against them — and two ~20KB copies
|
|
10
|
-
// per frame are microseconds.
|
|
11
|
-
import { RezePhysics } from "./physics";
|
|
12
|
-
import { Mat4 } from "../math";
|
|
13
|
-
import { RigidbodyType } from "./types";
|
|
14
|
-
const ctx = self;
|
|
15
|
-
let physics = null;
|
|
16
|
-
let inverseBind = new Float32Array(0);
|
|
17
|
-
let pose = new Float32Array(0);
|
|
18
|
-
let mats = [];
|
|
19
|
-
ctx.onmessage = (e) => {
|
|
20
|
-
const msg = e.data;
|
|
21
|
-
if (msg.cmd === "init") {
|
|
22
|
-
physics = new RezePhysics(msg.rigidbodies, msg.joints);
|
|
23
|
-
inverseBind = msg.inverseBind;
|
|
24
|
-
const boneCount = inverseBind.length / 16;
|
|
25
|
-
pose = new Float32Array(boneCount * 16);
|
|
26
|
-
mats = new Array(boneCount);
|
|
27
|
-
for (let i = 0; i < boneCount; i++)
|
|
28
|
-
mats[i] = new Mat4(pose.subarray(i * 16, i * 16 + 16));
|
|
29
|
-
// The host copies back only the bones physics can write — the bones of
|
|
30
|
-
// dynamic bodies. Kinematic bones must NOT round-trip: in the pipelined
|
|
31
|
-
// protocol they would drag a stale (frame-old) pose over the live one.
|
|
32
|
-
const dynamicBones = [];
|
|
33
|
-
for (const rb of msg.rigidbodies) {
|
|
34
|
-
if (rb.type === RigidbodyType.Dynamic && rb.mass > 0 && rb.boneIndex >= 0)
|
|
35
|
-
dynamicBones.push(rb.boneIndex);
|
|
36
|
-
}
|
|
37
|
-
ctx.postMessage({ cmd: "ready", dynamicBones });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!physics)
|
|
41
|
-
return;
|
|
42
|
-
const incoming = new Float32Array(msg.bones);
|
|
43
|
-
pose.set(incoming);
|
|
44
|
-
const t0 = performance.now();
|
|
45
|
-
if (msg.cmd === "step")
|
|
46
|
-
physics.step(msg.dt, mats, inverseBind);
|
|
47
|
-
else
|
|
48
|
-
physics.reset(mats);
|
|
49
|
-
const stepMs = performance.now() - t0;
|
|
50
|
-
incoming.set(pose);
|
|
51
|
-
ctx.postMessage({ cmd: "stepped", bones: msg.bones, stepMs }, [msg.bones]);
|
|
52
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/physics/profile.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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,28 +0,0 @@
|
|
|
1
|
-
import type { Rigidbody, Joint } from "./types";
|
|
2
|
-
import type { Mat4 } from "../math";
|
|
3
|
-
export declare class WorkerPhysics {
|
|
4
|
-
private readonly worker;
|
|
5
|
-
private dynamicBones;
|
|
6
|
-
private readonly boneCount;
|
|
7
|
-
/** Transfer buffer when idle; null while a step is in flight. */
|
|
8
|
-
private buf;
|
|
9
|
-
/** Latest completed pose from the worker (copied out of the transfer buffer). */
|
|
10
|
-
private readonly result;
|
|
11
|
-
private hasResult;
|
|
12
|
-
private pendingDt;
|
|
13
|
-
private queuedReset;
|
|
14
|
-
/** Worker-side cost of the last completed step — for engine stats. */
|
|
15
|
-
stepMs: number;
|
|
16
|
-
private constructor();
|
|
17
|
-
static supported(): boolean;
|
|
18
|
-
static create(rigidbodies: Rigidbody[], joints: Joint[], inverseBind: Float32Array): Promise<WorkerPhysics>;
|
|
19
|
-
/** Same signature as RezePhysics.step — the engine cannot tell them apart.
|
|
20
|
-
* (inverseBind was shipped to the worker at init; the param is unused.) */
|
|
21
|
-
step(dt: number, boneWorldMatrices: Mat4[], _inverseBind: Float32Array): void;
|
|
22
|
-
/** Reset rides the same pipeline: the next posted frame carries a reset
|
|
23
|
-
* command instead of a step, and stale results stop applying immediately. */
|
|
24
|
-
reset(boneWorldMatrices: Mat4[]): void;
|
|
25
|
-
dispose(): void;
|
|
26
|
-
private onStepped;
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=worker-physics.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"worker-physics.d.ts","sourceRoot":"","sources":["../../src/physics/worker-physics.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAYnC,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,iEAAiE;IACjE,OAAO,CAAC,GAAG,CAAoB;IAC/B,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,WAAW,CAAQ;IAC3B,sEAAsE;IACtE,MAAM,SAAI;IAEV,OAAO;IAOP,MAAM,CAAC,SAAS,IAAI,OAAO;IAI3B,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IA6B3G;gFAC4E;IAC5E,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAuB7E;kFAC8E;IAC9E,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAOtC,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,SAAS;CAQlB"}
|