reze-engine 0.68.0 → 0.68.2

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/_dbg-loader.mjs +1100 -0
  3. package/dist/engine.d.ts +114 -0
  4. package/dist/engine.d.ts.map +1 -1
  5. package/dist/engine.js +220 -4
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/shaders/passes/aov.d.ts +37 -0
  10. package/dist/shaders/passes/aov.d.ts.map +1 -0
  11. package/dist/shaders/passes/aov.js +180 -0
  12. package/package.json +1 -1
  13. package/src/engine.ts +259 -6
  14. package/src/index.ts +4 -0
  15. package/src/shaders/passes/aov.ts +187 -0
  16. package/dist/graph/presets/pool_floor.d.ts +0 -3
  17. package/dist/graph/presets/pool_floor.d.ts.map +0 -1
  18. package/dist/graph/presets/pool_floor.js +0 -42
  19. package/dist/graph/presets/water.d.ts +0 -3
  20. package/dist/graph/presets/water.d.ts.map +0 -1
  21. package/dist/graph/presets/water.js +0 -31
  22. package/dist/physics/autofit.d.ts +0 -147
  23. package/dist/physics/autofit.d.ts.map +0 -1
  24. package/dist/physics/autofit.js +0 -501
  25. package/dist/shaders/agx-lut.d.ts +0 -11
  26. package/dist/shaders/agx-lut.d.ts.map +0 -1
  27. package/dist/shaders/agx-lut.js +0 -29
  28. package/dist/shaders/dfg_lut.d.ts +0 -3
  29. package/dist/shaders/dfg_lut.d.ts.map +0 -1
  30. package/dist/shaders/dfg_lut.js +0 -129
  31. package/dist/shaders/ltc_mag_lut.d.ts +0 -3
  32. package/dist/shaders/ltc_mag_lut.d.ts.map +0 -1
  33. package/dist/shaders/ltc_mag_lut.js +0 -1033
  34. package/dist/shaders/passes/field-blit.d.ts +0 -26
  35. package/dist/shaders/passes/field-blit.d.ts.map +0 -1
  36. package/dist/shaders/passes/field-blit.js +0 -65
  37. package/dist/shaders/passes/ghosts.d.ts +0 -22
  38. package/dist/shaders/passes/ghosts.d.ts.map +0 -1
  39. package/dist/shaders/passes/ghosts.js +0 -75
  40. package/dist/shaders/passes/ground-noise.d.ts +0 -7
  41. package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
  42. package/dist/shaders/passes/ground-noise.js +0 -88
  43. package/dist/shaders/passes/sim.d.ts +0 -34
  44. package/dist/shaders/passes/sim.d.ts.map +0 -1
  45. package/dist/shaders/passes/sim.js +0 -169
  46. package/dist/shaders/score-api.d.ts +0 -10
  47. package/dist/shaders/score-api.d.ts.map +0 -1
  48. package/dist/shaders/score-api.js +0 -114
  49. package/dist/shaders/shadow-api.d.ts +0 -29
  50. package/dist/shaders/shadow-api.d.ts.map +0 -1
  51. package/dist/shaders/shadow-api.js +0 -112
package/src/engine.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  } from "./asset-reader"
28
28
  import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
29
29
  import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug"
30
+ import { AOV_PIXELS_WGSL, AOV_PIXEL_FIELDS, AOV_PIXEL_STRIDE, AOV_PROBE_HEAD, AOV_PROBE_STRIDE, AOV_PROBE_WGSL } from "./shaders/passes/aov"
30
31
  import { paramChanged, sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
31
32
  import {
32
33
  advanceSim,
@@ -471,6 +472,38 @@ export type ModelTransform = {
471
472
  }
472
473
 
473
474
  /** How a model rides another — MMD's 外部親 (outside parent). See setModelParent. */
475
+ /**
476
+ * The frame read back as numbers (Engine.readAovs). `data` holds `stride`
477
+ * floats per texel, row-major from the top-left, named by `fields`; `ids` one
478
+ * word per texel, material id in the low 16 bits and object id in the high
479
+ * (Engine.getIdLegend names them; 0 is nothing). Colour is LINEAR and before
480
+ * `exposure` (stops) is applied.
481
+ */
482
+ export type SceneAovs = {
483
+ width: number
484
+ height: number
485
+ stride: number
486
+ fields: typeof AOV_PIXEL_FIELDS
487
+ data: Float32Array
488
+ ids: Uint32Array
489
+ exposure: number
490
+ }
491
+
492
+ /** The light arriving at one point (Engine.probeLight). Colours are linear
493
+ * light, before the surface's albedo: the sun's is its colour times strength,
494
+ * unshadowed and unfaced — multiply by sunShadow and max(sunFacing, 0). */
495
+ export type LightProbe = {
496
+ sunShadow: number
497
+ sunFacing: number
498
+ sun: [number, number, number]
499
+ ambient: [number, number, number]
500
+ /** Each lamp's light here, in the engine's lamp order. */
501
+ lamps: [number, number, number][]
502
+ /** How many of `lamps` are the document's (setLights, non-directional, in
503
+ * order); the rest an effect emits. */
504
+ documentLamps: number
505
+ }
506
+
474
507
  export type ModelAttachment = {
475
508
  /** Model key of the parent. */
476
509
  model: string
@@ -967,6 +1000,10 @@ interface ModelInstance {
967
1000
  * change reads as a glitch rather than as a cut.
968
1001
  */
969
1002
  simulateWhileHidden: boolean
1003
+ /** The host's per-model switch over the shadow map — see setModelCastShadow.
1004
+ * Off drops every draw from the shadow list; on hands it back to the PMX
1005
+ * flag on each material. */
1006
+ castShadow: boolean
970
1007
  basePath: string
971
1008
  assetReader: AssetReader
972
1009
  gpuBuffers: GPUBuffer[]
@@ -2275,6 +2312,11 @@ export class Engine {
2275
2312
  private idDebugBindGroupLayout: GPUBindGroupLayout | null = null
2276
2313
  private idDebugBindGroup: GPUBindGroup | null = null
2277
2314
  private idDebug = false
2315
+ /** Store ids and depth for readAovs — see setAovCapture. */
2316
+ private aovCapture = false
2317
+ private aovPixelsPipeline: GPUComputePipeline | null = null
2318
+ private aovProbePipeline: GPUComputePipeline | null = null
2319
+ private aovSampler: GPUSampler | null = null
2278
2320
  private multisampleMaskTexture!: GPUTexture
2279
2321
  private maskResolveTexture!: GPUTexture
2280
2322
  private maskResolveView!: GPUTextureView
@@ -3421,6 +3463,189 @@ export class Engine {
3421
3463
  return this.idView !== null
3422
3464
  }
3423
3465
 
3466
+ /**
3467
+ * Keep the frame's ids and depth for readAovs.
3468
+ *
3469
+ * Both are discarded at the end of an ordinary frame (see the store ops in
3470
+ * renderFrame); while this is on they are stored, on exactly the terms the
3471
+ * id-debug view and a masking effect already get. Turn it on, render the
3472
+ * frame to measure, read it, turn it off — the stores cost real bandwidth
3473
+ * and nothing else reads them.
3474
+ *
3475
+ * Returns false when the device has no id attachment, in which case
3476
+ * readAovs answers null.
3477
+ */
3478
+ setAovCapture(on: boolean): boolean {
3479
+ this.aovCapture = on && this.idView !== null
3480
+ return this.aovCapture || !on
3481
+ }
3482
+
3483
+ /**
3484
+ * What drew each id: the model's object id, and its materials' ids in the
3485
+ * order the id attachment counts them (materials that produced a draw,
3486
+ * 1-based). The floor is GROUND_OBJECT_ID / GROUND_MATERIAL_ID.
3487
+ */
3488
+ getIdLegend(): { objectId: number; model: string; materials: { id: number; name: string }[] }[] {
3489
+ const out: { objectId: number; model: string; materials: { id: number; name: string }[] }[] = []
3490
+ for (const [name, inst] of this.modelInstances) {
3491
+ const materials: { id: number; name: string }[] = []
3492
+ let id = 0
3493
+ for (const mat of inst.model.getMaterials()) {
3494
+ if (mat.vertexCount === 0) continue
3495
+ materials.push({ id: ++id, name: mat.name })
3496
+ }
3497
+ out.push({ objectId: inst.objectId, model: name, materials })
3498
+ }
3499
+ return out
3500
+ }
3501
+
3502
+ /**
3503
+ * The last frame, as numbers: per texel its linear colour, what drew it, how
3504
+ * far it is, and the light arriving there — the sun's shadow and facing, the
3505
+ * lamps', the world's. See shaders/passes/aov.ts for what each field is and
3506
+ * how the normal is rebuilt.
3507
+ *
3508
+ * Reads the frame ALREADY RENDERED, at the render size it was rendered at:
3509
+ * call it after renderFrame with setAovCapture on, before anything renders
3510
+ * again (a capture stops the loop for exactly this). Null when the device
3511
+ * has no id attachment or capture was not on.
3512
+ */
3513
+ async readAovs(): Promise<SceneAovs | null> {
3514
+ if (!this.aovCapture || !this.idView || !this.depthReadView || !this.camera) return null
3515
+ const width = this.hdrResolveTexture.width
3516
+ const height = this.hdrResolveTexture.height
3517
+ const pipeline = this.aovPixelsPipeline ??= this.device.createComputePipeline({
3518
+ label: "aov pixels",
3519
+ layout: "auto",
3520
+ compute: { module: this.device.createShaderModule({ label: "aov pixels", code: AOV_PIXELS_WGSL }), entryPoint: "pixels" },
3521
+ })
3522
+
3523
+ const invVP = this.camera.getProjectionMatrix().multiply(this.camera.getViewMatrix()).inverse().values
3524
+ const eye = this.camera.getPosition()
3525
+ const uniform = new ArrayBuffer(64 + 16 + 16)
3526
+ new Float32Array(uniform, 0, 16).set(invVP)
3527
+ new Float32Array(uniform, 64, 4).set([eye.x, eye.y, eye.z, 1])
3528
+ new Uint32Array(uniform, 80, 4).set([width, height, 0, 0])
3529
+ const ubuf = this.device.createBuffer({ label: "aov uniforms", size: uniform.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
3530
+ this.device.queue.writeBuffer(ubuf, 0, uniform)
3531
+
3532
+ const texels = width * height
3533
+ const fBytes = texels * AOV_PIXEL_STRIDE * 4
3534
+ const idBytes = texels * 4
3535
+ const fBuf = this.device.createBuffer({ label: "aov out", size: fBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC })
3536
+ const idBuf = this.device.createBuffer({ label: "aov ids", size: idBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC })
3537
+ const fRead = this.device.createBuffer({ label: "aov out read", size: fBytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST })
3538
+ const idRead = this.device.createBuffer({ label: "aov ids read", size: idBytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST })
3539
+
3540
+ const bind = this.device.createBindGroup({
3541
+ label: "aov pixels",
3542
+ layout: pipeline.getBindGroupLayout(0),
3543
+ entries: [
3544
+ ...this.aovLightEntries(),
3545
+ { binding: 7, resource: this.idView },
3546
+ { binding: 8, resource: this.depthReadView },
3547
+ { binding: 9, resource: this.hdrResolveTexture.createView() },
3548
+ { binding: 10, resource: { buffer: ubuf } },
3549
+ { binding: 11, resource: { buffer: fBuf } },
3550
+ { binding: 12, resource: { buffer: idBuf } },
3551
+ ],
3552
+ })
3553
+ const encoder = this.device.createCommandEncoder({ label: "aov pixels" })
3554
+ const pass = encoder.beginComputePass({ label: "aov pixels" })
3555
+ pass.setPipeline(pipeline)
3556
+ pass.setBindGroup(0, bind)
3557
+ pass.dispatchWorkgroups(Math.ceil(width / 8), Math.ceil(height / 8))
3558
+ pass.end()
3559
+ encoder.copyBufferToBuffer(fBuf, 0, fRead, 0, fBytes)
3560
+ encoder.copyBufferToBuffer(idBuf, 0, idRead, 0, idBytes)
3561
+ this.device.queue.submit([encoder.finish()])
3562
+
3563
+ try {
3564
+ await Promise.all([fRead.mapAsync(GPUMapMode.READ), idRead.mapAsync(GPUMapMode.READ)])
3565
+ const f = new Float32Array(fRead.getMappedRange().slice(0))
3566
+ const ids = new Uint32Array(idRead.getMappedRange().slice(0))
3567
+ const exposure = this.getViewTransformOptions().exposure ?? 0
3568
+ return { width, height, stride: AOV_PIXEL_STRIDE, fields: AOV_PIXEL_FIELDS, data: f, ids, exposure }
3569
+ } finally {
3570
+ for (const b of [ubuf, fBuf, idBuf, fRead, idRead]) b.destroy()
3571
+ }
3572
+ }
3573
+
3574
+ /**
3575
+ * The light arriving at points the caller names — a face, a hand, a spot on
3576
+ * the floor — facing the normal given: the sun's shadow, facing and colour,
3577
+ * the world's ambient, and EACH LAMP'S share kept apart, in the order the
3578
+ * engine holds them (the document's lamps first, as setLights got them,
3579
+ * then any an effect emits).
3580
+ *
3581
+ * Reads the shadow atlas and lamp records of the last frame rendered.
3582
+ */
3583
+ async probeLight(points: readonly { position: { x: number; y: number; z: number }; normal: { x: number; y: number; z: number } }[]): Promise<LightProbe[]> {
3584
+ if (points.length === 0) return []
3585
+ const pipeline = this.aovProbePipeline ??= this.device.createComputePipeline({
3586
+ label: "aov probe",
3587
+ layout: "auto",
3588
+ compute: { module: this.device.createShaderModule({ label: "aov probe", code: AOV_PROBE_WGSL }), entryPoint: "probe" },
3589
+ })
3590
+ const data = new Float32Array(points.length * 8)
3591
+ points.forEach((pt, i) => data.set([pt.position.x, pt.position.y, pt.position.z, 1, pt.normal.x, pt.normal.y, pt.normal.z, 0], i * 8))
3592
+ const inBuf = this.device.createBuffer({ label: "aov probe points", size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST })
3593
+ this.device.queue.writeBuffer(inBuf, 0, data)
3594
+ const outBytes = points.length * AOV_PROBE_STRIDE * 4
3595
+ const outBuf = this.device.createBuffer({ label: "aov probe out", size: outBytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC })
3596
+ const readBuf = this.device.createBuffer({ label: "aov probe read", size: outBytes, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST })
3597
+ const bind = this.device.createBindGroup({
3598
+ label: "aov probe",
3599
+ layout: pipeline.getBindGroupLayout(0),
3600
+ entries: [...this.aovLightEntries(), { binding: 7, resource: { buffer: inBuf } }, { binding: 8, resource: { buffer: outBuf } }],
3601
+ })
3602
+ const encoder = this.device.createCommandEncoder({ label: "aov probe" })
3603
+ const pass = encoder.beginComputePass({ label: "aov probe" })
3604
+ pass.setPipeline(pipeline)
3605
+ pass.setBindGroup(0, bind)
3606
+ pass.dispatchWorkgroups(points.length)
3607
+ pass.end()
3608
+ encoder.copyBufferToBuffer(outBuf, 0, readBuf, 0, outBytes)
3609
+ this.device.queue.submit([encoder.finish()])
3610
+ try {
3611
+ await readBuf.mapAsync(GPUMapMode.READ)
3612
+ const f = new Float32Array(readBuf.getMappedRange().slice(0))
3613
+ return points.map((_, i) => {
3614
+ const o = i * AOV_PROBE_STRIDE
3615
+ const count = Math.round(f[o + 8])
3616
+ const lamps: [number, number, number][] = []
3617
+ for (let l = 0; l < count; l++) {
3618
+ const b = o + AOV_PROBE_HEAD + l * 4
3619
+ lamps.push([f[b], f[b + 1], f[b + 2]])
3620
+ }
3621
+ return {
3622
+ sunShadow: f[o],
3623
+ sunFacing: f[o + 1],
3624
+ sun: [f[o + 2], f[o + 3], f[o + 4]],
3625
+ ambient: [f[o + 5], f[o + 6], f[o + 7]],
3626
+ lamps,
3627
+ documentLamps: Math.round(f[o + 9]),
3628
+ }
3629
+ })
3630
+ } finally {
3631
+ for (const b of [inBuf, outBuf, readBuf]) b.destroy()
3632
+ }
3633
+ }
3634
+
3635
+ /** The scene light API and the lamps, as aov.ts's prelude binds them. */
3636
+ private aovLightEntries(): GPUBindGroupEntry[] {
3637
+ this.aovSampler ??= this.device.createSampler({ label: "aov cookie sampler", magFilter: "linear", minFilter: "linear" })
3638
+ return [
3639
+ { binding: 0, resource: { buffer: this.lightUniformBuffer } },
3640
+ { binding: 1, resource: { buffer: this.shadowLightVPBuffer } },
3641
+ { binding: 2, resource: this.shadowAtlasView },
3642
+ { binding: 3, resource: this.shadowComparisonSampler },
3643
+ { binding: 4, resource: { buffer: this.lightsBuffer } },
3644
+ { binding: 5, resource: this.cookieView },
3645
+ { binding: 6, resource: this.aovSampler },
3646
+ ]
3647
+ }
3648
+
3424
3649
  /** The pass that draws it, built lazily so a scene that never asks for the
3425
3650
  * debug view never compiles it. */
3426
3651
  private ensureIdDebugPipeline(): boolean {
@@ -7094,6 +7319,12 @@ export class Engine {
7094
7319
  * frame. Nothing recompiles, so this is safe to drive per frame from a
7095
7320
  * timeline.
7096
7321
  */
7322
+ /** How many effects are installed — the indices setEffectInfluence and its
7323
+ * siblings address are 0 up to this. */
7324
+ getEffectCount(): number {
7325
+ return this.effects.length
7326
+ }
7327
+
7097
7328
  setEffectInfluence(index: number, influence: number): void {
7098
7329
  const fx = this.effects[index]
7099
7330
  if (!fx) return
@@ -13482,6 +13713,27 @@ export class Engine {
13482
13713
  return true
13483
13714
  }
13484
13715
 
13716
+ /**
13717
+ * Whether one model casts a shadow at all.
13718
+ *
13719
+ * Off takes it out of the shadow map, the shadow the cast lays on a stage and
13720
+ * the caster sphere the floor fits its shadow to; it still receives shadows
13721
+ * from everything else. On defers to the PMX author's flag per material, so a
13722
+ * material its author excluded stays excluded.
13723
+ */
13724
+ setModelCastShadow(modelName: string, on: boolean): boolean {
13725
+ const inst = this.modelInstances.get(modelName)
13726
+ if (!inst) return false
13727
+ if (inst.castShadow === on) return true
13728
+ inst.castShadow = on
13729
+ this.sortDrawCalls(inst)
13730
+ return true
13731
+ }
13732
+
13733
+ getModelCastShadow(modelName: string): boolean {
13734
+ return this.modelInstances.get(modelName)?.castShadow ?? true
13735
+ }
13736
+
13485
13737
  getPhysicsFloor(): boolean {
13486
13738
  return this.physicsFloor
13487
13739
  }
@@ -13732,7 +13984,7 @@ export class Engine {
13732
13984
  this.cullMetaF32[f + 4] = b[3]
13733
13985
  this.cullMetaF32[f + 5] = b[4]
13734
13986
  this.cullMetaF32[f + 6] = b[5]
13735
- this.cullMetaU32[f + 7] = draw.castsShadow === true ? Engine.CULL_DRAW_CASTS_SHADOW : 0
13987
+ this.cullMetaU32[f + 7] = inst.castShadow && draw.castsShadow === true ? Engine.CULL_DRAW_CASTS_SHADOW : 0
13736
13988
  // Everything but instanceCount is structural, so the compute never writes
13737
13989
  // it — one fewer store per draw per frame, and the args stay readable in a
13738
13990
  // capture as "this is the draw, that is whether it survived".
@@ -14889,6 +15141,7 @@ export class Engine {
14889
15141
  name,
14890
15142
  model,
14891
15143
  simulateWhileHidden: false,
15144
+ castShadow: true,
14892
15145
  basePath,
14893
15146
  assetReader,
14894
15147
  gpuBuffers,
@@ -17220,7 +17473,7 @@ export class Engine {
17220
17473
  // The scattering pass is the third reader: it rejects taps across a depth
17221
17474
  // step and scales its width by distance, so a scene wearing a subsurface
17222
17475
  // look stores depth for as long as it does.
17223
- const depthRead = dofOn || this.effects.some((e) => e.hasForeground) || this.subsurfaceInUse()
17476
+ const depthRead = dofOn || this.aovCapture || this.effects.some((e) => e.hasForeground) || this.subsurfaceInUse()
17224
17477
  this.renderPassDescriptor.depthStencilAttachment!.depthStoreOp = depthRead ? "store" : "discard"
17225
17478
  if (depthRead) this.writeDepthOfFieldUniforms()
17226
17479
 
@@ -17242,7 +17495,7 @@ export class Engine {
17242
17495
  if (idAtt) {
17243
17496
  // The flood seeds from the id attachment, so an effect that reads only the
17244
17497
  // DISTANCE still needs the ids kept — it just never names them itself.
17245
- const idsRead = this.idDebug || this.effects.some((e) => e.readsIds || e.readsCastDistance)
17498
+ const idsRead = this.idDebug || this.aovCapture || this.effects.some((e) => e.readsIds || e.readsCastDistance)
17246
17499
  idAtt.storeOp = idsRead ? "store" : "discard"
17247
17500
  }
17248
17501
 
@@ -18265,9 +18518,9 @@ export class Engine {
18265
18518
  inst.drawCalls.sort(
18266
18519
  (a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b),
18267
18520
  )
18268
- inst.shadowDrawCalls = inst.drawCalls.filter(
18269
- (d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true,
18270
- )
18521
+ inst.shadowDrawCalls = inst.castShadow
18522
+ ? inst.drawCalls.filter((d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true)
18523
+ : []
18271
18524
  // The sort reorders drawCalls, and a draw's position in that array is its
18272
18525
  // slot in every cull buffer.
18273
18526
  this.cullListDirty = true
package/src/index.ts CHANGED
@@ -23,6 +23,8 @@ export {
23
23
  RENDERING_LAYER_DEFAULT,
24
24
  RENDERING_LAYER_CHARACTER,
25
25
  type ModelAttachment,
26
+ type SceneAovs,
27
+ type LightProbe,
26
28
  type GizmoDragEvent,
27
29
  type GizmoDragCallback,
28
30
  type GizmoDragKind,
@@ -227,3 +229,5 @@ export type { EyeTrackingOptions } from "./model"
227
229
  export type { NativeStagePackage, NativeStageReader } from "./unity/stage"
228
230
  export type { NativeLook, NativeMaterialSpec, NativeTexture } from "./unity/looks"
229
231
  export type { NativeShader, NativeShaderInfo } from "./unity/host"
232
+ export { AOV_PIXEL_FIELDS, AOV_PIXEL_STRIDE } from "./shaders/passes/aov"
233
+ export { GROUND_MATERIAL_ID, GROUND_OBJECT_ID } from "./shaders/passes/ground"
@@ -0,0 +1,187 @@
1
+ import { sceneLightApi } from "../scene-light-api"
2
+ import { lightsApi, MAX_LIGHTS } from "../lights"
3
+
4
+ /**
5
+ * The frame, read back as numbers — what an agent (or a test) measures instead
6
+ * of squinting at a picture.
7
+ *
8
+ * A screenshot answers "what does it look like". It cannot answer the questions
9
+ * a lighting artist actually asks of a render: is her face in the sun's shadow,
10
+ * which lamp is carrying the key, how many stops over is that highlight, which
11
+ * pixels are skin. Every one of those is a number the frame already computed
12
+ * and threw away; these passes keep them.
13
+ *
14
+ * TWO ENTRY POINTS over one prelude:
15
+ *
16
+ * pixels — one invocation per texel of the frame just rendered. Reads the id
17
+ * attachment, the depth and the linear HDR resolve, rebuilds the
18
+ * world point from the depth and a normal from its neighbours, and
19
+ * asks the same shadow atlas and lamp records the materials asked.
20
+ * probe — the same light questions at points the caller names (a bone, a
21
+ * face), with each lamp's share kept apart.
22
+ *
23
+ * The normal is RECONSTRUCTED from depth, not the one the material shaded
24
+ * with: the frame never stores normals, and storing them for every frame to
25
+ * serve an occasional readback would be the cost the id attachment's
26
+ * store-only-when-read rule exists to avoid. On a silhouette edge it is the
27
+ * flatter of the two neighbours, which is right for every texel that is not
28
+ * the edge itself.
29
+ */
30
+
31
+ /** Floats per texel in the pixels output — see PIXEL_FIELDS. */
32
+ export const AOV_PIXEL_STRIDE = 12
33
+
34
+ /** What each of a texel's floats is, in order. */
35
+ export const AOV_PIXEL_FIELDS = [
36
+ "r", "g", "b", // linear HDR, before exposure and the view transform
37
+ "depth", // distance from the eye, world units; 0 where nothing drew
38
+ "sunShadow", // 1 lit, 0 in a caster's shadow; -1 where nothing drew
39
+ "sunFacing", // N·L toward the sun, -1..1 (the toon ramp's input)
40
+ "lampR", "lampG", "lampB", // every positional lamp's light arriving here
41
+ "ambR", "ambG", "ambB", // the world's light arriving here
42
+ ] as const
43
+
44
+ /** Floats per probe point before its per-lamp block. */
45
+ export const AOV_PROBE_HEAD = 12
46
+ /** Floats per probe point: the head, then rgb+pad for every lamp slot. */
47
+ export const AOV_PROBE_STRIDE = AOV_PROBE_HEAD + MAX_LIGHTS * 4
48
+
49
+ // Bindings 0..3 are the scene light API, 4..6 the lamps and their cookies.
50
+ const PRELUDE = /* wgsl */ `
51
+ ${sceneLightApi(true, 0, 0)}
52
+ ${lightsApi(0, 4, "0xffffffffu", { binding: 5, sampler: "_aovSampler" })}
53
+ @group(0) @binding(6) var _aovSampler: sampler;
54
+
55
+ /** The sun's cast shadow at p, nudged along n off the surface — the
56
+ * materials' rzSunOcclusion, without the facing test (sunFacing carries it). */
57
+ fn aovSunShadow(p: vec3f, n: vec3f) -> f32 {
58
+ let castAmt = _rzLight.lights[0].direction.w;
59
+ if (castAmt <= 0.0) { return 1.0; }
60
+ let a = _rzSunLocate(p + n * 0.08);
61
+ if (a.w < 0.0) { return 1.0; }
62
+ return mix(1.0, _rzSunTaps(a), castAmt);
63
+ }
64
+
65
+ fn aovToSun() -> vec3f { return normalize(-_rzLight.lights[0].direction.xyz); }
66
+ `
67
+
68
+ export const AOV_PIXELS_WGSL = /* wgsl */ `
69
+ ${PRELUDE}
70
+ @group(0) @binding(7) var idTex: texture_multisampled_2d<u32>;
71
+ @group(0) @binding(8) var depthTex: texture_depth_multisampled_2d;
72
+ @group(0) @binding(9) var hdrTex: texture_2d<f32>;
73
+ struct AovU { invViewProj: mat4x4f, eye: vec4f, size: vec4u, }
74
+ @group(0) @binding(10) var<uniform> u: AovU;
75
+ @group(0) @binding(11) var<storage, read_write> outF: array<f32>;
76
+ @group(0) @binding(12) var<storage, read_write> outId: array<u32>;
77
+
78
+ fn worldAt(c: vec2i, z: f32) -> vec3f {
79
+ let uv = (vec2f(c) + 0.5) / vec2f(u.size.xy);
80
+ let h = u.invViewProj * vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
81
+ return h.xyz / h.w;
82
+ }
83
+
84
+ fn drawn(c: vec2i) -> bool {
85
+ if (any(c < vec2i(0)) || any(c >= vec2i(u.size.xy))) { return false; }
86
+ let id = textureLoad(idTex, c, 0).xy;
87
+ return id.x != 0u || id.y != 0u;
88
+ }
89
+
90
+ /** The step from p to the neighbour at c + d (or c - d, negated), whichever is
91
+ * nearer in depth: across a silhouette the far neighbour belongs to something
92
+ * else, and a normal built from it tilts toward the background. */
93
+ fn tangent(c: vec2i, d: vec2i, p: vec3f) -> vec3f {
94
+ let a = c + d;
95
+ let b = c - d;
96
+ let okA = drawn(a);
97
+ let okB = drawn(b);
98
+ var ta = vec3f(0.0);
99
+ var tb = vec3f(0.0);
100
+ if (okA) { ta = worldAt(a, textureLoad(depthTex, a, 0)) - p; }
101
+ if (okB) { tb = p - worldAt(b, textureLoad(depthTex, b, 0)); }
102
+ if (okA && okB) { return select(tb, ta, dot(ta, ta) <= dot(tb, tb)); }
103
+ if (okA) { return ta; }
104
+ return tb;
105
+ }
106
+
107
+ @compute @workgroup_size(8, 8)
108
+ fn pixels(@builtin(global_invocation_id) gid: vec3u) {
109
+ if (gid.x >= u.size.x || gid.y >= u.size.y) { return; }
110
+ let c = vec2i(gid.xy);
111
+ let i = gid.y * u.size.x + gid.x;
112
+ let o = i * ${AOV_PIXEL_STRIDE}u;
113
+ let id = textureLoad(idTex, c, 0).xy;
114
+ let rgb = textureLoad(hdrTex, c, 0).rgb;
115
+ // Material in the low half, object in the high — the attachment's own order
116
+ // (scene-contract: vec2u(material, object)).
117
+ outId[i] = (id.y << 16u) | (id.x & 0xffffu);
118
+ outF[o] = rgb.r;
119
+ outF[o + 1u] = rgb.g;
120
+ outF[o + 2u] = rgb.b;
121
+ for (var k = 3u; k < ${AOV_PIXEL_STRIDE}u; k++) { outF[o + k] = 0.0; }
122
+ if (id.x == 0u && id.y == 0u) {
123
+ outF[o + 4u] = -1.0;
124
+ return;
125
+ }
126
+ let p = worldAt(c, textureLoad(depthTex, c, 0));
127
+ var n = cross(tangent(c, vec2i(0, 1), p), tangent(c, vec2i(1, 0), p));
128
+ let toEye = u.eye.xyz - p;
129
+ if (dot(n, n) < 1e-12) { n = toEye; }
130
+ n = normalize(n);
131
+ // Toward the eye: the side of the surface this texel shows.
132
+ if (dot(n, toEye) < 0.0) { n = -n; }
133
+ let lamps = rzLightsDiffuse(p, n);
134
+ let amb = rzWorldAmbient(n);
135
+ outF[o + 3u] = length(toEye);
136
+ outF[o + 4u] = aovSunShadow(p, n);
137
+ outF[o + 5u] = dot(n, aovToSun());
138
+ outF[o + 6u] = lamps.r;
139
+ outF[o + 7u] = lamps.g;
140
+ outF[o + 8u] = lamps.b;
141
+ outF[o + 9u] = amb.r;
142
+ outF[o + 10u] = amb.g;
143
+ outF[o + 11u] = amb.b;
144
+ }
145
+ `
146
+
147
+ export const AOV_PROBE_WGSL = /* wgsl */ `
148
+ ${PRELUDE}
149
+ struct ProbePoint { p: vec4f, n: vec4f, }
150
+ @group(0) @binding(7) var<storage, read> points: array<ProbePoint>;
151
+ @group(0) @binding(8) var<storage, read_write> outF: array<f32>;
152
+
153
+ @compute @workgroup_size(1)
154
+ fn probe(@builtin(global_invocation_id) gid: vec3u) {
155
+ let i = gid.x;
156
+ if (i >= arrayLength(&points)) { return; }
157
+ let p = points[i].p.xyz;
158
+ let n = normalize(points[i].n.xyz);
159
+ let o = i * ${AOV_PROBE_STRIDE}u;
160
+ // Colour and strength travel apart in the light uniform (writeSun); the
161
+ // light arriving is their product.
162
+ let sun = _rzLight.lights[0].color.rgb * _rzLight.lights[0].color.w;
163
+ let amb = rzWorldAmbient(n);
164
+ outF[o] = aovSunShadow(p, n);
165
+ outF[o + 1u] = dot(n, aovToSun());
166
+ outF[o + 2u] = sun.r;
167
+ outF[o + 3u] = sun.g;
168
+ outF[o + 4u] = sun.b;
169
+ outF[o + 5u] = amb.r;
170
+ outF[o + 6u] = amb.g;
171
+ outF[o + 7u] = amb.b;
172
+ let count = rzLightCount();
173
+ outF[o + 8u] = f32(count);
174
+ // The document's lamps come first (setLights order); the rest are effects'.
175
+ outF[o + 9u] = f32(_rzLightDocCount());
176
+ for (var k = 10u; k < ${AOV_PROBE_HEAD}u; k++) { outF[o + k] = 0.0; }
177
+ for (var l = 0u; l < ${MAX_LIGHTS}u; l++) {
178
+ var c = vec3f(0.0);
179
+ if (l < count) { c = _rzLightOne(l, p, n); }
180
+ let b = o + ${AOV_PROBE_HEAD}u + l * 4u;
181
+ outF[b] = c.r;
182
+ outF[b + 1u] = c.g;
183
+ outF[b + 2u] = c.b;
184
+ outF[b + 3u] = 0.0;
185
+ }
186
+ }
187
+ `
@@ -1,3 +0,0 @@
1
- import type { ShaderGraph } from "../schema";
2
- export declare const POOL_FLOOR_GRAPH: ShaderGraph;
3
- //# sourceMappingURL=pool_floor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pool_floor.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/pool_floor.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,gBAAgB,EAAE,WA6B9B,CAAA"}
@@ -1,42 +0,0 @@
1
- // Pool floor — the tiled bottom seen through water, with caustics on it.
2
- //
3
- // The other half of a pool, and the half that does most of the work. A water
4
- // surface can be tuned for a long time without the shot reading as a pool;
5
- // putting the focused-light web on the FLOOR is what settles it, because that
6
- // is the cue a photograph actually carries. It is a floor material, so it is
7
- // applied to the tile under the water rather than to the water itself.
8
- //
9
- // Caustics land between the albedo and the shading deliberately: they are
10
- // light, so they should still be dimmed by the floor's own shadow rather than
11
- // glowing through it. That is the difference between water-focused sun and a
12
- // decal.
13
- export const POOL_FLOOR_GRAPH = {
14
- version: 1,
15
- name: "Pool floor",
16
- tags: ["stage", "water", "pool"],
17
- nodes: [
18
- { id: "tex", type: "texture" },
19
- { id: "mat", type: "material_diffuse" },
20
- { id: "base", type: "mix/multiply", inputs: { fac: 1.0 } },
21
- { id: "caust", type: "caustics" },
22
- {
23
- id: "principled",
24
- type: "principled",
25
- inputs: { metallic: 0.0, specular_ior_level: 0.4, roughness: 0.45, spec_clamp: 10.0, sheen_weight: 0.0, sheen_tint: 0.0 },
26
- },
27
- ],
28
- links: [
29
- { from: { node: "tex", socket: "color" }, to: { node: "base", socket: "a" } },
30
- { from: { node: "mat", socket: "color" }, to: { node: "base", socket: "b" } },
31
- { from: { node: "base", socket: "color" }, to: { node: "caust", socket: "color" } },
32
- { from: { node: "caust", socket: "color" }, to: { node: "principled", socket: "base_color" } },
33
- ],
34
- output: { node: "principled", socket: "color" },
35
- params: [
36
- { id: "tint", label: "Light tint", target: { node: "caust", socket: "tint" }, kind: "color", default: [1.0, 1.0, 1.0] },
37
- { id: "scale", label: "Caustic scale", target: { node: "caust", socket: "scale" }, kind: "float", min: 0.01, max: 1, default: 0.12 },
38
- { id: "speed", label: "Caustic speed", target: { node: "caust", socket: "speed" }, kind: "float", min: 0, max: 1, default: 0.15 },
39
- { id: "strength", label: "Caustic strength", target: { node: "caust", socket: "strength" }, kind: "float", min: 0, max: 3, default: 0.6 },
40
- { id: "fade", label: "Depth fade", target: { node: "caust", socket: "fade" }, kind: "float", min: 1, max: 200, default: 30 },
41
- ],
42
- };
@@ -1,3 +0,0 @@
1
- import type { ShaderGraph } from "../schema";
2
- export declare const WATER_GRAPH: ShaderGraph;
3
- //# sourceMappingURL=water.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"water.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/water.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,WAAW,EAAE,WAgBzB,CAAA"}
@@ -1,31 +0,0 @@
1
- // Water — a planar reflection, rippled, seen through Fresnel.
2
- //
3
- // Put a stage's water materials in a style group carrying this graph and the
4
- // pool starts reflecting. The reflection itself is the engine's mirror pass,
5
- // which this is the only graph to read; everything else here is the shaping
6
- // that keeps it from reading as a mirror laid flat in the floor.
7
- //
8
- // The exposed params are the ones worth a slider while judging a shot. Scale
9
- // and speed are the ripple; a real pool's surface is slow and broad, so both
10
- // defaults sit low. Tint is the water's own colour — a game rip carries that
11
- // in its material, and the converter hands it straight over.
12
- //
13
- // How see-through the water is belongs to the MATERIAL's alpha, not to this
14
- // graph, which is where every other group's coverage comes from too.
15
- export const WATER_GRAPH = {
16
- version: 1,
17
- name: "Water",
18
- tags: ["water", "stage"],
19
- nodes: [{ id: "water", type: "water" }],
20
- links: [],
21
- output: { node: "water", socket: "color" },
22
- params: [
23
- { id: "tint", label: "Tint", target: { node: "water", socket: "tint" }, kind: "color", default: [0.04, 0.62, 0.66] },
24
- { id: "scale", label: "Ripple scale", target: { node: "water", socket: "scale" }, kind: "float", min: 0.01, max: 1, default: 0.18 },
25
- { id: "speed", label: "Ripple speed", target: { node: "water", socket: "speed" }, kind: "float", min: 0, max: 1, default: 0.12 },
26
- { id: "strength", label: "Ripple depth", target: { node: "water", socket: "strength" }, kind: "float", min: 0, max: 2, default: 0.7 },
27
- { id: "blur", label: "Reflection blur", target: { node: "water", socket: "blur" }, kind: "float", min: 0, max: 1, default: 0.25 },
28
- { id: "mirroring", label: "Mirroring", target: { node: "water", socket: "mirroring" }, kind: "float", min: 0, max: 1, default: 0.20 },
29
- { id: "sparkle", label: "Sparkle", target: { node: "water", socket: "sparkle" }, kind: "float", min: 0, max: 4, default: 1.0 },
30
- ],
31
- };