reze-engine 0.66.3 → 0.66.5

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
@@ -79,7 +79,7 @@ import {
79
79
  } from "./shaders/lights"
80
80
  import { buildLightGrid } from "./light-grid"
81
81
  import { groundShaderWgsl, GROUND_NOISE_BAKE_WGSL, GROUND_NOISE_SIZE } from "./shaders/passes/ground"
82
- import { outlineShaderWgsl, RZ_OUTLINE_DISSOLVE_OFFSET } from "./shaders/passes/outline"
82
+ import { outlineShaderWgsl, RZ_OUTLINE_COLOR_OFFSET, RZ_OUTLINE_DISSOLVE_OFFSET, RZ_OUTLINE_WIDTH_OFFSET } from "./shaders/passes/outline"
83
83
  import { transparentDepthPrepassWgsl } from "./shaders/passes/depth-prepass"
84
84
  import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection"
85
85
  import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo"
@@ -3324,6 +3324,51 @@ export class Engine {
3324
3324
  this.bundlesDirty = true
3325
3325
  }
3326
3326
 
3327
+ /**
3328
+ * Every outline's width, as a multiple of what each PMX material asked for
3329
+ * (its edgeSize): 1 is the author's own, 0 draws none. A multiplier rather
3330
+ * than a width because a model's lines are not one width — the hair's rim
3331
+ * and a sleeve's crease differ on purpose, and scaling keeps that.
3332
+ *
3333
+ * A write, not a re-record: one float in each hull's uniform, so a slider
3334
+ * can drive it. Hulls built later take the current value at creation.
3335
+ */
3336
+ private outlineWidth = 1
3337
+ setOutlineWidth(scale: number): void {
3338
+ const v = Number.isFinite(scale) ? Math.max(0, scale) : 1
3339
+ if (this.outlineWidth === v) return
3340
+ this.outlineWidth = v
3341
+ if (!this.device) return
3342
+ const one = new Float32Array([v])
3343
+ for (const inst of this.modelInstances.values()) {
3344
+ for (const buffer of inst.outlineUniformBuffers) {
3345
+ this.device.queue.writeBuffer(buffer, RZ_OUTLINE_WIDTH_OFFSET, one)
3346
+ }
3347
+ }
3348
+ }
3349
+
3350
+ /**
3351
+ * One colour for every outline, in place of each PMX material's edge colour;
3352
+ * null hands each material its own back. Written as the edge colour is —
3353
+ * the values a PMX would carry — so a picked black is MMD's black. The
3354
+ * material's edge ALPHA is kept: only the hue is the scene's.
3355
+ *
3356
+ * A write, like setOutlineWidth: hulls built later take it at creation.
3357
+ */
3358
+ private outlineColor: [number, number, number, number] = [0, 0, 0, 0]
3359
+ setOutlineColor(color: Vec3 | { x: number; y: number; z: number } | null): void {
3360
+ const next: [number, number, number, number] = color ? [color.x, color.y, color.z, 1] : [0, 0, 0, 0]
3361
+ if (next.every((v, i) => v === this.outlineColor[i])) return
3362
+ this.outlineColor = next
3363
+ if (!this.device) return
3364
+ const data = new Float32Array(next)
3365
+ for (const inst of this.modelInstances.values()) {
3366
+ for (const buffer of inst.outlineUniformBuffers) {
3367
+ this.device.queue.writeBuffer(buffer, RZ_OUTLINE_COLOR_OFFSET, data)
3368
+ }
3369
+ }
3370
+ }
3371
+
3327
3372
  /**
3328
3373
  * Draw the id attachment instead of the scene.
3329
3374
  *
@@ -9981,6 +10026,46 @@ export class Engine {
9981
10026
  if (this.cameraPoseOverride) this.camera.setVmdPose(this.cameraPoseOverride)
9982
10027
  }
9983
10028
 
10029
+ /**
10030
+ * The host's adjustment to a camera track, added to every pose it samples.
10031
+ *
10032
+ * A downloaded camera motion is framed for whoever it was made with: a
10033
+ * shorter model wants it lower, a wider stage wants it further back. This
10034
+ * moves the shot without touching its ANGLE — the motion's rotation and roll
10035
+ * are its choreography, and they stay the track's.
10036
+ *
10037
+ * - `target`: world units, added to the look-at point, so the whole shot
10038
+ * slides with the angle unchanged.
10039
+ * - `distance`: positive pulls the eye further back along its own line of
10040
+ * sight. VMD distances are signed (negative is behind the target, the usual
10041
+ * case), so "further" means away from zero on whichever side it is.
10042
+ * - `fov`: radians, added to the track's lens, never below a sliver.
10043
+ *
10044
+ * Only a loaded track reads it: the orbit has its own sliders, and a pose
10045
+ * forced through setCameraPose is a statement of where the camera IS.
10046
+ */
10047
+ private cameraTrackOffset = { target: new Vec3(0, 0, 0), distance: 0, fov: 0 }
10048
+ setCameraTrackOffset(offset: { target?: { x: number; y: number; z: number }; distance?: number; fov?: number } | null): void {
10049
+ const t = offset?.target
10050
+ this.cameraTrackOffset = {
10051
+ target: new Vec3(t?.x ?? 0, t?.y ?? 0, t?.z ?? 0),
10052
+ distance: offset?.distance ?? 0,
10053
+ fov: offset?.fov ?? 0,
10054
+ }
10055
+ }
10056
+
10057
+ /** A sampled track pose with the host's offset laid on. */
10058
+ private offsetTrackPose(pose: CameraPose): CameraPose {
10059
+ const o = this.cameraTrackOffset
10060
+ if (o.distance === 0 && o.fov === 0 && o.target.x === 0 && o.target.y === 0 && o.target.z === 0) return pose
10061
+ return {
10062
+ target: new Vec3(pose.target.x + o.target.x, pose.target.y + o.target.y, pose.target.z + o.target.z),
10063
+ rotation: pose.rotation,
10064
+ distance: pose.distance + Math.sign(pose.distance || -1) * o.distance,
10065
+ fov: Math.min(Math.PI * 0.95, Math.max(0.01, pose.fov + o.fov)),
10066
+ }
10067
+ }
10068
+
9984
10069
  /** The pose currently forced from outside, or null when nothing is. */
9985
10070
  getCameraPoseOverride(): CameraPose | null {
9986
10071
  return this.cameraPoseOverride
@@ -10974,6 +11059,12 @@ export class Engine {
10974
11059
  { binding: 12, resource: { buffer: this.probeUniformBuffer } },
10975
11060
  ],
10976
11061
  })
11062
+ // The recorded phases bind these groups by value (sceneView), so a rebuild
11063
+ // they never hear about leaves them drawing with the old one: the first
11064
+ // light cookie landed on the ground (re-bound each frame) and not on the
11065
+ // cast, and a swapped sky kept reflecting the one it replaced. Only init,
11066
+ // a world install and the first cookie get here, so re-recording is cheap.
11067
+ this.bundlesDirty = true
10977
11068
  return true
10978
11069
  }
10979
11070
 
@@ -15458,8 +15549,14 @@ export class Engine {
15458
15549
  // block's layout would be 32 bytes of padding per outlined material
15459
15550
  // to carry one float. See RZ_OUTLINE_DISSOLVE_OFFSET.
15460
15551
  1,
15552
+ // The scene's width multiplier, at RZ_OUTLINE_WIDTH_OFFSET.
15553
+ this.outlineWidth,
15461
15554
  0,
15462
- 0,
15555
+ // The scene's colour override, at RZ_OUTLINE_COLOR_OFFSET.
15556
+ this.outlineColor[0],
15557
+ this.outlineColor[1],
15558
+ this.outlineColor[2],
15559
+ this.outlineColor[3],
15463
15560
  ])
15464
15561
  const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
15465
15562
  inst.gpuBuffers.push(outlineUniformBuffer)
@@ -17035,7 +17132,7 @@ export class Engine {
17035
17132
  } else if (this.camera.vmdDriven && this.cameraAnimation) {
17036
17133
  // Drive the shot from the camera VMD (synced to the animated model's clock).
17037
17134
  const pose = this.cameraAnimation.sample(this.transportTime())
17038
- if (pose) this.camera.setVmdPose(pose)
17135
+ if (pose) this.camera.setVmdPose(this.offsetTrackPose(pose))
17039
17136
  }
17040
17137
 
17041
17138
  // Before the cast is written, which carries every subject's dissolve to the
@@ -683,6 +683,21 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
683
683
  emit: (a) => `rz_glossy_direct(${a.normal}, l, v, sun, shadow, input.worldPos, 1.0 - (${a.smoothness}))`,
684
684
  takesLight: true,
685
685
  },
686
+ /**
687
+ * The lamps, handed to the graph — Shader Graph's Additional Lights: their
688
+ * diffuse at `normal`, and a Blinn-Phong highlight pow(N·H, exponent), both
689
+ * untinted. A graph that reads them combines them as its shader does (the
690
+ * game's water adds the diffuse into its body before its colour, and glints
691
+ * each lamp at exponent 128·smoothness), and owns the lamps' diffuse: the
692
+ * layer the engine otherwise adds after the graph is not added.
693
+ */
694
+ additional_lights: {
695
+ inputs: { exponent: F(64), normal: { type: "vector", contextDefault: "n" } },
696
+ outputs: { diffuse: "color", specular: "color" },
697
+ outputSelect: { diffuse: ".d", specular: ".s" },
698
+ emit: (a) => `rz_additional_lights(input.worldPos, ${a.normal}, v, ${a.exponent})`,
699
+ takesLight: true,
700
+ },
686
701
  reflection_probe: {
687
702
  inputs: {
688
703
  vector: { type: "vector", contextDefault: "reflect(-v, n)" },
@@ -1193,6 +1193,81 @@ fn rzLampsSpecular(p: vec3f, n: vec3f, v: vec3f, roughness: f32) -> vec3f {
1193
1193
  return s;
1194
1194
  }
1195
1195
 
1196
+ // ─── Additional lights, handed to the graph ─────────────
1197
+ //
1198
+ // Shader Graph's "Additional Lights" idiom: every lamp's diffuse and a
1199
+ // Blinn-Phong highlight, pow(N·H, e), for the graph to combine the way its
1200
+ // shader does. The game's water adds the diffuse into its body BEFORE tinting
1201
+ // it by its own colour and glints each lamp on that lobe — neither of which
1202
+ // the epilogue's layer (lamps × the slot-0 picture, added after) can say. So a
1203
+ // graph that reads them owns the lamps' diffuse and the epilogue adds none.
1204
+ //
1205
+ // Same reach as _rzLightOne: bulb, window, squared cone, cookie. A point
1206
+ // behind the surface gets neither term, as the game's step(N·L ≥ 0) has it.
1207
+
1208
+ fn _rzAddLightOne(i: u32, p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
1209
+ var out = _RzLampPair(vec3f(0.0), vec3f(0.0));
1210
+ if (!_rzLightReaches(i)) { return out; }
1211
+ let pr = _rzLightVec(i, 0u);
1212
+ let d = pr.xyz - p;
1213
+ let dist = length(d);
1214
+ if (dist >= pr.w) { return out; }
1215
+ let toLight = d / max(dist, 1e-4);
1216
+ let ndl = dot(n, toLight);
1217
+ if (ndl < 0.0) { return out; }
1218
+ let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
1219
+ let t2 = t * t;
1220
+ let window = 1.0 - t2 * t2;
1221
+ let bulb = rzLightNear(i);
1222
+ let falloff = window * window / max(dist * dist, bulb * bulb);
1223
+ let cone = rzLightCone(i);
1224
+ let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
1225
+ let reach = rzLightColor(i) * rzLightCookie(i, toLight) * (falloff * aim * aim);
1226
+ out.d = reach * ndl;
1227
+ out.s = reach * pow(saturate(dot(n, normalize(toLight + v))), e);
1228
+ return out;
1229
+ }
1230
+
1231
+ fn _rzAddLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
1232
+ var acc = _RzLampPair(vec3f(0.0), vec3f(0.0));
1233
+ var bits = bits0;
1234
+ loop {
1235
+ if (bits == 0u) { break; }
1236
+ let i = base + firstTrailingBit(bits);
1237
+ bits = bits & (bits - 1u);
1238
+ let one = _rzAddLightOne(i, p, n, v, e);
1239
+ acc.d = acc.d + one.d;
1240
+ acc.s = acc.s + one.s;
1241
+ }
1242
+ return acc;
1243
+ }
1244
+
1245
+ fn rz_additional_lights(p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
1246
+ var acc = _RzLampPair(vec3f(0.0), vec3f(0.0));
1247
+ let count = rzLightCount();
1248
+ let docs = _rzLightDocCount();
1249
+ if (docs > 0u) {
1250
+ let m = _rzLightCellMask(p);
1251
+ let w0 = _rzAddLightWord(m.lo.x, 0u, p, n, v, e);
1252
+ let w1 = _rzAddLightWord(m.lo.y, 32u, p, n, v, e);
1253
+ let w2 = _rzAddLightWord(m.lo.z, 64u, p, n, v, e);
1254
+ let w3 = _rzAddLightWord(m.lo.w, 96u, p, n, v, e);
1255
+ let w4 = _rzAddLightWord(m.hi.x, 128u, p, n, v, e);
1256
+ let w5 = _rzAddLightWord(m.hi.y, 160u, p, n, v, e);
1257
+ let w6 = _rzAddLightWord(m.hi.z, 192u, p, n, v, e);
1258
+ let w7 = _rzAddLightWord(m.hi.w, 224u, p, n, v, e);
1259
+ acc.d = w0.d + w1.d + w2.d + w3.d + w4.d + w5.d + w6.d + w7.d;
1260
+ acc.s = w0.s + w1.s + w2.s + w3.s + w4.s + w5.s + w6.s + w7.s;
1261
+ }
1262
+ for (var i = docs; i < count; i = i + 1u) {
1263
+ let one = _rzAddLightOne(i, p, n, v, e);
1264
+ acc.d = acc.d + one.d;
1265
+ acc.s = acc.s + one.s;
1266
+ }
1267
+ _rzLampDiffuseTaken = true;
1268
+ return acc;
1269
+ }
1270
+
1196
1271
  // ─── Lit: URP's PBR, term for term ─────────────
1197
1272
  //
1198
1273
  // What Unity's Lit shader (URP's UniversalFragmentPBR) computes, so a
@@ -27,6 +27,12 @@ import { DISSOLVE_WGSL } from "../materials/common"
27
27
  * edgeSize 16..20, so this is 20.
28
28
  */
29
29
  export const RZ_OUTLINE_DISSOLVE_OFFSET = 20
30
+ /** Where the scene-wide width multiplier sits, in bytes — the float after
31
+ * dissolve (Engine.setOutlineWidth). */
32
+ export const RZ_OUTLINE_WIDTH_OFFSET = 24
33
+ /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
34
+ * for the material's own edge colour (Engine.setOutlineColor). */
35
+ export const RZ_OUTLINE_COLOR_OFFSET = 32
30
36
 
31
37
  export function outlineShaderWgsl(): string {
32
38
  return /* wgsl */ `
@@ -40,7 +46,7 @@ struct CameraUniforms {
40
46
  viewportHeight: f32,
41
47
  };
42
48
 
43
- // THE HULL'S OWN BLOCK, not the material's. 32 bytes of edge data, which is all
49
+ // THE HULL'S OWN BLOCK, not the material's. 48 bytes of edge data, which is all
44
50
  // this pass shades with — and dissolve, which it has to obey.
45
51
  //
46
52
  // It reached for the material block's own layout first, declaring the skipped
@@ -53,8 +59,14 @@ struct MaterialUniforms {
53
59
  edgeColor: vec4f,
54
60
  edgeSize: f32,
55
61
  dissolve: f32,
56
- _padding2: f32,
62
+ /** The scene's width multiplier (setOutlineWidth), the same on every hull:
63
+ * scales the author's edgeSize rather than replacing it, so a model's thin
64
+ * and thick lines keep their proportion. */
65
+ widthScale: f32,
57
66
  _padding3: f32,
67
+ /** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
68
+ * material's own alpha still holds — a line its author made sheer stays so. */
69
+ colorOverride: vec4f,
58
70
  };
59
71
 
60
72
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
@@ -158,7 +170,7 @@ const RZ_OUTLINE_MIN_PX = 1.5;
158
170
  // multiplies it all, as vertex colour alpha does in the game.
159
171
  let w = max(clipPos.w, 1e-4);
160
172
  let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
161
- let nearNdc = viewNormal.xy * (material.edgeSize * outlineNormal.w * 4.0 / refViewport);
173
+ let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
162
174
  var ndc = nearNdc * (min(w, breakDepth) / w);
163
175
 
164
176
  // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
@@ -212,7 +224,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
212
224
  // alpha above), it is drawn right after the surface it traces so what lies
213
225
  // under a faded line is already there, and 4× MSAA would quantize a fade to
214
226
  // four steps.
215
- out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA * input.coverage);
227
+ let rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
228
+ out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
216
229
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
217
230
  ${sceneIdPadWgsl("out")} return out;
218
231
  }