reze-engine 0.61.4 → 0.61.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +49 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +232 -25
- 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/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/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 +242 -24
- package/src/graph/registry.ts +11 -0
- package/src/graph/slots.ts +2 -2
- 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). */
|
|
@@ -2082,15 +2089,18 @@ export class Engine {
|
|
|
2082
2089
|
/** Stencil value stamped by eye draws so hair can stencil-test against it and
|
|
2083
2090
|
* alpha-blend a second pass over eye silhouette pixels (see-through-hair effect). */
|
|
2084
2091
|
private static readonly STENCIL_EYE_VALUE = 1
|
|
2085
|
-
/** Aux MRT alongside HDR color.
|
|
2092
|
+
/** Aux MRT alongside HDR color. Three channels:
|
|
2086
2093
|
* .r — bloom mask (1 = model geometry, 0 = ground; sampled by bloom blit to gate prefilter).
|
|
2087
2094
|
* .g — accumulated alpha (the channel that used to live in hdr.a before the HDR format
|
|
2088
2095
|
* switched to rg11b10ufloat, which has no alpha). Sampled by composite/bloom to
|
|
2089
2096
|
* un-premultiply color for tonemap and to produce the canvas-drawable alpha used by
|
|
2090
2097
|
* the premultiplied alphaMode compositor (so the page background still shows through
|
|
2091
2098
|
* cleared / edge-faded regions like before).
|
|
2092
|
-
*
|
|
2093
|
-
|
|
2099
|
+
* .b — subsurface strength, set by a graph's `subsurface` node and 0 everywhere
|
|
2100
|
+
* else; the scattering pass reads it to find skin (passes/subsurface.ts).
|
|
2101
|
+
* rgba8unorm at 4× MSAA is 16 bytes/texel; it never leaves tile memory (storeOp
|
|
2102
|
+
* discard), only its single-sample resolve does. */
|
|
2103
|
+
private static readonly BLOOM_MASK_FORMAT: GPUTextureFormat = "rgba8unorm"
|
|
2094
2104
|
/**
|
|
2095
2105
|
* The master switch for the id attachment. OFF — and off having been proven
|
|
2096
2106
|
* to work, not off because it was never finished.
|
|
@@ -2633,6 +2643,22 @@ export class Engine {
|
|
|
2633
2643
|
// composite adds bloomUp mip 0 × (color × intensity) to HDR before Filmic.
|
|
2634
2644
|
// Matches EEVEE energy: tint/intensity applied at composite, not prefilter.
|
|
2635
2645
|
private bloomSampler!: GPUSampler
|
|
2646
|
+
// Screen-space subsurface scattering (passes/subsurface.ts). The scratch target
|
|
2647
|
+
// is the X pass's output and lives with the canvas size; the bind groups name
|
|
2648
|
+
// it and the HDR resolve, so a resize drops them and the next frame rebuilds.
|
|
2649
|
+
private sssPipelineX: GPURenderPipeline | null = null
|
|
2650
|
+
private sssPipelineY: GPURenderPipeline | null = null
|
|
2651
|
+
private sssScratch: GPUTexture | null = null
|
|
2652
|
+
private sssBindGroupX: GPUBindGroup | null = null
|
|
2653
|
+
private sssBindGroupY: GPUBindGroup | null = null
|
|
2654
|
+
private sssUniformX: GPUBuffer | null = null
|
|
2655
|
+
private sssUniformY: GPUBuffer | null = null
|
|
2656
|
+
private readonly sssUniformData = new Float32Array(8)
|
|
2657
|
+
// Model pipelines' descriptors, and the single-sided twins built from them —
|
|
2658
|
+
// one per pipeline per view (see sidedPipeline).
|
|
2659
|
+
private readonly pipelineDescs = new WeakMap<GPURenderPipeline, GPURenderPipelineDescriptor>()
|
|
2660
|
+
private readonly singleSided = new WeakMap<GPURenderPipeline, GPURenderPipeline>()
|
|
2661
|
+
private readonly singleSidedMirror = new WeakMap<GPURenderPipeline, GPURenderPipeline>()
|
|
2636
2662
|
private bloomBlitUniformBuffer!: GPUBuffer
|
|
2637
2663
|
private bloomUpsampleUniformBuffer!: GPUBuffer
|
|
2638
2664
|
private readonly bloomBlitUniformData = new Float32Array(4)
|
|
@@ -6143,6 +6169,134 @@ export class Engine {
|
|
|
6143
6169
|
/** The user's field mounts, drawn at half resolution for the composite to
|
|
6144
6170
|
* upsample. Runs the whole quad — uniform control flow, so effects may use
|
|
6145
6171
|
* derivatives freely, which the old inline path had to forbid. */
|
|
6172
|
+
/** Whether any installed style group routes through a `subsurface` node —
|
|
6173
|
+
* the scattering pass, and the depth it reads, cost nothing otherwise. */
|
|
6174
|
+
private subsurfaceInUse(): boolean {
|
|
6175
|
+
for (const inst of this.modelInstances.values()) {
|
|
6176
|
+
for (const g of inst.styleGroups.values()) {
|
|
6177
|
+
if (g.group.materials.length && g.group.graph.nodes.some((n) => n.type === "subsurface")) return true
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
return false
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
/**
|
|
6184
|
+
* ray-mmd's skin scattering: a horizontal then a vertical depth-aware blur of
|
|
6185
|
+
* the pixels the scene pass marked as skin (aux .b), the second written back
|
|
6186
|
+
* into the HDR resolve through a blend that keeps ray-mmd's per-channel share
|
|
6187
|
+
* of the unblurred picture. See passes/subsurface.ts.
|
|
6188
|
+
*/
|
|
6189
|
+
private renderSubsurface(encoder: GPUCommandEncoder): void {
|
|
6190
|
+
if (!this.device || !this.subsurfaceInUse()) return
|
|
6191
|
+
if (!this.hdrResolveTexture || !this.maskResolveView || !this.depthReadView || !this.dofUniformBuffer) return
|
|
6192
|
+
const w = this.hdrResolveTexture.width
|
|
6193
|
+
const h = this.hdrResolveTexture.height
|
|
6194
|
+
|
|
6195
|
+
if (!this.sssPipelineX || !this.sssPipelineY) {
|
|
6196
|
+
const module = this.device.createShaderModule({ label: "subsurface scattering", code: SUBSURFACE_WGSL })
|
|
6197
|
+
this.sssPipelineX = this.device.createRenderPipeline({
|
|
6198
|
+
label: "subsurface blur x",
|
|
6199
|
+
layout: "auto",
|
|
6200
|
+
vertex: { module, entryPoint: "vs" },
|
|
6201
|
+
fragment: { module, entryPoint: "fsX", targets: [{ format: this.hdrFormat }] },
|
|
6202
|
+
primitive: { topology: "triangle-list" },
|
|
6203
|
+
})
|
|
6204
|
+
// result = blurred·(1 − spike) + original·spike, per channel: the
|
|
6205
|
+
// destination is the original, and the blend constant holds the spike.
|
|
6206
|
+
this.sssPipelineY = this.device.createRenderPipeline({
|
|
6207
|
+
label: "subsurface blur y",
|
|
6208
|
+
layout: "auto",
|
|
6209
|
+
vertex: { module, entryPoint: "vs" },
|
|
6210
|
+
fragment: {
|
|
6211
|
+
module,
|
|
6212
|
+
entryPoint: "fsY",
|
|
6213
|
+
targets: [
|
|
6214
|
+
{
|
|
6215
|
+
format: this.hdrFormat,
|
|
6216
|
+
blend: {
|
|
6217
|
+
color: { srcFactor: "one-minus-constant", dstFactor: "constant", operation: "add" },
|
|
6218
|
+
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
6219
|
+
},
|
|
6220
|
+
},
|
|
6221
|
+
],
|
|
6222
|
+
},
|
|
6223
|
+
primitive: { topology: "triangle-list" },
|
|
6224
|
+
})
|
|
6225
|
+
this.sssBindGroupX = null
|
|
6226
|
+
this.sssBindGroupY = null
|
|
6227
|
+
}
|
|
6228
|
+
if (!this.sssUniformX || !this.sssUniformY) {
|
|
6229
|
+
const make = (label: string) =>
|
|
6230
|
+
this.device.createBuffer({ label, size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
|
|
6231
|
+
this.sssUniformX = make("subsurface uniforms x")
|
|
6232
|
+
this.sssUniformY = make("subsurface uniforms y")
|
|
6233
|
+
}
|
|
6234
|
+
if (!this.sssScratch || this.sssScratch.width !== w || this.sssScratch.height !== h) {
|
|
6235
|
+
this.sssScratch?.destroy()
|
|
6236
|
+
this.sssScratch = this.device.createTexture({
|
|
6237
|
+
label: "subsurface scratch",
|
|
6238
|
+
size: [w, h],
|
|
6239
|
+
format: this.hdrFormat,
|
|
6240
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
6241
|
+
})
|
|
6242
|
+
this.sssBindGroupX = null
|
|
6243
|
+
this.sssBindGroupY = null
|
|
6244
|
+
}
|
|
6245
|
+
if (!this.sssBindGroupX || !this.sssBindGroupY) {
|
|
6246
|
+
const mask = this.maskResolveView
|
|
6247
|
+
const depth = this.depthReadView
|
|
6248
|
+
const dof = this.dofUniformBuffer
|
|
6249
|
+
const group = (pipeline: GPURenderPipeline, src: GPUTexture, uniforms: GPUBuffer, label: string) =>
|
|
6250
|
+
this.device.createBindGroup({
|
|
6251
|
+
label,
|
|
6252
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
6253
|
+
entries: [
|
|
6254
|
+
{ binding: 0, resource: src.createView() },
|
|
6255
|
+
{ binding: 1, resource: mask },
|
|
6256
|
+
{ binding: 2, resource: depth },
|
|
6257
|
+
{ binding: 3, resource: { buffer: dof } },
|
|
6258
|
+
{ binding: 4, resource: { buffer: uniforms } },
|
|
6259
|
+
],
|
|
6260
|
+
})
|
|
6261
|
+
this.sssBindGroupX = group(this.sssPipelineX, this.hdrResolveTexture, this.sssUniformX, "subsurface x")
|
|
6262
|
+
this.sssBindGroupY = group(this.sssPipelineY, this.sssScratch, this.sssUniformY, "subsurface y")
|
|
6263
|
+
}
|
|
6264
|
+
|
|
6265
|
+
const tanY = Math.tan(this.camera.fov * 0.5)
|
|
6266
|
+
const u = this.sssUniformData
|
|
6267
|
+
u[2] = 0
|
|
6268
|
+
u[3] = 0
|
|
6269
|
+
u[4] = tanY * this.camera.aspect
|
|
6270
|
+
u[5] = tanY
|
|
6271
|
+
u[6] = w
|
|
6272
|
+
u[7] = h
|
|
6273
|
+
u[0] = 1
|
|
6274
|
+
u[1] = 0
|
|
6275
|
+
this.device.queue.writeBuffer(this.sssUniformX, 0, u)
|
|
6276
|
+
u[0] = 0
|
|
6277
|
+
u[1] = 1
|
|
6278
|
+
this.device.queue.writeBuffer(this.sssUniformY, 0, u)
|
|
6279
|
+
|
|
6280
|
+
const px = encoder.beginRenderPass({
|
|
6281
|
+
label: "subsurface blur x",
|
|
6282
|
+
colorAttachments: [{ view: this.sssScratch.createView(), loadOp: "clear", clearValue: [0, 0, 0, 0], storeOp: "store" }],
|
|
6283
|
+
})
|
|
6284
|
+
px.setPipeline(this.sssPipelineX)
|
|
6285
|
+
px.setBindGroup(0, this.sssBindGroupX)
|
|
6286
|
+
px.draw(3)
|
|
6287
|
+
px.end()
|
|
6288
|
+
|
|
6289
|
+
const py = encoder.beginRenderPass({
|
|
6290
|
+
label: "subsurface blur y",
|
|
6291
|
+
colorAttachments: [{ view: this.hdrResolveTexture.createView(), loadOp: "load", storeOp: "store" }],
|
|
6292
|
+
})
|
|
6293
|
+
py.setPipeline(this.sssPipelineY)
|
|
6294
|
+
py.setBindGroup(0, this.sssBindGroupY)
|
|
6295
|
+
py.setBlendConstant({ r: SSS_SPIKE[0], g: SSS_SPIKE[1], b: SSS_SPIKE[2], a: 0 })
|
|
6296
|
+
py.draw(3)
|
|
6297
|
+
py.end()
|
|
6298
|
+
}
|
|
6299
|
+
|
|
6146
6300
|
private renderFieldPass(encoder: GPUCommandEncoder): void {
|
|
6147
6301
|
// TWO PREDICATES, deliberately, and they are not interchangeable.
|
|
6148
6302
|
//
|
|
@@ -7210,7 +7364,7 @@ export class Engine {
|
|
|
7210
7364
|
multisample?: GPUMultisampleState
|
|
7211
7365
|
}): GPURenderPipeline {
|
|
7212
7366
|
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined)
|
|
7213
|
-
return this.
|
|
7367
|
+
return this.createModelPipeline({
|
|
7214
7368
|
label: config.label,
|
|
7215
7369
|
layout: config.layout,
|
|
7216
7370
|
vertex: {
|
|
@@ -7653,7 +7807,7 @@ export class Engine {
|
|
|
7653
7807
|
depthCompare: this.depthAhead,
|
|
7654
7808
|
},
|
|
7655
7809
|
}
|
|
7656
|
-
this.depthPrepassPipeline = this.
|
|
7810
|
+
this.depthPrepassPipeline = this.createModelPipeline({
|
|
7657
7811
|
label: "opaque depth prepass",
|
|
7658
7812
|
...prepassDesc,
|
|
7659
7813
|
fragment: {
|
|
@@ -7665,7 +7819,7 @@ export class Engine {
|
|
|
7665
7819
|
// The SOLID prime: same module, cutoff forced to exactly 1.0. Only texels
|
|
7666
7820
|
// whose blend ignores the destination may pre-claim depth in the
|
|
7667
7821
|
// transparent phase — see the override's note in depth-prepass.ts.
|
|
7668
|
-
this.solidPrepassPipeline = this.
|
|
7822
|
+
this.solidPrepassPipeline = this.createModelPipeline({
|
|
7669
7823
|
label: "transparent solid prepass",
|
|
7670
7824
|
...prepassDesc,
|
|
7671
7825
|
fragment: {
|
|
@@ -7681,7 +7835,7 @@ export class Engine {
|
|
|
7681
7835
|
// hair depth from ever claiming the pixels the see-through-hair pass needs
|
|
7682
7836
|
// the eye to survive on. (Bundle draws use the PASS's stencil reference;
|
|
7683
7837
|
// only pipeline/bind/vertex state resets across executeBundles.)
|
|
7684
|
-
this.hairPrimePipeline = this.
|
|
7838
|
+
this.hairPrimePipeline = this.createModelPipeline({
|
|
7685
7839
|
label: "hair depth prime",
|
|
7686
7840
|
...prepassDesc,
|
|
7687
7841
|
depthStencil: {
|
|
@@ -8553,6 +8707,10 @@ export class Engine {
|
|
|
8553
8707
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
8554
8708
|
})
|
|
8555
8709
|
this.maskResolveView = this.maskResolveTexture.createView()
|
|
8710
|
+
this.sssScratch?.destroy()
|
|
8711
|
+
this.sssScratch = null
|
|
8712
|
+
this.sssBindGroupX = null
|
|
8713
|
+
this.sssBindGroupY = null
|
|
8556
8714
|
|
|
8557
8715
|
// The floor mirror's targets — FULL resolution, the same attachment
|
|
8558
8716
|
// contract and sample count as the scene pass, which is what lets the
|
|
@@ -10403,6 +10561,8 @@ export class Engine {
|
|
|
10403
10561
|
bindGroup: this.groundShadowBindGroup!,
|
|
10404
10562
|
materialName: "Ground",
|
|
10405
10563
|
groupId: null,
|
|
10564
|
+
// The ground's own pipeline decides its culling.
|
|
10565
|
+
doubleSided: true,
|
|
10406
10566
|
// The ground belongs to no model instance, so it is not in the cull list —
|
|
10407
10567
|
// cullIndex -1 leaves renderGround unconditional. Its box is filled in
|
|
10408
10568
|
// anyway rather than left a lie for whoever reads this next.
|
|
@@ -14479,6 +14639,7 @@ export class Engine {
|
|
|
14479
14639
|
groupId: null,
|
|
14480
14640
|
baseBindGroupEntries,
|
|
14481
14641
|
castsShadow,
|
|
14642
|
+
doubleSided: inst.isPlane || (mat.edgeFlag & 0x01) !== 0,
|
|
14482
14643
|
outline,
|
|
14483
14644
|
bounds,
|
|
14484
14645
|
cullIndex: -1,
|
|
@@ -16063,7 +16224,10 @@ export class Engine {
|
|
|
16063
16224
|
// The two real readers are both in the composite and both have their own
|
|
16064
16225
|
// flag above: linearDepth() feeds the DoF gather and the depth handed to a
|
|
16065
16226
|
// foreground mount. Nothing else binds depthTex at all.
|
|
16066
|
-
|
|
16227
|
+
// The scattering pass is the third reader: it rejects taps across a depth
|
|
16228
|
+
// step and scales its width by distance, so a scene wearing a subsurface
|
|
16229
|
+
// look stores depth for as long as it does.
|
|
16230
|
+
const depthRead = dofOn || this.effects.some((e) => e.hasForeground) || this.subsurfaceInUse()
|
|
16067
16231
|
this.renderPassDescriptor.depthStencilAttachment!.depthStoreOp = depthRead ? "store" : "discard"
|
|
16068
16232
|
if (depthRead) this.writeDepthOfFieldUniforms()
|
|
16069
16233
|
|
|
@@ -16261,6 +16425,9 @@ export class Engine {
|
|
|
16261
16425
|
// kept ribbons out of bloom.
|
|
16262
16426
|
this.drawTrails(pass, "camera")
|
|
16263
16427
|
pass.end()
|
|
16428
|
+
// Skin scattering, on the resolved scene and before anything reads it:
|
|
16429
|
+
// bloom should bleed the scattered skin, not the raw one.
|
|
16430
|
+
this.renderSubsurface(encoder)
|
|
16264
16431
|
// The field mounts, likewise: after the scene so foregrounds can read its
|
|
16265
16432
|
// depth, before the composite that samples both layers.
|
|
16266
16433
|
this.renderFieldPass(encoder)
|
|
@@ -17081,13 +17248,55 @@ export class Engine {
|
|
|
17081
17248
|
stencilWriteMask: 0xff,
|
|
17082
17249
|
}
|
|
17083
17250
|
}
|
|
17084
|
-
|
|
17251
|
+
const desc: GPURenderPipelineDescriptor = {
|
|
17085
17252
|
...base,
|
|
17086
17253
|
fragment: { module, constants, targets: blend === "additive" ? this.sceneTargetsAdditive : this.sceneTargets },
|
|
17087
17254
|
depthStencil,
|
|
17255
|
+
}
|
|
17256
|
+
return this.device.createRenderPipelineAsync(desc).then((pipeline) => {
|
|
17257
|
+
this.pipelineDescs.set(pipeline, desc)
|
|
17258
|
+
return pipeline
|
|
17088
17259
|
})
|
|
17089
17260
|
}
|
|
17090
17261
|
|
|
17262
|
+
/** A pipeline whose descriptor is kept, so a single-sided twin can be built
|
|
17263
|
+
* from it on demand (see sidedPipeline). */
|
|
17264
|
+
private createModelPipeline(desc: GPURenderPipelineDescriptor): GPURenderPipeline {
|
|
17265
|
+
const pipeline = this.device.createRenderPipeline(desc)
|
|
17266
|
+
this.pipelineDescs.set(pipeline, desc)
|
|
17267
|
+
return pipeline
|
|
17268
|
+
}
|
|
17269
|
+
|
|
17270
|
+
/**
|
|
17271
|
+
* The pipeline a draw should use once its double-sided flag is honoured.
|
|
17272
|
+
*
|
|
17273
|
+
* A single-sided material culls its back faces, as MMD does. PMX winds a front
|
|
17274
|
+
* face clockwise, which this engine sees as a BACK face — the eye's front cull
|
|
17275
|
+
* and the outline hull's back cull are the same fact — so the cull is "front"
|
|
17276
|
+
* for the camera and "back" in the mirror, whose reflection flips winding.
|
|
17277
|
+
*
|
|
17278
|
+
* Every pipeline that draws a material's surface goes through here, depth
|
|
17279
|
+
* primes included: a prime that kept a culled face would write depth for a
|
|
17280
|
+
* surface nobody sees and hide what is behind it. A pipeline that already
|
|
17281
|
+
* culls (the eye) is returned as it is.
|
|
17282
|
+
*/
|
|
17283
|
+
private sidedPipeline(pipeline: GPURenderPipeline, draw: DrawCall, mirrored: boolean): GPURenderPipeline {
|
|
17284
|
+
if (draw.doubleSided) return pipeline
|
|
17285
|
+
const desc = this.pipelineDescs.get(pipeline)
|
|
17286
|
+
if (!desc || (desc.primitive?.cullMode ?? "none") !== "none") return pipeline
|
|
17287
|
+
const cache = mirrored ? this.singleSidedMirror : this.singleSided
|
|
17288
|
+
let twin = cache.get(pipeline)
|
|
17289
|
+
if (!twin) {
|
|
17290
|
+
twin = this.device.createRenderPipeline({
|
|
17291
|
+
...desc,
|
|
17292
|
+
label: `${desc.label ?? "pipeline"} (single-sided${mirrored ? ", mirror" : ""})`,
|
|
17293
|
+
primitive: { ...desc.primitive, cullMode: mirrored ? "back" : "front" },
|
|
17294
|
+
})
|
|
17295
|
+
cache.set(pipeline, twin)
|
|
17296
|
+
}
|
|
17297
|
+
return twin
|
|
17298
|
+
}
|
|
17299
|
+
|
|
17091
17300
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
17092
17301
|
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
17093
17302
|
// draws on the CAST use the SAME depth-write-on pipeline — babylon-mmd's
|
|
@@ -17145,7 +17354,8 @@ export class Engine {
|
|
|
17145
17354
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17146
17355
|
bound = true
|
|
17147
17356
|
}
|
|
17148
|
-
const
|
|
17357
|
+
const mirrored = view.args === "mirror"
|
|
17358
|
+
const pipeline = this.sidedPipeline(this.pipelineForDrawCall(inst, draw, mirrored), draw, mirrored)
|
|
17149
17359
|
if (pipeline !== currentPipeline) {
|
|
17150
17360
|
pass.setPipeline(pipeline)
|
|
17151
17361
|
currentPipeline = pipeline
|
|
@@ -17268,14 +17478,17 @@ export class Engine {
|
|
|
17268
17478
|
inst: ModelInstance,
|
|
17269
17479
|
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
17270
17480
|
): void {
|
|
17271
|
-
let bound =
|
|
17481
|
+
let bound: GPURenderPipeline | null = null
|
|
17272
17482
|
for (const draw of inst.drawCalls) {
|
|
17273
17483
|
if (draw.type !== "opaque" || !this.isHairDraw(inst, draw)) continue
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
pass.
|
|
17277
|
-
|
|
17278
|
-
|
|
17484
|
+
const pipeline = this.sidedPipeline(this.hairPrimePipeline, draw, view.args === "mirror")
|
|
17485
|
+
if (bound !== pipeline) {
|
|
17486
|
+
pass.setPipeline(pipeline)
|
|
17487
|
+
if (!bound) {
|
|
17488
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17489
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17490
|
+
}
|
|
17491
|
+
bound = pipeline
|
|
17279
17492
|
}
|
|
17280
17493
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17281
17494
|
this.issueDraw(pass, draw, view.args)
|
|
@@ -17338,6 +17551,7 @@ export class Engine {
|
|
|
17338
17551
|
if (install?.alphaMode === "hashed") pipeline = this.solidPrepassPipeline
|
|
17339
17552
|
else if (install && install.alphaMode !== "opaque") continue
|
|
17340
17553
|
}
|
|
17554
|
+
pipeline = this.sidedPipeline(pipeline, draw, view.args === "mirror")
|
|
17341
17555
|
if (bound !== pipeline) {
|
|
17342
17556
|
pass.setPipeline(pipeline)
|
|
17343
17557
|
// One layout for both prepass pipelines, so groups 0 and 1 carry over.
|
|
@@ -17375,14 +17589,17 @@ export class Engine {
|
|
|
17375
17589
|
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
17376
17590
|
): void {
|
|
17377
17591
|
if (inst.isStage) return
|
|
17378
|
-
let bound =
|
|
17592
|
+
let bound: GPURenderPipeline | null = null
|
|
17379
17593
|
for (const draw of inst.drawCalls) {
|
|
17380
17594
|
if (draw.type !== "transparent") continue
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
pass.
|
|
17384
|
-
|
|
17385
|
-
|
|
17595
|
+
const pipeline = this.sidedPipeline(this.solidPrepassPipeline, draw, view.args === "mirror")
|
|
17596
|
+
if (bound !== pipeline) {
|
|
17597
|
+
pass.setPipeline(pipeline)
|
|
17598
|
+
if (!bound) {
|
|
17599
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17600
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17601
|
+
}
|
|
17602
|
+
bound = pipeline
|
|
17386
17603
|
}
|
|
17387
17604
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17388
17605
|
this.issueDraw(pass, draw, view.args)
|
|
@@ -17442,8 +17659,9 @@ export class Engine {
|
|
|
17442
17659
|
let currentPipeline: GPURenderPipeline | null = null
|
|
17443
17660
|
for (const draw of inst.drawCalls) {
|
|
17444
17661
|
if (draw.type !== "opaque") continue
|
|
17445
|
-
const
|
|
17446
|
-
if (!
|
|
17662
|
+
const hairOverEyes = this.overEyesPipelineFor(inst, draw)
|
|
17663
|
+
if (!hairOverEyes) continue
|
|
17664
|
+
const overEyes = this.sidedPipeline(hairOverEyes, draw, view.args === "mirror")
|
|
17447
17665
|
if (!bound) {
|
|
17448
17666
|
pass.setBindGroup(0, view.perFrame)
|
|
17449
17667
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
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
|
}
|
|
@@ -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.
|
|
@@ -391,13 +391,13 @@ struct FSOut {
|
|
|
391
391
|
// writes it; a blended one writes all four, which is what not declaring it
|
|
392
392
|
// would have meant.
|
|
393
393
|
@builtin(sample_mask) samples: u32,
|
|
394
|
-
// The scene's aux target: (bloom mask, coverage). Materials write
|
|
395
|
-
// particle that skipped it would punch a hole in the mask of whatever
|
|
396
|
-
// over.
|
|
394
|
+
// The scene's aux target: (bloom mask, coverage, subsurface). Materials write
|
|
395
|
+
// it, so a particle that skipped it would punch a hole in the mask of whatever
|
|
396
|
+
// it drew over. A particle is never skin, so its .b is 0.
|
|
397
397
|
//
|
|
398
|
-
// vec4f
|
|
399
|
-
//
|
|
400
|
-
//
|
|
398
|
+
// vec4f because that target's blend factors reference SrcAlpha, and a fragment
|
|
399
|
+
// with no alpha channel is rejected outright — "reading alpha but it is
|
|
400
|
+
// missing from fragment output".
|
|
401
401
|
// The material shaders declare vec4f here for the same reason.
|
|
402
402
|
@location(1) mask: vec4f,
|
|
403
403
|
// Declared whenever the pass carries the attachment, and padded rather than
|
|
@@ -127,7 +127,7 @@ const ADD_KEEP_ALPHA: GPUBlendState = {
|
|
|
127
127
|
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/** Both channels sum.
|
|
130
|
+
/** Both channels sum. rgba8unorm clamps at 1, which is the saturation alpha-over
|
|
131
131
|
* would have reached anyway. */
|
|
132
132
|
const ADD_BOTH: GPUBlendState = {
|
|
133
133
|
color: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen-space subsurface scattering — ray-mmd's skin pass (PostProcessScattering.fxsub,
|
|
3
|
+
* SSSGaussBlurPS), ported constant for constant.
|
|
4
|
+
*
|
|
5
|
+
* A skin surface is not shaded by the light at its own pixel alone: light that
|
|
6
|
+
* enters a little way off comes back out here, red furthest. ray-mmd models that
|
|
7
|
+
* as a separable blur of the lit skin in screen space, one axis per pass, with a
|
|
8
|
+
* per-channel Gaussian (red widest) and a depth reject so light never crosses from
|
|
9
|
+
* a forearm onto the body behind it. The width is a length ON THE SURFACE, so it
|
|
10
|
+
* is divided by depth, and it narrows where the surface turns away from the view.
|
|
11
|
+
* Then a share of the unblurred picture is laid back over it — the "spike" that
|
|
12
|
+
* keeps pores, painted lines and the terminator from dissolving.
|
|
13
|
+
*
|
|
14
|
+
* What marks a pixel as skin is the scene pass's aux .b, written by the graph's
|
|
15
|
+
* `subsurface` node (0 everywhere else), as a quarter of its strength. The strength
|
|
16
|
+
* scales the radius, as ray-mmd's sssStrength does; 1 is ray-mmd's skin, 4 the most.
|
|
17
|
+
*
|
|
18
|
+
* One departure, and it is a routing one: ray-mmd blurs the lit colour BEFORE the
|
|
19
|
+
* albedo multiply and adds specular after. This engine resolves one HDR colour per
|
|
20
|
+
* pixel, so the blur runs over the finished colour. At this radius — a few pixels,
|
|
21
|
+
* with the spike keeping 14–20% of the original — the texture is not visibly
|
|
22
|
+
* softened; the lighting is.
|
|
23
|
+
*
|
|
24
|
+
* Two entry points share the module: X reads the HDR resolve and writes the
|
|
25
|
+
* scratch target, Y reads the scratch and writes back INTO the HDR resolve. The
|
|
26
|
+
* spike mix (lerp toward the original) is done by the Y pipeline's blend state,
|
|
27
|
+
* with the blend constant holding ray-mmd's per-channel spike: the destination IS
|
|
28
|
+
* the original, so no third copy of the frame is needed.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** ray-mmd, skin profile (index 1): profileSpikeRadArr[1].xyz × (1 − sssAmount),
|
|
32
|
+
* with sssAmount = customA = 0.6 in Materials/Skin/material_skin.fx. */
|
|
33
|
+
export const SSS_SPIKE: [number, number, number] = [0.35 * 0.4, 0.4 * 0.4, 0.5 * 0.4]
|
|
34
|
+
|
|
35
|
+
export const SUBSURFACE_WGSL = /* wgsl */ `
|
|
36
|
+
@group(0) @binding(0) var srcTex: texture_2d<f32>;
|
|
37
|
+
@group(0) @binding(1) var maskTex: texture_2d<f32>;
|
|
38
|
+
@group(0) @binding(2) var depthTex: texture_depth_multisampled_2d;
|
|
39
|
+
// dofU[2] = (projA, projB): the same z-buffer inversion the composite uses.
|
|
40
|
+
@group(0) @binding(3) var<uniform> dofU: array<vec4f, 3>;
|
|
41
|
+
// sssU[0] = (direction.xy, radius, _); sssU[1] = (tanHalfFov·aspect, tanHalfFov, width, height).
|
|
42
|
+
@group(0) @binding(4) var<uniform> sssU: array<vec4f, 2>;
|
|
43
|
+
|
|
44
|
+
struct VsOut { @builtin(position) pos: vec4f };
|
|
45
|
+
|
|
46
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> VsOut {
|
|
47
|
+
let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
|
48
|
+
var o: VsOut;
|
|
49
|
+
o.pos = vec4f(p * 2.0 - 1.0, 0.0, 1.0);
|
|
50
|
+
return o;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn size() -> vec2i { return vec2i(sssU[1].zw); }
|
|
54
|
+
|
|
55
|
+
fn texel(uv: vec2f) -> vec2i {
|
|
56
|
+
return clamp(vec2i(floor(uv * sssU[1].zw)), vec2i(0), size() - 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn linearDepth(c: vec2i) -> f32 {
|
|
60
|
+
let z = textureLoad(depthTex, c, 0);
|
|
61
|
+
return clamp(dofU[2].y / (z - dofU[2].x), 0.05, 100000.0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// View-space position of a texel centre — ray-mmd's GetViewPosition, with y up.
|
|
65
|
+
fn viewPos(c: vec2i, d: f32) -> vec3f {
|
|
66
|
+
let uv = (vec2f(c) + 0.5) / sssU[1].zw;
|
|
67
|
+
let ndc = vec2f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
|
68
|
+
return vec3f(ndc * sssU[1].xy * d, d);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn skin(c: vec2i) -> f32 { return textureLoad(maskTex, c, 0).b; }
|
|
72
|
+
|
|
73
|
+
fn blur(fragPos: vec4f) -> vec4f {
|
|
74
|
+
let c = vec2i(fragPos.xy);
|
|
75
|
+
let amount = skin(c) * 4.0;
|
|
76
|
+
let dir = sssU[0].xy;
|
|
77
|
+
let uv = (vec2f(c) + 0.5) / sssU[1].zw;
|
|
78
|
+
let centre = textureLoad(srcTex, c, 0);
|
|
79
|
+
|
|
80
|
+
// ray-mmd, verbatim: six offsets, and the skin variances per channel.
|
|
81
|
+
// A var, not a const: a const array cannot be indexed by the loop counter.
|
|
82
|
+
var offsets = array<f32, 6>(0.352, 0.719, 1.117, 1.579, 2.177, 3.213);
|
|
83
|
+
const profileVar = vec3f(9.6, 2.8, 2.2);
|
|
84
|
+
// mSSSSScale at the controller's default; it sets both the width and how hard
|
|
85
|
+
// a depth step rejects.
|
|
86
|
+
const SSSS_SCALE: f32 = 0.04;
|
|
87
|
+
|
|
88
|
+
let d = linearDepth(c);
|
|
89
|
+
|
|
90
|
+
// The surface's normal, rebuilt from depth (ray-mmd reads it from its G-buffer;
|
|
91
|
+
// this engine keeps none). Only its angle to the view matters: the blur narrows
|
|
92
|
+
// along an axis the surface is turning away on.
|
|
93
|
+
let s = size();
|
|
94
|
+
let cx = min(c + vec2i(1, 0), s - 1);
|
|
95
|
+
let cy = min(c + vec2i(0, 1), s - 1);
|
|
96
|
+
let P = viewPos(c, d);
|
|
97
|
+
let N = cross(viewPos(cx, linearDepth(cx)) - P, viewPos(cy, linearDepth(cy)) - P);
|
|
98
|
+
let nxz = select(vec2f(0.0, 1.0), normalize(N.xz), dot(N.xz, N.xz) > 1e-12);
|
|
99
|
+
let nyz = select(vec2f(0.0, 1.0), normalize(N.yz), dot(N.yz, N.yz) > 1e-12);
|
|
100
|
+
let scaleX = abs(dot(nxz, normalize(P.xz)));
|
|
101
|
+
let scaleY = abs(dot(nyz, normalize(P.yz)));
|
|
102
|
+
let perspectiveScale = max(dot(dir, vec2f(scaleX, scaleY)), 0.3);
|
|
103
|
+
|
|
104
|
+
let radius = 0.0055 * amount * perspectiveScale / SSSS_SCALE;
|
|
105
|
+
let step = dir * radius / d;
|
|
106
|
+
|
|
107
|
+
var weightSum = vec3f(1.0);
|
|
108
|
+
var colour = centre.rgb;
|
|
109
|
+
for (var i = 0; i < 6; i++) {
|
|
110
|
+
let o = offsets[i] / 5.5 * step;
|
|
111
|
+
for (var side = 0; side < 2; side++) {
|
|
112
|
+
let tc = texel(select(uv - o, uv + o, side == 0));
|
|
113
|
+
// Skin only: ray-mmd zeroes a non-skin pixel's depth so the depth term
|
|
114
|
+
// rejects it; the mask says the same thing directly.
|
|
115
|
+
let isSkin = select(0.0, 1.0, skin(tc) > 0.004);
|
|
116
|
+
let dd = (linearDepth(tc) - d) * 1000.0 * SSSS_SCALE;
|
|
117
|
+
let w = exp(-(offsets[i] * offsets[i] + dd * dd) / profileVar) * isSkin;
|
|
118
|
+
weightSum += w;
|
|
119
|
+
colour += w * textureLoad(srcTex, tc, 0).rgb;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return vec4f(colour / weightSum, centre.a);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@fragment fn fsX(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
126
|
+
// Nothing reads the scratch target off skin: the Y pass rejects those taps.
|
|
127
|
+
if (skin(vec2i(pos.xy)) <= 0.004) { return vec4f(0.0); }
|
|
128
|
+
return blur(pos);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@fragment fn fsY(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
132
|
+
// Off skin the HDR resolve must stay exactly as the scene left it.
|
|
133
|
+
if (skin(vec2i(pos.xy)) <= 0.004) { discard; }
|
|
134
|
+
return blur(pos);
|
|
135
|
+
}
|
|
136
|
+
`
|