reze-engine 0.60.4 → 0.61.0
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/_dbg-loader.mjs +1100 -0
- package/dist/engine.d.ts +121 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +284 -19
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +14 -2
- package/dist/graph/slots.d.ts +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +19 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/physics-debug.d.ts +30 -0
- package/dist/physics-debug.d.ts.map +1 -0
- package/dist/physics-debug.js +526 -0
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +19 -0
- package/dist/shaders/materials/body.d.ts +2 -0
- package/dist/shaders/materials/body.d.ts.map +1 -0
- package/dist/shaders/materials/body.js +95 -0
- package/dist/shaders/materials/cloth_rough.d.ts +2 -0
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_rough.js +69 -0
- package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_smooth.js +61 -0
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +47 -0
- package/dist/shaders/materials/default.d.ts +2 -0
- package/dist/shaders/materials/default.d.ts.map +1 -0
- package/dist/shaders/materials/default.js +43 -0
- package/dist/shaders/materials/eye.d.ts +2 -0
- package/dist/shaders/materials/eye.d.ts.map +1 -0
- package/dist/shaders/materials/eye.js +60 -0
- package/dist/shaders/materials/face.d.ts +2 -0
- package/dist/shaders/materials/face.d.ts.map +1 -0
- package/dist/shaders/materials/face.js +95 -0
- package/dist/shaders/materials/hair.d.ts +2 -0
- package/dist/shaders/materials/hair.d.ts.map +1 -0
- package/dist/shaders/materials/hair.js +90 -0
- package/dist/shaders/materials/metal.d.ts +2 -0
- package/dist/shaders/materials/metal.d.ts.map +1 -0
- package/dist/shaders/materials/metal.js +77 -0
- package/dist/shaders/materials/mmd_classic.d.ts +2 -0
- package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
- package/dist/shaders/materials/mmd_classic.js +66 -0
- 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 +110 -23
- package/dist/shaders/materials/stockings.d.ts +2 -0
- package/dist/shaders/materials/stockings.d.ts.map +1 -0
- package/dist/shaders/materials/stockings.js +122 -0
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +34 -2
- package/dist/shaders/passes/physics-debug.d.ts +2 -0
- package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
- package/dist/shaders/passes/physics-debug.js +69 -0
- package/dist/shaders/scene-tap.js +1 -1
- package/package.json +1 -1
- package/src/engine.ts +312 -19
- package/src/graph/compile.ts +4 -1
- package/src/graph/registry.ts +14 -2
- package/src/graph/slots.ts +20 -5
- package/src/index.ts +4 -0
- package/src/shaders/lights.ts +19 -0
- package/src/shaders/materials/common.ts +47 -0
- package/src/shaders/materials/nodes.ts +110 -23
- package/src/shaders/passes/composite.ts +34 -2
- package/src/shaders/scene-tap.ts +1 -1
- package/dist/graph/presets/pool_floor.d.ts +0 -3
- package/dist/graph/presets/pool_floor.d.ts.map +0 -1
- package/dist/graph/presets/pool_floor.js +0 -42
- package/dist/graph/presets/water.d.ts +0 -3
- package/dist/graph/presets/water.d.ts.map +0 -1
- package/dist/graph/presets/water.js +0 -31
- package/dist/physics/autofit.d.ts +0 -147
- package/dist/physics/autofit.d.ts.map +0 -1
- package/dist/physics/autofit.js +0 -501
- package/dist/shaders/passes/field-blit.d.ts +0 -26
- package/dist/shaders/passes/field-blit.d.ts.map +0 -1
- package/dist/shaders/passes/field-blit.js +0 -65
- package/dist/shaders/passes/ground-noise.d.ts +0 -7
- package/dist/shaders/passes/ground-noise.d.ts.map +0 -1
- package/dist/shaders/passes/ground-noise.js +0 -88
- package/dist/shaders/passes/sim.d.ts +0 -34
- package/dist/shaders/passes/sim.d.ts.map +0 -1
- package/dist/shaders/passes/sim.js +0 -169
- package/dist/shaders/score-api.d.ts +0 -10
- package/dist/shaders/score-api.d.ts.map +0 -1
- package/dist/shaders/score-api.js +0 -114
- package/dist/shaders/shadow-api.d.ts +0 -29
- package/dist/shaders/shadow-api.d.ts.map +0 -1
- package/dist/shaders/shadow-api.js +0 -112
package/src/engine.ts
CHANGED
|
@@ -638,6 +638,35 @@ export const DEFAULT_COLOR_GRADING: ColorGradingOptions = {
|
|
|
638
638
|
saturation: 1,
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
/** The cast's shadow on a stage — see Engine.setStageCastShadow. */
|
|
642
|
+
export type StageCastShadow = {
|
|
643
|
+
/** The way TO the light, this engine's axes. */
|
|
644
|
+
direction: { x: number; y: number; z: number }
|
|
645
|
+
/** Linear. */
|
|
646
|
+
color: { x: number; y: number; z: number }
|
|
647
|
+
amount: number
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** One layer of scene fog — see Engine.setSceneFog. */
|
|
651
|
+
export type SceneFogLayer = {
|
|
652
|
+
color: { x: number; y: number; z: number }
|
|
653
|
+
amount: number
|
|
654
|
+
/** (slope, offset): f = saturate(depth·slope + offset), 1 = clear. */
|
|
655
|
+
distance: [number, number]
|
|
656
|
+
/** (reference, range): h = clamp((reference − y) / range, −1, 1). */
|
|
657
|
+
height: [number, number]
|
|
658
|
+
}
|
|
659
|
+
export type SceneFog = SceneFogLayer & { dyn?: SceneFogLayer | null }
|
|
660
|
+
|
|
661
|
+
/** A scene's own grade as a cube — see Engine.setStageGrade. */
|
|
662
|
+
export type StageGradeLut = {
|
|
663
|
+
/** Texels along each edge. */
|
|
664
|
+
size: number
|
|
665
|
+
/** size³ texels, 8-bit sRGB-encoded, red fastest then green then blue;
|
|
666
|
+
* RGB (3 bytes a texel) or RGBA (4). */
|
|
667
|
+
data: Uint8Array
|
|
668
|
+
}
|
|
669
|
+
|
|
641
670
|
export type GizmoDragKind = "rotate" | "translate"
|
|
642
671
|
|
|
643
672
|
export interface GizmoDragEvent {
|
|
@@ -1792,7 +1821,16 @@ export class Engine {
|
|
|
1792
1821
|
private lightUniformBuffer!: GPUBuffer
|
|
1793
1822
|
// ambient vec4 (4) + 4 lights x 2 vec4 (32) + 9 irradiance-SH vec4s (36),
|
|
1794
1823
|
// padded to 80. sh[0].w is the IBL flag: 0 = flat world colour, 1 = the sky.
|
|
1795
|
-
|
|
1824
|
+
// …then the scene fog at [72..87] — see setSceneFog — and the cast's
|
|
1825
|
+
// shadow on a stage at [88..107] (colour, amount; its view-projection).
|
|
1826
|
+
private lightData = new Float32Array(112)
|
|
1827
|
+
private castShadow: StageCastShadow | null = null
|
|
1828
|
+
private castShadowTexture!: GPUTexture
|
|
1829
|
+
private castShadowView!: GPUTextureView
|
|
1830
|
+
private castShadowVPBuffer!: GPUBuffer
|
|
1831
|
+
private castShadowBundle: GPURenderBundle | null = null
|
|
1832
|
+
private castShadowCleared = false
|
|
1833
|
+
private readonly castSphereScratch = new Float32Array(4)
|
|
1796
1834
|
private lightCount = 0
|
|
1797
1835
|
private resizeObserver: ResizeObserver | null = null
|
|
1798
1836
|
private resizePending = false
|
|
@@ -2457,6 +2495,7 @@ export class Engine {
|
|
|
2457
2495
|
private depthOfField: DepthOfFieldOptions = { ...DEFAULT_DEPTH_OF_FIELD_OPTIONS }
|
|
2458
2496
|
private dofUniformBuffer!: GPUBuffer
|
|
2459
2497
|
private dofUniformData = new Float32Array(12)
|
|
2498
|
+
private sceneFog: SceneFog | null = null
|
|
2460
2499
|
private dofFocusScratch = new Vec3(0, 0, 0)
|
|
2461
2500
|
/** Depth-only view of the scene's MSAA depth buffer, read by the DoF gather. */
|
|
2462
2501
|
private depthReadView: GPUTextureView | null = null
|
|
@@ -2490,6 +2529,9 @@ export class Engine {
|
|
|
2490
2529
|
private worldGradientSH: Float32Array | null = null
|
|
2491
2530
|
/** The installed HDRI's folded irradiance SH (27 floats), or null. */
|
|
2492
2531
|
private worldSH: Float32Array | null = null
|
|
2532
|
+
/** A diffuse ambient stated outright (setWorldAmbient), outranking the one
|
|
2533
|
+
* fitted to the picture; the picture still answers reflections. */
|
|
2534
|
+
private worldAmbientSH: Float32Array | null = null
|
|
2493
2535
|
private fallbackEquirectTexture!: GPUTexture
|
|
2494
2536
|
private fallbackEquirectView!: GPUTextureView
|
|
2495
2537
|
// The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
|
|
@@ -2569,6 +2611,11 @@ export class Engine {
|
|
|
2569
2611
|
private trailDue = 0
|
|
2570
2612
|
private agxLutTexture: GPUTexture | null = null
|
|
2571
2613
|
private agxFallbackTexture!: GPUTexture
|
|
2614
|
+
/** The scene's own grade (setStageGrade): the cube as handed over, kept so a
|
|
2615
|
+
* call before init() still lands, and its texture once uploaded. */
|
|
2616
|
+
private stageGradeCube: StageGradeLut | null = null
|
|
2617
|
+
private stageGradeTexture: GPUTexture | null = null
|
|
2618
|
+
private stageGradeFallback!: GPUTexture
|
|
2572
2619
|
/** Bound at composite binding 7 when no effect (or a param-less one) is set. */
|
|
2573
2620
|
private bgParamsDummyBuffer!: GPUBuffer
|
|
2574
2621
|
private compositePipelineLayout!: GPUPipelineLayout
|
|
@@ -2829,6 +2876,57 @@ export class Engine {
|
|
|
2829
2876
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
2830
2877
|
}
|
|
2831
2878
|
|
|
2879
|
+
/**
|
|
2880
|
+
* The grade a scene arrived with, as a cube — a game's own colour grading,
|
|
2881
|
+
* baked by the converter that brought its stage. Applied to the formed frame
|
|
2882
|
+
* before setColorGrading's, which stays the author's to lay on top. Looked up
|
|
2883
|
+
* as the game looks it up: the view transform's output, linear, trilinear
|
|
2884
|
+
* between texel centres. Null takes it off.
|
|
2885
|
+
*
|
|
2886
|
+
* Bytes are the cube as that game stored it: 8-bit sRGB-encoded, red fastest,
|
|
2887
|
+
* then green, then blue; three bytes a texel or four. A cube whose byte count
|
|
2888
|
+
* does not match its size is refused rather than drawn wrong.
|
|
2889
|
+
*/
|
|
2890
|
+
setStageGrade(lut: StageGradeLut | null): void {
|
|
2891
|
+
if (lut) {
|
|
2892
|
+
const stride = lut.data.byteLength / lut.size ** 3
|
|
2893
|
+
if (!Number.isInteger(lut.size) || lut.size < 2 || lut.size > 64 || (stride !== 3 && stride !== 4)) {
|
|
2894
|
+
throw new Error(`setStageGrade: ${lut.data.byteLength} bytes is not a ${lut.size}³ cube`)
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
this.stageGradeCube = lut
|
|
2898
|
+
if (this.device && this.stageGradeFallback) this.uploadStageGrade()
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
private uploadStageGrade(): void {
|
|
2902
|
+
const lut = this.stageGradeCube
|
|
2903
|
+
this.stageGradeTexture?.destroy()
|
|
2904
|
+
this.stageGradeTexture = null
|
|
2905
|
+
if (lut) {
|
|
2906
|
+
const n = lut.size
|
|
2907
|
+
const texels = n ** 3
|
|
2908
|
+
const stride = lut.data.byteLength / texels
|
|
2909
|
+
const rgba = new Uint8Array(texels * 4)
|
|
2910
|
+
for (let i = 0; i < texels; i++) {
|
|
2911
|
+
rgba[i * 4] = lut.data[i * stride]
|
|
2912
|
+
rgba[i * 4 + 1] = lut.data[i * stride + 1]
|
|
2913
|
+
rgba[i * 4 + 2] = lut.data[i * stride + 2]
|
|
2914
|
+
rgba[i * 4 + 3] = 255
|
|
2915
|
+
}
|
|
2916
|
+
const tex = this.device.createTexture({
|
|
2917
|
+
label: `stage grade ${n}³`,
|
|
2918
|
+
size: [n, n, n],
|
|
2919
|
+
dimension: "3d",
|
|
2920
|
+
format: "rgba8unorm-srgb",
|
|
2921
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
2922
|
+
})
|
|
2923
|
+
this.device.queue.writeTexture({ texture: tex }, rgba, { bytesPerRow: n * 4, rowsPerImage: n }, [n, n, n])
|
|
2924
|
+
this.stageGradeTexture = tex
|
|
2925
|
+
}
|
|
2926
|
+
this.rebuildCompositeBindGroup()
|
|
2927
|
+
if (this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2832
2930
|
/** Current grade (for serialization into a scene descriptor). */
|
|
2833
2931
|
getColorGrading(): ColorGradingOptions {
|
|
2834
2932
|
const g = this.colorGrading
|
|
@@ -2969,7 +3067,8 @@ export class Engine {
|
|
|
2969
3067
|
u[32] === 1 && u[33] === 1 && u[34] === 1 &&
|
|
2970
3068
|
u[36] === 1 && u[37] === 1 && u[38] === 1 &&
|
|
2971
3069
|
g.contrast === 1 && g.saturation === 1
|
|
2972
|
-
|
|
3070
|
+
// Bit 0 the CDL above, bit 1 the scene's own cube — see _rzGradeScene.
|
|
3071
|
+
u[39] = (neutral ? 0 : 1) | (this.stageGradeTexture ? 2 : 0)
|
|
2973
3072
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u)
|
|
2974
3073
|
}
|
|
2975
3074
|
|
|
@@ -3096,9 +3195,10 @@ export class Engine {
|
|
|
3096
3195
|
down: [number, number, number]
|
|
3097
3196
|
} {
|
|
3098
3197
|
const s = this.world.strength
|
|
3099
|
-
|
|
3198
|
+
const fitted = this.worldAmbientSH ?? this.worldSH
|
|
3199
|
+
if (fitted) {
|
|
3100
3200
|
const at = (n: { x: number; y: number; z: number }) =>
|
|
3101
|
-
evalIrradianceSH(
|
|
3201
|
+
evalIrradianceSH(fitted, n).map((v) => Math.max(v * s, 0)) as [number, number, number]
|
|
3102
3202
|
return { source: "hdri", strength: s, up: at({ x: 0, y: 1, z: 0 }), down: at({ x: 0, y: -1, z: 0 }) }
|
|
3103
3203
|
}
|
|
3104
3204
|
const c = this.world.color
|
|
@@ -3828,6 +3928,7 @@ export class Engine {
|
|
|
3828
3928
|
{ binding: 8, resource: this.depthReadView },
|
|
3829
3929
|
{ binding: 9, resource: { buffer: this.dofUniformBuffer } },
|
|
3830
3930
|
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
3931
|
+
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
3831
3932
|
{ binding: 11, resource: { buffer: this.castBuffer } },
|
|
3832
3933
|
{ binding: 13, resource: { buffer: this.audioBuffer } },
|
|
3833
3934
|
{ binding: 19, resource: { buffer: this.midiBuffer } },
|
|
@@ -4266,6 +4367,7 @@ export class Engine {
|
|
|
4266
4367
|
{ binding: 2, resource: this.bloomSampler },
|
|
4267
4368
|
{ binding: 5, resource: this.filmicLutView },
|
|
4268
4369
|
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
4370
|
+
{ binding: 12, resource: (this.stageGradeTexture ?? this.stageGradeFallback).createView({ dimension: "3d" }) },
|
|
4269
4371
|
{ binding: 1, resource: bloom },
|
|
4270
4372
|
{ binding: 4, resource: this.maskResolveView },
|
|
4271
4373
|
],
|
|
@@ -7251,6 +7353,7 @@ export class Engine {
|
|
|
7251
7353
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
7252
7354
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7253
7355
|
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
7356
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
7254
7357
|
// And the scene's COVERAGE and bloom, without which the tap cannot
|
|
7255
7358
|
// reconstruct a pixel: the HDR target is premultiplied, so colour alone
|
|
7256
7359
|
// reads a half-transparent ground as a dark opaque one.
|
|
@@ -7419,7 +7522,8 @@ export class Engine {
|
|
|
7419
7522
|
label: "main per-frame bind group layout",
|
|
7420
7523
|
entries: [
|
|
7421
7524
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7422
|
-
|
|
7525
|
+
// The vertex stage reads it too: the scene fog is per vertex (setSceneFog).
|
|
7526
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7423
7527
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
7424
7528
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
7425
7529
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
@@ -7433,6 +7537,8 @@ export class Engine {
|
|
|
7433
7537
|
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
7434
7538
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7435
7539
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7540
|
+
// The cast's shadow on a stage — setStageCastShadow.
|
|
7541
|
+
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
7436
7542
|
],
|
|
7437
7543
|
})
|
|
7438
7544
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -7654,6 +7760,22 @@ export class Engine {
|
|
|
7654
7760
|
}),
|
|
7655
7761
|
)
|
|
7656
7762
|
this.shadowMapDepthViews = this.shadowMapTextures.map((t) => t.createView())
|
|
7763
|
+
// The cast's shadow on a stage (setStageCastShadow): one map, only the cast
|
|
7764
|
+
// in it, from the stage's own direction. Always allocated so every material
|
|
7765
|
+
// bind group and every instance's shadow bind group stays one shape; drawn
|
|
7766
|
+
// only while a stage asks for it.
|
|
7767
|
+
this.castShadowTexture = this.device.createTexture({
|
|
7768
|
+
label: "stage cast shadow map",
|
|
7769
|
+
size: [1024, 1024],
|
|
7770
|
+
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
7771
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7772
|
+
})
|
|
7773
|
+
this.castShadowView = this.castShadowTexture.createView()
|
|
7774
|
+
this.castShadowVPBuffer = this.device.createBuffer({
|
|
7775
|
+
label: "stage cast shadow view-projection",
|
|
7776
|
+
size: 64,
|
|
7777
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7778
|
+
})
|
|
7657
7779
|
|
|
7658
7780
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
7659
7781
|
this.bakeBrdfLut()
|
|
@@ -7666,6 +7788,14 @@ export class Engine {
|
|
|
7666
7788
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
7667
7789
|
})
|
|
7668
7790
|
void this.loadAgxLut()
|
|
7791
|
+
this.stageGradeFallback = this.device.createTexture({
|
|
7792
|
+
label: "stage grade fallback",
|
|
7793
|
+
size: [1, 1, 1],
|
|
7794
|
+
dimension: "3d",
|
|
7795
|
+
format: "rgba8unorm-srgb",
|
|
7796
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
7797
|
+
})
|
|
7798
|
+
this.uploadStageGrade()
|
|
7669
7799
|
this.bakeFilmicLut()
|
|
7670
7800
|
|
|
7671
7801
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
@@ -8100,6 +8230,9 @@ export class Engine {
|
|
|
8100
8230
|
// AgX's 57³ cube. Decompressed and uploaded off the critical path, so a
|
|
8101
8231
|
// 1×1×1 stand-in keeps the bind group valid until it arrives.
|
|
8102
8232
|
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
8233
|
+
// The scene's own grade cube (setStageGrade) — 1×1×1 stand-in while it
|
|
8234
|
+
// has none, which the flag bit keeps from ever being sampled.
|
|
8235
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
8103
8236
|
// The cast, for rzSubject/rzAnchor. Always bound so the base shader's
|
|
8104
8237
|
// layout matches; the base shader simply never reads it.
|
|
8105
8238
|
{ binding: 11, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
@@ -9876,7 +10009,7 @@ export class Engine {
|
|
|
9876
10009
|
private setupLighting() {
|
|
9877
10010
|
this.lightUniformBuffer = this.device.createBuffer({
|
|
9878
10011
|
label: "light uniforms",
|
|
9879
|
-
size:
|
|
10012
|
+
size: 112 * 4, // ambient (4) + 4 lights x 2 vec4 (32) + irradiance SH 9 x vec4 (36) + fog 4 x vec4 (16) + cast shadow (4 + 16)
|
|
9880
10013
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
9881
10014
|
})
|
|
9882
10015
|
this.lightData.fill(0)
|
|
@@ -9961,6 +10094,7 @@ export class Engine {
|
|
|
9961
10094
|
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
9962
10095
|
{ binding: 8, resource: env },
|
|
9963
10096
|
{ binding: 9, resource: this.brdfLutView },
|
|
10097
|
+
{ binding: 10, resource: this.castShadowView },
|
|
9964
10098
|
],
|
|
9965
10099
|
})
|
|
9966
10100
|
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
@@ -9977,6 +10111,7 @@ export class Engine {
|
|
|
9977
10111
|
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
9978
10112
|
{ binding: 8, resource: env },
|
|
9979
10113
|
{ binding: 9, resource: this.brdfLutView },
|
|
10114
|
+
{ binding: 10, resource: this.castShadowView },
|
|
9980
10115
|
],
|
|
9981
10116
|
})
|
|
9982
10117
|
return true
|
|
@@ -10001,7 +10136,7 @@ export class Engine {
|
|
|
10001
10136
|
// replaces the colour, strength applies to either).
|
|
10002
10137
|
// An HDRI outranks a gradient outranks the flat colour, which is the order
|
|
10003
10138
|
// of how much each one knows about the sky.
|
|
10004
|
-
const sh = this.worldSH ?? this.worldGradientSH
|
|
10139
|
+
const sh = this.worldAmbientSH ?? this.worldSH ?? this.worldGradientSH
|
|
10005
10140
|
for (let i = 0; i < 9; i++) {
|
|
10006
10141
|
const b = 36 + i * 4
|
|
10007
10142
|
if (sh) {
|
|
@@ -10044,6 +10179,115 @@ export class Engine {
|
|
|
10044
10179
|
}
|
|
10045
10180
|
|
|
10046
10181
|
/** Update the world environment (Blender: World Background). Ambient recomputes immediately. */
|
|
10182
|
+
/**
|
|
10183
|
+
* Distance fog on every lit surface, as a game on SimPipeline lays it: per
|
|
10184
|
+
* layer, t = saturate(f − (1 − f)·h) with f = saturate(depth·distance[0] +
|
|
10185
|
+
* distance[1]) and h = clamp((height[0] − y) / height[1], −1, 1), and the
|
|
10186
|
+
* colour pulled toward the layer's by (1 − t)·amount — the haze first, then
|
|
10187
|
+
* `dyn`, which X340 uses to darken toward black. Depth is along the view
|
|
10188
|
+
* axis and y the world height, both in this engine's units; colours linear.
|
|
10189
|
+
*
|
|
10190
|
+
* PER VERTEX, as the game computes it, and that is load-bearing rather than
|
|
10191
|
+
* a saving: a floor built of long triangles carries the fog of its far
|
|
10192
|
+
* corners into the near half, which is most of what X340's checker floor
|
|
10193
|
+
* shows of its haze. Emission-only graphs take none, as the game's effect
|
|
10194
|
+
* shaders take none. Null clears.
|
|
10195
|
+
*/
|
|
10196
|
+
setSceneFog(fog: SceneFog | null): void {
|
|
10197
|
+
this.sceneFog = fog
|
|
10198
|
+
const u = this.lightData
|
|
10199
|
+
const layer = (at: number, l: SceneFogLayer | undefined | null) => {
|
|
10200
|
+
u[at] = l?.color.x ?? 0
|
|
10201
|
+
u[at + 1] = l?.color.y ?? 0
|
|
10202
|
+
u[at + 2] = l?.color.z ?? 0
|
|
10203
|
+
u[at + 3] = l ? Math.max(l.amount, 0) : 0
|
|
10204
|
+
u[at + 4] = l?.distance[0] ?? 0
|
|
10205
|
+
u[at + 5] = l?.distance[1] ?? 1
|
|
10206
|
+
u[at + 6] = l?.height[0] ?? 0
|
|
10207
|
+
u[at + 7] = l?.height[1] ?? 1
|
|
10208
|
+
}
|
|
10209
|
+
layer(72, fog)
|
|
10210
|
+
layer(80, fog?.dyn)
|
|
10211
|
+
this.updateLightBuffer()
|
|
10212
|
+
}
|
|
10213
|
+
|
|
10214
|
+
/**
|
|
10215
|
+
* The cast's shadow on a stage, as a game on SimPipeline lays its ground
|
|
10216
|
+
* shadow: the characters drawn into a map of their own from `direction` (the
|
|
10217
|
+
* way TO the light, this engine's axes), and every Unity-mode stage surface
|
|
10218
|
+
* multiplied toward `color` (linear) by amount × how much of the cast stands
|
|
10219
|
+
* in the way. Independent of the sun — X340's is dim moonlight, which left a
|
|
10220
|
+
* kneeling figure no shadow at all. Only stage materials read it; a scene
|
|
10221
|
+
* without a stage is untouched. Null turns it off.
|
|
10222
|
+
*/
|
|
10223
|
+
setStageCastShadow(opts: StageCastShadow | null): void {
|
|
10224
|
+
this.castShadow = opts
|
|
10225
|
+
const u = this.lightData
|
|
10226
|
+
u[88] = opts?.color.x ?? 1
|
|
10227
|
+
u[89] = opts?.color.y ?? 1
|
|
10228
|
+
u[90] = opts?.color.z ?? 1
|
|
10229
|
+
u[91] = 0 // lit per frame by updateCastShadowVP, once there is a cast to fit
|
|
10230
|
+
this.updateLightBuffer()
|
|
10231
|
+
}
|
|
10232
|
+
|
|
10233
|
+
/** Fit the cast map to this frame's cast; false when there is nothing to draw. */
|
|
10234
|
+
private updateCastShadowVP(stage: boolean): boolean {
|
|
10235
|
+
const opts = this.castShadow
|
|
10236
|
+
let on = false
|
|
10237
|
+
if (opts && stage) {
|
|
10238
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
10239
|
+
for (const inst of this.modelInstances.values()) {
|
|
10240
|
+
if (inst.isStage || inst.isPlane || inst.isProp || !inst.model.visible) continue
|
|
10241
|
+
const s = this.castSphereScratch
|
|
10242
|
+
this.writeCullSphere(inst, s, 0)
|
|
10243
|
+
if (!(s[3] < 1e6)) continue
|
|
10244
|
+
minX = Math.min(minX, s[0] - s[3]); maxX = Math.max(maxX, s[0] + s[3])
|
|
10245
|
+
minY = Math.min(minY, s[1] - s[3]); maxY = Math.max(maxY, s[1] + s[3])
|
|
10246
|
+
minZ = Math.min(minZ, s[2] - s[3]); maxZ = Math.max(maxZ, s[2] + s[3])
|
|
10247
|
+
}
|
|
10248
|
+
if (minX <= maxX) {
|
|
10249
|
+
const c = new Vec3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2)
|
|
10250
|
+
const r = Math.max(maxX - minX, maxY - minY, maxZ - minZ) / 2
|
|
10251
|
+
const d = new Vec3(opts.direction.x, opts.direction.y, opts.direction.z).normalize()
|
|
10252
|
+
// Wide enough for the shadow a low light throws, deep enough to reach
|
|
10253
|
+
// the floor under the figure from well above it.
|
|
10254
|
+
const half = r * 2.5
|
|
10255
|
+
const reach = r * 8
|
|
10256
|
+
const eye = new Vec3(c.x + d.x * reach, c.y + d.y * reach, c.z + d.z * reach)
|
|
10257
|
+
const up = Math.abs(d.y) > 0.99 ? new Vec3(0, 0, 1) : new Vec3(0, 1, 0)
|
|
10258
|
+
const vp = Mat4.orthographicLh(-half, half, -half, half, 1, reach * 2).multiply(Mat4.lookAt(eye, c, up)).values
|
|
10259
|
+
this.device.queue.writeBuffer(this.castShadowVPBuffer, 0, new Float32Array(vp))
|
|
10260
|
+
this.lightData.set(vp, 92)
|
|
10261
|
+
on = true
|
|
10262
|
+
}
|
|
10263
|
+
}
|
|
10264
|
+
const amount = on ? Math.max(opts!.amount, 0) : 0
|
|
10265
|
+
if (this.lightData[91] !== amount || on) {
|
|
10266
|
+
this.lightData[91] = amount
|
|
10267
|
+
this.device.queue.writeBuffer(this.lightUniformBuffer, 88 * 4, this.lightData, 88, 20)
|
|
10268
|
+
}
|
|
10269
|
+
return on
|
|
10270
|
+
}
|
|
10271
|
+
|
|
10272
|
+
/**
|
|
10273
|
+
* The world's DIFFUSE light, stated rather than fitted: nine RGB coefficients
|
|
10274
|
+
* (27 floats) in the form rzWorldAmbient evaluates — c0 + c1·y + c2·z + c3·x +
|
|
10275
|
+
* c4·xy + c5·yz + c6·(3z²−1) + c7·xz + c8·(x²−y²), in this engine's axes, the
|
|
10276
|
+
* value a white surface facing n takes. Null goes back to the SH fitted to
|
|
10277
|
+
* the world's picture or gradient.
|
|
10278
|
+
*
|
|
10279
|
+
* A game lights a room's surfaces from an ambient probe that is not its
|
|
10280
|
+
* reflection probe — X340's is a dim blue trilight while its reflections
|
|
10281
|
+
* carry every lamp the probe baked — and one picture cannot be both. With
|
|
10282
|
+
* this set, the picture answers reflections alone. World strength scales it
|
|
10283
|
+
* as it scales the fitted one.
|
|
10284
|
+
*/
|
|
10285
|
+
setWorldAmbient(sh: ArrayLike<number> | null): void {
|
|
10286
|
+
if (sh && sh.length !== 27) throw new Error(`setWorldAmbient: ${sh.length} floats, not 27`)
|
|
10287
|
+
this.worldAmbientSH = sh ? Float32Array.from(sh) : null
|
|
10288
|
+
if (this.device && this.lightUniformBuffer) this.writeWorld()
|
|
10289
|
+
}
|
|
10290
|
+
|
|
10047
10291
|
setWorld(options: WorldOptions): void {
|
|
10048
10292
|
if (options.color) this.world.color = options.color
|
|
10049
10293
|
if (options.strength !== undefined) this.world.strength = options.strength
|
|
@@ -10334,6 +10578,8 @@ export class Engine {
|
|
|
10334
10578
|
this.canvas.removeEventListener("touchend", this.handleCanvasTouch)
|
|
10335
10579
|
}
|
|
10336
10580
|
|
|
10581
|
+
this.stageGradeTexture?.destroy()
|
|
10582
|
+
this.stageGradeTexture = null
|
|
10337
10583
|
this.overlayDepthTexture?.destroy()
|
|
10338
10584
|
this.overlayDepthTexture = null
|
|
10339
10585
|
this.overlayMsaaTexture?.destroy()
|
|
@@ -12953,6 +13199,17 @@ export class Engine {
|
|
|
12953
13199
|
this.forEachInstance((inst) => this.drawInstanceShadow(shadow, inst, ci))
|
|
12954
13200
|
return shadow.finish({ label: `shadow pass, cascade ${ci}` })
|
|
12955
13201
|
})
|
|
13202
|
+
// The cast alone, for the shadow it lays on a stage.
|
|
13203
|
+
const cast = this.device.createRenderBundleEncoder({
|
|
13204
|
+
label: "stage cast shadow pass",
|
|
13205
|
+
colorFormats: [],
|
|
13206
|
+
depthStencilFormat: Engine.SHADOW_DEPTH_FORMAT,
|
|
13207
|
+
})
|
|
13208
|
+
cast.setPipeline(this.shadowDepthPipeline)
|
|
13209
|
+
this.forEachInstance((inst) => {
|
|
13210
|
+
if (!inst.isStage && !inst.isPlane && !inst.isProp) this.drawInstanceShadow(cast, inst, SHADOW_CASCADES.length)
|
|
13211
|
+
})
|
|
13212
|
+
this.castShadowBundle = cast.finish({ label: "stage cast shadow pass" })
|
|
12956
13213
|
this.bundleRecords++
|
|
12957
13214
|
}
|
|
12958
13215
|
|
|
@@ -13426,6 +13683,18 @@ export class Engine {
|
|
|
13426
13683
|
],
|
|
13427
13684
|
}),
|
|
13428
13685
|
)
|
|
13686
|
+
// …and one more past the cascades, for the cast's shadow on a stage.
|
|
13687
|
+
shadowBindGroups.push(
|
|
13688
|
+
this.device.createBindGroup({
|
|
13689
|
+
label: `${name}: shadow bind, stage cast shadow`,
|
|
13690
|
+
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
|
13691
|
+
entries: [
|
|
13692
|
+
{ binding: 0, resource: { buffer: this.castShadowVPBuffer } },
|
|
13693
|
+
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
13694
|
+
{ binding: 2, resource: this.materialSampler },
|
|
13695
|
+
],
|
|
13696
|
+
}),
|
|
13697
|
+
)
|
|
13429
13698
|
|
|
13430
13699
|
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, null)
|
|
13431
13700
|
|
|
@@ -15858,6 +16127,18 @@ export class Engine {
|
|
|
15858
16127
|
sp.end()
|
|
15859
16128
|
this.shadowCascadeCleared[ci] = !wanted
|
|
15860
16129
|
}
|
|
16130
|
+
// The cast's shadow on a stage: drawn while a stage asks for it and has a
|
|
16131
|
+
// cast to shade it with; cleared once when it stops, then left alone.
|
|
16132
|
+
const castOn = this.updateCastShadowVP(stage)
|
|
16133
|
+
if (castOn || !this.castShadowCleared) {
|
|
16134
|
+
const cp = encoder.beginRenderPass({
|
|
16135
|
+
colorAttachments: [],
|
|
16136
|
+
depthStencilAttachment: { view: this.castShadowView, depthClearValue: 1.0, depthLoadOp: "clear", depthStoreOp: "store" },
|
|
16137
|
+
})
|
|
16138
|
+
if (castOn && this.castShadowBundle) cp.executeBundles([this.castShadowBundle])
|
|
16139
|
+
cp.end()
|
|
16140
|
+
this.castShadowCleared = !castOn
|
|
16141
|
+
}
|
|
15861
16142
|
this.shadowMapPopulated = hasModels
|
|
15862
16143
|
}
|
|
15863
16144
|
|
|
@@ -16995,29 +17276,41 @@ export class Engine {
|
|
|
16995
17276
|
* each for a reason that would change pixels: EYE front-culls and gates on a
|
|
16996
17277
|
* bone read, and pre-filled hair depth over the socket would depth-reject
|
|
16997
17278
|
* the eye before it could write the stencil the see-through-hair pass needs
|
|
16998
|
-
* — which is also why HAIR stays out entirely.
|
|
16999
|
-
*
|
|
17000
|
-
*
|
|
17001
|
-
*
|
|
17002
|
-
*
|
|
17279
|
+
* — which is also why HAIR stays out entirely. They all still BENEFIT:
|
|
17280
|
+
* their fragments early-z against the primed depth of whatever plain opaque
|
|
17281
|
+
* surface sits in front of them.
|
|
17282
|
+
*
|
|
17283
|
+
* HASHED alpha primes through the SOLID pipeline (cutoff 1.0). Its colour
|
|
17284
|
+
* pass discards by a position hash this pass does not run, but that threshold
|
|
17285
|
+
* is clamped to at most 1, so a texel at alpha exactly 1 survives it always —
|
|
17286
|
+
* those are the only texels the solid prime claims, and it can never punch
|
|
17287
|
+
* the cutout into the wrong ones. A stage's foliage and railings are mostly
|
|
17288
|
+
* such texels, and without the prime every room surface behind them shaded
|
|
17289
|
+
* its full lamp walk and was then painted over.
|
|
17003
17290
|
*/
|
|
17004
17291
|
private drawOpaqueDepthPrepass(
|
|
17005
17292
|
pass: GPURenderPassEncoder | GPURenderBundleEncoder,
|
|
17006
17293
|
inst: ModelInstance,
|
|
17007
17294
|
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
17008
17295
|
): void {
|
|
17009
|
-
let bound =
|
|
17296
|
+
let bound: GPURenderPipeline | null = null
|
|
17010
17297
|
for (const draw of inst.drawCalls) {
|
|
17011
17298
|
if (draw.type !== "opaque") continue
|
|
17299
|
+
let pipeline = this.depthPrepassPipeline
|
|
17012
17300
|
if (draw.groupId) {
|
|
17013
17301
|
const install = inst.styleGroups.get(draw.groupId)
|
|
17014
|
-
if (install &&
|
|
17302
|
+
if (install && install.renderClass !== "auto") continue
|
|
17303
|
+
if (install?.alphaMode === "hashed") pipeline = this.solidPrepassPipeline
|
|
17304
|
+
else if (install && install.alphaMode !== "opaque") continue
|
|
17015
17305
|
}
|
|
17016
|
-
if (
|
|
17017
|
-
pass.setPipeline(
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17306
|
+
if (bound !== pipeline) {
|
|
17307
|
+
pass.setPipeline(pipeline)
|
|
17308
|
+
// One layout for both prepass pipelines, so groups 0 and 1 carry over.
|
|
17309
|
+
if (!bound) {
|
|
17310
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17311
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
17312
|
+
}
|
|
17313
|
+
bound = pipeline
|
|
17021
17314
|
}
|
|
17022
17315
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17023
17316
|
this.issueDraw(pass, draw, view.args)
|
package/src/graph/compile.ts
CHANGED
|
@@ -358,6 +358,9 @@ export function compileGraph(graph: ShaderGraph, opts: CompileOptions = {}): Com
|
|
|
358
358
|
if (opacity) lines.push(` let final_opacity = saturate(${outputExpr(opacity, "float")}); // @node:${opacity.node}`)
|
|
359
359
|
|
|
360
360
|
const fsBody = lines.join("\n")
|
|
361
|
-
|
|
361
|
+
// A graph takes light when its body reads any: the four shading nodes all
|
|
362
|
+
// pass `sun, amb`, and a hand-written node reaching for the lamps names them.
|
|
363
|
+
const takesLight = /\bsun\b|\bamb\b|rzLight|rzLamp/.test(fsBody)
|
|
364
|
+
const wgsl = assembleModule(opts.renderClass ?? "auto", opts.alphaMode ?? "opaque", fsBody, usesStyle.current, !!opacity, opts.blend ?? "over", takesLight)
|
|
362
365
|
return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes }
|
|
363
366
|
}
|
package/src/graph/registry.ts
CHANGED
|
@@ -837,6 +837,14 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
837
837
|
/** Ours, not Blender's: caps firefly speculars from noise-bumped NDF
|
|
838
838
|
* aliasing, which EEVEE hides behind TAA and we have none. */
|
|
839
839
|
spec_clamp: F(1e30),
|
|
840
|
+
/** Ours: how roughness picks the reflection's blur. 0 is the engine's
|
|
841
|
+
* sqrt ramp; 1 is Unity's probe curve, p·(1.7−0.7p) mip-steps per
|
|
842
|
+
* eighth, stopping where a cube's last face texel does — what a stage
|
|
843
|
+
* ripped from a Unity game was authored against. See rzWorldSpecularLod. */
|
|
844
|
+
reflection_lod: F(0),
|
|
845
|
+
/** Ours: 1 shades the sun's and the lamps' highlights with URP's
|
|
846
|
+
* direct-light BRDF, as a stage from a Unity game was lit — see bsdf_urp. */
|
|
847
|
+
unity_direct: F(0),
|
|
840
848
|
},
|
|
841
849
|
outputs: { color: "color" },
|
|
842
850
|
emit: (a) => {
|
|
@@ -848,11 +856,15 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
848
856
|
const bsdf =
|
|
849
857
|
`eval_principled(PrincipledIn(${a.base_color}, ${a.metallic}, ` +
|
|
850
858
|
`${spec}, ${a.roughness}, ` +
|
|
851
|
-
`${a.spec_clamp}, ${a.sheen_weight}, ${a.sheen_tint}), ${a.normal}, l, v, sun, amb, shadow, input.worldPos)`
|
|
859
|
+
`${a.spec_clamp}, ${a.sheen_weight}, ${a.sheen_tint}, ${a.reflection_lod}, ${a.unity_direct}), ${a.normal}, l, v, sun, amb, shadow, input.worldPos)`
|
|
852
860
|
// v2 defaults Emission Strength to 0, which is the overwhelming case. Emit
|
|
853
861
|
// the term only when it can do something, so the common shader carries no
|
|
854
862
|
// dead add and the output stays readable.
|
|
855
|
-
|
|
863
|
+
const lit = a.emission_strength === "0.0" ? bsdf : `${bsdf} + ${a.emission_color} * ${a.emission_strength}`
|
|
864
|
+
// Unity mode also ends as SimPipeline's surfaces end: saturate()d, so no
|
|
865
|
+
// lit or glowing surface passes 1.0 before the post — its bloom works on
|
|
866
|
+
// what is left under that. A literal, as every stage graph states it.
|
|
867
|
+
return a.unity_direct === "1.0" ? `min(${lit}, vec3f(1.0))` : lit
|
|
856
868
|
},
|
|
857
869
|
},
|
|
858
870
|
}
|
package/src/graph/slots.ts
CHANGED
|
@@ -144,6 +144,9 @@ ${gate}
|
|
|
144
144
|
let amb = rzWorldAmbient(n) + modelLight.fill.rgb;
|
|
145
145
|
let shadow = sampleShadow(input.worldPos, n);
|
|
146
146
|
let tex_color = tex_s.rgb;
|
|
147
|
+
// The normal the lamps' diffuse layer is lit by, for a principled walk to
|
|
148
|
+
// compute it on the way (rzLightsDiffuseOnce). n is final from here on.
|
|
149
|
+
_rzLampN = n;
|
|
147
150
|
|
|
148
151
|
`
|
|
149
152
|
}
|
|
@@ -151,7 +154,7 @@ ${gate}
|
|
|
151
154
|
// Tail of fs(): consumes `final_color` + locals, writes FSOut. hashed forces output
|
|
152
155
|
// alpha to 1 (the discard already did the cutout); hair scales alpha for the over-eyes
|
|
153
156
|
// pass when IS_OVER_EYES is compiled true.
|
|
154
|
-
function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: boolean, blend: StyleBlend): string {
|
|
157
|
+
function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: boolean, blend: StyleBlend, takesLight: boolean): string {
|
|
155
158
|
// A graph that computes its own opacity wins, including over hashed: hashed
|
|
156
159
|
// writes 1 because its discard already decided the cutout, and a graph asking
|
|
157
160
|
// for a curve is asking for the opposite of a cutout. Additive writes 1 for a
|
|
@@ -178,7 +181,13 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: bo
|
|
|
178
181
|
// so the fix has an address — when graphs gain an optional albedo output
|
|
179
182
|
// (material-track era), it lands here and every light is corrected at once,
|
|
180
183
|
// instead of a hunt through the epilogue's string templates.
|
|
181
|
-
|
|
184
|
+
//
|
|
185
|
+
// ONLY ON A GRAPH THAT TAKES LIGHT. An emission-only graph — Unlit, a stage's
|
|
186
|
+
// painted sheet — is Blender's Emission shader, which no lamp reaches; adding
|
|
187
|
+
// the lamps to it turned X340's floor shadow, a white-RGB picture at a soft
|
|
188
|
+
// alpha, into a glowing disc under the spot above it.
|
|
189
|
+
const LIT = takesLight ? ` + rzLightsDiffuseOnce(input.worldPos, n) * albedo` : ""
|
|
190
|
+
|
|
182
191
|
const ALBEDO = ` let albedo = tex_color;
|
|
183
192
|
`
|
|
184
193
|
// The dissolve's burning edge, ADDED after the graph and after the lights —
|
|
@@ -186,19 +195,24 @@ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: bo
|
|
|
186
195
|
// on every fragment of every material that is not dissolving, which is what
|
|
187
196
|
// keeps this out of the way of a scene that never uses it.
|
|
188
197
|
const BURN = ` + RZ_BURN_COLOR * rz_burn`
|
|
198
|
+
// The scene fog, last, as the game lays it over a lit surface; an
|
|
199
|
+
// emission-only graph takes none (the game's effect shaders have no fog).
|
|
200
|
+
const FOG = takesLight
|
|
201
|
+
? ` out.color = vec4f(mix(mix(out.color.rgb, light.fog[0].rgb, input.fog.x), light.fog[2].rgb, input.fog.y), out.color.a);\n`
|
|
202
|
+
: ""
|
|
189
203
|
if (renderClass === "hair") {
|
|
190
204
|
return `${ALBEDO} var outAlpha = ${alphaBase};
|
|
191
205
|
if (IS_OVER_EYES) { outAlpha = ${alphaBase} * 0.25; }
|
|
192
206
|
|
|
193
207
|
var out: FSOut;
|
|
194
208
|
out.color = vec4f(final_color${LIT}${BURN}, outAlpha);
|
|
195
|
-
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
209
|
+
${FOG} out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
196
210
|
${ID_WRITE} return out;
|
|
197
211
|
`
|
|
198
212
|
}
|
|
199
213
|
return `${ALBEDO} var out: FSOut;
|
|
200
214
|
out.color = vec4f(final_color${LIT}${BURN}, ${alphaBase});
|
|
201
|
-
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
215
|
+
${FOG} out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
202
216
|
${ID_WRITE} return out;
|
|
203
217
|
`
|
|
204
218
|
}
|
|
@@ -215,6 +229,7 @@ export function assembleModule(
|
|
|
215
229
|
includeStyleUniforms: boolean,
|
|
216
230
|
hasOpacity = false,
|
|
217
231
|
blend: StyleBlend = "over",
|
|
232
|
+
takesLight = true,
|
|
218
233
|
): string {
|
|
219
234
|
return (
|
|
220
235
|
NODES_WGSL +
|
|
@@ -228,7 +243,7 @@ export function assembleModule(
|
|
|
228
243
|
prelude(renderClass, alphaMode, blend) +
|
|
229
244
|
fsBody +
|
|
230
245
|
"\n" +
|
|
231
|
-
epilogue(renderClass, alphaMode, hasOpacity, blend) +
|
|
246
|
+
epilogue(renderClass, alphaMode, hasOpacity, blend, takesLight) +
|
|
232
247
|
"}\n"
|
|
233
248
|
)
|
|
234
249
|
}
|
package/src/index.ts
CHANGED