reze-engine 0.56.5 → 0.56.7
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/dist/effect-schedule.d.ts +93 -0
- package/dist/effect-schedule.d.ts.map +1 -1
- package/dist/effect-schedule.js +118 -9
- package/dist/engine.d.ts +33 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +135 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/effect-schedule.ts +150 -6
- package/src/engine.ts +152 -55
- package/src/index.ts +9 -1
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 {
|
|
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
|
-
|
|
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
|
|
@@ -1533,6 +1529,19 @@ interface EffectInstance {
|
|
|
1533
1529
|
* is what makes a scheduled effect cost nothing outside its window.
|
|
1534
1530
|
*/
|
|
1535
1531
|
weight: number
|
|
1532
|
+
/** Where this effect's simulation stood last frame, while scheduled. See advanceSim. */
|
|
1533
|
+
sim: SimClock
|
|
1534
|
+
/** This frame's particle and grid step in seconds, from the transport. Read
|
|
1535
|
+
* only while the effect is scheduled; an unscheduled one steps by the render delta. */
|
|
1536
|
+
simStep: number
|
|
1537
|
+
/** Empty the particle pool and the grid before this frame's step. */
|
|
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
|
|
1536
1545
|
/** This effect's OWN clock, as a uniform the field shader reads. Per effect
|
|
1537
1546
|
* because the shared one (viewU[6].x) is measured from the first installed
|
|
1538
1547
|
* effect's epoch, so everything later started mid-stream. Null when the
|
|
@@ -4547,6 +4556,10 @@ export class Engine {
|
|
|
4547
4556
|
influence: 1,
|
|
4548
4557
|
window: null,
|
|
4549
4558
|
weight: 1,
|
|
4559
|
+
sim: { start: null, time: null },
|
|
4560
|
+
simStep: 0,
|
|
4561
|
+
simReset: false,
|
|
4562
|
+
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
4550
4563
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
4551
4564
|
// for full res is not promoted because a neighbour did.
|
|
4552
4565
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -4823,10 +4836,11 @@ export class Engine {
|
|
|
4823
4836
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render")
|
|
4824
4837
|
if (Array.isArray(renderModule)) return { ok: false, diagnostics: renderModule }
|
|
4825
4838
|
|
|
4839
|
+
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
4826
4840
|
const buffer = this.device.createBuffer({
|
|
4827
4841
|
label: "particle pool",
|
|
4828
4842
|
size: count * PARTICLE_STRIDE,
|
|
4829
|
-
usage: GPUBufferUsage.STORAGE,
|
|
4843
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4830
4844
|
})
|
|
4831
4845
|
const uniform = this.device.createBuffer({
|
|
4832
4846
|
label: "particle uniforms",
|
|
@@ -5092,6 +5106,14 @@ export class Engine {
|
|
|
5092
5106
|
for (const e of this.effects) {
|
|
5093
5107
|
const p = e.particles
|
|
5094
5108
|
if (!p) continue
|
|
5109
|
+
const scheduled = e.window !== null
|
|
5110
|
+
// A scheduled pool that has not moved holds still: a paused transport, or
|
|
5111
|
+
// a time outside every window. A dispatch at dt 0 would still run the
|
|
5112
|
+
// author's step, and not every step is a function of dt.
|
|
5113
|
+
if (scheduled && !e.simReset && e.simStep === 0) continue
|
|
5114
|
+
// Entering its window, or going back in it: the pool starts empty, and
|
|
5115
|
+
// every slot spawns on this dispatch from the effect's own local time.
|
|
5116
|
+
if (e.simReset) encoder.clearBuffer(p.buffer)
|
|
5095
5117
|
p.data[0] = this.sceneClock - e.epochScene
|
|
5096
5118
|
// The SIMULATION runs at every weight, 0 included — only the draw stops.
|
|
5097
5119
|
// A scheduled effect that froze while faded out would resume from the
|
|
@@ -5100,7 +5122,9 @@ export class Engine {
|
|
|
5100
5122
|
p.data[4] = e.weight
|
|
5101
5123
|
// Clamped: a backgrounded tab returns with a delta of whole seconds, and an
|
|
5102
5124
|
// unclamped step flings every particle out of the scene in one frame.
|
|
5103
|
-
|
|
5125
|
+
// A scheduled effect steps by the transport instead, clamped the same way
|
|
5126
|
+
// in advanceSim, so a pause freezes it and an export steps it exactly.
|
|
5127
|
+
p.data[1] = scheduled ? e.simStep : Math.min(0.1, Math.max(0, deltaTime))
|
|
5104
5128
|
p.counts[2] = p.count
|
|
5105
5129
|
p.counts[3] = this.particleFrame++
|
|
5106
5130
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer as ArrayBuffer)
|
|
@@ -5554,7 +5578,8 @@ export class Engine {
|
|
|
5554
5578
|
label: `grid grid ${n}`,
|
|
5555
5579
|
size: [size, size],
|
|
5556
5580
|
format: SIM_FORMAT,
|
|
5557
|
-
|
|
5581
|
+
// RENDER_ATTACHMENT so a scheduled effect can clear it when its window starts.
|
|
5582
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
5558
5583
|
})
|
|
5559
5584
|
const textures: [GPUTexture, GPUTexture] = [make(0), make(1)]
|
|
5560
5585
|
const read: [GPUTextureView, GPUTextureView] = [textures[0].createView(), textures[1].createView()]
|
|
@@ -5624,11 +5649,28 @@ export class Engine {
|
|
|
5624
5649
|
for (const e of this.effects) {
|
|
5625
5650
|
const grid = e.grid
|
|
5626
5651
|
if (!grid) continue
|
|
5652
|
+
const scheduled = e.window !== null
|
|
5653
|
+
// Held still like the particles, for the same reason.
|
|
5654
|
+
if (scheduled && !e.simReset && e.simStep === 0) continue
|
|
5655
|
+
if (e.simReset) {
|
|
5656
|
+
// Back to how install left it: both textures empty and frame 0 again,
|
|
5657
|
+
// the frame an effect seeds its grid on.
|
|
5658
|
+
for (const view of grid.read) {
|
|
5659
|
+
encoder
|
|
5660
|
+
.beginRenderPass({
|
|
5661
|
+
label: "grid reset",
|
|
5662
|
+
colorAttachments: [{ view, loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }],
|
|
5663
|
+
})
|
|
5664
|
+
.end()
|
|
5665
|
+
}
|
|
5666
|
+
grid.frame = 0
|
|
5667
|
+
grid.parity = 0
|
|
5668
|
+
}
|
|
5627
5669
|
grid.data[0] = this.sceneClock - e.epochScene
|
|
5628
5670
|
// Clamped like the particle step: a backgrounded tab returns with a delta of
|
|
5629
5671
|
// whole seconds, and one unclamped step of an advection kernel throws the
|
|
5630
5672
|
// whole grid off its own edge.
|
|
5631
|
-
grid.data[1] = Math.min(0.1, Math.max(0, deltaTime))
|
|
5673
|
+
grid.data[1] = scheduled ? e.simStep : Math.min(0.1, Math.max(0, deltaTime))
|
|
5632
5674
|
grid.data[2] = grid.size
|
|
5633
5675
|
grid.data[3] = grid.frame++
|
|
5634
5676
|
this.device.queue.writeBuffer(grid.uniform, 0, grid.data.buffer as ArrayBuffer)
|
|
@@ -5727,6 +5769,11 @@ export class Engine {
|
|
|
5727
5769
|
const fx = this.effects[index]
|
|
5728
5770
|
if (!fx) return
|
|
5729
5771
|
fx.window = windows && windows.length ? windows : null
|
|
5772
|
+
// Unscheduled forgets where its simulation stood. A changed lane keeps it:
|
|
5773
|
+
// advanceSim restarts the pool only if the window the transport is in now
|
|
5774
|
+
// starts somewhere else, so trimming a block, or turning the influence
|
|
5775
|
+
// that is sent with every lane, does not restart a burst mid-flight.
|
|
5776
|
+
if (!fx.window) fx.sim = { start: null, time: null }
|
|
5730
5777
|
}
|
|
5731
5778
|
|
|
5732
5779
|
getEffectSchedule(index: number): readonly EffectWindow[] | null {
|
|
@@ -5745,12 +5792,20 @@ export class Engine {
|
|
|
5745
5792
|
// Read ONCE: it walks the cast, and every effect wants the same answer.
|
|
5746
5793
|
const transport = this.transportTime()
|
|
5747
5794
|
for (const fx of this.effects) {
|
|
5795
|
+
fx.simReset = false
|
|
5748
5796
|
if (!fx.window || fx.window.length === 0) {
|
|
5749
5797
|
fx.weight = fx.influence
|
|
5750
5798
|
continue
|
|
5751
5799
|
}
|
|
5752
5800
|
const at = effectState(fx.window, fx.influence, transport)
|
|
5753
5801
|
fx.weight = at.weight
|
|
5802
|
+
// The particles and the grid carry state from frame to frame, so they get
|
|
5803
|
+
// more than a clock: a restart on entering a window or going back in one,
|
|
5804
|
+
// and a step measured on the transport. See advanceSim.
|
|
5805
|
+
const sim = advanceSim(fx.sim, fx.window, transport)
|
|
5806
|
+
fx.sim = sim.clock
|
|
5807
|
+
fx.simStep = sim.step
|
|
5808
|
+
fx.simReset = sim.reset
|
|
5754
5809
|
// Its own clock, expressed the way the mounts read it. Every mount
|
|
5755
5810
|
// derives time from the epoch against sceneClock, so this one write moves
|
|
5756
5811
|
// the field, the particles, the ribbons, lightEmit and the grid together
|
|
@@ -14338,6 +14393,9 @@ export class Engine {
|
|
|
14338
14393
|
if (pose) this.camera.setVmdPose(pose)
|
|
14339
14394
|
}
|
|
14340
14395
|
|
|
14396
|
+
// Before the cast is written, which carries every subject's dissolve to the
|
|
14397
|
+
// effects reading it.
|
|
14398
|
+
this.evaluateDissolves()
|
|
14341
14399
|
this.updateCameraUniforms()
|
|
14342
14400
|
this.updateShadowLightVP()
|
|
14343
14401
|
|
|
@@ -14479,7 +14537,6 @@ export class Engine {
|
|
|
14479
14537
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
14480
14538
|
// Material parameters on the scene clock, before anything reads their
|
|
14481
14539
|
// uniforms this frame.
|
|
14482
|
-
this.evaluateDissolveCycles()
|
|
14483
14540
|
this.evaluateParamTracks()
|
|
14484
14541
|
// FIRST among the things that read an effect, because every one of them
|
|
14485
14542
|
// reads what this writes: the sim's clock, the particle uniform's weight,
|
|
@@ -14960,22 +15017,6 @@ export class Engine {
|
|
|
14960
15017
|
return true
|
|
14961
15018
|
}
|
|
14962
15019
|
|
|
14963
|
-
/**
|
|
14964
|
-
* A repeating dissolve, on the scene clock.
|
|
14965
|
-
*
|
|
14966
|
-
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
14967
|
-
* the wrong shape twice: an exported take stepped at another rate would land
|
|
14968
|
-
* on different values than the preview did, and the effect drawing the sparks
|
|
14969
|
-
* would be reading a number some other clock wrote. Here the engine samples it
|
|
14970
|
-
* where it samples everything else time-driven, so a take reproduces exactly
|
|
14971
|
-
* and rzSubject().dissolve is the same value the material shell used on that
|
|
14972
|
-
* very frame.
|
|
14973
|
-
*
|
|
14974
|
-
* The five numbers are seconds within one cycle: when she starts to go, when
|
|
14975
|
-
* she is fully gone, when she starts to come back, and when she is whole. The
|
|
14976
|
-
* gaps between them are the timing, and the hold between the middle two is how
|
|
14977
|
-
* long she is away.
|
|
14978
|
-
*/
|
|
14979
15020
|
/**
|
|
14980
15021
|
* Eyes on the camera, per model — see Model.setEyeTracking. Live: solved
|
|
14981
15022
|
* every frame against wherever the camera is, orbit or motion alike. Null
|
|
@@ -14988,6 +15029,20 @@ export class Engine {
|
|
|
14988
15029
|
return true
|
|
14989
15030
|
}
|
|
14990
15031
|
|
|
15032
|
+
/**
|
|
15033
|
+
* A repeating dissolve on one model, on the scene clock.
|
|
15034
|
+
*
|
|
15035
|
+
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
15036
|
+
* the wrong shape twice: an exported take stepped at another rate would land
|
|
15037
|
+
* on different values than the preview did, and the effect drawing the sparks
|
|
15038
|
+
* would be reading a number some other clock wrote. Here the engine samples it
|
|
15039
|
+
* where it samples everything else time-driven, so a take reproduces exactly
|
|
15040
|
+
* and rzSubject().dissolve is the same value the material shell used on that
|
|
15041
|
+
* very frame.
|
|
15042
|
+
*
|
|
15043
|
+
* An effect that declares `#dissolve` needs none of this: it carries its own
|
|
15044
|
+
* cycle and follows its own clips — see evaluateDissolves.
|
|
15045
|
+
*/
|
|
14991
15046
|
setModelDissolveCycle(modelName: string, cycle: DissolveCycle | null): boolean {
|
|
14992
15047
|
if (!this.modelInstances.has(modelName)) return false
|
|
14993
15048
|
if (!cycle) {
|
|
@@ -14998,23 +15053,65 @@ export class Engine {
|
|
|
14998
15053
|
return true
|
|
14999
15054
|
}
|
|
15000
15055
|
|
|
15001
|
-
/**
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15056
|
+
/** The model at one cast slot — the subject rzSubject(index) reads. */
|
|
15057
|
+
private castSubjectName(index: number): string | null {
|
|
15058
|
+
let n = 0
|
|
15059
|
+
let found: string | null = null
|
|
15060
|
+
this.forEachInstance((inst) => {
|
|
15061
|
+
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
|
|
15062
|
+
if (n === index) found = inst.name
|
|
15063
|
+
n++
|
|
15064
|
+
})
|
|
15065
|
+
return found
|
|
15066
|
+
}
|
|
15067
|
+
|
|
15068
|
+
/** One effect's four durations: its dials where it declares them, the
|
|
15069
|
+
* constants it wrote where it does not. */
|
|
15070
|
+
private dissolveTimingsOf(fx: EffectInstance, wrote: DissolveTimings): DissolveTimings {
|
|
15071
|
+
const t = { ...wrote }
|
|
15072
|
+
for (const [key, name] of DISSOLVE_PARAMS) {
|
|
15073
|
+
const slot = fx.paramLayout.get(name)
|
|
15074
|
+
if (slot) t[key] = Math.max(0, fx.paramsData[slot.offset])
|
|
15075
|
+
}
|
|
15076
|
+
return t
|
|
15077
|
+
}
|
|
15078
|
+
|
|
15079
|
+
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
15080
|
+
* more comes back whole instead of holding wherever it was left. */
|
|
15081
|
+
private dissolveTouched = new Set<string>()
|
|
15082
|
+
|
|
15083
|
+
/**
|
|
15084
|
+
* Every dissolve, once a frame, before the cast is written — so the value a
|
|
15085
|
+
* mote reads as rzSubject(i).dissolve is the one her materials wore on that
|
|
15086
|
+
* very frame, rather than the one they wore on the last.
|
|
15087
|
+
*
|
|
15088
|
+
* Two things ask for one. A cycle set on a model (setModelDissolveCycle) runs
|
|
15089
|
+
* on the scene clock and repeats, as it always has. An effect declaring
|
|
15090
|
+
* `#dissolve` takes subject 0 apart on ITS OWN terms: scheduled, the cycle on
|
|
15091
|
+
* each clip's clock with her whole between clips; unscheduled, the same
|
|
15092
|
+
* repeating cycle as before. Where both ask, the more dissolved wins — two
|
|
15093
|
+
* answers about one body, and half a body is not one of them.
|
|
15094
|
+
*/
|
|
15095
|
+
private evaluateDissolves(): void {
|
|
15096
|
+
const want = new Map<string, number>()
|
|
15097
|
+
const put = (name: string, v: number) => want.set(name, Math.min(want.get(name) ?? 1, v))
|
|
15098
|
+
for (const [name, c] of this.dissolveCycles) put(name, sampleDissolveCycle(c, this.sceneClock))
|
|
15099
|
+
let transport: number | null = null
|
|
15100
|
+
for (const fx of this.effects) {
|
|
15101
|
+
if (!fx.dissolve) continue
|
|
15102
|
+
const cycle = dissolveCycleOf(this.dissolveTimingsOf(fx, fx.dissolve))
|
|
15103
|
+
const subject = this.castSubjectName(0)
|
|
15104
|
+
if (!cycle || !subject) continue
|
|
15105
|
+
if (fx.window && fx.window.length > 0) {
|
|
15106
|
+
transport ??= this.transportTime()
|
|
15107
|
+
put(subject, scheduledDissolve(fx.window, cycle, transport))
|
|
15108
|
+
} else {
|
|
15109
|
+
put(subject, sampleDissolveCycle(cycle, this.sceneClock))
|
|
15015
15110
|
}
|
|
15016
|
-
this.setModelDissolve(name, v)
|
|
15017
15111
|
}
|
|
15112
|
+
for (const name of this.dissolveTouched) if (!want.has(name)) this.setModelDissolve(name, 1)
|
|
15113
|
+
this.dissolveTouched = new Set(want.keys())
|
|
15114
|
+
for (const [name, v] of want) this.setModelDissolve(name, v)
|
|
15018
15115
|
}
|
|
15019
15116
|
|
|
15020
15117
|
/** What setModelDissolve last set, or 1 for a model that has never dissolved. */
|
package/src/index.ts
CHANGED
|
@@ -39,7 +39,15 @@ export { sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
|
|
|
39
39
|
// The strip an effect is scheduled by, and the pure evaluator behind it —
|
|
40
40
|
// exported so a caller can draw a lane against the same numbers the engine
|
|
41
41
|
// renders from, rather than a second copy of the ramp maths.
|
|
42
|
-
export {
|
|
42
|
+
export {
|
|
43
|
+
activeWindow,
|
|
44
|
+
advanceSim,
|
|
45
|
+
effectState,
|
|
46
|
+
SIM_MAX_STEP,
|
|
47
|
+
type EffectWindow,
|
|
48
|
+
type EffectState,
|
|
49
|
+
type SimClock,
|
|
50
|
+
} from "./effect-schedule"
|
|
43
51
|
// Who a model hangs from over time, and the lookup behind it — exported so a
|
|
44
52
|
// timeline draws its holds from the same rule the engine switches by.
|
|
45
53
|
export { parentKeyIndex, type ModelParentKey } from "./parent-keys"
|