reze-engine 0.61.4 → 0.61.6
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.d.ts +69 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +310 -36
- package/dist/graph/presets/pool_floor.d.ts +3 -0
- package/dist/graph/presets/pool_floor.d.ts.map +1 -0
- package/dist/graph/presets/pool_floor.js +42 -0
- package/dist/graph/presets/water.d.ts +3 -0
- package/dist/graph/presets/water.d.ts.map +1 -0
- package/dist/graph/presets/water.js +31 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +11 -0
- package/dist/graph/slots.js +2 -2
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/shaders/directives.d.ts +9 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/materials/common.js +5 -5
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +12 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ghosts.d.ts +22 -0
- package/dist/shaders/passes/ghosts.d.ts.map +1 -0
- package/dist/shaders/passes/ghosts.js +75 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/particles.js +6 -6
- package/dist/shaders/passes/scene-contract.js +1 -1
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/subsurface.d.ts +34 -0
- package/dist/shaders/passes/subsurface.d.ts.map +1 -0
- package/dist/shaders/passes/subsurface.js +134 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shaders/shadow-api.d.ts +29 -0
- package/dist/shaders/shadow-api.d.ts.map +1 -0
- package/dist/shaders/shadow-api.js +112 -0
- package/package.json +1 -1
- package/src/engine.ts +309 -26
- package/src/graph/registry.ts +11 -0
- package/src/graph/slots.ts +2 -2
- package/src/shaders/directives.ts +14 -0
- package/src/shaders/materials/common.ts +5 -5
- package/src/shaders/materials/nodes.ts +12 -0
- package/src/shaders/passes/particles.ts +6 -6
- package/src/shaders/passes/scene-contract.ts +1 -1
- package/src/shaders/passes/subsurface.ts +136 -0
- package/dist/_dbg-loader.mjs +0 -1100
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/src/engine.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
EFFECT_TRAIL_BASE,
|
|
113
113
|
EFFECT_TRAIL_SAMPLES,
|
|
114
114
|
} from "./shaders/passes/composite"
|
|
115
|
+
import { SUBSURFACE_WGSL, SSS_SPIKE } from "./shaders/passes/subsurface"
|
|
115
116
|
import {
|
|
116
117
|
buildParticleComputeShader,
|
|
117
118
|
buildParticleRenderShader,
|
|
@@ -761,6 +762,12 @@ interface DrawCall {
|
|
|
761
762
|
/** Material draws only: false = excluded from the shadow map (PMX cast-shadow
|
|
762
763
|
* flag off). Sheer texels are cut per fragment by the shadow pass's alpha test. */
|
|
763
764
|
castsShadow?: boolean
|
|
765
|
+
/** The PMX author's double-sided flag (bit 0x01). Off means MMD draws the
|
|
766
|
+
* material's front faces only — an inner lining authored as a flipped copy
|
|
767
|
+
* of the outer layer is invisible from outside, which is what the author
|
|
768
|
+
* built it around. Media planes stay double-sided: they build their own
|
|
769
|
+
* index list and say so there. */
|
|
770
|
+
doubleSided: boolean
|
|
764
771
|
/** Edge-flagged materials: interleaved inverted-hull outline drawn right after
|
|
765
772
|
* this material with the outline pipeline. Shares this call's index range;
|
|
766
773
|
* own bind group (edge uniforms + diffuse texture for the alpha test). */
|
|
@@ -1650,6 +1657,8 @@ interface EffectInstance {
|
|
|
1650
1657
|
/** This effect IS a mirror — see the `#mirror` directive. Draws no shader;
|
|
1651
1658
|
* the engine reads its dials and folds the scene through the plane. */
|
|
1652
1659
|
hasMirror: boolean
|
|
1660
|
+
/** `#stepped` — the cast it is aimed at moves on twos. */
|
|
1661
|
+
stepped: boolean
|
|
1653
1662
|
/** Mounted under the scene. */
|
|
1654
1663
|
hasBackground: boolean
|
|
1655
1664
|
/** Mounted over the finished frame — and the reason the scene pass has to
|
|
@@ -2082,15 +2091,18 @@ export class Engine {
|
|
|
2082
2091
|
/** Stencil value stamped by eye draws so hair can stencil-test against it and
|
|
2083
2092
|
* alpha-blend a second pass over eye silhouette pixels (see-through-hair effect). */
|
|
2084
2093
|
private static readonly STENCIL_EYE_VALUE = 1
|
|
2085
|
-
/** Aux MRT alongside HDR color.
|
|
2094
|
+
/** Aux MRT alongside HDR color. Three channels:
|
|
2086
2095
|
* .r — bloom mask (1 = model geometry, 0 = ground; sampled by bloom blit to gate prefilter).
|
|
2087
2096
|
* .g — accumulated alpha (the channel that used to live in hdr.a before the HDR format
|
|
2088
2097
|
* switched to rg11b10ufloat, which has no alpha). Sampled by composite/bloom to
|
|
2089
2098
|
* un-premultiply color for tonemap and to produce the canvas-drawable alpha used by
|
|
2090
2099
|
* the premultiplied alphaMode compositor (so the page background still shows through
|
|
2091
2100
|
* cleared / edge-faded regions like before).
|
|
2092
|
-
*
|
|
2093
|
-
|
|
2101
|
+
* .b — subsurface strength, set by a graph's `subsurface` node and 0 everywhere
|
|
2102
|
+
* else; the scattering pass reads it to find skin (passes/subsurface.ts).
|
|
2103
|
+
* rgba8unorm at 4× MSAA is 16 bytes/texel; it never leaves tile memory (storeOp
|
|
2104
|
+
* discard), only its single-sample resolve does. */
|
|
2105
|
+
private static readonly BLOOM_MASK_FORMAT: GPUTextureFormat = "rgba8unorm"
|
|
2094
2106
|
/**
|
|
2095
2107
|
* The master switch for the id attachment. OFF — and off having been proven
|
|
2096
2108
|
* to work, not off because it was never finished.
|
|
@@ -2633,6 +2645,28 @@ export class Engine {
|
|
|
2633
2645
|
// composite adds bloomUp mip 0 × (color × intensity) to HDR before Filmic.
|
|
2634
2646
|
// Matches EEVEE energy: tint/intensity applied at composite, not prefilter.
|
|
2635
2647
|
private bloomSampler!: GPUSampler
|
|
2648
|
+
// Screen-space subsurface scattering (passes/subsurface.ts). The scratch target
|
|
2649
|
+
// is the X pass's output and lives with the canvas size; the bind groups name
|
|
2650
|
+
// it and the HDR resolve, so a resize drops them and the next frame rebuilds.
|
|
2651
|
+
private sssPipelineX: GPURenderPipeline | null = null
|
|
2652
|
+
private sssPipelineY: GPURenderPipeline | null = null
|
|
2653
|
+
private sssScratch: GPUTexture | null = null
|
|
2654
|
+
private sssBindGroupX: GPUBindGroup | null = null
|
|
2655
|
+
private sssBindGroupY: GPUBindGroup | null = null
|
|
2656
|
+
private sssUniformX: GPUBuffer | null = null
|
|
2657
|
+
private sssUniformY: GPUBuffer | null = null
|
|
2658
|
+
private readonly sssUniformData = new Float32Array(8)
|
|
2659
|
+
// Model pipelines' descriptors, and the single-sided twins built from them —
|
|
2660
|
+
// one per pipeline per view (see sidedPipeline).
|
|
2661
|
+
private readonly pipelineDescs = new WeakMap<GPURenderPipeline, GPURenderPipelineDescriptor>()
|
|
2662
|
+
private readonly singleSided = new WeakMap<GPURenderPipeline, GPURenderPipeline>()
|
|
2663
|
+
private readonly singleSidedMirror = new WeakMap<GPURenderPipeline, GPURenderPipeline>()
|
|
2664
|
+
// Stepped motion (#stepped). The models whose pose is being HELD this frame —
|
|
2665
|
+
// their skinning and morph uploads wait for the stepped clock's next tick —
|
|
2666
|
+
// and the tick each was last shown at. Empty while no effect declares it.
|
|
2667
|
+
private readonly steppedHeld = new Set<string>()
|
|
2668
|
+
private readonly steppedTick = new Map<string, number>()
|
|
2669
|
+
private steppedLastClock = -1
|
|
2636
2670
|
private bloomBlitUniformBuffer!: GPUBuffer
|
|
2637
2671
|
private bloomUpsampleUniformBuffer!: GPUBuffer
|
|
2638
2672
|
private readonly bloomBlitUniformData = new Float32Array(4)
|
|
@@ -3410,6 +3444,55 @@ export class Engine {
|
|
|
3410
3444
|
* Gated on `weight`, so a scheduled mirror appears and leaves with its strip
|
|
3411
3445
|
* and a faded-out one costs no reflection pass at all.
|
|
3412
3446
|
*/
|
|
3447
|
+
/** The dials a stepped effect is read by, by name — MIRROR_DIALS' pattern. */
|
|
3448
|
+
private static readonly STEPPED_DIALS = {
|
|
3449
|
+
/** Poses per second of scene time: 12 is 24fps animation on twos, 6 on fours. */
|
|
3450
|
+
fps: "FPS",
|
|
3451
|
+
} as const
|
|
3452
|
+
|
|
3453
|
+
/**
|
|
3454
|
+
* Decide which models hold their pose this frame.
|
|
3455
|
+
*
|
|
3456
|
+
* Nothing underneath is slowed: animation and physics run every frame as they
|
|
3457
|
+
* always do, so the pose that lands on each tick is the true one for that
|
|
3458
|
+
* moment and a hold never drifts behind the music. What steps is only when it
|
|
3459
|
+
* reaches the GPU — the skinning upload and the morph dispatch wait for the
|
|
3460
|
+
* next tick of a clock running at FPS on the scene clock, which is what an
|
|
3461
|
+
* export steps.
|
|
3462
|
+
*
|
|
3463
|
+
* FIRST match wins, and only the models it is aimed at hold: a stage, a prop
|
|
3464
|
+
* and every effect keep moving on ones, which is what makes the cast read as
|
|
3465
|
+
* animated rather than the playback as broken.
|
|
3466
|
+
*/
|
|
3467
|
+
private syncSteppedFromEffects(): void {
|
|
3468
|
+
this.steppedHeld.clear()
|
|
3469
|
+
let fx: EffectInstance | null = null
|
|
3470
|
+
for (const e of this.effects) {
|
|
3471
|
+
if (e.stepped && e.weight > 0) {
|
|
3472
|
+
fx = e
|
|
3473
|
+
break
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (!fx) {
|
|
3477
|
+
this.steppedTick.clear()
|
|
3478
|
+
return
|
|
3479
|
+
}
|
|
3480
|
+
// Paused, nothing holds: time is not moving, so there is nothing to step,
|
|
3481
|
+
// and a pose edited on a stopped timeline has to show as it is edited.
|
|
3482
|
+
const paused = this.sceneClock === this.steppedLastClock
|
|
3483
|
+
this.steppedLastClock = this.sceneClock
|
|
3484
|
+
if (paused) return
|
|
3485
|
+
const D = Engine.STEPPED_DIALS
|
|
3486
|
+
const fps = Math.max(this.effectDial(fx, D.fps, 6), 1)
|
|
3487
|
+
const tick = Math.floor(this.sceneClock * fps + 1e-6)
|
|
3488
|
+
this.forEachInstance((inst) => {
|
|
3489
|
+
const slot = this.castSlotOf.get(inst.name)
|
|
3490
|
+
if (slot === undefined || !(fx!.subjectMask & (1 << slot))) return
|
|
3491
|
+
if (this.steppedTick.get(inst.name) === tick) this.steppedHeld.add(inst.name)
|
|
3492
|
+
else this.steppedTick.set(inst.name, tick)
|
|
3493
|
+
})
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3413
3496
|
private syncMirrorFromEffects(): void {
|
|
3414
3497
|
let fx: EffectInstance | null = null
|
|
3415
3498
|
for (const e of this.effects) {
|
|
@@ -4761,14 +4844,15 @@ export class Engine {
|
|
|
4761
4844
|
// #mirror is a mount on the same footing, and for the same reason: a
|
|
4762
4845
|
// reflection re-renders the scene from a folded camera, which is a pass and
|
|
4763
4846
|
// not a shader, so the effect declares it and draws nothing itself.
|
|
4764
|
-
|
|
4847
|
+
// #stepped too: it decides when a pose reaches the screen, not what is drawn.
|
|
4848
|
+
if (!hasBackground && !hasForeground && !wantsParticles && !wantsTrails && !hasLightEmit(wgsl) && !d.mirror && !d.stepped) {
|
|
4765
4849
|
return { ok: false, diagnostics: [
|
|
4766
4850
|
"an effect must define fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f, " +
|
|
4767
4851
|
"fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f, " +
|
|
4768
4852
|
"the particle trio (particleInit/particleStep/particleShade), " +
|
|
4769
4853
|
"the ribbon pair (trailWidth/trailShade), " +
|
|
4770
4854
|
"fn lightEmit(i: u32) -> RzLight with #lights <n>, " +
|
|
4771
|
-
"or #
|
|
4855
|
+
"#mirror, or #stepped",
|
|
4772
4856
|
], mounts: noMounts, params: [], duration: 0, readsCast: false }
|
|
4773
4857
|
}
|
|
4774
4858
|
const mounts = { background: hasBackground, foreground: hasForeground }
|
|
@@ -5080,6 +5164,7 @@ export class Engine {
|
|
|
5080
5164
|
paramsBuffer,
|
|
5081
5165
|
paramsData,
|
|
5082
5166
|
hasMirror: d.mirror,
|
|
5167
|
+
stepped: d.stepped,
|
|
5083
5168
|
hasBackground,
|
|
5084
5169
|
hasForeground,
|
|
5085
5170
|
// The author's OWN source, not the assembled module: the assembled one
|
|
@@ -6143,6 +6228,134 @@ export class Engine {
|
|
|
6143
6228
|
/** The user's field mounts, drawn at half resolution for the composite to
|
|
6144
6229
|
* upsample. Runs the whole quad — uniform control flow, so effects may use
|
|
6145
6230
|
* derivatives freely, which the old inline path had to forbid. */
|
|
6231
|
+
/** Whether any installed style group routes through a `subsurface` node —
|
|
6232
|
+
* the scattering pass, and the depth it reads, cost nothing otherwise. */
|
|
6233
|
+
private subsurfaceInUse(): boolean {
|
|
6234
|
+
for (const inst of this.modelInstances.values()) {
|
|
6235
|
+
for (const g of inst.styleGroups.values()) {
|
|
6236
|
+
if (g.group.materials.length && g.group.graph.nodes.some((n) => n.type === "subsurface")) return true
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
return false
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
/**
|
|
6243
|
+
* ray-mmd's skin scattering: a horizontal then a vertical depth-aware blur of
|
|
6244
|
+
* the pixels the scene pass marked as skin (aux .b), the second written back
|
|
6245
|
+
* into the HDR resolve through a blend that keeps ray-mmd's per-channel share
|
|
6246
|
+
* of the unblurred picture. See passes/subsurface.ts.
|
|
6247
|
+
*/
|
|
6248
|
+
private renderSubsurface(encoder: GPUCommandEncoder): void {
|
|
6249
|
+
if (!this.device || !this.subsurfaceInUse()) return
|
|
6250
|
+
if (!this.hdrResolveTexture || !this.maskResolveView || !this.depthReadView || !this.dofUniformBuffer) return
|
|
6251
|
+
const w = this.hdrResolveTexture.width
|
|
6252
|
+
const h = this.hdrResolveTexture.height
|
|
6253
|
+
|
|
6254
|
+
if (!this.sssPipelineX || !this.sssPipelineY) {
|
|
6255
|
+
const module = this.device.createShaderModule({ label: "subsurface scattering", code: SUBSURFACE_WGSL })
|
|
6256
|
+
this.sssPipelineX = this.device.createRenderPipeline({
|
|
6257
|
+
label: "subsurface blur x",
|
|
6258
|
+
layout: "auto",
|
|
6259
|
+
vertex: { module, entryPoint: "vs" },
|
|
6260
|
+
fragment: { module, entryPoint: "fsX", targets: [{ format: this.hdrFormat }] },
|
|
6261
|
+
primitive: { topology: "triangle-list" },
|
|
6262
|
+
})
|
|
6263
|
+
// result = blurred·(1 − spike) + original·spike, per channel: the
|
|
6264
|
+
// destination is the original, and the blend constant holds the spike.
|
|
6265
|
+
this.sssPipelineY = this.device.createRenderPipeline({
|
|
6266
|
+
label: "subsurface blur y",
|
|
6267
|
+
layout: "auto",
|
|
6268
|
+
vertex: { module, entryPoint: "vs" },
|
|
6269
|
+
fragment: {
|
|
6270
|
+
module,
|
|
6271
|
+
entryPoint: "fsY",
|
|
6272
|
+
targets: [
|
|
6273
|
+
{
|
|
6274
|
+
format: this.hdrFormat,
|
|
6275
|
+
blend: {
|
|
6276
|
+
color: { srcFactor: "one-minus-constant", dstFactor: "constant", operation: "add" },
|
|
6277
|
+
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
6278
|
+
},
|
|
6279
|
+
},
|
|
6280
|
+
],
|
|
6281
|
+
},
|
|
6282
|
+
primitive: { topology: "triangle-list" },
|
|
6283
|
+
})
|
|
6284
|
+
this.sssBindGroupX = null
|
|
6285
|
+
this.sssBindGroupY = null
|
|
6286
|
+
}
|
|
6287
|
+
if (!this.sssUniformX || !this.sssUniformY) {
|
|
6288
|
+
const make = (label: string) =>
|
|
6289
|
+
this.device.createBuffer({ label, size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
|
|
6290
|
+
this.sssUniformX = make("subsurface uniforms x")
|
|
6291
|
+
this.sssUniformY = make("subsurface uniforms y")
|
|
6292
|
+
}
|
|
6293
|
+
if (!this.sssScratch || this.sssScratch.width !== w || this.sssScratch.height !== h) {
|
|
6294
|
+
this.sssScratch?.destroy()
|
|
6295
|
+
this.sssScratch = this.device.createTexture({
|
|
6296
|
+
label: "subsurface scratch",
|
|
6297
|
+
size: [w, h],
|
|
6298
|
+
format: this.hdrFormat,
|
|
6299
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6300
|
+
})
|
|
6301
|
+
this.sssBindGroupX = null
|
|
6302
|
+
this.sssBindGroupY = null
|
|
6303
|
+
}
|
|
6304
|
+
if (!this.sssBindGroupX || !this.sssBindGroupY) {
|
|
6305
|
+
const mask = this.maskResolveView
|
|
6306
|
+
const depth = this.depthReadView
|
|
6307
|
+
const dof = this.dofUniformBuffer
|
|
6308
|
+
const group = (pipeline: GPURenderPipeline, src: GPUTexture, uniforms: GPUBuffer, label: string) =>
|
|
6309
|
+
this.device.createBindGroup({
|
|
6310
|
+
label,
|
|
6311
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
6312
|
+
entries: [
|
|
6313
|
+
{ binding: 0, resource: src.createView() },
|
|
6314
|
+
{ binding: 1, resource: mask },
|
|
6315
|
+
{ binding: 2, resource: depth },
|
|
6316
|
+
{ binding: 3, resource: { buffer: dof } },
|
|
6317
|
+
{ binding: 4, resource: { buffer: uniforms } },
|
|
6318
|
+
],
|
|
6319
|
+
})
|
|
6320
|
+
this.sssBindGroupX = group(this.sssPipelineX, this.hdrResolveTexture, this.sssUniformX, "subsurface x")
|
|
6321
|
+
this.sssBindGroupY = group(this.sssPipelineY, this.sssScratch, this.sssUniformY, "subsurface y")
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
const tanY = Math.tan(this.camera.fov * 0.5)
|
|
6325
|
+
const u = this.sssUniformData
|
|
6326
|
+
u[2] = 0
|
|
6327
|
+
u[3] = 0
|
|
6328
|
+
u[4] = tanY * this.camera.aspect
|
|
6329
|
+
u[5] = tanY
|
|
6330
|
+
u[6] = w
|
|
6331
|
+
u[7] = h
|
|
6332
|
+
u[0] = 1
|
|
6333
|
+
u[1] = 0
|
|
6334
|
+
this.device.queue.writeBuffer(this.sssUniformX, 0, u)
|
|
6335
|
+
u[0] = 0
|
|
6336
|
+
u[1] = 1
|
|
6337
|
+
this.device.queue.writeBuffer(this.sssUniformY, 0, u)
|
|
6338
|
+
|
|
6339
|
+
const px = encoder.beginRenderPass({
|
|
6340
|
+
label: "subsurface blur x",
|
|
6341
|
+
colorAttachments: [{ view: this.sssScratch.createView(), loadOp: "clear", clearValue: [0, 0, 0, 0], storeOp: "store" }],
|
|
6342
|
+
})
|
|
6343
|
+
px.setPipeline(this.sssPipelineX)
|
|
6344
|
+
px.setBindGroup(0, this.sssBindGroupX)
|
|
6345
|
+
px.draw(3)
|
|
6346
|
+
px.end()
|
|
6347
|
+
|
|
6348
|
+
const py = encoder.beginRenderPass({
|
|
6349
|
+
label: "subsurface blur y",
|
|
6350
|
+
colorAttachments: [{ view: this.hdrResolveTexture.createView(), loadOp: "load", storeOp: "store" }],
|
|
6351
|
+
})
|
|
6352
|
+
py.setPipeline(this.sssPipelineY)
|
|
6353
|
+
py.setBindGroup(0, this.sssBindGroupY)
|
|
6354
|
+
py.setBlendConstant({ r: SSS_SPIKE[0], g: SSS_SPIKE[1], b: SSS_SPIKE[2], a: 0 })
|
|
6355
|
+
py.draw(3)
|
|
6356
|
+
py.end()
|
|
6357
|
+
}
|
|
6358
|
+
|
|
6146
6359
|
private renderFieldPass(encoder: GPUCommandEncoder): void {
|
|
6147
6360
|
// TWO PREDICATES, deliberately, and they are not interchangeable.
|
|
6148
6361
|
//
|
|
@@ -7210,7 +7423,7 @@ export class Engine {
|
|
|
7210
7423
|
multisample?: GPUMultisampleState
|
|
7211
7424
|
}): GPURenderPipeline {
|
|
7212
7425
|
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined)
|
|
7213
|
-
return this.
|
|
7426
|
+
return this.createModelPipeline({
|
|
7214
7427
|
label: config.label,
|
|
7215
7428
|
layout: config.layout,
|
|
7216
7429
|
vertex: {
|
|
@@ -7653,7 +7866,7 @@ export class Engine {
|
|
|
7653
7866
|
depthCompare: this.depthAhead,
|
|
7654
7867
|
},
|
|
7655
7868
|
}
|
|
7656
|
-
this.depthPrepassPipeline = this.
|
|
7869
|
+
this.depthPrepassPipeline = this.createModelPipeline({
|
|
7657
7870
|
label: "opaque depth prepass",
|
|
7658
7871
|
...prepassDesc,
|
|
7659
7872
|
fragment: {
|
|
@@ -7665,7 +7878,7 @@ export class Engine {
|
|
|
7665
7878
|
// The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
|
|
7666
7879
|
// whose blend ignores the destination may pre-claim depth in the
|
|
7667
7880
|
// transparent phase — see the override's note in depth-prepass.ts.
|
|
7668
|
-
this.solidPrepassPipeline = this.
|
|
7881
|
+
this.solidPrepassPipeline = this.createModelPipeline({
|
|
7669
7882
|
label: "transparent solid prepass",
|
|
7670
7883
|
...prepassDesc,
|
|
7671
7884
|
fragment: {
|
|
@@ -7681,7 +7894,7 @@ export class Engine {
|
|
|
7681
7894
|
// hair depth from ever claiming the pixels the see-through-hair pass needs
|
|
7682
7895
|
// the eye to survive on. (Bundle draws use the PASS's stencil reference;
|
|
7683
7896
|
// only pipeline/bind/vertex state resets across executeBundles.)
|
|
7684
|
-
this.hairPrimePipeline = this.
|
|
7897
|
+
this.hairPrimePipeline = this.createModelPipeline({
|
|
7685
7898
|
label: "hair depth prime",
|
|
7686
7899
|
...prepassDesc,
|
|
7687
7900
|
depthStencil: {
|
|
@@ -8553,6 +8766,10 @@ export class Engine {
|
|
|
8553
8766
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
8554
8767
|
})
|
|
8555
8768
|
this.maskResolveView = this.maskResolveTexture.createView()
|
|
8769
|
+
this.sssScratch?.destroy()
|
|
8770
|
+
this.sssScratch = null
|
|
8771
|
+
this.sssBindGroupX = null
|
|
8772
|
+
this.sssBindGroupY = null
|
|
8556
8773
|
|
|
8557
8774
|
// The floor mirror's targets — FULL resolution, the same attachment
|
|
8558
8775
|
// contract and sample count as the scene pass, which is what lets the
|
|
@@ -10403,6 +10620,8 @@ export class Engine {
|
|
|
10403
10620
|
bindGroup: this.groundShadowBindGroup!,
|
|
10404
10621
|
materialName: "Ground",
|
|
10405
10622
|
groupId: null,
|
|
10623
|
+
// The ground's own pipeline decides its culling.
|
|
10624
|
+
doubleSided: true,
|
|
10406
10625
|
// The ground belongs to no model instance, so it is not in the cull list —
|
|
10407
10626
|
// cullIndex -1 leaves renderGround unconditional. Its box is filled in
|
|
10408
10627
|
// anyway rather than left a lie for whoever reads this next.
|
|
@@ -12549,6 +12768,8 @@ export class Engine {
|
|
|
12549
12768
|
for (const inst of this.modelInstances.values()) {
|
|
12550
12769
|
const gm = inst.gpuMorph
|
|
12551
12770
|
if (!gm || !gm.dispatchNeeded) continue
|
|
12771
|
+
// The face holds with the body (see syncSteppedFromEffects).
|
|
12772
|
+
if (this.steppedHeld.has(inst.name)) continue
|
|
12552
12773
|
if (!pass) {
|
|
12553
12774
|
pass = encoder.beginComputePass({ label: "morph compute", timestampWrites: this.stamps("morph") })
|
|
12554
12775
|
pass.setPipeline(this.morphComputePipeline)
|
|
@@ -14479,6 +14700,7 @@ export class Engine {
|
|
|
14479
14700
|
groupId: null,
|
|
14480
14701
|
baseBindGroupEntries,
|
|
14481
14702
|
castsShadow,
|
|
14703
|
+
doubleSided: inst.isPlane || (mat.edgeFlag & 0x01) !== 0,
|
|
14482
14704
|
outline,
|
|
14483
14705
|
bounds,
|
|
14484
14706
|
cullIndex: -1,
|
|
@@ -15966,6 +16188,7 @@ export class Engine {
|
|
|
15966
16188
|
}
|
|
15967
16189
|
|
|
15968
16190
|
const hasModels = this.modelInstances.size > 0
|
|
16191
|
+
this.syncSteppedFromEffects()
|
|
15969
16192
|
if (hasModels) {
|
|
15970
16193
|
this.updateInstances(deltaTime)
|
|
15971
16194
|
this.updateSkinMatrices()
|
|
@@ -16063,7 +16286,10 @@ export class Engine {
|
|
|
16063
16286
|
// The two real readers are both in the composite and both have their own
|
|
16064
16287
|
// flag above: linearDepth() feeds the DoF gather and the depth handed to a
|
|
16065
16288
|
// foreground mount. Nothing else binds depthTex at all.
|
|
16066
|
-
|
|
16289
|
+
// The scattering pass is the third reader: it rejects taps across a depth
|
|
16290
|
+
// step and scales its width by distance, so a scene wearing a subsurface
|
|
16291
|
+
// look stores depth for as long as it does.
|
|
16292
|
+
const depthRead = dofOn || this.effects.some((e) => e.hasForeground) || this.subsurfaceInUse()
|
|
16067
16293
|
this.renderPassDescriptor.depthStencilAttachment!.depthStoreOp = depthRead ? "store" : "discard"
|
|
16068
16294
|
if (depthRead) this.writeDepthOfFieldUniforms()
|
|
16069
16295
|
|
|
@@ -16261,6 +16487,9 @@ export class Engine {
|
|
|
16261
16487
|
// kept ribbons out of bloom.
|
|
16262
16488
|
this.drawTrails(pass, "camera")
|
|
16263
16489
|
pass.end()
|
|
16490
|
+
// Skin scattering, on the resolved scene and before anything reads it:
|
|
16491
|
+
// bloom should bleed the scattered skin, not the raw one.
|
|
16492
|
+
this.renderSubsurface(encoder)
|
|
16264
16493
|
// The field mounts, likewise: after the scene so foregrounds can read its
|
|
16265
16494
|
// depth, before the composite that samples both layers.
|
|
16266
16495
|
this.renderFieldPass(encoder)
|
|
@@ -17081,13 +17310,55 @@ export class Engine {
|
|
|
17081
17310
|
stencilWriteMask: 0xff,
|
|
17082
17311
|
}
|
|
17083
17312
|
}
|
|
17084
|
-
|
|
17313
|
+
const desc: GPURenderPipelineDescriptor = {
|
|
17085
17314
|
...base,
|
|
17086
17315
|
fragment: { module, constants, targets: blend === "additive" ? this.sceneTargetsAdditive : this.sceneTargets },
|
|
17087
17316
|
depthStencil,
|
|
17317
|
+
}
|
|
17318
|
+
return this.device.createRenderPipelineAsync(desc).then((pipeline) => {
|
|
17319
|
+
this.pipelineDescs.set(pipeline, desc)
|
|
17320
|
+
return pipeline
|
|
17088
17321
|
})
|
|
17089
17322
|
}
|
|
17090
17323
|
|
|
17324
|
+
/** A pipeline whose descriptor is kept, so a single-sided twin can be built
|
|
17325
|
+
* from it on demand (see sidedPipeline). */
|
|
17326
|
+
private createModelPipeline(desc: GPURenderPipelineDescriptor): GPURenderPipeline {
|
|
17327
|
+
const pipeline = this.device.createRenderPipeline(desc)
|
|
17328
|
+
this.pipelineDescs.set(pipeline, desc)
|
|
17329
|
+
return pipeline
|
|
17330
|
+
}
|
|
17331
|
+
|
|
17332
|
+
/**
|
|
17333
|
+
* The pipeline a draw should use once its double-sided flag is honoured.
|
|
17334
|
+
*
|
|
17335
|
+
* A single-sided material culls its back faces, as MMD does. PMX winds a front
|
|
17336
|
+
* face clockwise, which this engine sees as a BACK face — the eye's front cull
|
|
17337
|
+
* and the outline hull's back cull are the same fact — so the cull is "front"
|
|
17338
|
+
* for the camera and "back" in the mirror, whose reflection flips winding.
|
|
17339
|
+
*
|
|
17340
|
+
* Every pipeline that draws a material's surface goes through here, depth
|
|
17341
|
+
* primes included: a prime that kept a culled face would write depth for a
|
|
17342
|
+
* surface nobody sees and hide what is behind it. A pipeline that already
|
|
17343
|
+
* culls (the eye) is returned as it is.
|
|
17344
|
+
*/
|
|
17345
|
+
private sidedPipeline(pipeline: GPURenderPipeline, draw: DrawCall, mirrored: boolean): GPURenderPipeline {
|
|
17346
|
+
if (draw.doubleSided) return pipeline
|
|
17347
|
+
const desc = this.pipelineDescs.get(pipeline)
|
|
17348
|
+
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none") return pipeline
|
|
17349
|
+
const cache = mirrored ? this.singleSidedMirror : this.singleSided
|
|
17350
|
+
let twin = cache.get(pipeline)
|
|
17351
|
+
if (!twin) {
|
|
17352
|
+
twin = this.device.createRenderPipeline({
|
|
17353
|
+
...desc,
|
|
17354
|
+
label: `${desc.label ?? "pipeline"} (single-sided${mirrored ? ", mirror" : ""})`,
|
|
17355
|
+
primitive: { ...desc.primitive, cullMode: mirrored ? "back" : "front" },
|
|
17356
|
+
})
|
|
17357
|
+
cache.set(pipeline, twin)
|
|
17358
|
+
}
|
|
17359
|
+
return twin
|
|
17360
|
+
}
|
|
17361
|
+
|
|
17091
17362
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
17092
17363
|
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
17093
17364
|
// draws on the CAST use the SAME depth-write-on pipeline — babylon-mmd's
|
|
@@ -17145,7 +17416,8 @@ export class Engine {
|
|
|
17145
17416
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17146
17417
|
bound = true
|
|
17147
17418
|
}
|
|
17148
|
-
const
|
|
17419
|
+
const mirrored = view.args === "mirror"
|
|
17420
|
+
const pipeline = this.sidedPipeline(this.pipelineForDrawCall(inst, draw, mirrored), draw, mirrored)
|
|
17149
17421
|
if (pipeline !== currentPipeline) {
|
|
17150
17422
|
pass.setPipeline(pipeline)
|
|
17151
17423
|
currentPipeline = pipeline
|
|
@@ -17268,14 +17540,17 @@ export class Engine {
|
|
|
17268
17540
|
inst: ModelInstance,
|
|
17269
17541
|
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
17270
17542
|
): void {
|
|
17271
|
-
let bound =
|
|
17543
|
+
let bound: GPURenderPipeline | null = null
|
|
17272
17544
|
for (const draw of inst.drawCalls) {
|
|
17273
17545
|
if (draw.type !== "opaque" || !this.isHairDraw(inst, draw)) continue
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
pass.
|
|
17277
|
-
|
|
17278
|
-
|
|
17546
|
+
const pipeline = this.sidedPipeline(this.hairPrimePipeline, draw, view.args === "mirror")
|
|
17547
|
+
if (bound !== pipeline) {
|
|
17548
|
+
pass.setPipeline(pipeline)
|
|
17549
|
+
if (!bound) {
|
|
17550
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17551
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17552
|
+
}
|
|
17553
|
+
bound = pipeline
|
|
17279
17554
|
}
|
|
17280
17555
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17281
17556
|
this.issueDraw(pass, draw, view.args)
|
|
@@ -17338,6 +17613,7 @@ export class Engine {
|
|
|
17338
17613
|
if (install?.alphaMode === "hashed") pipeline = this.solidPrepassPipeline
|
|
17339
17614
|
else if (install && install.alphaMode !== "opaque") continue
|
|
17340
17615
|
}
|
|
17616
|
+
pipeline = this.sidedPipeline(pipeline, draw, view.args === "mirror")
|
|
17341
17617
|
if (bound !== pipeline) {
|
|
17342
17618
|
pass.setPipeline(pipeline)
|
|
17343
17619
|
// One layout for both prepass pipelines, so groups 0 and 1 carry over.
|
|
@@ -17375,14 +17651,17 @@ export class Engine {
|
|
|
17375
17651
|
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
17376
17652
|
): void {
|
|
17377
17653
|
if (inst.isStage) return
|
|
17378
|
-
let bound =
|
|
17654
|
+
let bound: GPURenderPipeline | null = null
|
|
17379
17655
|
for (const draw of inst.drawCalls) {
|
|
17380
17656
|
if (draw.type !== "transparent") continue
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
pass.
|
|
17384
|
-
|
|
17385
|
-
|
|
17657
|
+
const pipeline = this.sidedPipeline(this.solidPrepassPipeline, draw, view.args === "mirror")
|
|
17658
|
+
if (bound !== pipeline) {
|
|
17659
|
+
pass.setPipeline(pipeline)
|
|
17660
|
+
if (!bound) {
|
|
17661
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17662
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17663
|
+
}
|
|
17664
|
+
bound = pipeline
|
|
17386
17665
|
}
|
|
17387
17666
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17388
17667
|
this.issueDraw(pass, draw, view.args)
|
|
@@ -17442,8 +17721,9 @@ export class Engine {
|
|
|
17442
17721
|
let currentPipeline: GPURenderPipeline | null = null
|
|
17443
17722
|
for (const draw of inst.drawCalls) {
|
|
17444
17723
|
if (draw.type !== "opaque") continue
|
|
17445
|
-
const
|
|
17446
|
-
if (!
|
|
17724
|
+
const hairOverEyes = this.overEyesPipelineFor(inst, draw)
|
|
17725
|
+
if (!hairOverEyes) continue
|
|
17726
|
+
const overEyes = this.sidedPipeline(hairOverEyes, draw, view.args === "mirror")
|
|
17447
17727
|
if (!bound) {
|
|
17448
17728
|
pass.setBindGroup(0, view.perFrame)
|
|
17449
17729
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
@@ -17895,6 +18175,9 @@ export class Engine {
|
|
|
17895
18175
|
// re-uploading bones×64 bytes for scenery that never moves is the one
|
|
17896
18176
|
// per-frame cost a stage would otherwise still pay in full.
|
|
17897
18177
|
if (!inst.skinMatricesDirty) return
|
|
18178
|
+
// Held on a stepped effect's tick: stays dirty, so the pose that is current
|
|
18179
|
+
// when the next tick comes is the one uploaded.
|
|
18180
|
+
if (this.steppedHeld.has(inst.name)) return
|
|
17898
18181
|
const skinMatrices = inst.model.getSkinMatrices()
|
|
17899
18182
|
this.device.queue.writeBuffer(
|
|
17900
18183
|
inst.skinMatrixBuffer,
|
package/src/graph/registry.ts
CHANGED
|
@@ -724,6 +724,17 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
724
724
|
outputs: { value: "float" },
|
|
725
725
|
emit: () => `shader_to_rgb_diffuse(n, l, sun, amb, shadow)`,
|
|
726
726
|
},
|
|
727
|
+
/**
|
|
728
|
+
* Hands this surface to the screen-space scattering pass: the colour passes
|
|
729
|
+
* through, and the pixel is blurred afterwards the way ray-mmd blurs skin —
|
|
730
|
+
* red furthest, never across a depth step. Strength scales the width; 1 is
|
|
731
|
+
* ray-mmd's skin, up to 4. It has to lie on the path to the output to take effect.
|
|
732
|
+
*/
|
|
733
|
+
subsurface: {
|
|
734
|
+
inputs: { color: C([1, 1, 1], true), strength: F(1) },
|
|
735
|
+
outputs: { color: "color" },
|
|
736
|
+
emit: (a) => `rz_subsurface(${a.strength}, ${a.color})`,
|
|
737
|
+
},
|
|
727
738
|
|
|
728
739
|
/**
|
|
729
740
|
* The same closure as a COLOUR, which is what Blender's Shader to RGB
|
package/src/graph/slots.ts
CHANGED
|
@@ -220,13 +220,13 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: bo
|
|
|
220
220
|
|
|
221
221
|
var out: FSOut;
|
|
222
222
|
out.color = vec4f(final_color${LIT}${BURN}, outAlpha);
|
|
223
|
-
${FOG} out.mask = vec4f(1.0, 1.0,
|
|
223
|
+
${FOG} out.mask = vec4f(1.0, 1.0, _rzSubsurface, out.color.a);
|
|
224
224
|
${ID_WRITE} return out;
|
|
225
225
|
`
|
|
226
226
|
}
|
|
227
227
|
return `${ALBEDO} var out: FSOut;
|
|
228
228
|
out.color = vec4f(final_color${LIT}${BURN}, ${alphaBase});
|
|
229
|
-
${FOG} out.mask = vec4f(1.0, 1.0,
|
|
229
|
+
${FOG} out.mask = vec4f(1.0, 1.0, _rzSubsurface, out.color.a);
|
|
230
230
|
${ID_WRITE} return out;
|
|
231
231
|
`
|
|
232
232
|
}
|
|
@@ -87,6 +87,15 @@ export type EffectDirectives = {
|
|
|
87
87
|
* it publishes, forks, schedules and fades like every other one.
|
|
88
88
|
*/
|
|
89
89
|
mirror: boolean
|
|
90
|
+
/**
|
|
91
|
+
* `#stepped`: the cast this effect is aimed at moves ON TWOS — its pose, and
|
|
92
|
+
* its face, held for a few frames and then snapped to where it is now, the way
|
|
93
|
+
* limited animation and stop-motion move. A mount with no shader, on
|
|
94
|
+
* #mirror's footing: it changes WHEN a pose reaches the screen, which no
|
|
95
|
+
* shader can. The rate comes from the effect's own FPS dial by name — see
|
|
96
|
+
* the engine's STEPPED_DIALS.
|
|
97
|
+
*/
|
|
98
|
+
stepped: boolean
|
|
90
99
|
/**
|
|
91
100
|
* How long ONE firing of this effect lasts, in seconds. 0 = undeclared.
|
|
92
101
|
*
|
|
@@ -118,6 +127,7 @@ const SPEC = {
|
|
|
118
127
|
bloom: 0,
|
|
119
128
|
dissolve: 0,
|
|
120
129
|
mirror: 0,
|
|
130
|
+
stepped: 0,
|
|
121
131
|
duration: 1,
|
|
122
132
|
ground: "rest",
|
|
123
133
|
} as const
|
|
@@ -177,6 +187,7 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
177
187
|
dissolve: false,
|
|
178
188
|
ground: null,
|
|
179
189
|
mirror: false,
|
|
190
|
+
stepped: false,
|
|
180
191
|
duration: 0,
|
|
181
192
|
}
|
|
182
193
|
const errors: string[] = []
|
|
@@ -296,6 +307,9 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
296
307
|
case "mirror":
|
|
297
308
|
d.mirror = true
|
|
298
309
|
return
|
|
310
|
+
case "stepped":
|
|
311
|
+
d.stepped = true
|
|
312
|
+
return
|
|
299
313
|
case "duration": {
|
|
300
314
|
// SECONDS, like every other time a directive states (see #dissolve).
|
|
301
315
|
// The document above works in frames and converts once; an author
|
|
@@ -330,17 +330,17 @@ const COMMON_VS_WGSL = /* wgsl */ `
|
|
|
330
330
|
// Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
|
|
331
331
|
// has no alpha channel, but the blend equation still uses the .a you write here
|
|
332
332
|
// as the src-alpha factor that premultiplies rgb into the HDR target).
|
|
333
|
-
// Location 1: auxiliary
|
|
333
|
+
// Location 1: auxiliary rgba8unorm carrying
|
|
334
334
|
// .r = bloom mask (1 = contributes to bloom, 0 = skip — e.g. ground).
|
|
335
335
|
// .g = accumulated canvas alpha — the channel that used to live in hdr.a
|
|
336
336
|
// before the switch to rg11b10ufloat. Sampled by composite to
|
|
337
337
|
// un-premultiply color for tonemap and to set the final drawable alpha
|
|
338
338
|
// (needed for the `premultiplied` canvas alphaMode that blends the
|
|
339
339
|
// WebGPU surface over the page background).
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// Materials write mask = vec4f(1.0, 1.0,
|
|
340
|
+
// .b = subsurface strength ÷ 4 — what a graph's `subsurface` node set, 0 for
|
|
341
|
+
// everything else. Read by the scattering pass (passes/subsurface.ts).
|
|
342
|
+
// FS output at location 1 must be vec4f — the blend state references src.a.
|
|
343
|
+
// Materials write mask = vec4f(1.0, 1.0, _rzSubsurface, color.a); ground writes
|
|
344
344
|
// vec4f(0.0, 1.0, 0.0, edgeFade). With src.a coming from the 4th component and
|
|
345
345
|
// src-alpha blending enabled:
|
|
346
346
|
// out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
|
|
@@ -654,6 +654,18 @@ fn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f,
|
|
|
654
654
|
return luminance_rec709_linear(shader_to_rgb_lit(n, l, sun_rgb, ambient_rgb, shadow));
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
// ─── SUBSURFACE node ────────────────────────────────────────────────
|
|
658
|
+
// Marks this fragment for the screen-space scattering pass (passes/subsurface.ts)
|
|
659
|
+
// and passes its colour through. The strength lands in the aux target's .b via
|
|
660
|
+
// the epilogue, stored as a quarter so an 8-bit channel carries 0–4: 1 is
|
|
661
|
+
// ray-mmd's skin width, 0 leaves the pixel out of the pass.
|
|
662
|
+
var<private> _rzSubsurface: f32 = 0.0;
|
|
663
|
+
|
|
664
|
+
fn rz_subsurface(strength: f32, color: vec3f) -> vec3f {
|
|
665
|
+
_rzSubsurface = clamp(strength, 0.0, 4.0) * 0.25;
|
|
666
|
+
return color;
|
|
667
|
+
}
|
|
668
|
+
|
|
657
669
|
// ─── BUMP node ──────────────────────────────────────────────────────
|
|
658
670
|
// Screen-space bump from a scalar height field. Needs dFdx/dFdy which
|
|
659
671
|
// WGSL provides as dpdx/dpdy.
|