reze-engine 0.61.3 → 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 +74 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +260 -34
- package/dist/graph/presets/body.js +1 -1
- package/dist/graph/presets/cloth_rough.js +1 -1
- package/dist/graph/presets/cloth_smooth.js +1 -1
- package/dist/graph/presets/eye.d.ts.map +1 -1
- package/dist/graph/presets/eye.js +19 -0
- package/dist/graph/presets/face.js +1 -1
- package/dist/graph/presets/hair.js +1 -1
- package/dist/graph/presets/metal.js +1 -1
- 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.d.ts.map +1 -1
- package/dist/graph/slots.js +19 -5
- 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.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -9
- 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/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +11 -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 +275 -36
- package/src/graph/presets/body.ts +1 -1
- package/src/graph/presets/cloth_rough.ts +1 -1
- package/src/graph/presets/cloth_smooth.ts +1 -1
- package/src/graph/presets/eye.ts +19 -0
- package/src/graph/presets/face.ts +1 -1
- package/src/graph/presets/hair.ts +1 -1
- package/src/graph/presets/metal.ts +1 -1
- package/src/graph/registry.ts +11 -0
- package/src/graph/slots.ts +19 -5
- package/src/shaders/materials/common.ts +10 -9
- 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/src/shaders/scene-light-api.ts +11 -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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { audioApi } from "../audio-api";
|
|
2
|
+
import { scoreApi } from "../score-api";
|
|
3
|
+
import { EFFECT_SCENE_API } from "./composite";
|
|
4
|
+
// A persistent grid an effect can step and read: the one thing an effect could
|
|
5
|
+
// not have before, which is MEMORY.
|
|
6
|
+
//
|
|
7
|
+
// Every other mount is a pure function of position and time. A field shader
|
|
8
|
+
// recomputes its noise from scratch every frame; a trail reads history the
|
|
9
|
+
// engine happens to keep. Neither can build on what it produced last frame, and
|
|
10
|
+
// a whole class of effect is nothing but that:
|
|
11
|
+
//
|
|
12
|
+
// Fog that a dancer walks through does not "look displaced near her" — it IS
|
|
13
|
+
// displaced, and stays displaced, and creeps back over seconds. Water does not
|
|
14
|
+
// look rippled where a foot landed; a ripple leaves and propagates and
|
|
15
|
+
// reflects off the far edge long after the foot is gone. Both are the same
|
|
16
|
+
// shape of thing — a grid, stepped by its own previous value.
|
|
17
|
+
//
|
|
18
|
+
// It is also the only way to get the LOOK of a fluid, not merely its motion.
|
|
19
|
+
// Advection compounds: each frame's field is the last one pushed sideways, and a
|
|
20
|
+
// few hundred frames of that stretches and folds a smooth field into filaments
|
|
21
|
+
// and vortex sheets finer than the grid storing them. No noise function
|
|
22
|
+
// produces that, at any octave count, because the structure is not a function of
|
|
23
|
+
// position at all — it is a function of history.
|
|
24
|
+
//
|
|
25
|
+
// The engine owns the ping-pong, the dispatch and the sampling. The author owns
|
|
26
|
+
// the KERNEL, which is the part that differs: advection for smoke, the wave
|
|
27
|
+
// equation for water, a decaying stamp for footprints. That split is the same
|
|
28
|
+
// one the particle mounts make, and for the same reason — the loop is identical
|
|
29
|
+
// in every such effect and the physics never is.
|
|
30
|
+
// A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
|
|
31
|
+
// actually bounds this is the STEP: one invocation per texel per frame, each
|
|
32
|
+
// taking a dozen samples, so doubling the side quadruples the work.
|
|
33
|
+
export const SIM_MAX = 1024;
|
|
34
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
35
|
+
export const SIM_FORMAT = "rgba16float";
|
|
36
|
+
/** `// @sim 256` — the grid's resolution, in texels per side. */
|
|
37
|
+
export function parseSimSize(wgsl, max) {
|
|
38
|
+
const m = /^\s*\/\/\s*@sim\s+(\d+)\s*$/m.exec(wgsl);
|
|
39
|
+
if (!m)
|
|
40
|
+
return 0;
|
|
41
|
+
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
42
|
+
// 4096 gets the most the engine will give and a scene that still runs.
|
|
43
|
+
return Math.max(8, Math.min(max, parseInt(m[1], 10)));
|
|
44
|
+
}
|
|
45
|
+
/** Whether this effect drives a grid at all. */
|
|
46
|
+
export function simEntryPoint(wgsl) {
|
|
47
|
+
return /\bfn\s+simStep\s*\(/.test(wgsl);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `rzSim(uv)`, for a shader that READS the grid rather than steps it.
|
|
51
|
+
*
|
|
52
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
53
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
54
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
55
|
+
*/
|
|
56
|
+
export function simReadApi(group, tex, samp, size) {
|
|
57
|
+
return /* wgsl */ `
|
|
58
|
+
const RZ_SIM_SIZE: f32 = ${size > 0 ? size : 1}.0;
|
|
59
|
+
@group(${group}) @binding(${tex}) var _rzSimTex: texture_2d<f32>;
|
|
60
|
+
@group(${group}) @binding(${samp}) var _rzSimSamp: sampler;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
|
|
64
|
+
* the world is the effect's own business — the engine deliberately does not
|
|
65
|
+
* impose a mapping, because a fog laid over a stage and a ripple field around a
|
|
66
|
+
* character want different ones and both are two lines of arithmetic.
|
|
67
|
+
*/
|
|
68
|
+
fn rzSim(uv: vec2f) -> vec4f {
|
|
69
|
+
return textureSampleLevel(_rzSimTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
70
|
+
}
|
|
71
|
+
fn rzSimSize() -> f32 { return RZ_SIM_SIZE; }
|
|
72
|
+
fn rzSimTexel() -> f32 { return 1.0 / RZ_SIM_SIZE; }
|
|
73
|
+
|
|
74
|
+
// ── The step-only half, defined here too and never called here ──
|
|
75
|
+
//
|
|
76
|
+
// One effect file is spliced into EVERY module it has a mount in, so a file
|
|
77
|
+
// with a grid compiles its simStep inside the field and particle shaders as
|
|
78
|
+
// well — where it is dead code that nothing calls, but still code that has to
|
|
79
|
+
// resolve. Leaving these out is a compile error on a function the author was
|
|
80
|
+
// right to write. rzSimPrev reads the current grid rather than a previous one,
|
|
81
|
+
// which is the honest answer outside the step: there is no previous frame here.
|
|
82
|
+
fn rzSimPrev(uv: vec2f) -> vec4f { return rzSim(uv); }
|
|
83
|
+
fn rzSimFrame() -> i32 { return 1; }
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
/** `rzTime`/`rzDt`, for a module that has no clock of its own — the field pass.
|
|
87
|
+
* Same reason as above: a spliced simStep refers to them. */
|
|
88
|
+
export function simClockApi(timeExpr) {
|
|
89
|
+
return /* wgsl */ `
|
|
90
|
+
fn rzTime() -> f32 { return ${timeExpr}; }
|
|
91
|
+
fn rzDt() -> f32 { return 0.0; }
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
const SIM_UNIFORMS = /* wgsl */ `
|
|
95
|
+
struct SimU {
|
|
96
|
+
time: f32,
|
|
97
|
+
dt: f32,
|
|
98
|
+
size: f32,
|
|
99
|
+
frame: f32,
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
/**
|
|
103
|
+
* The step shader: one invocation per texel, once per frame.
|
|
104
|
+
*
|
|
105
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
106
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
107
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
108
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
109
|
+
*
|
|
110
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
111
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
112
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
113
|
+
* what turns smoke into a staircase.
|
|
114
|
+
*/
|
|
115
|
+
export function buildSimShader(wgsl, size, _cast) {
|
|
116
|
+
return (SIM_UNIFORMS +
|
|
117
|
+
/* wgsl */ `
|
|
118
|
+
@group(0) @binding(0) var<uniform> su: SimU;
|
|
119
|
+
@group(0) @binding(1) var _rzSimPrevTex: texture_2d<f32>;
|
|
120
|
+
@group(0) @binding(2) var _rzSimSamp: sampler;
|
|
121
|
+
@group(0) @binding(3) var _rzSimOut: texture_storage_2d<${SIM_FORMAT}, write>;
|
|
122
|
+
@group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
|
|
123
|
+
// The same view uniform the composite reads, so the scene API below is the real
|
|
124
|
+
// thing here rather than a stub — a kernel can ask where a bone is, and a
|
|
125
|
+
// foreground spliced into this module resolves its camera calls.
|
|
126
|
+
@group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
127
|
+
|
|
128
|
+
fn rzTime() -> f32 { return su.time; }
|
|
129
|
+
fn rzDt() -> f32 { return su.dt; }
|
|
130
|
+
/** Texels per side. */
|
|
131
|
+
fn rzSimSize() -> f32 { return su.size; }
|
|
132
|
+
/** One texel, in uv — the step a neighbour lookup takes. */
|
|
133
|
+
fn rzSimTexel() -> f32 { return 1.0 / su.size; }
|
|
134
|
+
/**
|
|
135
|
+
* Steps since this effect was installed. Zero on the very first one, which is
|
|
136
|
+
* the only chance to seed a grid: everything after it builds on what is there.
|
|
137
|
+
*/
|
|
138
|
+
fn rzSimFrame() -> i32 { return i32(su.frame); }
|
|
139
|
+
|
|
140
|
+
/** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
|
|
141
|
+
fn rzSimPrev(uv: vec2f) -> vec4f {
|
|
142
|
+
return textureSampleLevel(_rzSimPrevTex, _rzSimSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
143
|
+
}
|
|
144
|
+
` +
|
|
145
|
+
EFFECT_SCENE_API +
|
|
146
|
+
audioApi(0, 5) +
|
|
147
|
+
scoreApi(0, 7) +
|
|
148
|
+
// rzSim itself, so a kernel may read the grid it is writing — through the
|
|
149
|
+
// PREVIOUS frame's texture, which is the only version of it that exists
|
|
150
|
+
// while the current one is still being written.
|
|
151
|
+
/* wgsl */ `
|
|
152
|
+
fn rzSim(uv: vec2f) -> vec4f { return rzSimPrev(uv); }
|
|
153
|
+
` +
|
|
154
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
155
|
+
wgsl +
|
|
156
|
+
/* wgsl */ `
|
|
157
|
+
|
|
158
|
+
@compute @workgroup_size(8, 8)
|
|
159
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
160
|
+
let n = u32(${size});
|
|
161
|
+
if (gid.x >= n || gid.y >= n) { return; }
|
|
162
|
+
let xy = vec2i(i32(gid.x), i32(gid.y));
|
|
163
|
+
// The texel CENTRE, so a kernel that reads its own uv back through rzSimPrev
|
|
164
|
+
// lands on itself rather than a quarter-texel off.
|
|
165
|
+
let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
|
|
166
|
+
textureStore(_rzSimOut, xy, simStep(uv, textureLoad(_rzSimPrevTex, xy, 0), su.dt));
|
|
167
|
+
}
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/** ray-mmd, skin profile (index 1): profileSpikeRadArr[1].xyz × (1 − sssAmount),
|
|
31
|
+
* with sssAmount = customA = 0.6 in Materials/Skin/material_skin.fx. */
|
|
32
|
+
export declare const SSS_SPIKE: [number, number, number];
|
|
33
|
+
export declare const SUBSURFACE_WGSL = "\n@group(0) @binding(0) var srcTex: texture_2d<f32>;\n@group(0) @binding(1) var maskTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_depth_multisampled_2d;\n// dofU[2] = (projA, projB): the same z-buffer inversion the composite uses.\n@group(0) @binding(3) var<uniform> dofU: array<vec4f, 3>;\n// sssU[0] = (direction.xy, radius, _); sssU[1] = (tanHalfFov\u00B7aspect, tanHalfFov, width, height).\n@group(0) @binding(4) var<uniform> sssU: array<vec4f, 2>;\n\nstruct VsOut { @builtin(position) pos: vec4f };\n\n@vertex fn vs(@builtin(vertex_index) i: u32) -> VsOut {\n let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u));\n var o: VsOut;\n o.pos = vec4f(p * 2.0 - 1.0, 0.0, 1.0);\n return o;\n}\n\nfn size() -> vec2i { return vec2i(sssU[1].zw); }\n\nfn texel(uv: vec2f) -> vec2i {\n return clamp(vec2i(floor(uv * sssU[1].zw)), vec2i(0), size() - 1);\n}\n\nfn linearDepth(c: vec2i) -> f32 {\n let z = textureLoad(depthTex, c, 0);\n return clamp(dofU[2].y / (z - dofU[2].x), 0.05, 100000.0);\n}\n\n// View-space position of a texel centre \u2014 ray-mmd's GetViewPosition, with y up.\nfn viewPos(c: vec2i, d: f32) -> vec3f {\n let uv = (vec2f(c) + 0.5) / sssU[1].zw;\n let ndc = vec2f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n return vec3f(ndc * sssU[1].xy * d, d);\n}\n\nfn skin(c: vec2i) -> f32 { return textureLoad(maskTex, c, 0).b; }\n\nfn blur(fragPos: vec4f) -> vec4f {\n let c = vec2i(fragPos.xy);\n let amount = skin(c) * 4.0;\n let dir = sssU[0].xy;\n let uv = (vec2f(c) + 0.5) / sssU[1].zw;\n let centre = textureLoad(srcTex, c, 0);\n\n // ray-mmd, verbatim: six offsets, and the skin variances per channel.\n // A var, not a const: a const array cannot be indexed by the loop counter.\n var offsets = array<f32, 6>(0.352, 0.719, 1.117, 1.579, 2.177, 3.213);\n const profileVar = vec3f(9.6, 2.8, 2.2);\n // mSSSSScale at the controller's default; it sets both the width and how hard\n // a depth step rejects.\n const SSSS_SCALE: f32 = 0.04;\n\n let d = linearDepth(c);\n\n // The surface's normal, rebuilt from depth (ray-mmd reads it from its G-buffer;\n // this engine keeps none). Only its angle to the view matters: the blur narrows\n // along an axis the surface is turning away on.\n let s = size();\n let cx = min(c + vec2i(1, 0), s - 1);\n let cy = min(c + vec2i(0, 1), s - 1);\n let P = viewPos(c, d);\n let N = cross(viewPos(cx, linearDepth(cx)) - P, viewPos(cy, linearDepth(cy)) - P);\n let nxz = select(vec2f(0.0, 1.0), normalize(N.xz), dot(N.xz, N.xz) > 1e-12);\n let nyz = select(vec2f(0.0, 1.0), normalize(N.yz), dot(N.yz, N.yz) > 1e-12);\n let scaleX = abs(dot(nxz, normalize(P.xz)));\n let scaleY = abs(dot(nyz, normalize(P.yz)));\n let perspectiveScale = max(dot(dir, vec2f(scaleX, scaleY)), 0.3);\n\n let radius = 0.0055 * amount * perspectiveScale / SSSS_SCALE;\n let step = dir * radius / d;\n\n var weightSum = vec3f(1.0);\n var colour = centre.rgb;\n for (var i = 0; i < 6; i++) {\n let o = offsets[i] / 5.5 * step;\n for (var side = 0; side < 2; side++) {\n let tc = texel(select(uv - o, uv + o, side == 0));\n // Skin only: ray-mmd zeroes a non-skin pixel's depth so the depth term\n // rejects it; the mask says the same thing directly.\n let isSkin = select(0.0, 1.0, skin(tc) > 0.004);\n let dd = (linearDepth(tc) - d) * 1000.0 * SSSS_SCALE;\n let w = exp(-(offsets[i] * offsets[i] + dd * dd) / profileVar) * isSkin;\n weightSum += w;\n colour += w * textureLoad(srcTex, tc, 0).rgb;\n }\n }\n return vec4f(colour / weightSum, centre.a);\n}\n\n@fragment fn fsX(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n // Nothing reads the scratch target off skin: the Y pass rejects those taps.\n if (skin(vec2i(pos.xy)) <= 0.004) { return vec4f(0.0); }\n return blur(pos);\n}\n\n@fragment fn fsY(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n // Off skin the HDR resolve must stay exactly as the scene left it.\n if (skin(vec2i(pos.xy)) <= 0.004) { discard; }\n return blur(pos);\n}\n";
|
|
34
|
+
//# sourceMappingURL=subsurface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subsurface.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/subsurface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;yEACyE;AACzE,eAAO,MAAM,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAsC,CAAA;AAErF,eAAO,MAAM,eAAe,48HAqG3B,CAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
/** ray-mmd, skin profile (index 1): profileSpikeRadArr[1].xyz × (1 − sssAmount),
|
|
31
|
+
* with sssAmount = customA = 0.6 in Materials/Skin/material_skin.fx. */
|
|
32
|
+
export const SSS_SPIKE = [0.35 * 0.4, 0.4 * 0.4, 0.5 * 0.4];
|
|
33
|
+
export const SUBSURFACE_WGSL = /* wgsl */ `
|
|
34
|
+
@group(0) @binding(0) var srcTex: texture_2d<f32>;
|
|
35
|
+
@group(0) @binding(1) var maskTex: texture_2d<f32>;
|
|
36
|
+
@group(0) @binding(2) var depthTex: texture_depth_multisampled_2d;
|
|
37
|
+
// dofU[2] = (projA, projB): the same z-buffer inversion the composite uses.
|
|
38
|
+
@group(0) @binding(3) var<uniform> dofU: array<vec4f, 3>;
|
|
39
|
+
// sssU[0] = (direction.xy, radius, _); sssU[1] = (tanHalfFov·aspect, tanHalfFov, width, height).
|
|
40
|
+
@group(0) @binding(4) var<uniform> sssU: array<vec4f, 2>;
|
|
41
|
+
|
|
42
|
+
struct VsOut { @builtin(position) pos: vec4f };
|
|
43
|
+
|
|
44
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> VsOut {
|
|
45
|
+
let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
|
46
|
+
var o: VsOut;
|
|
47
|
+
o.pos = vec4f(p * 2.0 - 1.0, 0.0, 1.0);
|
|
48
|
+
return o;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn size() -> vec2i { return vec2i(sssU[1].zw); }
|
|
52
|
+
|
|
53
|
+
fn texel(uv: vec2f) -> vec2i {
|
|
54
|
+
return clamp(vec2i(floor(uv * sssU[1].zw)), vec2i(0), size() - 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn linearDepth(c: vec2i) -> f32 {
|
|
58
|
+
let z = textureLoad(depthTex, c, 0);
|
|
59
|
+
return clamp(dofU[2].y / (z - dofU[2].x), 0.05, 100000.0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// View-space position of a texel centre — ray-mmd's GetViewPosition, with y up.
|
|
63
|
+
fn viewPos(c: vec2i, d: f32) -> vec3f {
|
|
64
|
+
let uv = (vec2f(c) + 0.5) / sssU[1].zw;
|
|
65
|
+
let ndc = vec2f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
|
66
|
+
return vec3f(ndc * sssU[1].xy * d, d);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn skin(c: vec2i) -> f32 { return textureLoad(maskTex, c, 0).b; }
|
|
70
|
+
|
|
71
|
+
fn blur(fragPos: vec4f) -> vec4f {
|
|
72
|
+
let c = vec2i(fragPos.xy);
|
|
73
|
+
let amount = skin(c) * 4.0;
|
|
74
|
+
let dir = sssU[0].xy;
|
|
75
|
+
let uv = (vec2f(c) + 0.5) / sssU[1].zw;
|
|
76
|
+
let centre = textureLoad(srcTex, c, 0);
|
|
77
|
+
|
|
78
|
+
// ray-mmd, verbatim: six offsets, and the skin variances per channel.
|
|
79
|
+
// A var, not a const: a const array cannot be indexed by the loop counter.
|
|
80
|
+
var offsets = array<f32, 6>(0.352, 0.719, 1.117, 1.579, 2.177, 3.213);
|
|
81
|
+
const profileVar = vec3f(9.6, 2.8, 2.2);
|
|
82
|
+
// mSSSSScale at the controller's default; it sets both the width and how hard
|
|
83
|
+
// a depth step rejects.
|
|
84
|
+
const SSSS_SCALE: f32 = 0.04;
|
|
85
|
+
|
|
86
|
+
let d = linearDepth(c);
|
|
87
|
+
|
|
88
|
+
// The surface's normal, rebuilt from depth (ray-mmd reads it from its G-buffer;
|
|
89
|
+
// this engine keeps none). Only its angle to the view matters: the blur narrows
|
|
90
|
+
// along an axis the surface is turning away on.
|
|
91
|
+
let s = size();
|
|
92
|
+
let cx = min(c + vec2i(1, 0), s - 1);
|
|
93
|
+
let cy = min(c + vec2i(0, 1), s - 1);
|
|
94
|
+
let P = viewPos(c, d);
|
|
95
|
+
let N = cross(viewPos(cx, linearDepth(cx)) - P, viewPos(cy, linearDepth(cy)) - P);
|
|
96
|
+
let nxz = select(vec2f(0.0, 1.0), normalize(N.xz), dot(N.xz, N.xz) > 1e-12);
|
|
97
|
+
let nyz = select(vec2f(0.0, 1.0), normalize(N.yz), dot(N.yz, N.yz) > 1e-12);
|
|
98
|
+
let scaleX = abs(dot(nxz, normalize(P.xz)));
|
|
99
|
+
let scaleY = abs(dot(nyz, normalize(P.yz)));
|
|
100
|
+
let perspectiveScale = max(dot(dir, vec2f(scaleX, scaleY)), 0.3);
|
|
101
|
+
|
|
102
|
+
let radius = 0.0055 * amount * perspectiveScale / SSSS_SCALE;
|
|
103
|
+
let step = dir * radius / d;
|
|
104
|
+
|
|
105
|
+
var weightSum = vec3f(1.0);
|
|
106
|
+
var colour = centre.rgb;
|
|
107
|
+
for (var i = 0; i < 6; i++) {
|
|
108
|
+
let o = offsets[i] / 5.5 * step;
|
|
109
|
+
for (var side = 0; side < 2; side++) {
|
|
110
|
+
let tc = texel(select(uv - o, uv + o, side == 0));
|
|
111
|
+
// Skin only: ray-mmd zeroes a non-skin pixel's depth so the depth term
|
|
112
|
+
// rejects it; the mask says the same thing directly.
|
|
113
|
+
let isSkin = select(0.0, 1.0, skin(tc) > 0.004);
|
|
114
|
+
let dd = (linearDepth(tc) - d) * 1000.0 * SSSS_SCALE;
|
|
115
|
+
let w = exp(-(offsets[i] * offsets[i] + dd * dd) / profileVar) * isSkin;
|
|
116
|
+
weightSum += w;
|
|
117
|
+
colour += w * textureLoad(srcTex, tc, 0).rgb;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return vec4f(colour / weightSum, centre.a);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@fragment fn fsX(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
124
|
+
// Nothing reads the scratch target off skin: the Y pass rejects those taps.
|
|
125
|
+
if (skin(vec2i(pos.xy)) <= 0.004) { return vec4f(0.0); }
|
|
126
|
+
return blur(pos);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@fragment fn fsY(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
130
|
+
// Off skin the HDR resolve must stay exactly as the scene left it.
|
|
131
|
+
if (skin(vec2i(pos.xy)) <= 0.004) { discard; }
|
|
132
|
+
return blur(pos);
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"scene-light-api.d.ts","sourceRoot":"","sources":["../../src/shaders/scene-light-api.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,MAAM,WAwBzC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2DjF"}
|
|
@@ -36,6 +36,17 @@ fn rzWorldAmbient(n: vec3f) -> vec3f {
|
|
|
36
36
|
+ ${u}.sh[8].xyz * (x * x - y * y);
|
|
37
37
|
return max(c, vec3f(0.0));
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The world's light averaged over every direction: its colour and brightness,
|
|
42
|
+
* with no shape. Every term but sh[0] averages to zero over the sphere (x, xy,
|
|
43
|
+
* 3z²-1 ... all do), so the average is sh[0] itself; a flat world is its own.
|
|
44
|
+
* What an anime cast takes from a sky - see ModelLight.opts.
|
|
45
|
+
*/
|
|
46
|
+
fn rzWorldAmbientAvg() -> vec3f {
|
|
47
|
+
if (${u}.sh[0].w < 0.5) { return ${u}.ambientColor.xyz; }
|
|
48
|
+
return max(${u}.sh[0].xyz, vec3f(0.0));
|
|
49
|
+
}
|
|
39
50
|
`;
|
|
40
51
|
/**
|
|
41
52
|
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Floats before the key map: count, pitch range, clock, duration, release. */
|
|
2
|
+
export declare const SCORE_HEADER = 8;
|
|
3
|
+
/** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
|
|
4
|
+
export declare const SCORE_KEYS = 128;
|
|
5
|
+
/** Where the note records begin. */
|
|
6
|
+
export declare const SCORE_NOTES: number;
|
|
7
|
+
/** Floats per note: start, duration, pitch, velocity. */
|
|
8
|
+
export declare const SCORE_STRIDE = 4;
|
|
9
|
+
/** The rzNote*/ export declare function scoreApi(group: number, binding: number): string;
|
|
10
|
+
//# sourceMappingURL=score-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score-api.d.ts","sourceRoot":"","sources":["../../src/shaders/score-api.ts"],"names":[],"mappings":"AA8BA,+EAA+E;AAC/E,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,iFAAiF;AACjF,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,oCAAoC;AACpC,eAAO,MAAM,WAAW,QAA4B,CAAA;AACpD,yDAAyD;AACzD,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B,gBAAgB,CAChB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2E/D"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The score data interface, as WGSL — shared verbatim by every effect module.
|
|
2
|
+
//
|
|
3
|
+
// The sibling of the audio interface, and precomputed for the same reason: an
|
|
4
|
+
// export steps the engine frame by frame rather than playing in real time, so
|
|
5
|
+
// anything read live from a player would render empty into the exported video.
|
|
6
|
+
// A note list is inherently precomputed, so it cannot even be tempted.
|
|
7
|
+
//
|
|
8
|
+
// WHY THIS EXISTS BESIDE rzAudio* RATHER THAN INSTEAD OF IT. The audio interface
|
|
9
|
+
// answers "how loud is the bass right now" — a spectrum, smeared across
|
|
10
|
+
// frequency and time. That is the right shape for a reactive backdrop and the
|
|
11
|
+
// wrong shape for a note: you cannot recover a discrete onset, a pitch and a
|
|
12
|
+
// duration from band energy, and the crispness of a falling note is the whole
|
|
13
|
+
// point of the look. The two are complementary — a scene can pulse its fog on
|
|
14
|
+
// rzAudioOnset while its notes fall on rzNoteAge.
|
|
15
|
+
//
|
|
16
|
+
// LAYOUT. An 8-float header, then a 128-float live key map, then the notes at 4
|
|
17
|
+
// floats each (start, duration, pitch, velocity). `time` and `playing` are
|
|
18
|
+
// rewritten every frame by whoever owns playback, exactly as the audio header
|
|
19
|
+
// is, so "now" is always the clock actually running.
|
|
20
|
+
//
|
|
21
|
+
// THE KEY MAP IS THE POINT OF THE DESIGN. Falling notes index the note list
|
|
22
|
+
// directly — one particle per note, which is what makes the geometry trivial.
|
|
23
|
+
// But a keyboard glow asks the opposite question, per pixel: is anything
|
|
24
|
+
// sounding at THIS pitch? Answering that by scanning the note list would be
|
|
25
|
+
// thousands of iterations per fragment. So the engine keeps a 128-entry
|
|
26
|
+
// per-pitch energy map, updated once per frame on the CPU, and the glow becomes
|
|
27
|
+
// one lookup — the same trade the audio interface makes by precomputing onset
|
|
28
|
+
// so "a beat-triggered effect is one comparison instead of a per-pixel history
|
|
29
|
+
// scan".
|
|
30
|
+
/** Floats before the key map: count, pitch range, clock, duration, release. */
|
|
31
|
+
export const SCORE_HEADER = 8;
|
|
32
|
+
/** One energy slot per MIDI pitch. 128 is the MIDI range, not a cap we chose. */
|
|
33
|
+
export const SCORE_KEYS = 128;
|
|
34
|
+
/** Where the note records begin. */
|
|
35
|
+
export const SCORE_NOTES = SCORE_HEADER + SCORE_KEYS;
|
|
36
|
+
/** Floats per note: start, duration, pitch, velocity. */
|
|
37
|
+
export const SCORE_STRIDE = 4;
|
|
38
|
+
/** The rzNote*/ /* rzKey* accessors, with the buffer declared at the given binding. */
|
|
39
|
+
export function scoreApi(group, binding) {
|
|
40
|
+
return /* wgsl */ `
|
|
41
|
+
@group(${group}) @binding(${binding}) var<storage, read> _rzScore: array<f32>;
|
|
42
|
+
|
|
43
|
+
/** Notes in the score; 0 when none is loaded, which every accessor below
|
|
44
|
+
* tolerates by returning zero rather than reading past the end. */
|
|
45
|
+
fn rzNoteCount() -> i32 { return i32(_rzScore[0]); }
|
|
46
|
+
/** Where the score is NOW, in seconds — the clock every age below hangs from. */
|
|
47
|
+
fn rzScoreTime() -> f32 { return _rzScore[3]; }
|
|
48
|
+
/** 1 while the score is advancing, 0 paused or absent. */
|
|
49
|
+
fn rzScorePlaying() -> f32 { return select(0.0, _rzScore[4], i32(_rzScore[0]) > 0); }
|
|
50
|
+
/** Last note-off, in seconds — the length of the piece. */
|
|
51
|
+
fn rzScoreDuration() -> f32 { return _rzScore[5]; }
|
|
52
|
+
/** Lowest and highest pitch the score actually uses. Lay a keyboard out against
|
|
53
|
+
* these rather than against 0..127 and a piano piece fills the screen instead
|
|
54
|
+
* of occupying its middle third. */
|
|
55
|
+
fn rzPitchLow() -> f32 { return _rzScore[1]; }
|
|
56
|
+
fn rzPitchHigh() -> f32 { return _rzScore[2]; }
|
|
57
|
+
|
|
58
|
+
fn _rzNoteAt(i: i32, field: i32) -> f32 {
|
|
59
|
+
if (i < 0 || i >= i32(_rzScore[0])) { return 0.0; }
|
|
60
|
+
return _rzScore[${SCORE_NOTES} + i * ${SCORE_STRIDE} + field];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** When note i begins, in seconds. */
|
|
64
|
+
fn rzNoteStart(i: i32) -> f32 { return _rzNoteAt(i, 0); }
|
|
65
|
+
/** How long note i sounds, in seconds. */
|
|
66
|
+
fn rzNoteLength(i: i32) -> f32 { return _rzNoteAt(i, 1); }
|
|
67
|
+
/** MIDI pitch of note i — 60 is middle C. */
|
|
68
|
+
fn rzNotePitch(i: i32) -> f32 { return _rzNoteAt(i, 2); }
|
|
69
|
+
/** How hard note i was struck, 0..1. */
|
|
70
|
+
fn rzNoteVelocity(i: i32) -> f32 { return _rzNoteAt(i, 3); }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Seconds since note i began — NEGATIVE before it does.
|
|
74
|
+
*
|
|
75
|
+
* The falling-note function, and the sign is the whole of it: a note's height is
|
|
76
|
+
* -rzNoteAge(i) * speed, so a note not yet played sits above the keyboard at a
|
|
77
|
+
* distance proportional to how long is left, crosses zero exactly as it sounds,
|
|
78
|
+
* and keeps going. One expression, no state, no spawning — which is why one
|
|
79
|
+
* particle per note is the natural mapping and the pool index IS the note index.
|
|
80
|
+
*/
|
|
81
|
+
fn rzNoteAge(i: i32) -> f32 { return _rzScore[3] - rzNoteStart(i); }
|
|
82
|
+
|
|
83
|
+
/** 1 while note i is sounding, 0 either side of it. */
|
|
84
|
+
fn rzNoteHeld(i: i32) -> f32 {
|
|
85
|
+
let age = rzNoteAge(i);
|
|
86
|
+
return select(0.0, 1.0, age >= 0.0 && age < rzNoteLength(i));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Energy at a MIDI pitch right now: 1 the instant it is struck, holding while
|
|
91
|
+
* sounding, then falling away over the release set at install time.
|
|
92
|
+
*
|
|
93
|
+
* Per PITCH rather than per note, because that is the question a keyboard asks
|
|
94
|
+
* and a note list cannot answer cheaply — see the header. Fractional pitches
|
|
95
|
+
* read the nearest key, so a caller can hand this a continuous x across the
|
|
96
|
+
* keyboard without rounding first.
|
|
97
|
+
*/
|
|
98
|
+
fn rzKeyEnergy(pitch: f32) -> f32 {
|
|
99
|
+
let k = i32(round(pitch));
|
|
100
|
+
if (k < 0 || k >= ${SCORE_KEYS}) { return 0.0; }
|
|
101
|
+
return _rzScore[${SCORE_HEADER} + k];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A pitch's place across the score's own range, 0..1 — the x of a falling note
|
|
106
|
+
* and of the key it lands on, which have to agree or the effect is nonsense.
|
|
107
|
+
*/
|
|
108
|
+
fn rzPitchX(pitch: f32) -> f32 {
|
|
109
|
+
let lo = _rzScore[1];
|
|
110
|
+
let hi = _rzScore[2];
|
|
111
|
+
return select(0.5, (pitch - lo) / max(hi - lo, 1.0), hi > lo);
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
3
|
+
*
|
|
4
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
5
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
6
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
7
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pcf9: (map: string, ts: string, sampler?: string) => string;
|
|
10
|
+
/** Bindings the real accessor takes, from `binding` upward. */
|
|
11
|
+
export declare const SHADOW_API_BINDINGS = 5;
|
|
12
|
+
/**
|
|
13
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
14
|
+
* caster's shadow, soft in between.
|
|
15
|
+
*
|
|
16
|
+
* With `on`, declares five bindings from `binding`:
|
|
17
|
+
* +0 the light uniform (the sun's direction, and in .w the shadow switch)
|
|
18
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
19
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
20
|
+
* +4 the comparison sampler
|
|
21
|
+
*
|
|
22
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
23
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
24
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
25
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
26
|
+
* root off a stage floor that casts into the same map.
|
|
27
|
+
*/
|
|
28
|
+
export declare function shadowApi(on: boolean, group: number, binding: number): string;
|
|
29
|
+
//# sourceMappingURL=shadow-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow-api.d.ts","sourceRoot":"","sources":["../../src/shaders/shadow-api.ts"],"names":[],"mappings":"AAcA;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,gBAAyB,WActE,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyD7E"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SHADOW_CASCADES } from "../shadow-cascades";
|
|
2
|
+
// The sun's shadow, as an effect author sees it.
|
|
3
|
+
//
|
|
4
|
+
// A lawn standing in a character's shadow has to darken where the floor under
|
|
5
|
+
// it does, or the figure floats: the ground catcher draws her shadow, and the
|
|
6
|
+
// blades drawn over it hide it. The map is already there — the materials and
|
|
7
|
+
// the ground read it every frame — so this hands the same lookup to effects.
|
|
8
|
+
//
|
|
9
|
+
// Bound for real in the particle SHADING stage only. That is where geometry an
|
|
10
|
+
// effect draws inside the scene pass is shaded, and the shadow pass has already
|
|
11
|
+
// run by then. Every other module that compiles the author's file gets the
|
|
12
|
+
// stub, which answers 1: lit is what a point with no occlusion information is,
|
|
13
|
+
// the same answer sampleShadow gives outside every cascade.
|
|
14
|
+
/**
|
|
15
|
+
* 3×3 PCF over one cascade's map, for a point already in that cascade's NDC.
|
|
16
|
+
*
|
|
17
|
+
* Shared with the materials, so an effect's shadow and her own shadow soften
|
|
18
|
+
* alike. The sampler is linear-filtered, which makes each tap a 2×2 bilinear
|
|
19
|
+
* compare. Unrolled — Safari's Metal backend doesn't unroll nested shadow
|
|
20
|
+
* loops reliably. The body expects `ndc` in scope and returns.
|
|
21
|
+
*/
|
|
22
|
+
export const pcf9 = (map, ts, sampler = "shadowSampler") => /* wgsl */ `
|
|
23
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
24
|
+
let cmpZ = ndc.z - 0.001;
|
|
25
|
+
let ts = ${ts};
|
|
26
|
+
let s00 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, -ts), cmpZ);
|
|
27
|
+
let s10 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, -ts), cmpZ);
|
|
28
|
+
let s20 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, -ts), cmpZ);
|
|
29
|
+
let s01 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, 0.0), cmpZ);
|
|
30
|
+
let s11 = textureSampleCompareLevel(${map}, ${sampler}, suv, cmpZ);
|
|
31
|
+
let s21 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, 0.0), cmpZ);
|
|
32
|
+
let s02 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(-ts, ts), cmpZ);
|
|
33
|
+
let s12 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f(0.0, ts), cmpZ);
|
|
34
|
+
let s22 = textureSampleCompareLevel(${map}, ${sampler}, suv + vec2f( ts, ts), cmpZ);
|
|
35
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
36
|
+
`;
|
|
37
|
+
/** Bindings the real accessor takes, from `binding` upward. */
|
|
38
|
+
export const SHADOW_API_BINDINGS = 5;
|
|
39
|
+
/**
|
|
40
|
+
* `rzShadow(p)`: how much of the sun reaches world point `p` — 1 lit, 0 in a
|
|
41
|
+
* caster's shadow, soft in between.
|
|
42
|
+
*
|
|
43
|
+
* With `on`, declares five bindings from `binding`:
|
|
44
|
+
* +0 the light uniform (the sun's direction, and in .w the shadow switch)
|
|
45
|
+
* +1 the cascade view-projections, then the caster sphere
|
|
46
|
+
* +2 the near cascade's map +3 the far cascade's map
|
|
47
|
+
* +4 the comparison sampler
|
|
48
|
+
*
|
|
49
|
+
* It takes a POINT and no normal. The materials' sampleShadow also rejects a
|
|
50
|
+
* surface facing away from the sun, which is right for skin and wrong for a
|
|
51
|
+
* blade of grass or a petal: those are thin, lit through, and shaded by their
|
|
52
|
+
* own N·L. The point is nudged toward the sun instead, which lifts a blade's
|
|
53
|
+
* root off a stage floor that casts into the same map.
|
|
54
|
+
*/
|
|
55
|
+
export function shadowApi(on, group, binding) {
|
|
56
|
+
if (!on) {
|
|
57
|
+
return /* wgsl */ `
|
|
58
|
+
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
const n = SHADOW_CASCADES.length;
|
|
62
|
+
return /* wgsl */ `
|
|
63
|
+
// The first two vec4s of the light uniform: world ambient, then the sun.
|
|
64
|
+
struct _RzShadowSun { ambient: vec4f, direction: vec4f, }
|
|
65
|
+
// The cascades' matrices, inner to outer, then every caster in one sphere
|
|
66
|
+
// (x, y, z, radius): radius 0 = nothing casts, negative = do not test.
|
|
67
|
+
struct _RzShadowBlock { viewProj: array<mat4x4f, ${n}>, casters: vec4f, }
|
|
68
|
+
@group(${group}) @binding(${binding}) var<uniform> _rzShadowSun: _RzShadowSun;
|
|
69
|
+
@group(${group}) @binding(${binding + 1}) var<uniform> _rzShadowVP: _RzShadowBlock;
|
|
70
|
+
@group(${group}) @binding(${binding + 2}) var _rzShadowNear: texture_depth_2d;
|
|
71
|
+
@group(${group}) @binding(${binding + 3}) var _rzShadowFar: texture_depth_2d;
|
|
72
|
+
@group(${group}) @binding(${binding + 4}) var _rzShadowCmp: sampler_comparison;
|
|
73
|
+
|
|
74
|
+
fn _rzShadowNearTaps(ndc: vec3f) -> f32 {
|
|
75
|
+
${pcf9("_rzShadowNear", `1.0 / ${SHADOW_CASCADES[0].mapSize}.0`, "_rzShadowCmp")}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn _rzShadowFarTaps(ndc: vec3f) -> f32 {
|
|
79
|
+
${pcf9("_rzShadowFar", `1.0 / ${SHADOW_CASCADES[n - 1].mapSize}.0`, "_rzShadowCmp")}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn rzShadow(p: vec3f) -> f32 {
|
|
83
|
+
// The scene's one shadow switch, on the sun — see sampleShadow.
|
|
84
|
+
let castAmt = _rzShadowSun.direction.w;
|
|
85
|
+
if (castAmt <= 0.0) { return 1.0; }
|
|
86
|
+
let toSun = normalize(-_rzShadowSun.direction.xyz);
|
|
87
|
+
// Can anything cast onto this point at all? The ground's test, for the same
|
|
88
|
+
// two reasons: most of a lawn is nowhere near her, and a scene with no
|
|
89
|
+
// models never renders the map, whose untouched zeroes compare as shadowed.
|
|
90
|
+
let cs = _rzShadowVP.casters;
|
|
91
|
+
if (cs.w == 0.0) { return 1.0; }
|
|
92
|
+
if (cs.w > 0.0) {
|
|
93
|
+
let toCaster = cs.xyz - p;
|
|
94
|
+
let along = dot(toCaster, toSun);
|
|
95
|
+
let perp = length(toCaster - toSun * along);
|
|
96
|
+
if (along <= -cs.w || perp > cs.w) { return 1.0; }
|
|
97
|
+
}
|
|
98
|
+
let q = p + toSun * 0.08;
|
|
99
|
+
let c0 = _rzShadowVP.viewProj[0] * vec4f(q, 1.0);
|
|
100
|
+
let n0 = c0.xyz / max(c0.w, 1e-6);
|
|
101
|
+
if (all(abs(n0.xy) < vec2f(0.98)) && n0.z > 0.0 && n0.z < 1.0) {
|
|
102
|
+
return mix(1.0, _rzShadowNearTaps(n0), castAmt);
|
|
103
|
+
}
|
|
104
|
+
let c1 = _rzShadowVP.viewProj[${n - 1}] * vec4f(q, 1.0);
|
|
105
|
+
let n1 = c1.xyz / max(c1.w, 1e-6);
|
|
106
|
+
if (all(abs(n1.xy) < vec2f(0.98)) && n1.z > 0.0 && n1.z < 1.0) {
|
|
107
|
+
return mix(1.0, _rzShadowFarTaps(n1), castAmt);
|
|
108
|
+
}
|
|
109
|
+
return 1.0;
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
}
|