reze-engine 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -5,7 +5,6 @@ import { MORPH_COMPUTE_WGSL } from "./shaders/passes/morph";
5
5
  import { decodeTga } from "./tga-loader";
6
6
  import { VMDLoader } from "./vmd-loader";
7
7
  import { CameraAnimation } from "./camera-animation";
8
- import { ParticleSystem } from "./particles";
9
8
  import { PmxLoader } from "./pmx-loader";
10
9
  import { RezePhysics } from "./physics";
11
10
  import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
@@ -145,6 +144,16 @@ export const DEFAULT_BLOOM_OPTIONS = {
145
144
  intensity: 0.05,
146
145
  clamp: 0.0,
147
146
  };
147
+ export const DEFAULT_DEPTH_OF_FIELD_OPTIONS = {
148
+ enabled: false,
149
+ focusMode: "auto",
150
+ focusDistance: 25,
151
+ focusRange: 2,
152
+ aperture: 1,
153
+ maxBlurRadius: 18,
154
+ bladeCount: 6,
155
+ quality: "balanced",
156
+ };
148
157
  // Matches the reference Blender project: Filmic view, Medium High Contrast look,
149
158
  // exposure 0.3, gamma 1.0, sRGB display, no curves.
150
159
  export const DEFAULT_VIEW_TRANSFORM = {
@@ -287,22 +296,29 @@ export class Engine {
287
296
  // the fragment shader and treats missing dst.a as 1, so the blend math is
288
297
  // unchanged).
289
298
  this.hdrFormat = "rgba16float";
290
- /** One-shot particle emissions (emitParticles); lazily built on first emit. */
291
- this.particles = null;
299
+ this.depthOfField = { ...DEFAULT_DEPTH_OF_FIELD_OPTIONS };
300
+ this.dofUniformData = new Float32Array(12);
301
+ this.dofFocusScratch = new Vec3(0, 0, 0);
302
+ /** Depth-only view of the scene's MSAA depth buffer, read by the DoF gather. */
303
+ this.depthReadView = null;
292
304
  // [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
293
- this.compositeUniformData = new Float32Array(40);
305
+ // 11 × vec4f — see the viewU comment in composite.ts. The last one is the
306
+ // camera's world position, which is what lets a foreground effect turn the
307
+ // depth it is handed into a PLACE (bgWorldPos) rather than a distance.
308
+ this.compositeUniformData = new Float32Array(44);
294
309
  /** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
295
310
  this.backgroundColor = null;
296
311
  // 360 backdrop (equirectangular skybox, sampled by view ray in composite).
297
312
  this.backdropEquirectTexture = null;
298
313
  this.backdropEquirectView = null;
299
- // User WGSL background effect (background mode 3, setBackgroundEffect). The
300
- // composite pipelines are REBUILT with the user code injected; params live in
301
- // their own uniform buffer so setBackgroundEffectParam is a write, not a
314
+ // The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
315
+ // scene, over it, or both — whichever of background()/foreground() the code
316
+ // defines. The composite pipelines are REBUILT with the user code injected;
317
+ // params live in their own uniform buffer so setEffectParam is a write, not a
302
318
  // recompile (the same instant tier as setStyleParam).
303
- this.backgroundEffect = null;
304
- /** time=0 origin for the active effect — reset each setBackgroundEffect. */
305
- this.bgEffectEpochMs = 0;
319
+ this.effect = null;
320
+ /** time=0 origin for the active effect — reset each setEffect. */
321
+ this.effectEpochMs = 0;
306
322
  this.compositeBloomView = null;
307
323
  this.bloomBlitUniformData = new Float32Array(4);
308
324
  this.bloomUpsampleUniformData = new Float32Array(4);
@@ -725,10 +741,10 @@ export class Engine {
725
741
  u[8] = bg?.x ?? 0;
726
742
  u[9] = bg?.y ?? 0;
727
743
  u[10] = bg?.z ?? 0;
728
- // Base-layer mode; a user effect is a separate LAYER flagged at u[25] and
729
- // over-composited onto whichever base is active.
744
+ // Base-layer mode only. A user effect is a separate LAYER over whichever
745
+ // base is active, and needs no flag of its own: the composite pipeline is
746
+ // rebuilt per effect, so the compiled variant IS the flag. u[25] is spare.
730
747
  u[11] = this.backdropEquirectView ? 2 : bg ? 1 : 0;
731
- u[25] = this.backgroundEffect ? 1 : 0;
732
748
  u[26] = this.canvas.width;
733
749
  u[27] = this.canvas.height;
734
750
  // ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
@@ -774,7 +790,7 @@ export class Engine {
774
790
  this.outlineEnabled = on;
775
791
  }
776
792
  rebuildCompositeBindGroup() {
777
- if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView)
793
+ if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView || !this.depthReadView)
778
794
  return;
779
795
  this.compositeBindGroup = this.device.createBindGroup({
780
796
  label: "composite bind group",
@@ -787,7 +803,9 @@ export class Engine {
787
803
  { binding: 4, resource: this.maskResolveView },
788
804
  { binding: 5, resource: this.filmicLutView },
789
805
  { binding: 6, resource: this.backdropEquirectView ?? this.fallbackEquirectView },
790
- { binding: 7, resource: { buffer: this.backgroundEffect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
806
+ { binding: 7, resource: { buffer: this.effect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
807
+ { binding: 8, resource: this.depthReadView },
808
+ { binding: 9, resource: { buffer: this.dofUniformBuffer } },
791
809
  ],
792
810
  });
793
811
  }
@@ -850,42 +868,71 @@ export class Engine {
850
868
  });
851
869
  }
852
870
  /**
853
- * Install a WGSL background effect (shadertoy-style) as a LAYER between the
854
- * base background and the scene: rendered per-pixel in the composite pass and
855
- * over-composited onto whichever base is active (solid color, 360 equirect,
856
- * or transparency) — its alpha lets the base show through, so a starfield is
857
- * stars over the user's background color. Display-space: never affects
858
- * lighting, bloom, or tonemapping, and is captured by offline export like any
859
- * background.
860
- *
861
- * `wgsl` must define:
871
+ * Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
872
+ * composite pass. ONE effect per scene, and the code says where it mounts by
873
+ * which of these it defines — either, or both in one file:
862
874
  *
863
875
  * fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
876
+ * fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f
877
+ *
878
+ * `background` is a LAYER between the base background and the scene,
879
+ * over-composited onto whichever base is active (solid color, 360 equirect, or
880
+ * transparency) — its alpha lets the base show through, so a starfield is
881
+ * stars over the user's background color. `foreground` composites over the
882
+ * finished frame instead, which is where rain, snow, petals and fog live, and
883
+ * is handed `depth`: the camera-space distance in metres of whatever the scene
884
+ * drew at that pixel (the far plane where it drew nothing). Compare a
885
+ * particle's own distance against it and the model occludes it; fog just reads
886
+ * it, since fog's alpha IS a function of distance.
864
887
  *
865
- * where `ray` is the pixel's normalized world-space view direction (LH, +Z
866
- * forward — what the skybox samples by), `uv` is 0..1 bottom-left origin,
867
- * `time` is seconds since apply, and `bgResolution()` gives the canvas size.
868
- * Return sRGB + alpha. Declared `params` arrive as `params.<name>` (number →
869
- * f32, Vec3 → vec3f) and are later tweaked without recompiling via
870
- * setBackgroundEffectParam.
888
+ * `ray` is the pixel's normalized world-space view direction (LH, +Z forward —
889
+ * what the skybox samples by), `uv` is 0..1 bottom-left origin, `time` is
890
+ * seconds since apply, and `bgResolution()` gives the canvas size. Return sRGB
891
+ * + alpha; alpha is the only "how much does this replace" control there is.
892
+ * Declared `params` arrive as `params.<name>` (number → f32, Vec3 → vec3f),
893
+ * shared by both mounts, and are later tweaked without recompiling via
894
+ * setEffectParam.
871
895
  *
872
- * Compiles off the hot path (async pipelines): on failure the previous
873
- * background is KEPT and diagnostics are returned with line numbers relative
874
- * to the user's WGSL. Pass null to remove the effect.
896
+ * Both mounts are display-space: neither affects lighting, bloom or
897
+ * tonemapping, and both are captured by offline export. A foreground makes the
898
+ * scene pass STORE its depth buffer (it otherwise discards it into tile
899
+ * memory) for as long as one is installed.
900
+ *
901
+ * Compiles off the hot path (async pipelines): on failure the previous effect
902
+ * is KEPT and diagnostics are returned with line numbers relative to the
903
+ * user's WGSL. Pass null to remove the effect.
875
904
  */
876
- async setBackgroundEffect(wgsl, params) {
905
+ async setEffect(wgsl, params) {
906
+ const noMounts = { background: false, foreground: false };
877
907
  if (!this.device)
878
- return { ok: false, diagnostics: ["setBackgroundEffect requires init() to have run"] };
908
+ return { ok: false, diagnostics: ["setEffect requires init() to have run"], mounts: noMounts };
879
909
  if (wgsl === null) {
880
- this.backgroundEffect?.paramsBuffer?.destroy();
881
- this.backgroundEffect = null;
910
+ this.effect?.paramsBuffer?.destroy();
911
+ this.effect = null;
882
912
  const module = this.device.createShaderModule({ label: "composite shader", code: buildCompositeShader(null) });
883
913
  this.compositePipelineIdentity = this.makeCompositePipeline(module, false, "composite pipeline (gamma=1)");
884
914
  this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)");
885
915
  this.rebuildCompositeBindGroup();
886
916
  this.writeCompositeViewUniforms();
887
- return { ok: true, diagnostics: [] };
917
+ return { ok: true, diagnostics: [], mounts: noMounts };
918
+ }
919
+ // ── Which mounts did the author ask for? A declaration, not a setting: the
920
+ // entry points present in the source are the ones compiled in. Matching the
921
+ // `fn` keyword is enough to be safe against a `foreground` LOCAL or a call
922
+ // to one — those never follow `fn`.
923
+ const hasBackground = /\bfn\s+background\s*\(/.test(wgsl);
924
+ const hasForeground = /\bfn\s+foreground\s*\(/.test(wgsl);
925
+ if (!hasBackground && !hasForeground) {
926
+ return {
927
+ ok: false,
928
+ diagnostics: [
929
+ "an effect must define fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f " +
930
+ "or fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f (or both)",
931
+ ],
932
+ mounts: noMounts,
933
+ };
888
934
  }
935
+ const mounts = { background: hasBackground, foreground: hasForeground };
889
936
  // ── Params: codegen a WGSL struct and mirror its uniform layout on the CPU.
890
937
  // Fields are emitted in declaration order; offsets follow WGSL's natural
891
938
  // uniform rules (f32 align 4, vec3f align 16 size 12), computed identically
@@ -896,7 +943,7 @@ export class Engine {
896
943
  let cursor = 0;
897
944
  for (const [name, value] of entries) {
898
945
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
899
- return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`] };
946
+ return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`], mounts };
900
947
  }
901
948
  const isVec = typeof value !== "number";
902
949
  const align = isVec ? 16 : 4;
@@ -917,14 +964,14 @@ export class Engine {
917
964
  }
918
965
  }
919
966
  const paramsDecl = entries.length
920
- ? `struct BgParams {\n${fields.join("\n")}\n}\n@group(0) @binding(7) var<uniform> params: BgParams;\n`
967
+ ? `struct EffectParams {\n${fields.join("\n")}\n}\n@group(0) @binding(7) var<uniform> params: EffectParams;\n`
921
968
  : "";
922
969
  // ── Compile with validation captured, not thrown at the console. Line
923
970
  // numbers in diagnostics are rebased to the USER's source.
924
- const source = buildCompositeShader({ wgsl, paramsDecl });
971
+ const source = buildCompositeShader({ wgsl, paramsDecl, hasBackground, hasForeground });
925
972
  const userLineOffset = source.slice(0, source.indexOf(wgsl)).split("\n").length - 1;
926
973
  this.device.pushErrorScope("validation");
927
- const module = this.device.createShaderModule({ label: "composite shader (bg effect)", code: source });
974
+ const module = this.device.createShaderModule({ label: "composite shader (effect)", code: source });
928
975
  const info = await module.getCompilationInfo();
929
976
  const scopeErr = await this.device.popErrorScope();
930
977
  const diagnostics = info.messages
@@ -933,7 +980,7 @@ export class Engine {
933
980
  if (diagnostics.length === 0 && scopeErr)
934
981
  diagnostics.push(scopeErr.message);
935
982
  if (diagnostics.length > 0)
936
- return { ok: false, diagnostics };
983
+ return { ok: false, diagnostics, mounts };
937
984
  let identity;
938
985
  let gamma;
939
986
  try {
@@ -950,36 +997,40 @@ export class Engine {
950
997
  primitive: { topology: "triangle-list" },
951
998
  });
952
999
  [identity, gamma] = await Promise.all([
953
- make(false, "composite pipeline (bg effect, gamma=1)"),
954
- make(true, "composite pipeline (bg effect, gamma!=1)"),
1000
+ make(false, "composite pipeline (effect, gamma=1)"),
1001
+ make(true, "composite pipeline (effect, gamma!=1)"),
955
1002
  ]);
956
1003
  }
957
1004
  catch (e) {
958
- return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] };
1005
+ return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts };
959
1006
  }
960
1007
  // ── Swap — only now does the old effect (and its params buffer) go away.
961
- this.backgroundEffect?.paramsBuffer?.destroy();
1008
+ this.effect?.paramsBuffer?.destroy();
962
1009
  let paramsBuffer = null;
963
1010
  if (entries.length) {
964
1011
  paramsBuffer = this.device.createBuffer({
965
- label: "bg effect params",
1012
+ label: "effect params",
966
1013
  size: paramsData.byteLength,
967
1014
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
968
1015
  });
969
1016
  this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
970
1017
  }
971
- this.backgroundEffect = { wgsl, paramLayout: layout, paramsBuffer, paramsData };
1018
+ this.effect = { wgsl, paramLayout: layout, paramsBuffer, paramsData, hasBackground, hasForeground };
972
1019
  this.compositePipelineIdentity = identity;
973
1020
  this.compositePipelineGamma = gamma;
974
- this.bgEffectEpochMs = performance.now();
1021
+ this.effectEpochMs = performance.now();
975
1022
  this.rebuildCompositeBindGroup();
976
1023
  this.writeCompositeViewUniforms();
977
- return { ok: true, diagnostics: [] };
1024
+ return { ok: true, diagnostics: [], mounts };
1025
+ }
1026
+ /** Which mounts the installed effect declared. Both false when none is set. */
1027
+ getEffectMounts() {
1028
+ return { background: this.effect?.hasBackground ?? false, foreground: this.effect?.hasForeground ?? false };
978
1029
  }
979
- /** Write one background-effect param (declared at setBackgroundEffect) — a
980
- * uniform write, no recompile; the instant tier, like setStyleParam. */
981
- setBackgroundEffectParam(name, value) {
982
- const fx = this.backgroundEffect;
1030
+ /** Write one effect param (declared at setEffect) — a uniform write, no
1031
+ * recompile; the instant tier, like setStyleParam. */
1032
+ setEffectParam(name, value) {
1033
+ const fx = this.effect;
983
1034
  if (!fx || !fx.paramsBuffer)
984
1035
  return;
985
1036
  const slot = fx.paramLayout.get(name);
@@ -995,6 +1046,92 @@ export class Engine {
995
1046
  this.device.queue.writeBuffer(fx.paramsBuffer, 0, fx.paramsData);
996
1047
  }
997
1048
  /** Patch bloom; GPU uniforms update immediately if `init()` has run. */
1049
+ /** Camera depth of field (see DepthOfFieldOptions). Free while disabled —
1050
+ * the scene pass only stores its depth buffer on frames the gather reads. */
1051
+ setDepthOfField(patch) {
1052
+ this.depthOfField = { ...this.depthOfField, ...patch };
1053
+ if (!this.device || !this.dofUniformBuffer)
1054
+ return;
1055
+ if (this.depthOfField.enabled) {
1056
+ this.writeDepthOfFieldUniforms();
1057
+ }
1058
+ else {
1059
+ // One last write so the shader's uniform branch reads a clean zero.
1060
+ this.dofUniformData[0] = 0;
1061
+ this.device.queue.writeBuffer(this.dofUniformBuffer, 0, this.dofUniformData);
1062
+ }
1063
+ }
1064
+ getDepthOfField() {
1065
+ return { ...this.depthOfField };
1066
+ }
1067
+ /** Auto-focus target: the camera-space depth span of the first visible
1068
+ * character's bones. Focus sits at the span's midpoint; the range covers the
1069
+ * span plus a margin for what bones don't reach (shoes, hair, cloth). */
1070
+ getModelBodyFocus() {
1071
+ if (!this.camera)
1072
+ return null;
1073
+ const view = this.camera.getViewMatrix().values;
1074
+ for (const inst of this.modelInstances.values()) {
1075
+ if (!inst.model.visible || inst.isStage)
1076
+ continue;
1077
+ const model = inst.model;
1078
+ const matrices = model.getWorldMatrices();
1079
+ if (matrices.length === 0)
1080
+ continue;
1081
+ const scale = model.scale;
1082
+ const local = this.dofFocusScratch;
1083
+ let minDepth = Infinity;
1084
+ let maxDepth = -Infinity;
1085
+ for (const matrix of matrices) {
1086
+ const values = matrix.values;
1087
+ // Bone matrices are model-space; apply the same root transform the
1088
+ // renderer bakes into skinning, then take camera-space z.
1089
+ local.setXYZ(values[12] * scale, values[13] * scale, values[14] * scale);
1090
+ Quat.rotateVecInto(model.rotation, local, local);
1091
+ const x = model.position.x + local.x;
1092
+ const y = model.position.y + local.y;
1093
+ const z = model.position.z + local.z;
1094
+ const depth = view[2] * x + view[6] * y + view[10] * z + view[14];
1095
+ if (!Number.isFinite(depth) || depth <= this.camera.near)
1096
+ continue;
1097
+ minDepth = Math.min(minDepth, depth);
1098
+ maxDepth = Math.max(maxDepth, depth);
1099
+ }
1100
+ if (Number.isFinite(minDepth) && Number.isFinite(maxDepth)) {
1101
+ const span = Math.max(0, maxDepth - minDepth);
1102
+ const meshMargin = Math.max(2.0, span * 0.15);
1103
+ return { distance: (minDepth + maxDepth) * 0.5, range: Math.max(2.0, span + meshMargin) };
1104
+ }
1105
+ }
1106
+ return null;
1107
+ }
1108
+ writeDepthOfFieldUniforms() {
1109
+ if (!this.device || !this.dofUniformBuffer)
1110
+ return;
1111
+ const d = this.depthOfField;
1112
+ const u = this.dofUniformData;
1113
+ // `d.enabled &&`, because a foreground effect also drives this write (for
1114
+ // projA/projB alone) and auto-focus walks every visible character's bones —
1115
+ // work nothing would read with the gather switched off.
1116
+ const auto = d.enabled && d.focusMode === "auto" ? this.getModelBodyFocus() : null;
1117
+ u[0] = d.enabled ? 1 : 0;
1118
+ u[1] = auto?.distance ?? Math.max(d.focusDistance, 0.05);
1119
+ // In auto mode the authored range is a floor — the sharp band never cuts
1120
+ // into the character's own depth span.
1121
+ u[2] = Math.max(d.focusRange, auto?.range ?? 0.02, 0.02);
1122
+ u[3] = Math.max(d.aperture, 0);
1123
+ u[4] = Math.max(d.maxBlurRadius, 0);
1124
+ u[5] = Math.min(12, Math.max(3, Math.round(d.bladeCount)));
1125
+ u[6] = d.quality === "performance" ? 8 : d.quality === "cinematic" ? 24 : 16;
1126
+ u[7] = 1; // anamorphic ratio, reserved (the shader clamps ≥ 0.25)
1127
+ // viewZ = projB / (z − projA), the inverse of perspectiveInto's z mapping.
1128
+ // near/far track the camera radius, so these refresh every enabled frame.
1129
+ const near = this.camera.near;
1130
+ const far = this.camera.far;
1131
+ u[8] = (far + near) / (far - near);
1132
+ u[9] = (-2 * near * far) / (far - near);
1133
+ this.device.queue.writeBuffer(this.dofUniformBuffer, 0, u);
1134
+ }
998
1135
  setBloomOptions(patch) {
999
1136
  const b = this.bloomSettings;
1000
1137
  if (patch.enabled !== undefined)
@@ -1794,11 +1931,18 @@ export class Engine {
1794
1931
  // mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
1795
1932
  this.compositeUniformBuffer = this.device.createBuffer({
1796
1933
  label: "composite view uniforms",
1797
- // 10 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
1934
+ // 11 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
1798
1935
  // (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
1799
- // (time, _, canvas width, canvas height) for user background effects ·
1800
- // three grade vectors (CDL offset+contrast, power+saturation, slope+flag).
1801
- size: 160,
1936
+ // (time, _, canvas width, canvas height) for user effects · three grade
1937
+ // vectors (CDL offset+contrast, power+saturation, slope+flag) · camera
1938
+ // world position, for an effect placing itself in the scene.
1939
+ size: 176,
1940
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1941
+ });
1942
+ this.dofUniformBuffer = this.device.createBuffer({
1943
+ label: "depth of field uniforms",
1944
+ // 3 × vec4f — see the dofU comment in composite.ts.
1945
+ size: 48,
1802
1946
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1803
1947
  });
1804
1948
  this.bgParamsDummyBuffer = this.device.createBuffer({
@@ -1823,6 +1967,15 @@ export class Engine {
1823
1967
  // User background-effect params — dummy buffer when no effect is set. The
1824
1968
  // layout is explicit, so the base shader legally ignores the binding.
1825
1969
  { binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
1970
+ // The scene pass's MSAA depth, depth-only aspect — read by the DoF
1971
+ // gather. Contents are undefined while DoF is off (the scene pass
1972
+ // discards depth then), and the shader never reads it then either.
1973
+ {
1974
+ binding: 8,
1975
+ visibility: GPUShaderStage.FRAGMENT,
1976
+ texture: { sampleType: "depth", viewDimension: "2d", multisampled: true },
1977
+ },
1978
+ { binding: 9, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
1826
1979
  ],
1827
1980
  });
1828
1981
  this.fallbackEquirectTexture = this.device.createTexture({
@@ -2034,9 +2187,12 @@ export class Engine {
2034
2187
  size: [width, height],
2035
2188
  sampleCount: Engine.MULTISAMPLE_COUNT,
2036
2189
  format: "depth24plus-stencil8",
2037
- usage: GPUTextureUsage.RENDER_ATTACHMENT,
2190
+ // TEXTURE_BINDING for the DoF gather — a usage flag, not a copy; the
2191
+ // zero-cost-when-off story lives in depthStoreOp, not here.
2192
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2038
2193
  });
2039
2194
  const depthTextureView = this.depthTexture.createView();
2195
+ this.depthReadView = this.depthTexture.createView({ aspect: "depth-only" });
2040
2196
  // storeOp="discard" on MSAA views keeps per-sample data in Apple TBDR tile memory —
2041
2197
  // only the resolveTarget (hdrResolveTexture / maskResolveView) gets written to RAM.
2042
2198
  // With storeOp="store" Safari's Metal backend spills the full MS buffer every frame
@@ -2421,6 +2577,12 @@ export class Engine {
2421
2577
  hasCameraVmd() {
2422
2578
  return this.cameraAnimation !== null;
2423
2579
  }
2580
+ /** Seconds the loaded camera VMD runs for — its last keyframe — or 0 with none
2581
+ * loaded. A timeline cannot draw a lane to scale without it, and the camera's
2582
+ * length is its own: it does not have to match any model's clip. */
2583
+ getCameraVmdDuration() {
2584
+ return this.cameraAnimation?.duration ?? 0;
2585
+ }
2424
2586
  /** Drop the loaded camera VMD and return to orbit control. */
2425
2587
  clearCameraVmd() {
2426
2588
  this.cameraAnimation = null;
@@ -2471,24 +2633,6 @@ export class Engine {
2471
2633
  setCameraFov(fov) {
2472
2634
  this.camera.fov = fov;
2473
2635
  }
2474
- /** Fire a one-shot particle effect (see ParticleEmitOptions): "burst" scatters
2475
- * glowing sparks from the given world points, "converge" condenses them onto
2476
- * the points. Pair with Model.sampleSurfacePoints for dissolve/materialize
2477
- * transitions shaped like the model. Stateless GPU particles — the emission
2478
- * cleans itself up when it expires. */
2479
- emitParticles(opts) {
2480
- if (!this.device)
2481
- return;
2482
- if (!this.particles) {
2483
- this.particles = new ParticleSystem(this.device, this.cameraUniformBuffer, {
2484
- hdr: this.hdrFormat,
2485
- mask: Engine.BLOOM_MASK_FORMAT,
2486
- depth: "depth24plus-stencil8",
2487
- sampleCount: Engine.MULTISAMPLE_COUNT,
2488
- });
2489
- }
2490
- this.particles.emit(opts, performance.now() / 1000);
2491
- }
2492
2636
  // Step 5: Create lighting buffers
2493
2637
  setupLighting() {
2494
2638
  this.lightUniformBuffer = this.device.createBuffer({
@@ -2625,8 +2769,6 @@ export class Engine {
2625
2769
  Engine.instance = null;
2626
2770
  if (this.camera)
2627
2771
  this.camera.detachControl();
2628
- this.particles?.dispose();
2629
- this.particles = null;
2630
2772
  // Remove raycasting event listeners
2631
2773
  if (this.onRaycast) {
2632
2774
  this.canvas.removeEventListener("dblclick", this.handleCanvasDoubleClick);
@@ -4209,6 +4351,22 @@ export class Engine {
4209
4351
  }
4210
4352
  this.updateCameraUniforms();
4211
4353
  this.updateShadowLightVP();
4354
+ // Depth of field's entire disabled cost is this branch: depth stays in
4355
+ // TBDR tile memory (discard) unless something in the composite reads it this
4356
+ // frame. Two things can — the DoF gather, and the depth handed to a
4357
+ // foreground effect — and either one makes the pass store it.
4358
+ //
4359
+ // The uniform refresh is shared for the same reason: linearDepth() inverts
4360
+ // the z-buffer with projA/projB out of dofU[2], which track the camera's
4361
+ // near/far and so must be rewritten every frame either reader is live. A
4362
+ // foreground with a stale pair would read metres from the wrong frustum. The
4363
+ // write leaves dofU[0].x at 0 while DoF is off, so refreshing it does not
4364
+ // switch the gather on.
4365
+ const dofOn = this.depthOfField.enabled;
4366
+ const depthRead = dofOn || (this.effect?.hasForeground ?? false);
4367
+ this.renderPassDescriptor.depthStencilAttachment.depthStoreOp = depthRead ? "store" : "discard";
4368
+ if (depthRead)
4369
+ this.writeDepthOfFieldUniforms();
4212
4370
  const encoder = this.device.createCommandEncoder();
4213
4371
  // GPU vertex morphs: write morphed positions into vertex buffers before any pass reads
4214
4372
  // them. WebGPU inserts the storage→vertex barrier between this pass and the render passes.
@@ -4257,9 +4415,6 @@ export class Engine {
4257
4415
  if (inst.model.visible)
4258
4416
  this.renderModelTransparentPhase(pass, inst);
4259
4417
  });
4260
- // Particle emissions last: additive light over everything, depth-tested
4261
- // against the scene, feeding the bloom mask so the sparks glow.
4262
- this.particles?.draw(pass, performance.now() / 1000);
4263
4418
  pass.end();
4264
4419
  // Bloom pyramid (EEVEE 3.6):
4265
4420
  // 1. Blit: HDR → bloomDown[0] (Karis prefilter, half-res)
@@ -4838,7 +4993,7 @@ export class Engine {
4838
4993
  // is LEFT-HANDED (+Z forward, see Mat4.lookAtInto), so the world-space
4839
4994
  // right/up/FORWARD vectors are rows 0/1/2 of its rotation block directly
4840
4995
  // (column-major storage: row i = values[i], values[i+4], values[i+8]).
4841
- if ((this.backdropEquirectView || this.backgroundEffect) && this.compositeUniformBuffer) {
4996
+ if ((this.backdropEquirectView || this.effect) && this.compositeUniformBuffer) {
4842
4997
  const v = viewMatrix.values;
4843
4998
  const u = this.compositeUniformData;
4844
4999
  const tanHalf = Math.tan((this.camera.fov ?? Math.PI / 4) / 2);
@@ -4856,9 +5011,15 @@ export class Engine {
4856
5011
  u[22] = v[10];
4857
5012
  u[23] = 0;
4858
5013
  // Effect clock + canvas size (viewU[6]) — written on the same refresh.
4859
- u[24] = (performance.now() - this.bgEffectEpochMs) / 1000;
5014
+ u[24] = (performance.now() - this.effectEpochMs) / 1000;
4860
5015
  u[26] = this.canvas.width;
4861
5016
  u[27] = this.canvas.height;
5017
+ // Camera world position (viewU[10]) — the other half of bgWorldPos. It
5018
+ // rides this refresh rather than writeCompositeViewUniforms because it
5019
+ // changes every frame the camera does, exactly like the basis above.
5020
+ u[40] = cameraPos.x;
5021
+ u[41] = cameraPos.y;
5022
+ u[42] = cameraPos.z;
4862
5023
  this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u);
4863
5024
  }
4864
5025
  }
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAMD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI1C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAgBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AA6BD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAuPlD,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAMD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI1C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAgBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AA6BD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAkQlD,CAAA"}
@@ -126,6 +126,16 @@ export const NODE_REGISTRY = {
126
126
  outputs: { color: "color" },
127
127
  contextOutputs: { color: "material.diffuseColor" },
128
128
  },
129
+ // The PMX material's sphere map, which is where an MMD model keeps its
130
+ // highlights — every PMX ships one, and hair without it reads flat. The mode
131
+ // is the material's own (.sph multiplies the shaded base, .spa adds a
132
+ // highlight), so a graph asks for the effect and the model decides which; a
133
+ // material with no sphere texture is an exact no-op.
134
+ sphere_map: {
135
+ inputs: { base: C([0, 0, 0], true), strength: F(1) },
136
+ outputs: { color: "color" },
137
+ emit: (a) => `pmx_sphere_map(${a.base}, ${a.strength}, n)`,
138
+ },
129
139
  // ── Literals as nodes (for editor ergonomics; inlined literals work too) ──
130
140
  value: { inputs: { value: F(0) }, outputs: { value: "float" }, emit: (a) => a.value },
131
141
  rgb: { inputs: { color: C() }, outputs: { color: "color" }, emit: (a) => a.color },
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, type ColorGradingOptions, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, type BackgroundEffectParamValue, type BackgroundEffectResult, } from "./engine";
1
+ export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_DEPTH_OF_FIELD_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, type ColorGradingOptions, type EngineStats, type EngineOptions, type BloomOptions, type DepthOfFieldOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, type EffectParamValue, type EffectResult, } from "./engine";
2
2
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
3
3
  export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, type CompileResult, type StyleSlot, } from "./graph/compile";
4
4
  export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
@@ -26,5 +26,4 @@ export { PmxLoader } from "./pmx-loader";
26
26
  export { CameraAnimation, type CameraPose } from "./camera-animation";
27
27
  export { RezePhysics } from "./physics";
28
28
  export type { WindOptions } from "./physics/world";
29
- export { type ParticleEmitOptions } from "./particles";
30
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,8BAA8B,EAC9B,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EACL,KAAK,EACL,uBAAuB,EACvB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,aAAa,GACnB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAA;AACrE,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC5H,OAAO,EACL,GAAG,EACH,iBAAiB,EACjB,wBAAwB,EACxB,mCAAmC,GACpC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, } from "./engine";
1
+ export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_DEPTH_OF_FIELD_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, } from "./engine";
2
2
  export { parsePmxFolderInput, pmxFileAtRelativePath } from "./folder-upload";
3
3
  export { compileGraph, validateGraph, assignStyleSlots, } from "./graph/compile";
4
4
  export { NODE_REGISTRY } from "./graph/registry";
package/dist/model.d.ts CHANGED
@@ -312,11 +312,6 @@ export declare class Model {
312
312
  getWorldMatrices(): Mat4[];
313
313
  getBoneWorldMatrices(): Float32Array;
314
314
  getBoneInverseBindMatrices(): Float32Array;
315
- /** World-space positions of `count` vertices sampled evenly across the mesh
316
- * under the CURRENT pose — the model's skinning replayed on the CPU for
317
- * this one call, root transform included. Feed to Engine.emitParticles for
318
- * dissolve/materialize effects shaped like the model itself. */
319
- sampleSurfacePoints(count: number): Float32Array;
320
315
  getSkinMatrices(): Float32Array;
321
316
  setMorphWeight(name: string, weight: number, durationMs?: number): void;
322
317
  private applyMorphs;