reze-engine 0.56.6 → 0.56.8

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/src/engine.ts CHANGED
@@ -30,7 +30,19 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
30
30
  import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
31
31
  import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug"
32
32
  import { paramChanged, sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
33
- import { effectState, type EffectWindow, advanceSim, type SimClock } from "./effect-schedule"
33
+ import {
34
+ advanceSim,
35
+ DISSOLVE_PARAMS,
36
+ dissolveConstants,
37
+ dissolveCycleOf,
38
+ effectState,
39
+ sampleDissolveCycle,
40
+ scheduledDissolve,
41
+ type DissolveCycle,
42
+ type DissolveTimings,
43
+ type EffectWindow,
44
+ type SimClock,
45
+ } from "./effect-schedule"
34
46
  import { parentKeySpan, type ModelParentKey } from "./parent-keys"
35
47
  import { SHADOW_CASCADES, buildShadowVP } from "./shadow-cascades"
36
48
  import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection"
@@ -745,24 +757,8 @@ interface PickDrawCall {
745
757
  bindGroup: GPUBindGroup
746
758
  }
747
759
 
748
- /**
749
- * A repeating dissolve, in seconds within one cycle.
750
- *
751
- * Four moments rather than a duration and a delay: every one of them is a thing
752
- * you can see happen, and an author tuning this is watching for exactly those
753
- * four frames.
754
- */
755
- export interface DissolveCycle {
756
- period: number
757
- /** She starts to come apart. */
758
- breakAt: number
759
- /** Fully gone. */
760
- hiddenAt: number
761
- /** She starts to come back. */
762
- backAt: number
763
- /** Whole again. */
764
- doneAt: number
765
- }
760
+ /** Authored in effect-schedule, beside the evaluator that performs it. */
761
+ export type { DissolveCycle }
766
762
 
767
763
  interface ModelInstance {
768
764
  name: string
@@ -1540,6 +1536,12 @@ interface EffectInstance {
1540
1536
  simStep: number
1541
1537
  /** Empty the particle pool and the grid before this frame's step. */
1542
1538
  simReset: boolean
1539
+ /**
1540
+ * The four durations this effect's `#dissolve` takes the cast apart over, as
1541
+ * its source wrote them. Null unless it declared one. A dial of the same name
1542
+ * wins over the constant — see dissolveTimingsOf.
1543
+ */
1544
+ dissolve: DissolveTimings | null
1543
1545
  /** This effect's OWN clock, as a uniform the field shader reads. Per effect
1544
1546
  * because the shared one (viewU[6].x) is measured from the first installed
1545
1547
  * effect's epoch, so everything later started mid-stream. Null when the
@@ -2140,6 +2142,9 @@ export class Engine {
2140
2142
  // because two planes would need two of every attachment below. Which one is
2141
2143
  // in force is mirrorPlane, refreshed by updateMirrorCamera.
2142
2144
  private static readonly GROUND_PLANE: readonly number[] = [0, 1, 0, 0]
2145
+ /** Where addGround put the floor. The mirror reflects across it, so a raised
2146
+ * floor that kept y = 0 would reflect the scene into the wrong plane. */
2147
+ private groundY = 0
2143
2148
  private mirrorPlane = new Float32Array([0, 1, 0, 0])
2144
2149
  private mirrorCameraData = new Float32Array(40)
2145
2150
  private mirrorCameraBuffer!: GPUBuffer
@@ -3290,7 +3295,9 @@ export class Engine {
3290
3295
  const m = this.mirrorSurfaceModel
3291
3296
  planeFromPointNormal(m[12], m[13], m[14], m[8], m[9], m[10], this.mirrorPlane)
3292
3297
  } else {
3298
+ // The floor's own plane: up, offset to wherever addGround put it.
3293
3299
  this.mirrorPlane.set(Engine.GROUND_PLANE)
3300
+ this.mirrorPlane[3] = -this.groundY
3294
3301
  }
3295
3302
  // The ground inside the mirror pass clips against this same plane — a
3296
3303
  // mirror shows nothing behind itself, and the floor is what is behind one.
@@ -4557,6 +4564,7 @@ export class Engine {
4557
4564
  sim: { start: null, time: null },
4558
4565
  simStep: 0,
4559
4566
  simReset: false,
4567
+ dissolve: d.dissolve ? dissolveConstants(authored) : null,
4560
4568
  // Its OWN resolution, no longer the scene's: an effect that never asked
4561
4569
  // for full res is not promoted because a neighbour did.
4562
4570
  // FULL RESOLUTION UNLESS TOLD OTHERWISE.
@@ -9204,6 +9212,15 @@ export class Engine {
9204
9212
  addGround(options?: {
9205
9213
  width?: number
9206
9214
  height?: number
9215
+ /**
9216
+ * Where the plane sits on Y, in world units. 0 (default) is MMD's floor.
9217
+ *
9218
+ * A framing offset, not a stage: a camera motion authored for a taller or
9219
+ * shorter character fits by moving the model and the floor together, which
9220
+ * is cheaper than retiming the shot. What stands ON the floor does not
9221
+ * follow it — the physics floor is model space, at the figure's own feet.
9222
+ */
9223
+ y?: number
9207
9224
  diffuseColor?: Vec3
9208
9225
  fadeStart?: number
9209
9226
  fadeEnd?: number
@@ -9243,6 +9260,7 @@ export class Engine {
9243
9260
  const opts = {
9244
9261
  width: 160,
9245
9262
  height: 160,
9263
+ y: 0,
9246
9264
  diffuseColor: new Vec3(0.9, 0.1, 1.0),
9247
9265
  fadeStart: 10.0,
9248
9266
  fadeEnd: 80.0,
@@ -9258,7 +9276,8 @@ export class Engine {
9258
9276
  shadowSoftness: 0,
9259
9277
  ...options,
9260
9278
  }
9261
- this.createGroundGeometry(opts.width, opts.height)
9279
+ this.groundY = opts.y
9280
+ this.createGroundGeometry(opts.width, opts.height, opts.y)
9262
9281
  this.createShadowGroundResources(opts)
9263
9282
  this.hasGround = true
9264
9283
  this.groundDrawCall = {
@@ -9271,7 +9290,14 @@ export class Engine {
9271
9290
  // The ground belongs to no model instance, so it is not in the cull list —
9272
9291
  // cullIndex -1 leaves renderGround unconditional. Its box is filled in
9273
9292
  // anyway rather than left a lie for whoever reads this next.
9274
- bounds: new Float32Array([-opts.width / 2, 0, -opts.height / 2, opts.width / 2, 0, opts.height / 2]),
9293
+ bounds: new Float32Array([
9294
+ -opts.width / 2,
9295
+ opts.y,
9296
+ -opts.height / 2,
9297
+ opts.width / 2,
9298
+ opts.y,
9299
+ opts.height / 2,
9300
+ ]),
9275
9301
  cullIndex: -1,
9276
9302
  }
9277
9303
  }
@@ -12316,14 +12342,14 @@ export class Engine {
12316
12342
  }
12317
12343
  }
12318
12344
 
12319
- private createGroundGeometry(width: number = 100, height: number = 100) {
12345
+ private createGroundGeometry(width: number = 100, height: number = 100, y: number = 0) {
12320
12346
  const halfWidth = width / 2
12321
12347
  const halfHeight = height / 2
12322
12348
 
12323
12349
  const vertices = new Float32Array([
12324
12350
  // Bottom-left
12325
12351
  -halfWidth,
12326
- 0,
12352
+ y,
12327
12353
  -halfHeight, // position
12328
12354
  0,
12329
12355
  1,
@@ -12333,7 +12359,7 @@ export class Engine {
12333
12359
 
12334
12360
  // Bottom-right
12335
12361
  halfWidth,
12336
- 0,
12362
+ y,
12337
12363
  -halfHeight, // position
12338
12364
  0,
12339
12365
  1,
@@ -12343,7 +12369,7 @@ export class Engine {
12343
12369
 
12344
12370
  // Top-right
12345
12371
  halfWidth,
12346
- 0,
12372
+ y,
12347
12373
  halfHeight, // position
12348
12374
  0,
12349
12375
  1,
@@ -12353,7 +12379,7 @@ export class Engine {
12353
12379
 
12354
12380
  // Top-left
12355
12381
  -halfWidth,
12356
- 0,
12382
+ y,
12357
12383
  halfHeight, // position
12358
12384
  0,
12359
12385
  1,
@@ -12433,6 +12459,7 @@ export class Engine {
12433
12459
 
12434
12460
  private createShadowGroundResources(opts: {
12435
12461
  diffuseColor: Vec3
12462
+ y: number
12436
12463
  fadeStart: number
12437
12464
  fadeEnd: number
12438
12465
  shadowStrength: number
@@ -12448,6 +12475,7 @@ export class Engine {
12448
12475
  }) {
12449
12476
  const {
12450
12477
  diffuseColor,
12478
+ y,
12451
12479
  fadeStart,
12452
12480
  fadeEnd,
12453
12481
  shadowStrength,
@@ -12488,6 +12516,9 @@ export class Engine {
12488
12516
  // gb[18] — shadow edge softness. Was padding; the shader reads it as the
12489
12517
  // Vogel disk's radius, and 0 takes the sharp nine-tap path unchanged.
12490
12518
  gb[18] = Math.min(Math.max(shadowSoftness, 0), 1)
12519
+ // gb[19] — the floor's height. Was padding; the mirror branch reflects the
12520
+ // eye across it, and a hardcoded 0 slid the reflection off a raised floor.
12521
+ gb[19] = y
12491
12522
  // Which variant the draw picks. Zero is the sharp shader, which is the one
12492
12523
  // that existed before softness did.
12493
12524
  this.groundSoft = gb[18] > 0
@@ -14390,6 +14421,9 @@ export class Engine {
14390
14421
  if (pose) this.camera.setVmdPose(pose)
14391
14422
  }
14392
14423
 
14424
+ // Before the cast is written, which carries every subject's dissolve to the
14425
+ // effects reading it.
14426
+ this.evaluateDissolves()
14393
14427
  this.updateCameraUniforms()
14394
14428
  this.updateShadowLightVP()
14395
14429
 
@@ -14531,7 +14565,6 @@ export class Engine {
14531
14565
  // and a grid stepped after them is one frame stale in everything that used it.
14532
14566
  // Material parameters on the scene clock, before anything reads their
14533
14567
  // uniforms this frame.
14534
- this.evaluateDissolveCycles()
14535
14568
  this.evaluateParamTracks()
14536
14569
  // FIRST among the things that read an effect, because every one of them
14537
14570
  // reads what this writes: the sim's clock, the particle uniform's weight,
@@ -15012,22 +15045,6 @@ export class Engine {
15012
15045
  return true
15013
15046
  }
15014
15047
 
15015
- /**
15016
- * A repeating dissolve, on the scene clock.
15017
- *
15018
- * The alternative was a host calling setModelDissolve every frame, and it is
15019
- * the wrong shape twice: an exported take stepped at another rate would land
15020
- * on different values than the preview did, and the effect drawing the sparks
15021
- * would be reading a number some other clock wrote. Here the engine samples it
15022
- * where it samples everything else time-driven, so a take reproduces exactly
15023
- * and rzSubject().dissolve is the same value the material shell used on that
15024
- * very frame.
15025
- *
15026
- * The five numbers are seconds within one cycle: when she starts to go, when
15027
- * she is fully gone, when she starts to come back, and when she is whole. The
15028
- * gaps between them are the timing, and the hold between the middle two is how
15029
- * long she is away.
15030
- */
15031
15048
  /**
15032
15049
  * Eyes on the camera, per model — see Model.setEyeTracking. Live: solved
15033
15050
  * every frame against wherever the camera is, orbit or motion alike. Null
@@ -15040,6 +15057,20 @@ export class Engine {
15040
15057
  return true
15041
15058
  }
15042
15059
 
15060
+ /**
15061
+ * A repeating dissolve on one model, on the scene clock.
15062
+ *
15063
+ * The alternative was a host calling setModelDissolve every frame, and it is
15064
+ * the wrong shape twice: an exported take stepped at another rate would land
15065
+ * on different values than the preview did, and the effect drawing the sparks
15066
+ * would be reading a number some other clock wrote. Here the engine samples it
15067
+ * where it samples everything else time-driven, so a take reproduces exactly
15068
+ * and rzSubject().dissolve is the same value the material shell used on that
15069
+ * very frame.
15070
+ *
15071
+ * An effect that declares `#dissolve` needs none of this: it carries its own
15072
+ * cycle and follows its own clips — see evaluateDissolves.
15073
+ */
15043
15074
  setModelDissolveCycle(modelName: string, cycle: DissolveCycle | null): boolean {
15044
15075
  if (!this.modelInstances.has(modelName)) return false
15045
15076
  if (!cycle) {
@@ -15050,23 +15081,65 @@ export class Engine {
15050
15081
  return true
15051
15082
  }
15052
15083
 
15053
- /** Every dissolve cycle, at the current scene clock. Once per frame, before
15054
- * the cast is written and long before any effect reads it. */
15055
- private evaluateDissolveCycles(): void {
15056
- if (this.dissolveCycles.size === 0) return
15057
- for (const [name, c] of this.dissolveCycles) {
15058
- const period = Math.max(c.period, 1e-3)
15059
- const t = this.sceneClock - Math.floor(this.sceneClock / period) * period
15060
- let v = 1
15061
- if (t >= c.breakAt && t < c.hiddenAt) {
15062
- v = 1 - (t - c.breakAt) / Math.max(c.hiddenAt - c.breakAt, 1e-4)
15063
- } else if (t >= c.hiddenAt && t < c.backAt) {
15064
- v = 0
15065
- } else if (t >= c.backAt && t < c.doneAt) {
15066
- v = (t - c.backAt) / Math.max(c.doneAt - c.backAt, 1e-4)
15084
+ /** The model at one cast slot — the subject rzSubject(index) reads. */
15085
+ private castSubjectName(index: number): string | null {
15086
+ let n = 0
15087
+ let found: string | null = null
15088
+ this.forEachInstance((inst) => {
15089
+ if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
15090
+ if (n === index) found = inst.name
15091
+ n++
15092
+ })
15093
+ return found
15094
+ }
15095
+
15096
+ /** One effect's four durations: its dials where it declares them, the
15097
+ * constants it wrote where it does not. */
15098
+ private dissolveTimingsOf(fx: EffectInstance, wrote: DissolveTimings): DissolveTimings {
15099
+ const t = { ...wrote }
15100
+ for (const [key, name] of DISSOLVE_PARAMS) {
15101
+ const slot = fx.paramLayout.get(name)
15102
+ if (slot) t[key] = Math.max(0, fx.paramsData[slot.offset])
15103
+ }
15104
+ return t
15105
+ }
15106
+
15107
+ /** Models this wrote a dissolve onto last frame, so one nothing asks for any
15108
+ * more comes back whole instead of holding wherever it was left. */
15109
+ private dissolveTouched = new Set<string>()
15110
+
15111
+ /**
15112
+ * Every dissolve, once a frame, before the cast is written — so the value a
15113
+ * mote reads as rzSubject(i).dissolve is the one her materials wore on that
15114
+ * very frame, rather than the one they wore on the last.
15115
+ *
15116
+ * Two things ask for one. A cycle set on a model (setModelDissolveCycle) runs
15117
+ * on the scene clock and repeats, as it always has. An effect declaring
15118
+ * `#dissolve` takes subject 0 apart on ITS OWN terms: scheduled, the cycle on
15119
+ * each clip's clock with her whole between clips; unscheduled, the same
15120
+ * repeating cycle as before. Where both ask, the more dissolved wins — two
15121
+ * answers about one body, and half a body is not one of them.
15122
+ */
15123
+ private evaluateDissolves(): void {
15124
+ const want = new Map<string, number>()
15125
+ const put = (name: string, v: number) => want.set(name, Math.min(want.get(name) ?? 1, v))
15126
+ for (const [name, c] of this.dissolveCycles) put(name, sampleDissolveCycle(c, this.sceneClock))
15127
+ let transport: number | null = null
15128
+ for (const fx of this.effects) {
15129
+ if (!fx.dissolve) continue
15130
+ const cycle = dissolveCycleOf(this.dissolveTimingsOf(fx, fx.dissolve))
15131
+ const subject = this.castSubjectName(0)
15132
+ if (!cycle || !subject) continue
15133
+ if (fx.window && fx.window.length > 0) {
15134
+ transport ??= this.transportTime()
15135
+ put(subject, scheduledDissolve(fx.window, cycle, transport))
15136
+ } else {
15137
+ put(subject, sampleDissolveCycle(cycle, this.sceneClock))
15067
15138
  }
15068
- this.setModelDissolve(name, v)
15069
15139
  }
15140
+ for (const name of this.dissolveTouched) if (!want.has(name)) this.setModelDissolve(name, 1)
15141
+ this.dissolveTouched = new Set(want.keys())
15142
+ for (const [name, v] of want) this.setModelDissolve(name, v)
15070
15143
  }
15071
15144
 
15072
15145
  /** What setModelDissolve last set, or 1 for a model that has never dissolved. */
@@ -94,7 +94,7 @@ struct GroundShadowMat {
94
94
  gridLineColor: vec3f, mirror: f32,
95
95
  // farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
96
96
  // with no stage the far map is never drawn into, so its taps are known.
97
- mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, _mb2: f32,
97
+ mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
98
98
  // Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
99
99
  // nothing casts, negative means "do not use this" (a rigid caster has no
100
100
  // sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
@@ -362,20 +362,21 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
362
362
  // shared projection's pair (viewZ = projB / (z - projA); the formula
363
363
  // inverts both depth conventions, see the composite's linearDepth), and
364
364
  // reconstruct the reflected image point along the ray from the MIRRORED
365
- // eye through this fragment. The plane is y = 0, so mirroring the eye
366
- // and the camera forward is a sign flip on y, and the image point's
367
- // depth below the plane IS the reflected object's height above it.
365
+ // eye through this fragment. Mirroring across the floor's own plane is a
366
+ // sign flip on y about that height (the forward vector is a direction, so
367
+ // it flips alone), and the image point's depth below the plane IS the
368
+ // reflected object's height above it.
368
369
  let dims = vec2f(textureDimensions(mirrorDepth));
369
370
  let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
370
371
  let z = textureLoad(mirrorDepth, texel, 0);
371
372
  let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
372
- let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
373
+ let eyeM = vec3f(camera.viewPos.x, 2.0 * material.groundY - camera.viewPos.y, camera.viewPos.z);
373
374
  let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
374
375
  let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
375
376
  let toFrag = i.worldPos - eyeM;
376
377
  let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
377
378
  let t = viewZ / max(dot(dir, fwdM), 1e-4);
378
- let height = max(-(eyeM.y + dir.y * t), 0.0);
379
+ let height = max(material.groundY - (eyeM.y + dir.y * t), 0.0);
379
380
  // Full softness by BLUR_SPAN units above the floor — about a character's
380
381
  // height. The hardware clamps the level, so the nominal 5.0 needs no
381
382
  // knowledge of the real chain length, and blur 0 reads exactly level 0.