reze-engine 0.63.0 → 0.65.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/engine.d.ts +226 -96
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1187 -732
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +15 -18
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +10 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -1
- package/dist/outline-normals.d.ts +12 -0
- package/dist/outline-normals.d.ts.map +1 -0
- package/dist/outline-normals.js +100 -0
- package/dist/pmx-loader.d.ts +3 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +7 -2
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +65 -56
- 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 +210 -0
- package/dist/shaders/passes/bloom.d.ts +4 -3
- package/dist/shaders/passes/bloom.d.ts.map +1 -1
- package/dist/shaders/passes/bloom.js +58 -87
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +122 -64
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/mirror.d.ts +18 -12
- package/dist/shaders/passes/mirror.d.ts.map +1 -1
- package/dist/shaders/passes/mirror.js +59 -13
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +71 -14
- package/dist/shaders/passes/particles.d.ts +1 -1
- package/dist/shaders/passes/particles.js +1 -1
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +152 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +529 -0
- package/dist/unity/looks.d.ts +93 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +312 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +120 -0
- package/dist/unity/stage.d.ts +155 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +410 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +1535 -964
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +15 -18
- package/src/index.ts +12 -0
- package/src/model.ts +18 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +8 -2
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +65 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +122 -64
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +1 -1
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +630 -0
- package/src/unity/looks.ts +391 -0
- package/src/unity/skin.ts +131 -0
- package/src/unity/stage.ts +604 -0
- package/src/unity/state.ts +172 -0
- package/src/shaders/agx-lut.ts +0 -30
|
@@ -13,7 +13,7 @@ import { castDistanceApi, CAST_FIELD_DIV } from "./cast-distance"
|
|
|
13
13
|
import { lyricsApi, lyricsTextApi } from "../lyrics-api"
|
|
14
14
|
import { midiApi } from "../midi-api"
|
|
15
15
|
import { gridReadApi } from "./grid"
|
|
16
|
-
// Composite: HDR scene + bloom
|
|
16
|
+
// Composite: HDR scene + bloom → view transform (soft / neutral / aces / none) → gamma → swapchain.
|
|
17
17
|
// Bloom tint/intensity applied at combine (EEVEE treats them as combine-stage params, not prefilter).
|
|
18
18
|
//
|
|
19
19
|
// The shader is a TEMPLATE: buildCompositeShader() emits either the base pass or
|
|
@@ -138,14 +138,6 @@ override APPLY_GAMMA: bool = true;
|
|
|
138
138
|
// re-premultiply the tonemapped color for output so the premultiplied canvas
|
|
139
139
|
// alphaMode composites the WebGPU surface over the page background correctly.
|
|
140
140
|
@group(0) @binding(4) var maskTex: texture_2d<f32>;
|
|
141
|
-
// Filmic tone curve baked to a WIDTH×1 r16float LUT (bakeFilmicLut on the CPU side).
|
|
142
|
-
// Domain: log2(linear) mapped to [0,13]. Replaces the old array<f32,14> that was indexed
|
|
143
|
-
// by a runtime u32 (a Metal-backend smell — dynamic local-array indexing lowers to a
|
|
144
|
-
// per-invocation copy/switch) and interpolated piecewise-linearly (C0, so segment slope
|
|
145
|
-
// discontinuities showed as Mach bands in smooth skin/shadow gradients). The LUT is a
|
|
146
|
-
// monotone-cubic (Fritsch–Carlson) fit through the same 14 anchors — same values, C1
|
|
147
|
-
// continuity kills the banding — sampled with hardware linear filtering.
|
|
148
|
-
@group(0) @binding(5) var filmicLut: texture_2d<f32>;
|
|
149
141
|
// viewU[0] = (exposure, invGamma, grain amount, grain seed); viewU[1] = (tint.rgb, intensity)
|
|
150
142
|
// viewU[2] = (background.rgb, mode) — display-space sRGB, composited UNDER the
|
|
151
143
|
// scene post-tonemap. BASE-layer mode: 0 transparent (DOM shows),
|
|
@@ -177,9 +169,6 @@ override APPLY_GAMMA: bool = true;
|
|
|
177
169
|
// track the camera radius. Cleared depth (1.0) inverts to the far
|
|
178
170
|
// plane, so empty sky reads as maximally defocused background.
|
|
179
171
|
@group(0) @binding(9) var<uniform> dofU: array<vec4<f32>, 3>;
|
|
180
|
-
// Blender's AgX, as the 57³ lookup it ships as rather than a reconstruction of
|
|
181
|
-
// it. Sampled in the log-encoded E-Gamut space the cube expects — see agxTransform.
|
|
182
|
-
@group(0) @binding(10) var agxLut: texture_3d<f32>;
|
|
183
172
|
// The scene's own colour grade, as a cube — see _rzStageGrade. sRGB-encoded
|
|
184
173
|
// storage, so what a sample returns is already linear.
|
|
185
174
|
@group(0) @binding(12) var stageGradeLut: texture_3d<f32>;
|
|
@@ -215,8 +204,6 @@ override APPLY_GAMMA: bool = true;
|
|
|
215
204
|
fn rzFieldMerge(top: vec4f, bot: vec4f) -> vec4f {
|
|
216
205
|
return vec4f(top.rgb + bot.rgb * (1.0 - top.a), top.a + bot.a * (1.0 - top.a));
|
|
217
206
|
}
|
|
218
|
-
// Must match FILMIC_LUT_WIDTH in engine.ts (bakeFilmicLut).
|
|
219
|
-
const FILMIC_LUT_W: f32 = 256.0;
|
|
220
207
|
|
|
221
208
|
fn linearDepth(coord: vec2<i32>) -> f32 {
|
|
222
209
|
let z = textureLoad(depthTex, coord, 0);
|
|
@@ -249,67 +236,138 @@ fn sceneSample(coord: vec2<i32>, fullSzI: vec2<i32>, fullSz: vec2f) -> vec4f {
|
|
|
249
236
|
return vec4f((sHdr + sBloom) * sAlpha, sAlpha);
|
|
250
237
|
}
|
|
251
238
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// NOTE: version-pinned to Blender 3.6 — 4.x defaults to AgX, not Filmic.
|
|
256
|
-
let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
|
|
257
|
-
// Map t∈[0,13] to the texel-center of baked sample j = t·(W-1)/13.
|
|
258
|
-
let u = (t * (FILMIC_LUT_W - 1.0) / 13.0 + 0.5) / FILMIC_LUT_W;
|
|
259
|
-
// textureSampleLevel (explicit LOD, no derivatives) is legal in non-uniform flow.
|
|
260
|
-
return textureSampleLevel(filmicLut, bloomSamp, vec2f(u, 0.5), 0.0).r;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** The sRGB display encoding — Blender's "Standard" view transform, which is what
|
|
264
|
-
* NPR work uses: no film curve at all, so the colours a graph computes are the
|
|
265
|
-
* colours that land. Not a 2.2 power law; sRGB has a linear toe. */
|
|
239
|
+
/** The sRGB display encoding — "none": no tone curve at all, so the colours a
|
|
240
|
+
* graph computes are the colours that land. Not a 2.2 power law; sRGB has a
|
|
241
|
+
* linear toe. Every transform below ends in it. */
|
|
266
242
|
fn srgb_encode(x: f32) -> f32 {
|
|
267
243
|
let c = max(x, 0.0);
|
|
268
244
|
return select(1.055 * pow(c, 1.0 / 2.4) - 0.055, c * 12.92, c <= 0.0031308);
|
|
269
245
|
}
|
|
270
246
|
|
|
271
247
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
248
|
+
* "soft" — Aether Gazer's own curve, from its final pass
|
|
249
|
+
* (Hidden/SimPipeline/Final, decompiled): (1 - e^(-exposure x))^contrast, at the
|
|
250
|
+
* 2.5 and 1.4 its scenes set, into a linear target the swapchain then encodes.
|
|
251
|
+
* The game's ACES branch is off in every scene measured.
|
|
252
|
+
*/
|
|
253
|
+
fn softTransform(c: vec3f) -> vec3f {
|
|
254
|
+
let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(1.4));
|
|
255
|
+
return vec3f(srgb_encode(t.r), srgb_encode(t.g), srgb_encode(t.b));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn _rzSrgbEncode3(c: vec3f) -> vec3f {
|
|
259
|
+
return vec3f(srgb_encode(c.r), srgb_encode(c.g), srgb_encode(c.b));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* "neutral" — Unity URP's Neutral tonemapper as the Khronos PBR Neutral
|
|
264
|
+
* operator, to the published formula: below the start of compression colour
|
|
265
|
+
* passes through (less a small toe offset); above it the peak rolls off toward
|
|
266
|
+
* 1 and desaturates in proportion to how much it lost.
|
|
285
267
|
*/
|
|
286
|
-
fn
|
|
287
|
-
let
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
268
|
+
fn neutralTransform(color: vec3f) -> vec3f {
|
|
269
|
+
let startCompression = 0.8 - 0.04;
|
|
270
|
+
let desaturation = 0.15;
|
|
271
|
+
var c = max(color, vec3f(0.0));
|
|
272
|
+
let x = min(c.r, min(c.g, c.b));
|
|
273
|
+
let offset = select(0.04, x - 6.25 * x * x, x < 0.08);
|
|
274
|
+
c = c - vec3f(offset);
|
|
275
|
+
let peak = max(c.r, max(c.g, c.b));
|
|
276
|
+
if (peak >= startCompression) {
|
|
277
|
+
let d = 1.0 - startCompression;
|
|
278
|
+
let newPeak = 1.0 - d * d / (peak + d - startCompression);
|
|
279
|
+
c = c * (newPeak / peak);
|
|
280
|
+
let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
|
|
281
|
+
c = mix(c, vec3f(newPeak), g);
|
|
282
|
+
}
|
|
283
|
+
return _rzSrgbEncode3(clamp(c, vec3f(0.0), vec3f(1.0)));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// "aces" — Unity URP's ACES: unity_to_ACES then AcesTonemap, both from the Core
|
|
287
|
+
// RP's Color.hlsl (the branch URP compiles, not full ACES): the RRT's glow and
|
|
288
|
+
// red modifier and global desaturation, the luminance fit of RRT +
|
|
289
|
+
// ODT.Academy.RGBmonitor_100nits_dim, the dim-surround gamma, the ODT
|
|
290
|
+
// desaturation, then AP1 → XYZ → D60→D65 → Rec.709, saturated as URP's
|
|
291
|
+
// ApplyTonemap does. HLSL's row-major matrices are written as rows here.
|
|
292
|
+
fn _rzRows(r0: vec3f, r1: vec3f, r2: vec3f, v: vec3f) -> vec3f {
|
|
293
|
+
return vec3f(dot(r0, v), dot(r1, v), dot(r2, v));
|
|
294
|
+
}
|
|
295
|
+
fn _rzAp1ToXyz(v: vec3f) -> vec3f {
|
|
296
|
+
return _rzRows(vec3f(0.6624541811, 0.1340042065, 0.1561876870),
|
|
297
|
+
vec3f(0.2722287168, 0.6740817658, 0.0536895174),
|
|
298
|
+
vec3f(-0.0055746495, 0.0040607335, 1.0103391003), v);
|
|
299
|
+
}
|
|
300
|
+
fn _rzAp1Luma(v: vec3f) -> f32 {
|
|
301
|
+
return dot(v, vec3f(0.272229, 0.674082, 0.0536895));
|
|
302
|
+
}
|
|
303
|
+
fn acesTransform(color: vec3f) -> vec3f {
|
|
304
|
+
// unity_to_ACES: linear sRGB → AP0.
|
|
305
|
+
var aces = _rzRows(vec3f(0.4397010, 0.3829780, 0.1773350),
|
|
306
|
+
vec3f(0.0897923, 0.8134230, 0.0967616),
|
|
307
|
+
vec3f(0.0175440, 0.1115440, 0.8707040), max(color, vec3f(0.0)));
|
|
308
|
+
// Glow module.
|
|
309
|
+
let mi = min(aces.r, min(aces.g, aces.b));
|
|
310
|
+
let ma = max(aces.r, max(aces.g, aces.b));
|
|
311
|
+
let saturation = (max(ma, 1e-4) - max(mi, 1e-4)) / max(ma, 1e-2);
|
|
312
|
+
let k = max(aces.b * (aces.b - aces.g) + aces.g * (aces.g - aces.r) + aces.r * (aces.r - aces.b), 0.0);
|
|
313
|
+
let ycIn = (aces.b + aces.g + aces.r + 1.75 * sqrt(k)) / 3.0;
|
|
314
|
+
let sx = (saturation - 0.4) / 0.2;
|
|
315
|
+
let st = max(1.0 - abs(sx / 2.0), 0.0);
|
|
316
|
+
let s = (1.0 + select(-1.0, 1.0, sx >= 0.0) * (1.0 - st * st)) / 2.0;
|
|
317
|
+
let glowGain = 0.05 * s;
|
|
318
|
+
let glowMid = 0.08;
|
|
319
|
+
var glow = glowGain * (glowMid / max(ycIn, 1e-6) - 0.5);
|
|
320
|
+
if (ycIn <= 2.0 / 3.0 * glowMid) { glow = glowGain; }
|
|
321
|
+
if (ycIn >= 2.0 * glowMid) { glow = 0.0; }
|
|
322
|
+
aces = aces * (1.0 + glow);
|
|
323
|
+
// Red modifier.
|
|
324
|
+
var hue = 0.0;
|
|
325
|
+
if (!(aces.r == aces.g && aces.g == aces.b)) {
|
|
326
|
+
hue = degrees(atan2(sqrt(3.0) * (aces.g - aces.b), 2.0 * aces.r - aces.g - aces.b));
|
|
327
|
+
}
|
|
328
|
+
if (hue < 0.0) { hue = hue + 360.0; }
|
|
329
|
+
var centered = hue;
|
|
330
|
+
if (centered < -180.0) { centered = centered + 360.0; } else if (centered > 180.0) { centered = centered - 360.0; }
|
|
331
|
+
var hueWeight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centered / 135.0));
|
|
332
|
+
hueWeight = hueWeight * hueWeight;
|
|
333
|
+
aces.r = aces.r + hueWeight * saturation * (0.03 - aces.r) * (1.0 - 0.82);
|
|
334
|
+
// AP0 → AP1 (ACEScg), then the RRT's global desaturation.
|
|
335
|
+
var cg = max(_rzRows(vec3f(1.4514393161, -0.2365107469, -0.2149285693),
|
|
336
|
+
vec3f(-0.0765537734, 1.1762296998, -0.0996759264),
|
|
337
|
+
vec3f(0.0083161484, -0.0060324498, 0.9977163014), aces), vec3f(0.0));
|
|
338
|
+
cg = mix(vec3f(_rzAp1Luma(cg)), cg, vec3f(0.96));
|
|
339
|
+
// The luminance fit of RRT + ODT.Academy.RGBmonitor_100nits_dim.
|
|
340
|
+
let post = (cg * (2.785085 * cg + 0.107772)) / (cg * (2.936045 * cg + 0.887122) + 0.806889);
|
|
341
|
+
// Dark → dim surround: Y^0.9811, in xyY.
|
|
342
|
+
var xyz = _rzAp1ToXyz(post);
|
|
343
|
+
let div = max(xyz.x + xyz.y + xyz.z, 1e-4);
|
|
344
|
+
let xyY = vec3f(xyz.x / div, xyz.y / div, pow(clamp(xyz.y, 0.0, 65504.0), 0.9811));
|
|
345
|
+
let m = xyY.z / max(xyY.y, 1e-4);
|
|
346
|
+
xyz = vec3f(xyY.x * m, xyY.z, (1.0 - xyY.x - xyY.y) * m);
|
|
347
|
+
var lin = _rzRows(vec3f(1.6410233797, -0.3248032942, -0.2364246952),
|
|
348
|
+
vec3f(-0.6636628587, 1.6153315917, 0.0167563477),
|
|
349
|
+
vec3f(0.0117218943, -0.0082844420, 0.9883948585), xyz);
|
|
350
|
+
// The ODT's desaturation, then to the display's primaries.
|
|
351
|
+
lin = mix(vec3f(_rzAp1Luma(lin)), lin, vec3f(0.93));
|
|
352
|
+
xyz = _rzRows(vec3f(0.98722400, -0.00611327, 0.0159533),
|
|
353
|
+
vec3f(-0.00759836, 1.00186000, 0.0053302),
|
|
354
|
+
vec3f(0.00307257, -0.00509595, 1.0816800), _rzAp1ToXyz(lin));
|
|
355
|
+
let rec709 = _rzRows(vec3f(3.2409699419, -1.5373831776, -0.4986107603),
|
|
356
|
+
vec3f(-0.9692436363, 1.8759675015, 0.0415550574),
|
|
357
|
+
vec3f(0.0556300797, -0.2039769589, 1.0569715142), xyz);
|
|
358
|
+
return _rzSrgbEncode3(clamp(rec709, vec3f(0.0), vec3f(1.0)));
|
|
303
359
|
}
|
|
304
360
|
|
|
305
|
-
/** Which display transform, chosen per frame at viewU[6].y (0
|
|
306
|
-
* A uniform branch rather than a pipeline variant: switching
|
|
307
|
-
*
|
|
361
|
+
/** Which display transform, chosen per frame at viewU[6].y (0 soft, 1 none,
|
|
362
|
+
* 2 neutral, 3 aces). A uniform branch rather than a pipeline variant: switching
|
|
363
|
+
* is rare, and every arm is cheap enough that specialising the pipeline would
|
|
364
|
+
* buy nothing. */
|
|
308
365
|
fn viewTransform(c: vec3f) -> vec3f {
|
|
309
366
|
let mode = viewU[6].y;
|
|
310
|
-
if (mode >
|
|
311
|
-
if (mode >
|
|
312
|
-
|
|
367
|
+
if (mode > 2.5) { return acesTransform(c); }
|
|
368
|
+
if (mode > 1.5) { return neutralTransform(c); }
|
|
369
|
+
if (mode > 0.5) { return _rzSrgbEncode3(c); }
|
|
370
|
+
return softTransform(c);
|
|
313
371
|
}
|
|
314
372
|
|
|
315
373
|
fn _rzSrgbDecode(x: f32) -> f32 {
|
|
@@ -130,10 +130,14 @@ fn cs(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
130
130
|
// a hidden material is not culled, it is switched off, and answering "is this
|
|
131
131
|
// gone because of culling?" requires the two to stay separable.
|
|
132
132
|
let off = frusta.counts.y == 0u;
|
|
133
|
-
|
|
133
|
+
// hidden: bit 0 switched off; bit 1 drawn by the native-material host
|
|
134
|
+
// (unity/host.ts) rather than by its graph — no camera or mirror draw here,
|
|
135
|
+
// and still its shadow, which the engine's pass casts for every material.
|
|
136
|
+
let shown = (hidden[i] & 1u) == 0u;
|
|
137
|
+
let graph = (hidden[i] & 2u) == 0u;
|
|
134
138
|
let castsShadow = (dm.flags & DRAW_CASTS_SHADOW) != 0u;
|
|
135
|
-
cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown);
|
|
139
|
+
cameraArgs[i * 5u + 1u] = select(0u, 1u, (inCamera || off) && shown && graph);
|
|
136
140
|
shadowArgs[i * 5u + 1u] = select(0u, 1u, ((inLight && castsShadow) || off) && shown);
|
|
137
|
-
mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown);
|
|
141
|
+
mirrorArgs[i * 5u + 1u] = select(0u, 1u, (inMirror || off) && shown && graph);
|
|
138
142
|
}
|
|
139
143
|
`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract"
|
|
2
2
|
import { lightsApi } from "../lights"
|
|
3
|
-
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
3
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { SHADOW_MARGIN } from "../scene-light-api"
|
|
4
5
|
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
6
|
|
|
6
7
|
// Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
|
|
@@ -36,6 +37,9 @@ const SOFT_MAX_SPREAD = 14
|
|
|
36
37
|
* normalised, so there is no `/ 9.0` left at the call site.
|
|
37
38
|
*/
|
|
38
39
|
function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string, pad: string, soft: boolean): string {
|
|
40
|
+
// Every tap stays inside the cascade's own tile (tileLo..tileHi, in scope at
|
|
41
|
+
// the call): the soft disk reaches further than the selection margin, and a
|
|
42
|
+
// tap in the neighbouring tile compares against another cascade's depths.
|
|
39
43
|
const golden = 2.39996323
|
|
40
44
|
// ONE BRANCH IS COMPILED, NOT BOTH.
|
|
41
45
|
//
|
|
@@ -58,7 +62,7 @@ function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string,
|
|
|
58
62
|
` for (var x = -1; x <= 1; x++) {`,
|
|
59
63
|
// ...Level, not the implicit-derivative form: identical on a single-mip
|
|
60
64
|
// shadow map, and legal inside a branch.
|
|
61
|
-
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(f32(x), f32(y)) * st, ${z});`,
|
|
65
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(f32(x), f32(y)) * st, tileLo, tileHi), ${z});`,
|
|
62
66
|
` }`,
|
|
63
67
|
`}`,
|
|
64
68
|
`${acc} *= ${1 / 9};`,
|
|
@@ -71,7 +75,7 @@ function pcfWgsl(map: string, uv: string, texel: string, z: string, acc: string,
|
|
|
71
75
|
// keeps successive taps from lining up into spokes.
|
|
72
76
|
` let r = sqrt((fs + 0.5) * ${1 / SOFT_TAPS});`,
|
|
73
77
|
` let a = fs * ${golden} + rot;`,
|
|
74
|
-
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, ${uv} + vec2f(cos(a), sin(a)) * (r * radius), ${z});`,
|
|
78
|
+
` ${acc} += textureSampleCompareLevel(${map}, shadowSampler, clamp(${uv} + vec2f(cos(a), sin(a)) * (r * radius), tileLo, tileHi), ${z});`,
|
|
75
79
|
`}`,
|
|
76
80
|
`${acc} *= ${1 / SOFT_TAPS};`,
|
|
77
81
|
]
|
|
@@ -89,12 +93,10 @@ struct Light { direction: vec4f, color: vec4f, };
|
|
|
89
93
|
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
|
|
90
94
|
struct GroundShadowMat {
|
|
91
95
|
diffuseColor: vec3f, fadeStart: f32,
|
|
92
|
-
fadeEnd: f32, shadowStrength: f32,
|
|
96
|
+
fadeEnd: f32, shadowStrength: f32, _pad6: f32, gridSpacing: f32,
|
|
93
97
|
gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
|
|
94
98
|
gridLineColor: vec3f, mirror: f32,
|
|
95
|
-
|
|
96
|
-
// with no stage the far map is never drawn into, so its taps are known.
|
|
97
|
-
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
|
|
99
|
+
mirrorBlur: f32, _pad17: f32, shadowSoftness: f32, groundY: f32,
|
|
98
100
|
// Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
|
|
99
101
|
// nothing casts, negative means "do not use this" (a rigid caster has no
|
|
100
102
|
// sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
|
|
@@ -105,12 +107,11 @@ struct GroundShadowMat {
|
|
|
105
107
|
struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
106
108
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
107
109
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
108
|
-
|
|
110
|
+
// The sun's shadow atlas — see shadow-cascades.ts.
|
|
111
|
+
@group(0) @binding(2) var shadowAtlas: texture_depth_2d;
|
|
109
112
|
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
110
113
|
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
111
114
|
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
112
|
-
// The far cascade's map, binding 7 as in the materials' layout.
|
|
113
|
-
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
114
115
|
// The floor mirror (step 7D): the reflection target, the camera that rendered
|
|
115
116
|
// it, and an ordinary sampler — the two shadow bindings above are comparison.
|
|
116
117
|
// params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
|
|
@@ -144,7 +145,7 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
|
144
145
|
struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
|
|
145
146
|
@group(0) @binding(14) var<uniform> clip: MirrorClip;
|
|
146
147
|
${WORLD_AMBIENT_WGSL}
|
|
147
|
-
${lightsApi(0, 6)}
|
|
148
|
+
${lightsApi(0, 6, "1u")}
|
|
148
149
|
|
|
149
150
|
struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
|
|
150
151
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
|
|
@@ -178,21 +179,6 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
178
179
|
return out;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
// Outside a cascade's frustum there IS no shadow information — the clamped
|
|
182
|
-
// border samples compare against unrelated depths and read "shadowed",
|
|
183
|
-
// darkening the whole far plane with a visible band at the frustum edge
|
|
184
|
-
// (masked by the opaque surface normally, glaring in green-screen mode where
|
|
185
|
-
// only the shadow-catcher term renders). So each cascade fades out near its
|
|
186
|
-
// border — the near one INTO the far one's answer, the far one into lit.
|
|
187
|
-
let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
|
|
188
|
-
let ndc = l0.xyz / max(l0.w, 1e-6);
|
|
189
|
-
let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
|
|
190
|
-
let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
|
|
191
|
-
let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
|
|
192
|
-
let ndc1 = l1.xyz / max(l1.w, 1e-6);
|
|
193
|
-
let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
|
|
194
|
-
let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
|
|
195
|
-
|
|
196
182
|
// THE cost of this shader, measured: a full-screen ground ran 25 shadow
|
|
197
183
|
// comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
|
|
198
184
|
// samples per pixel. Gutting the rest of the shader changed nothing; cutting
|
|
@@ -201,11 +187,9 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
201
187
|
// 3x3 at two-texel spacing covers the same +/-2 texel penumbra the 5x5 at
|
|
202
188
|
// one-texel spacing did — that kernel was oversampled, its taps overlapping
|
|
203
189
|
// almost entirely once the comparison sampler's own 2x2 filter is counted.
|
|
204
|
-
// Nine taps instead of twenty-five for the same blur radius
|
|
190
|
+
// Nine taps instead of twenty-five for the same blur radius, and one
|
|
191
|
+
// cascade's worth per pixel: the first that holds it.
|
|
205
192
|
//
|
|
206
|
-
// The far cascade's taps run ONLY where the near one is fading or absent
|
|
207
|
-
// (frustum < 1), so a pixel in the near core costs exactly what it did with
|
|
208
|
-
// one cascade — and that core is where the camera usually looks.
|
|
209
193
|
// Can anything cast a shadow ONTO this point at all?
|
|
210
194
|
//
|
|
211
195
|
// The ground is vastly larger than what stands on it, and the answer for most
|
|
@@ -261,33 +245,26 @@ ${
|
|
|
261
245
|
let rot = ign * 6.28318530718;`
|
|
262
246
|
: ""
|
|
263
247
|
}
|
|
264
|
-
// The
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let
|
|
276
|
-
let
|
|
277
|
-
let
|
|
278
|
-
|
|
279
|
-
${pcfWgsl("shadowMapFar", "suv1_c", `${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize}`, "compareZ1", "acc1", " ", soft)}
|
|
280
|
-
vis = mix(1.0, acc1, frustum1);
|
|
281
|
-
}
|
|
282
|
-
if (frustum > 0.0) {
|
|
283
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
284
|
-
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
248
|
+
// The first cascade whose box holds the point, as the materials pick it.
|
|
249
|
+
// Outside a cascade's box there IS no shadow information — the clamped
|
|
250
|
+
// border samples compare against unrelated depths and read "shadowed",
|
|
251
|
+
// darkening the far floor with a band at the edge — so the outermost one
|
|
252
|
+
// fades into lit near its border. Inner cascades hand over to the next.
|
|
253
|
+
let n = ${SHADOW_CASCADES.length}u;
|
|
254
|
+
for (var ci = 0u; ci < n; ci++) {
|
|
255
|
+
let lc = lightVP.viewProj[ci] * vec4f(i.worldPos, 1.0);
|
|
256
|
+
let ndc = lc.xyz / max(lc.w, 1e-6);
|
|
257
|
+
if (!(all(abs(ndc.xy) < vec2f(${SHADOW_MARGIN})) && ndc.z > 0.0 && ndc.z < 1.0)) { continue; }
|
|
258
|
+
let frustum = select(1.0, (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))), ci == n - 1u);
|
|
259
|
+
let tile = vec2f(f32(ci % 2u), f32(ci / 2u)) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
|
|
260
|
+
let tileLo = tile + vec2f(${0.5 / SHADOW_ATLAS_SIZE});
|
|
261
|
+
let tileHi = tile + vec2f(${(SHADOW_CASCADES[0].mapSize - 0.5) / SHADOW_ATLAS_SIZE});
|
|
262
|
+
let suv = tile + vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5) * ${SHADOW_CASCADES[0].mapSize / SHADOW_ATLAS_SIZE};
|
|
285
263
|
let compareZ = ndc.z - 0.0035;
|
|
286
264
|
var acc = 0.0;
|
|
287
|
-
${pcfWgsl("
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
vis = mix(vis, acc, frustum);
|
|
265
|
+
${pcfWgsl("shadowAtlas", "suv", `${1 / SHADOW_ATLAS_SIZE}`, "compareZ", "acc", " ", soft)}
|
|
266
|
+
vis = mix(1.0, acc, frustum);
|
|
267
|
+
break;
|
|
291
268
|
}
|
|
292
269
|
}
|
|
293
270
|
|
|
@@ -218,20 +218,52 @@ ${sceneIdPadWgsl("out")}
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* quad is six generated vertices and the model matrix is the whole of it, while
|
|
225
|
-
* the ordinary shadow shader is built for skinned geometry with an alpha test.
|
|
226
|
-
*
|
|
227
|
-
* Depth only, no fragment stage. The pane is opaque across its whole rectangle
|
|
228
|
-
* — glass included, because this glass is not see-through — so there is nothing
|
|
229
|
-
* to alpha-test and nothing to colour.
|
|
230
|
-
*
|
|
231
|
-
* The cascade index arrives in its own tiny uniform rather than as a dynamic
|
|
232
|
-
* offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
|
|
233
|
-
* uniform offset has to be 256-aligned.
|
|
221
|
+
* Downsample for the reflection's COLOUR chain, one pass per mip level: the
|
|
222
|
+
* Jimenez/COD 13-tap dual box — five weighted 2x2 averages, which rejects the
|
|
223
|
+
* nyquist ringing a plain box would leave in a blurred reflection.
|
|
234
224
|
*/
|
|
225
|
+
export const MIRROR_DOWNSAMPLE_WGSL = /* wgsl */ `
|
|
226
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
227
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
228
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
229
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@group(0) @binding(0) var srcTex: texture_2d<f32>;
|
|
233
|
+
@group(0) @binding(1) var srcSamp: sampler;
|
|
234
|
+
|
|
235
|
+
fn samp(uv: vec2f, off: vec2f) -> vec3f {
|
|
236
|
+
return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
240
|
+
let srcDims = vec2f(textureDimensions(srcTex));
|
|
241
|
+
let t = 1.0 / srcDims;
|
|
242
|
+
// fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
|
|
243
|
+
let dstDims = srcDims * 0.5;
|
|
244
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
245
|
+
let A = samp(uv, t * vec2f(-2.0, -2.0));
|
|
246
|
+
let B = samp(uv, t * vec2f( 0.0, -2.0));
|
|
247
|
+
let C = samp(uv, t * vec2f( 2.0, -2.0));
|
|
248
|
+
let D = samp(uv, t * vec2f(-1.0, -1.0));
|
|
249
|
+
let E = samp(uv, t * vec2f( 1.0, -1.0));
|
|
250
|
+
let F = samp(uv, t * vec2f(-2.0, 0.0));
|
|
251
|
+
let G = samp(uv, t * vec2f( 0.0, 0.0));
|
|
252
|
+
let H = samp(uv, t * vec2f( 2.0, 0.0));
|
|
253
|
+
let I = samp(uv, t * vec2f(-1.0, 1.0));
|
|
254
|
+
let J = samp(uv, t * vec2f( 1.0, 1.0));
|
|
255
|
+
let K = samp(uv, t * vec2f(-2.0, 2.0));
|
|
256
|
+
let L = samp(uv, t * vec2f( 0.0, 2.0));
|
|
257
|
+
let M = samp(uv, t * vec2f( 2.0, 2.0));
|
|
258
|
+
var o = (D + E + I + J) * (0.5 / 4.0);
|
|
259
|
+
o = o + (A + B + G + F) * (0.125 / 4.0);
|
|
260
|
+
o = o + (B + C + H + G) * (0.125 / 4.0);
|
|
261
|
+
o = o + (F + G + L + K) * (0.125 / 4.0);
|
|
262
|
+
o = o + (G + H + M + L) * (0.125 / 4.0);
|
|
263
|
+
return vec4f(o, 1.0);
|
|
264
|
+
}
|
|
265
|
+
`
|
|
266
|
+
|
|
235
267
|
/**
|
|
236
268
|
* Downsample for the reflection's COVERAGE chain.
|
|
237
269
|
*
|
|
@@ -263,6 +295,21 @@ struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
|
|
|
263
295
|
}
|
|
264
296
|
`
|
|
265
297
|
|
|
298
|
+
/**
|
|
299
|
+
* The mirror in the SHADOW pass — the frame throwing shade on the floor.
|
|
300
|
+
*
|
|
301
|
+
* Its own shader because the mirror has no vertex buffer and no skeleton: the
|
|
302
|
+
* quad is six generated vertices and the model matrix is the whole of it, while
|
|
303
|
+
* the ordinary shadow shader is built for skinned geometry with an alpha test.
|
|
304
|
+
*
|
|
305
|
+
* Depth only, no fragment stage. The pane is opaque across its whole rectangle
|
|
306
|
+
* — glass included, because this glass is not see-through — so there is nothing
|
|
307
|
+
* to alpha-test and nothing to colour.
|
|
308
|
+
*
|
|
309
|
+
* The cascade index arrives in its own tiny uniform rather than as a dynamic
|
|
310
|
+
* offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
|
|
311
|
+
* uniform offset has to be 256-aligned.
|
|
312
|
+
*/
|
|
266
313
|
export function mirrorShadowWgsl(cascades: number): string {
|
|
267
314
|
return /* wgsl */ `
|
|
268
315
|
struct LightVP { viewProj: array<mat4x4f, ${cascades}>, };
|
|
@@ -73,14 +73,39 @@ struct VertexOutput {
|
|
|
73
73
|
/** The triangle's own threshold, flat — see the material VertexOutput. The
|
|
74
74
|
* hull traces the body's faces, so it has to lose the same ones. */
|
|
75
75
|
@location(2) @interpolate(flat) faceT: f32,
|
|
76
|
+
/** How much of a pixel-wide line this one really is (1 = all of it): a line
|
|
77
|
+
* held at the minimum width fades by what it was short. */
|
|
78
|
+
@location(3) coverage: f32,
|
|
76
79
|
};
|
|
77
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Half the frame height (engine units, at the subject) below which a line keeps
|
|
83
|
+
* its constant screen width; wider than that it thins as a world-size line.
|
|
84
|
+
* 12.5 = a frame 25 units tall — a full figure (an MMD model is ~20) with
|
|
85
|
+
* room around her.
|
|
86
|
+
*
|
|
87
|
+
* Not the game's own break (w = 1 game unit = 8 engine units). The game's
|
|
88
|
+
* near width is ~3× MMD's 2·edgeSize px; its world-size line comes down to
|
|
89
|
+
* MMD's width only at a full-figure framing, so that is where the break goes:
|
|
90
|
+
* close-ups and full figures keep today's width, and past that the game's rule
|
|
91
|
+
* holds — NDC offset ∝ 1/w, scaled by the zoom as its projection._m11 scales
|
|
92
|
+
* it. At the game's 8 the demo's own camera (33 units) drew a quarter of
|
|
93
|
+
* today's width, which put every line on the faded minimum.
|
|
94
|
+
*/
|
|
95
|
+
const RZ_OUTLINE_FULL_FIGURE = 12.5;
|
|
96
|
+
/** The thinnest a line is drawn, in pixels at 1080p (and never under this many
|
|
97
|
+
* device pixels). A line due to be thinner is drawn this wide and faded by
|
|
98
|
+
* the shortfall, so far lines thin out without aliasing into dashes. */
|
|
99
|
+
const RZ_OUTLINE_MIN_PX = 1.5;
|
|
100
|
+
|
|
78
101
|
@vertex fn vs(
|
|
79
102
|
@location(0) position: vec3f,
|
|
80
103
|
@location(1) normal: vec3f,
|
|
81
104
|
@location(2) uv: vec2f,
|
|
82
105
|
@location(3) joints0: vec4<u32>,
|
|
83
|
-
@location(4) weights0: vec4<f32
|
|
106
|
+
@location(4) weights0: vec4<f32>,
|
|
107
|
+
/** xyz: the smoothed rest normal (outline-normals.ts), w: PMX edge scale. */
|
|
108
|
+
@location(5) outlineNormal: vec4f
|
|
84
109
|
) -> VertexOutput {
|
|
85
110
|
var output: VertexOutput;
|
|
86
111
|
let pos4 = vec4f(position, 1.0);
|
|
@@ -97,30 +122,58 @@ struct VertexOutput {
|
|
|
97
122
|
let m = skinMats[j];
|
|
98
123
|
skinnedPos += (m * pos4) * w;
|
|
99
124
|
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
100
|
-
skinnedNrm += (r3 *
|
|
125
|
+
skinnedNrm += (r3 * outlineNormal.xyz) * w;
|
|
101
126
|
}
|
|
102
127
|
let worldPos = skinnedPos.xyz;
|
|
103
128
|
let worldNormal = normalize(skinnedNrm);
|
|
104
129
|
|
|
105
130
|
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
106
131
|
|
|
107
|
-
//
|
|
132
|
+
// The push direction is the view-space normal's XY, NOT renormalized — the
|
|
133
|
+
// game's: full length at the silhouette, where the normal lies across the
|
|
134
|
+
// view, and shrinking on surfaces turned toward the camera, so creases
|
|
135
|
+
// facing her draw finer than the rim. Skinned exactly as the colour pass
|
|
136
|
+
// skins the normal, from the smoothed rest normal, so every vertex sharing a
|
|
137
|
+
// position pushes the same way and the hull stays closed over hard edges
|
|
138
|
+
// and UV seams.
|
|
108
139
|
let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;
|
|
109
|
-
let snLen = length(viewNormal.xy);
|
|
110
|
-
let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);
|
|
111
140
|
|
|
112
141
|
// Reference-height normalization (babylon-mmd ships this variant commented
|
|
113
|
-
// out as \`renderHeight = 1080\`):
|
|
114
|
-
// frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't
|
|
115
|
-
// the rims to sub-pixel. Width follows the projection aspect.
|
|
142
|
+
// out as \`renderHeight = 1080\`): the near width is a constant FRACTION of
|
|
143
|
+
// the frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't
|
|
144
|
+
// thin the rims to sub-pixel. Width follows the projection aspect.
|
|
116
145
|
// projection[1][1]/projection[0][0] = width/height for a symmetric frustum.
|
|
117
146
|
let aspect = camera.projection[1][1] / camera.projection[0][0];
|
|
118
|
-
let
|
|
147
|
+
let refViewport = vec2f(1080.0 * aspect, 1080.0);
|
|
148
|
+
let deviceViewport = vec2f(camera.viewportHeight * aspect, camera.viewportHeight);
|
|
149
|
+
|
|
150
|
+
// The game's width (Character/Debug.shader, pass "Outline"): the clip offset
|
|
151
|
+
// is the near offset × min(w, break depth). Up close the ×w cancels the
|
|
152
|
+
// perspective divide — constant screen width; past the break it stops
|
|
153
|
+
// growing, so the NDC offset falls as 1/w like a line of fixed world width.
|
|
154
|
+
// The break is the depth at which the frame is a full figure tall, so a
|
|
155
|
+
// zoomed lens moves it out (see RZ_OUTLINE_FULL_FIGURE). Under an
|
|
156
|
+
// orthographic camera w is 1 and the same min() makes the line world-size
|
|
157
|
+
// once the frame is taller than a figure. The PMX per-vertex edge scale
|
|
158
|
+
// multiplies it all, as vertex colour alpha does in the game.
|
|
159
|
+
let w = max(clipPos.w, 1e-4);
|
|
160
|
+
let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
|
|
161
|
+
let nearNdc = viewNormal.xy * (material.edgeSize * outlineNormal.w * 4.0 / refViewport);
|
|
162
|
+
var ndc = nearNdc * (min(w, breakDepth) / w);
|
|
119
163
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
// Antialiased minimum (the game's _OutlineAntialias): held at the minimum
|
|
165
|
+
// width and faded by the shortfall, never thinner. The game fades by
|
|
166
|
+
// sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
|
|
167
|
+
// itself.
|
|
168
|
+
let px = length(ndc * deviceViewport * 0.5);
|
|
169
|
+
let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
|
|
170
|
+
var coverage = 1.0;
|
|
171
|
+
if (px < minPx) {
|
|
172
|
+
coverage = px / minPx;
|
|
173
|
+
ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
|
|
174
|
+
}
|
|
175
|
+
output.coverage = coverage;
|
|
176
|
+
output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
|
|
124
177
|
output.uv = uv;
|
|
125
178
|
output.restPos = position;
|
|
126
179
|
output.faceT = rz_dissolve_threshold(position);
|
|
@@ -155,7 +208,11 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
155
208
|
discard;
|
|
156
209
|
}
|
|
157
210
|
var out: FSOut;
|
|
158
|
-
|
|
211
|
+
// Blended, not alpha-to-coverage: this pass already blends (the texture
|
|
212
|
+
// alpha above), it is drawn right after the surface it traces so what lies
|
|
213
|
+
// under a faded line is already there, and 4× MSAA would quantize a fade to
|
|
214
|
+
// four steps.
|
|
215
|
+
out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA * input.coverage);
|
|
159
216
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
160
217
|
${sceneIdPadWgsl("out")} return out;
|
|
161
218
|
}
|
|
@@ -130,7 +130,7 @@ type ParticleSource = {
|
|
|
130
130
|
textures?: number
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** Where the shading stage's
|
|
133
|
+
/** Where the shading stage's four scene-light bindings start — see scene-light-api.ts. */
|
|
134
134
|
export const PARTICLE_LIGHT_BINDING = 10
|
|
135
135
|
/** The effect's named points (`#points`), in both stages — see points-api.ts. */
|
|
136
136
|
export const PARTICLE_POINTS_BINDING = 15
|