reze-engine 0.68.7 → 0.68.9

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.
Files changed (62) hide show
  1. package/dist/engine.d.ts +2 -6
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +39 -41
  4. package/dist/graph/presets/pool_floor.d.ts +3 -0
  5. package/dist/graph/presets/pool_floor.d.ts.map +1 -0
  6. package/dist/graph/presets/pool_floor.js +42 -0
  7. package/dist/graph/presets/water.d.ts +3 -0
  8. package/dist/graph/presets/water.d.ts.map +1 -0
  9. package/dist/graph/presets/water.js +31 -0
  10. package/dist/graph/slots.d.ts.map +1 -1
  11. package/dist/graph/slots.js +5 -4
  12. package/dist/jelly-eyes.d.ts.map +1 -1
  13. package/dist/jelly-eyes.js +43 -17
  14. package/dist/physics/autofit.d.ts +147 -0
  15. package/dist/physics/autofit.d.ts.map +1 -0
  16. package/dist/physics/autofit.js +501 -0
  17. package/dist/shaders/agx-lut.d.ts +11 -0
  18. package/dist/shaders/agx-lut.d.ts.map +1 -0
  19. package/dist/shaders/agx-lut.js +29 -0
  20. package/dist/shaders/dfg_lut.d.ts +3 -0
  21. package/dist/shaders/dfg_lut.d.ts.map +1 -0
  22. package/dist/shaders/dfg_lut.js +129 -0
  23. package/dist/shaders/ltc_mag_lut.d.ts +3 -0
  24. package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
  25. package/dist/shaders/ltc_mag_lut.js +1033 -0
  26. package/dist/shaders/materials/common.d.ts +6 -0
  27. package/dist/shaders/materials/common.d.ts.map +1 -1
  28. package/dist/shaders/materials/common.js +67 -60
  29. package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
  30. package/dist/shaders/passes/depth-prepass.js +11 -3
  31. package/dist/shaders/passes/field-blit.d.ts +26 -0
  32. package/dist/shaders/passes/field-blit.d.ts.map +1 -0
  33. package/dist/shaders/passes/field-blit.js +65 -0
  34. package/dist/shaders/passes/ghosts.d.ts +22 -0
  35. package/dist/shaders/passes/ghosts.d.ts.map +1 -0
  36. package/dist/shaders/passes/ghosts.js +75 -0
  37. package/dist/shaders/passes/ground-noise.d.ts +7 -0
  38. package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
  39. package/dist/shaders/passes/ground-noise.js +88 -0
  40. package/dist/shaders/passes/outline.d.ts.map +1 -1
  41. package/dist/shaders/passes/outline.js +13 -2
  42. package/dist/shaders/passes/shadow.d.ts +1 -1
  43. package/dist/shaders/passes/shadow.d.ts.map +1 -1
  44. package/dist/shaders/passes/shadow.js +11 -3
  45. package/dist/shaders/passes/sim.d.ts +34 -0
  46. package/dist/shaders/passes/sim.d.ts.map +1 -0
  47. package/dist/shaders/passes/sim.js +169 -0
  48. package/dist/shaders/score-api.d.ts +10 -0
  49. package/dist/shaders/score-api.d.ts.map +1 -0
  50. package/dist/shaders/score-api.js +114 -0
  51. package/dist/shaders/shadow-api.d.ts +29 -0
  52. package/dist/shaders/shadow-api.d.ts.map +1 -0
  53. package/dist/shaders/shadow-api.js +112 -0
  54. package/package.json +1 -1
  55. package/src/engine.ts +39 -47
  56. package/src/graph/slots.ts +5 -4
  57. package/src/jelly-eyes.ts +41 -17
  58. package/src/shaders/materials/common.ts +68 -60
  59. package/src/shaders/passes/depth-prepass.ts +11 -3
  60. package/src/shaders/passes/outline.ts +13 -2
  61. package/src/shaders/passes/shadow.ts +11 -3
  62. package/dist/_dbg-loader.mjs +0 -1100
package/src/engine.ts CHANGED
@@ -354,11 +354,6 @@ type GroupInstall = {
354
354
  pipelineNoDepthWrite: GPURenderPipeline
355
355
  /** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
356
356
  overEyesPipeline?: GPURenderPipeline
357
- /** eye render-class only: the same pipeline with its cull flipped, for the
358
- * mirror pass. The eye is the ONE material class that culls a face, and a
359
- * reflection flips winding, so in the mirror the ordinary pipeline keeps
360
- * exactly the faces it exists to discard. */
361
- mirrorPipeline?: GPURenderPipeline
362
357
  uniformBuffer: GPUBuffer
363
358
  /** The group's own image maps, uploaded once per apply and owned here — the
364
359
  * install destroys them when it is replaced, so a re-apply cannot leak. */
@@ -8512,6 +8507,8 @@ export class Engine {
8512
8507
  { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
8513
8508
  { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
8514
8509
  { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
8510
+ // The model's ObjectLight, for its jelly eyes.
8511
+ { binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
8515
8512
  ],
8516
8513
  })
8517
8514
  const shadowShader = this.device.createShaderModule({
@@ -15180,6 +15177,20 @@ export class Engine {
15180
15177
  physics.setFloor(this.physicsFloor)
15181
15178
  }
15182
15179
 
15180
+ // Its ObjectLight: the world's ambient, and the layers its kind draws on —
15181
+ // the cast on the character layer as well as the default one. Words:
15182
+ // layers [0..3], ambient flag [4..7], SH [8..43], fill [44..47], jelly
15183
+ // eyes [48..71] — see materials/common.ts.
15184
+ const objectLight = new Float32Array(96)
15185
+ new Uint32Array(objectLight.buffer)[0] =
15186
+ isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0
15187
+ const lightBuffer = this.device.createBuffer({
15188
+ label: `${name}: object light`,
15189
+ size: objectLight.byteLength,
15190
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
15191
+ })
15192
+ this.device.queue.writeBuffer(lightBuffer, 0, objectLight)
15193
+
15183
15194
  // One per cascade: the shadow VERTEX shader reads a single matrix, and
15184
15195
  // which one is the only thing that differs between the cascade passes.
15185
15196
  const shadowBindGroups = SHADOW_CASCADES.map((_, ci) =>
@@ -15190,6 +15201,7 @@ export class Engine {
15190
15201
  { binding: 0, resource: { buffer: this.shadowCascadeVPBuffers[ci] } },
15191
15202
  { binding: 1, resource: { buffer: skinMatrixBuffer } },
15192
15203
  { binding: 2, resource: this.materialSampler },
15204
+ { binding: 3, resource: { buffer: lightBuffer } },
15193
15205
  ],
15194
15206
  }),
15195
15207
  )
@@ -15202,23 +15214,10 @@ export class Engine {
15202
15214
  { binding: 0, resource: { buffer: this.castShadowVPBuffer } },
15203
15215
  { binding: 1, resource: { buffer: skinMatrixBuffer } },
15204
15216
  { binding: 2, resource: this.materialSampler },
15217
+ { binding: 3, resource: { buffer: lightBuffer } },
15205
15218
  ],
15206
15219
  }),
15207
15220
  )
15208
-
15209
- // Its ObjectLight: the world's ambient, and the layers its kind draws on —
15210
- // the cast on the character layer as well as the default one. Words:
15211
- // layers [0..3], ambient flag [4..7], SH [8..43], fill [44..47], jelly
15212
- // eyes [48..71] — see materials/common.ts.
15213
- const objectLight = new Float32Array(96)
15214
- new Uint32Array(objectLight.buffer)[0] =
15215
- isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0
15216
- const lightBuffer = this.device.createBuffer({
15217
- label: `${name}: object light`,
15218
- size: objectLight.byteLength,
15219
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
15220
- })
15221
- this.device.queue.writeBuffer(lightBuffer, 0, objectLight)
15222
15221
  const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, lightBuffer)
15223
15222
 
15224
15223
  const pickPerInstanceBindGroup = this.device.createBindGroup({
@@ -16189,6 +16188,10 @@ export class Engine {
16189
16188
  // Alpha is the switch; clamp the rest so a stacked multiply cannot send a
16190
16189
  // colour negative and light the material from the inside.
16191
16190
  for (let k = 0; k < 11; k++) if (d[k] < 0) d[k] = 0
16191
+ // Alpha also stops at 1, as in MMD. Models ship additive +1 alpha morphs
16192
+ // (照れ on 拾星梦语) that MMD's clamp makes a no-op; let through, alpha 2
16193
+ // blends as src*2 - dst and burns every material white.
16194
+ if (d[3] > 1) d[3] = 1
16192
16195
  if (d[3] < 0.0001) inst.morphHiddenMaterials.add(target.materialName)
16193
16196
  let changed = false
16194
16197
  for (let k = 0; k < 11; k++) {
@@ -18021,10 +18024,9 @@ export class Engine {
18021
18024
  if (!result.ok) return
18022
18025
  const module = this.cachedShaderModule(result.wgsl, `style group: ${g.id} (${renderClass})`)
18023
18026
  await Promise.all([
18024
- this.createRenderClassPipeline(renderClass, module, false, true, false, blend),
18025
- this.createRenderClassPipeline(renderClass, module, false, false, false, blend),
18026
- renderClass === "hair" ? this.createRenderClassPipeline(renderClass, module, true, true, false, blend) : null,
18027
- renderClass === "eye" ? this.createRenderClassPipeline(renderClass, module, false, true, true, blend) : null,
18027
+ this.createRenderClassPipeline(renderClass, module, false, true, blend),
18028
+ this.createRenderClassPipeline(renderClass, module, false, false, blend),
18029
+ renderClass === "hair" ? this.createRenderClassPipeline(renderClass, module, true, true, blend) : null,
18028
18030
  ]).catch(() => {})
18029
18031
  }),
18030
18032
  )
@@ -18401,20 +18403,17 @@ export class Engine {
18401
18403
  // The single-sided twins a draw of this group will ask for (sidedPipeline),
18402
18404
  // built with their pipelines rather than synchronously at the first draw.
18403
18405
  const singleSided = inst.drawCalls.some((dc) => !dc.doubleSided && group.materials.includes(dc.materialName))
18404
- const variant = (overEyes: boolean, depthWrite: boolean, mirrored: boolean) =>
18405
- this.createRenderClassPipeline(renderClass, module, overEyes, depthWrite, mirrored, blend).then(async (p) => {
18406
- if (singleSided && !mirrored) await this.buildSidedTwin(p, false)
18406
+ const variant = (overEyes: boolean, depthWrite: boolean) =>
18407
+ this.createRenderClassPipeline(renderClass, module, overEyes, depthWrite, blend).then(async (p) => {
18408
+ if (singleSided) await this.buildSidedTwin(p, false)
18407
18409
  return p
18408
18410
  })
18409
18411
  const variants = Promise.all([
18410
- variant(false, true, false),
18412
+ variant(false, true),
18411
18413
  // The depth-write-off twin: stage transparency draws with it (see
18412
18414
  // pipelineForDrawCall), and a future OIT path would too.
18413
- variant(false, false, false),
18414
- renderClass === "hair" ? variant(true, true, false) : undefined,
18415
- // Only the eye needs one: every other class culls "none", which a flipped
18416
- // winding leaves alone.
18417
- renderClass === "eye" ? variant(false, true, true) : undefined,
18415
+ variant(false, false),
18416
+ renderClass === "hair" ? variant(true, true) : undefined,
18418
18417
  ])
18419
18418
  // Handled below either way; this keeps a rejection off the console while
18420
18419
  // the diagnostics are read.
@@ -18435,9 +18434,8 @@ export class Engine {
18435
18434
  let pipeline: GPURenderPipeline
18436
18435
  let pipelineNoDepthWrite: GPURenderPipeline
18437
18436
  let overEyesPipeline: GPURenderPipeline | undefined
18438
- let mirrorPipeline: GPURenderPipeline | undefined
18439
18437
  try {
18440
- ;[pipeline, pipelineNoDepthWrite, overEyesPipeline, mirrorPipeline] = await variants
18438
+ ;[pipeline, pipelineNoDepthWrite, overEyesPipeline] = await variants
18441
18439
  } catch (e) {
18442
18440
  diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
18443
18441
  return { ok: false, diagnostics, slotMap: result.slotMap }
@@ -18467,7 +18465,6 @@ export class Engine {
18467
18465
  pipeline,
18468
18466
  pipelineNoDepthWrite,
18469
18467
  overEyesPipeline,
18470
- mirrorPipeline,
18471
18468
  uniformBuffer,
18472
18469
  images: this.uploadGroupImages(group),
18473
18470
  imagesByMaterial: group.imagesByMaterial
@@ -18643,29 +18640,25 @@ export class Engine {
18643
18640
  /**
18644
18641
  * Render-class pipeline state. A group's compiled graph swaps the fragment shading; the
18645
18642
  * render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
18646
- * eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
18643
+ * eye = stencil stamp; hair = stencil-test (+ the over-eyes variant). Cull comes from
18644
+ * the material's double-sided flag (sidedPipeline).
18647
18645
  */
18648
18646
  private createRenderClassPipeline(
18649
18647
  renderClass: RenderClass,
18650
18648
  module: GPUShaderModule,
18651
18649
  overEyes: boolean,
18652
18650
  depthWrite = true,
18653
- mirrored = false,
18654
18651
  blend: StyleBlend = "over",
18655
18652
  ): Promise<GPURenderPipeline> {
18656
18653
  const base = {
18657
18654
  label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
18658
18655
  layout: this.mainPipelineLayout,
18659
18656
  vertex: { module, buffers: this.materialVertexBufferLayouts },
18660
- // The eye front-culls — the see-through-hair look is draw order plus that
18661
- // cull plus the stencil stamp, and it is the deployed appearance of every
18662
- // published scene. In the MIRROR the winding is flipped (determinant -1,
18663
- // reflection.ts), so the same cull keeps the back of the eyeball and
18664
- // discards its front. `mirrored` flips it back rather than changing what
18665
- // the camera sees.
18666
- primitive: {
18667
- cullMode: (renderClass === "eye" ? (mirrored ? "back" : "front") : "none") as GPUCullMode,
18668
- },
18657
+ // Every class draws both sides here and takes the PMX double-sided flag
18658
+ // from sidedPipeline, whose single-sided twin culls front (back in the
18659
+ // mirror's flipped winding) — the eye's old fixed cull. A double-sided
18660
+ // iris sheet keeps the faces a turn of the eye swings away.
18661
+ primitive: { cullMode: "none" as GPUCullMode },
18669
18662
  multisample: { count: Engine.MULTISAMPLE_COUNT },
18670
18663
  }
18671
18664
  const plainDepth: GPUDepthStencilState = {
@@ -18956,7 +18949,6 @@ export class Engine {
18956
18949
  if (dc.groupId) {
18957
18950
  const install = inst.styleGroups.get(dc.groupId)
18958
18951
  if (install) {
18959
- if (mirrored && install.mirrorPipeline) return install.mirrorPipeline
18960
18952
  return stageGlass ? install.pipelineNoDepthWrite : install.pipeline
18961
18953
  }
18962
18954
  }
@@ -102,13 +102,14 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode, blend: StyleBle
102
102
  // camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
103
103
  // untouched), unlike a front_facing test, which PMX's clockwise winding
104
104
  // inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
105
- // shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
106
- // by design, so its visible fragments are back faces with intended normals.
105
+ // shading near-black through sheer outer layers. Eye is exempt: its normals
106
+ // shade as authored.
107
107
  const flip =
108
108
  renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);"
109
109
  return `@fragment fn fs(input: VertexOutput) -> FSOut {
110
- ${PLAIN_SAMPLE} // MMD alpha semantics: material alpha × texture alpha.
111
- let alpha = material.alpha * tex_s.a;
110
+ ${PLAIN_SAMPLE} // MMD alpha semantics: material alpha × texture alpha, capped at 1 as MMD
111
+ // does (a PMX can author alpha above 1; blended, it burns the material white).
112
+ let alpha = saturate(material.alpha * tex_s.a);
112
113
  ${discard}
113
114
 
114
115
  // ── Dissolve ──
package/src/jelly-eyes.ts CHANGED
@@ -90,23 +90,23 @@ const HL_SCALE_MIN = 0.85
90
90
  const HL_SCALE_MAX = 1.2
91
91
  // The roll: while a closing expression holds, the iris turns about its eye
92
92
  // bone's X axis (左目/右目, the eyeball's centre), rolling down with the lid —
93
- // the angle at weight 1, and simultaneous expressions add. And it comes
94
- // forward a little (iris radii) with the expression and with the spring, so
95
- // the roll does not sink it into the white. The MME rolls with the lid
96
- // exactly, which the lid hides; here the roll follows on a spring of the same
97
- // pace and decay as the blink's, so as the eye opens the iris swings up past
98
- // rest and wobbles back, where it can be seen.
93
+ // the angle at weight 1, and simultaneous expressions add. It comes forward
94
+ // only with the spring (iris radii): pushed with the expression as well, a
95
+ // shut eye's iris came through a lid set close in front of it (白里黑子). The MME rolls with the lid
96
+ // exactly, which the lid hides; here the roll follows on a spring twice the
97
+ // blink's pace with its decay, so as the eye opens the iris swings up past rest
98
+ // and wobbles back, where it can be seen. At the blink's own pace it trailed
99
+ // the opening lid and swung a third of an iris past centre: a jump.
99
100
  const ROLL: Record<string, number> = {
100
101
  まばたき: -15, blink: -15, Blink: -15, eye_close: -15, Fcl_EYE_Close: -15, はぅ: -15, 笑い: -5,
101
102
  ウィンク: -5, ウィンク: -5, wink: -5, Wink: -5, ウィンク2: -15, ウィンク2: -15, blink_L: -15, Fcl_EYE_Close_L: -15,
102
103
  ウィンク右: -5, ウィンク右: -5, wink_R: -5, Wink_R: -5, ウィンク2右: -15, ウィンク2右: -15, blink_R: -15, Fcl_EYE_Close_R: -15,
103
104
  }
104
- const ROLL_HZ = SPRING_HZ
105
- const ROLL_ZETA = Math.LN2 / HALF_LIFE / (2 * Math.PI * SPRING_HZ)
106
- // How much of the spring's swing about the expression angle shows: the whole
107
- // of it carried the iris up most of its radius after a blink.
108
- const ROLL_SWING = 0.4
109
- const PUSH_EXPRESSION = 0.15
105
+ const ROLL_HZ = 6
106
+ const ROLL_ZETA = Math.LN2 / HALF_LIFE / (2 * Math.PI * ROLL_HZ)
107
+ // How much of the spring's swing about the expression angle shows: a
108
+ // twenty-fifth of an iris radius past centre after a blink.
109
+ const ROLL_SWING = 0.25
110
110
  const PUSH_ELASTIC = 0.04
111
111
  // A highlight sheet by name, for rigs that keep it on its own material.
112
112
  const HIGHLIGHT = /ハイライト|ハイライト|高光|highlight|hilight|(^|[^a-z])hl([^a-z]|$)/i
@@ -288,10 +288,23 @@ export class JellyEyes {
288
288
  const l = index("左目")
289
289
  const r = index("右目")
290
290
  this.headBone = index("頭")
291
- const names = model.getMorphing().morphs.map((m) => m.name)
292
- const find = (list: string[]) => list.map((n) => names.indexOf(n)).filter((i) => i >= 0)
291
+ const all = model.getMorphing().morphs
292
+ const names = all.map((m) => m.name)
293
+ // The weights read are resolved, so a group whose every target is named
294
+ // (Reze's まばたき is ウィンク2 + ウィンク2右) arrives through its
295
+ // leaves; counted as well, a blink rolled the iris twice as far.
296
+ const known = new Set([...BOTH, ...LEFT, ...RIGHT])
297
+ const resolved = (i: number) => {
298
+ const refs = all[i].groupReferences
299
+ return !!refs?.length && refs.every((r) => known.has(names[r.morphIndex]))
300
+ }
301
+ const morphIndex = (n: string) => {
302
+ const i = names.indexOf(n)
303
+ return i >= 0 && !resolved(i) ? i : -1
304
+ }
305
+ const find = (list: string[]) => list.map(morphIndex).filter((i) => i >= 0)
293
306
  this.morphs = [find(BOTH), find(LEFT), find(RIGHT)]
294
- const rolls = (list: string[]) => list.map((n) => ({ morph: names.indexOf(n), deg: ROLL[n] ?? 0 })).filter((r) => r.morph >= 0 && r.deg !== 0)
307
+ const rolls = (list: string[]) => list.map((n) => ({ morph: morphIndex(n), deg: ROLL[n] ?? 0 })).filter((r) => r.morph >= 0 && r.deg !== 0)
295
308
  this.rolls = [rolls([...BOTH, ...LEFT]), rolls([...BOTH, ...RIGHT])]
296
309
  this.rest = findIrises(model, l, r, both)
297
310
  // The bone that carries each iris, which may be under the eye bone.
@@ -423,7 +436,7 @@ export class JellyEyes {
423
436
  const pivot = rest.pivot
424
437
  o.set(pivot ? [pivot[0], pivot[1], pivot[2], (this.roll[e] + (s.roll - this.roll[e]) * ROLL_SWING) * DEG * g] : [0, 0, 0, 0], 80 + e * 4)
425
438
  const hs = (k: 0 | 1) => Math.max(HL_SCALE_MIN, Math.min(HL_SCALE_MAX, 1 + x * HL_SCALE[k] * g))
426
- o.set([hs(0), hs(1), (s.closure * PUSH_EXPRESSION + Math.min(Math.abs(x), SPRING_LIMIT) * PUSH_ELASTIC) * r * g, 0], 88 + e * 4)
439
+ o.set([hs(0), hs(1), Math.min(Math.abs(x), SPRING_LIMIT) * PUSH_ELASTIC * r * g, 0], 88 + e * 4)
427
440
  }
428
441
  }
429
442
 
@@ -550,6 +563,11 @@ function blink(s: EyeState, dt: number): void {
550
563
  * As draw ids (1-based over the materials that draw, as the engine numbers
551
564
  * MaterialUniforms.materialId), at most two. A rig whose highlight is painted
552
565
  * into the iris has none, and only the iris moves.
566
+ *
567
+ * A layer drawn on vertices an earlier layer already uses is a copy of that
568
+ * layer (白里黑子's ☆ and ❤ are star and heart irises on the iris's own
569
+ * vertices), so it moves with it. Slid as a highlight, a copy of the dome sits
570
+ * in front of the iris on one half, flipping sides with the tremble.
553
571
  */
554
572
  function findHighlights(model: Model, rest: [EyeRest, EyeRest]): number[] {
555
573
  const materials = model.getMaterials?.() ?? []
@@ -557,6 +575,7 @@ function findHighlights(model: Model, rest: [EyeRest, EyeRest]): number[] {
557
575
  const iris = new Set([...rest[0].verts, ...rest[1].verts])
558
576
  if (!materials.length || !indices.length || !iris.size) return []
559
577
  const layers: { id: number; name: string; minX: number; maxX: number; minY: number; maxY: number }[] = []
578
+ const used = new Set<number>()
560
579
  let start = 0
561
580
  let id = 0
562
581
  for (const m of materials) {
@@ -564,16 +583,21 @@ function findHighlights(model: Model, rest: [EyeRest, EyeRest]): number[] {
564
583
  if (count === 0) continue
565
584
  id++
566
585
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
586
+ let copy = true
587
+ const mine: number[] = []
567
588
  for (let k = start; k < start + count; k++) {
568
589
  const v = indices[k]
569
590
  if (!iris.has(v)) continue
591
+ if (!used.has(v)) copy = false
592
+ mine.push(v)
570
593
  minX = Math.min(minX, positions[v * 3])
571
594
  maxX = Math.max(maxX, positions[v * 3])
572
595
  minY = Math.min(minY, positions[v * 3 + 1])
573
596
  maxY = Math.max(maxY, positions[v * 3 + 1])
574
597
  }
575
598
  start += count
576
- if (minX <= maxX) layers.push({ id, name: m.name, minX, maxX, minY, maxY })
599
+ for (const v of mine) used.add(v)
600
+ if (minX <= maxX && !copy) layers.push({ id, name: m.name, minX, maxX, minY, maxY })
577
601
  }
578
602
  const out: number[] = []
579
603
  layers.forEach((l, i) => {
@@ -26,6 +26,73 @@ import { WORLD_AMBIENT_WGSL } from "../lights"
26
26
  // group(1): per-model skinning
27
27
  // group(2): per-material (diffuse texture + material uniforms)
28
28
 
29
+ /** The jelly eyes' vertex move (jelly-eyes.ts). Every pass that places model
30
+ * vertices takes it, or a pass that skips it draws the iris where it was —
31
+ * the shadow map then shades half the moved iris with its own unmoved copy.
32
+ * Reads `objectLight.jelly` and `material.materialId`, which the including
33
+ * module declares. */
34
+ export const JELLY_VERTEX_WGSL = /* wgsl */ `
35
+ fn _rzJellyTurn(q: vec2f, a: f32) -> vec2f {
36
+ let c = cos(a);
37
+ let s = sin(a);
38
+ return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);
39
+ }
40
+
41
+ /**
42
+ * Where the jelly puts an iris vertex. The iris is the part of the eye the eye
43
+ * bones carry, so it is moved as a whole, in bind space before skinning:
44
+ * scaled about its centre (the CPU composes the blink's spring, the lid's
45
+ * press and her motion's knock into one scale), turned and drifted a hair on
46
+ * the spring, brought forward a hair, and rolled about the eye bone with a
47
+ * closing expression — inside the white, which stays where it is. A highlight
48
+ * layer (this draw's material is one of jelly[1].w's) takes the iris's move,
49
+ * then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones
50
+ * (jelly[2].w, jelly[3].w), so nothing else on the face moves.
51
+ */
52
+ fn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {
53
+ let j = objectLight.jelly;
54
+ let ids = u32(j[1].w);
55
+ let mid = u32(material.materialId);
56
+ let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));
57
+ var out = p;
58
+ for (var e = 0u; e < 2u; e++) {
59
+ let bone = j[2u + e].w;
60
+ if (bone < 0.0) { continue; }
61
+ let b = u32(bone);
62
+ let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));
63
+ if (w <= 0.0) { continue; }
64
+ let c = j[e].xyz;
65
+ // A rig that carries both eyes on one bone: each vertex follows its own side.
66
+ if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }
67
+ if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }
68
+ let t = j[4u + e];
69
+ let m = j[6u + e];
70
+ let h = j[10u + e];
71
+ var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;
72
+ // Forward is −Z (MMD faces −Z).
73
+ var push = h.z;
74
+ if (hl) {
75
+ // Sliding over the iris's dome on its own, a highlight sheet a hair in
76
+ // front of it would sink into it where the dome bulges more and the two
77
+ // fight for depth; so it comes forward by as far as it slid — a slope of
78
+ // one, steeper than the dome anywhere it draws.
79
+ let before = q;
80
+ q = _rzJellyTurn(q * h.xy, t.w) + m.zw;
81
+ push += length(q - before);
82
+ }
83
+ var moved = vec3f(c.xy + q, p.z - push);
84
+ let roll = j[8u + e];
85
+ if (roll.w != 0.0) {
86
+ let d = moved - roll.xyz;
87
+ let rd = _rzJellyTurn(d.yz, roll.w);
88
+ moved = roll.xyz + vec3f(d.x, rd.x, rd.y);
89
+ }
90
+ out = mix(p, moved, w);
91
+ }
92
+ return out;
93
+ }
94
+ `
95
+
29
96
  export const COMMON_BINDINGS_WGSL = /* wgsl */ `
30
97
 
31
98
  struct CameraUniforms {
@@ -234,66 +301,7 @@ fn _rzCastShadow(wp: vec3f) -> f32 {
234
301
  struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
235
302
  @group(1) @binding(1) var<uniform> objectLight: ObjectLight;
236
303
 
237
- fn _rzJellyTurn(q: vec2f, a: f32) -> vec2f {
238
- let c = cos(a);
239
- let s = sin(a);
240
- return vec2f(c * q.x - s * q.y, s * q.x + c * q.y);
241
- }
242
-
243
- /**
244
- * Where the jelly puts an iris vertex. The iris is the part of the eye the eye
245
- * bones carry, so it is moved as a whole, in bind space before skinning:
246
- * scaled about its centre (the CPU composes the blink's spring, the lid's
247
- * press and her motion's knock into one scale), turned and drifted a hair on
248
- * the spring, brought forward a hair, and rolled about the eye bone with a
249
- * closing expression — inside the white, which stays where it is. A highlight
250
- * layer (this draw's material is one of jelly[1].w's) takes the iris's move,
251
- * then its own scale, turn and tremble, before the roll. Vertices take it by their weight on the eye bones
252
- * (jelly[2].w, jelly[3].w), so nothing else on the face moves.
253
- */
254
- fn rzJellyVertex(p: vec3f, joints: vec4<u32>, nw: vec4f) -> vec3f {
255
- let j = objectLight.jelly;
256
- let ids = u32(j[1].w);
257
- let mid = u32(material.materialId);
258
- let hl = mid > 0u && (mid == (ids & 4095u) || mid == (ids >> 12u));
259
- var out = p;
260
- for (var e = 0u; e < 2u; e++) {
261
- let bone = j[2u + e].w;
262
- if (bone < 0.0) { continue; }
263
- let b = u32(bone);
264
- let w = dot(nw, select(vec4f(0.0), vec4f(1.0), joints == vec4<u32>(b)));
265
- if (w <= 0.0) { continue; }
266
- let c = j[e].xyz;
267
- // A rig that carries both eyes on one bone: each vertex follows its own side.
268
- if (j[2].w == j[3].w && e == 0u && distance(p.xy, j[1].xy) < distance(p.xy, j[0].xy)) { continue; }
269
- if (j[2].w == j[3].w && e == 1u && distance(p.xy, j[0].xy) <= distance(p.xy, j[1].xy)) { continue; }
270
- let t = j[4u + e];
271
- let m = j[6u + e];
272
- let h = j[10u + e];
273
- var q = _rzJellyTurn((p.xy - c.xy) * t.xy, t.z) + m.xy;
274
- // Forward is −Z (MMD faces −Z).
275
- var push = h.z;
276
- if (hl) {
277
- // Sliding over the iris's dome on its own, a highlight sheet a hair in
278
- // front of it would sink into it where the dome bulges more and the two
279
- // fight for depth; so it comes forward by as far as it slid — a slope of
280
- // one, steeper than the dome anywhere it draws.
281
- let before = q;
282
- q = _rzJellyTurn(q * h.xy, t.w) + m.zw;
283
- push += length(q - before);
284
- }
285
- var moved = vec3f(c.xy + q, p.z - push);
286
- let roll = j[8u + e];
287
- if (roll.w != 0.0) {
288
- let d = moved - roll.xyz;
289
- let rd = _rzJellyTurn(d.yz, roll.w);
290
- moved = roll.xyz + vec3f(d.x, rd.x, rd.y);
291
- }
292
- out = mix(p, moved, w);
293
- }
294
- return out;
295
- }
296
-
304
+ ${JELLY_VERTEX_WGSL}
297
305
  /** The light arriving at a surface of this object facing n: its own ambient
298
306
  * where loaded data gave it one (a stage's probes, a character's), else the
299
307
  * world's. The same polynomial rzWorldAmbient evaluates. */
@@ -27,7 +27,7 @@
27
27
  // attachment, and that answer does not exist at import time. The constant could
28
28
  // not have taken the outputs at all, which is most of why it did not have them.
29
29
 
30
- import { DISSOLVE_WGSL } from "../materials/common"
30
+ import { DISSOLVE_WGSL, JELLY_VERTEX_WGSL } from "../materials/common"
31
31
  import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract"
32
32
 
33
33
  export function transparentDepthPrepassWgsl(): string {
@@ -44,13 +44,20 @@ struct MaterialUniforms {
44
44
  alpha: f32,
45
45
  _skip0: vec4f,
46
46
  _skip1: vec4f,
47
- _skip2: vec3f,
47
+ _skip2: f32,
48
+ materialId: f32,
49
+ _skip3: f32,
48
50
  dissolve: f32,
49
51
  };
50
52
 
51
53
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
52
54
  @group(0) @binding(2) var diffuseSampler: sampler;
53
55
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
56
+ // The model's ObjectLight, for its jelly eyes: depth goes down where the colour
57
+ // pass will draw the iris.
58
+ struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
59
+ @group(1) @binding(1) var<uniform> objectLight: ObjectLight;
60
+ ${JELLY_VERTEX_WGSL}
54
61
  @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
55
62
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
56
63
 
@@ -74,10 +81,11 @@ struct VSOut {
74
81
  @location(3) joints0: vec4<u32>,
75
82
  @location(4) weights0: vec4<f32>,
76
83
  ) -> VSOut {
77
- let pos4 = vec4f(position, 1.0);
78
84
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
79
85
  let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
80
86
  let w = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
87
+ var pos4 = vec4f(position, 1.0);
88
+ if (objectLight.jelly[0].w > 0.0) { pos4 = vec4f(rzJellyVertex(position, joints0, w), 1.0); }
81
89
  var skinned = vec4f(0.0);
82
90
  for (var i = 0u; i < 4u; i++) {
83
91
  skinned += (skinMats[joints0[i]] * pos4) * w[i];
@@ -197,12 +197,22 @@ fn safe_normal2(v: vec2f) -> vec2f {
197
197
  let w = max(clipPos.w, 1e-4);
198
198
  var ndc: vec2f;
199
199
  var coverage = 1.0;
200
+ // The scene's width dial splits around 1×. Above it, it widens the line
201
+ // before the minimum, as it always has. Below it, it thins the line AFTER
202
+ // the minimum: a line held at the minimum is drawn that wide and faded, and
203
+ // it still writes depth across all of it — so a dial at 0.1× or at 0.0001×
204
+ // left the same 1.5 px ring for whatever drew or read depth after it, and
205
+ // the dial did nothing visible. Thinned past the minimum instead, the hull
206
+ // is genuinely sub-pixel, the multisampling covers only what it covers, and
207
+ // the line fades with the dial and claims depth only where it is.
208
+ let widen = max(material.widthScale, 1.0);
209
+ let thin = min(material.widthScale, 1.0);
200
210
  if (material.textured > 0.5) {
201
211
  // The game's: a view-space offset of edgeSize world units along the view
202
212
  // normal, projected, then each axis held between 1.2 px and 1.2 px ×
203
213
  // (1 + _OutlineMaxOffsetMultiplier, 1.5) of a 1920-wide frame; a line the
204
214
  // minimum had to push fades by how far it was pushed.
205
- let off = viewNormal.xy * material.edgeSize * material.widthScale * outlineNormal.w;
215
+ let off = viewNormal.xy * material.edgeSize * widen * outlineNormal.w;
206
216
  let d1 = vec2f(off.x * camera.projection[0][0], off.y * camera.projection[1][1]) / w;
207
217
  // the minimum along the push direction (the game's dir · 2/screen · 1.2)
208
218
  let pxNdc = 2.0 / refViewport * 1.2 * safe_normal2(viewNormal.xy);
@@ -214,7 +224,7 @@ fn safe_normal2(v: vec2f) -> vec2f {
214
224
  ndc = o * select(vec2f(1.0), sign(d1), abs(d1) > vec2f(0.0));
215
225
  } else {
216
226
  let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
217
- let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
227
+ let nearNdc = viewNormal.xy * (material.edgeSize * widen * outlineNormal.w * 4.0 / refViewport);
218
228
  ndc = nearNdc * (min(w, breakDepth) / w);
219
229
 
220
230
  // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
@@ -228,6 +238,7 @@ fn safe_normal2(v: vec2f) -> vec2f {
228
238
  ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
229
239
  }
230
240
  }
241
+ ndc *= thin;
231
242
  output.normal = worldNormal;
232
243
  output.coverage = coverage;
233
244
  output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
@@ -1,4 +1,4 @@
1
- import { DISSOLVE_WGSL } from "../materials/common"
1
+ import { DISSOLVE_WGSL, JELLY_VERTEX_WGSL } from "../materials/common"
2
2
 
3
3
  // Shadow map depth pass. Skinned VS + alpha-test FS (depth-only attachment, no
4
4
  // color targets): texels where diffuse-texture alpha × material alpha fall below
@@ -21,10 +21,17 @@ struct MaterialDiffuse {
21
21
  diffuse: vec4f,
22
22
  _skip0: vec4f,
23
23
  _skip1: vec4f,
24
- _skip2: vec3f,
24
+ _skip2: f32,
25
+ materialId: f32,
26
+ _skip3: f32,
25
27
  dissolve: f32,
26
28
  };
27
29
  @group(1) @binding(1) var<uniform> material: MaterialDiffuse;
30
+ // The model's ObjectLight, for its jelly eyes: the map must hold the iris where
31
+ // the colour pass draws it.
32
+ struct ObjectLight { layers: vec4u, ambient: vec4f, sh: array<vec4f, 9>, fill: vec4f, jelly: array<vec4f, 12> }
33
+ @group(0) @binding(3) var<uniform> objectLight: ObjectLight;
34
+ ${JELLY_VERTEX_WGSL}
28
35
 
29
36
  struct VSOut {
30
37
  @builtin(position) position: vec4f,
@@ -38,10 +45,11 @@ struct VSOut {
38
45
 
39
46
  @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
40
47
  @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {
41
- let pos4 = vec4f(position, 1.0);
42
48
  let ws = weights0.x + weights0.y + weights0.z + weights0.w;
43
49
  let inv = select(1.0, 1.0 / ws, ws > 0.0001);
44
50
  let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
51
+ var pos4 = vec4f(position, 1.0);
52
+ if (objectLight.jelly[0].w > 0.0) { pos4 = vec4f(rzJellyVertex(position, joints0, nw), 1.0); }
45
53
  var sp = vec4f(0.0);
46
54
  for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
47
55
  var out: VSOut;