reze-engine 0.64.0 → 0.66.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 +300 -94
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1628 -787
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +5 -0
- package/dist/graph/presets/body.d.ts.map +1 -1
- package/dist/graph/presets/body.js +26 -24
- package/dist/graph/presets/cloth_rough.js +8 -8
- package/dist/graph/presets/cloth_smooth.js +8 -8
- package/dist/graph/presets/default.js +4 -4
- package/dist/graph/presets/eye.js +10 -10
- package/dist/graph/presets/face.d.ts.map +1 -1
- package/dist/graph/presets/face.js +28 -26
- package/dist/graph/presets/hair.js +6 -6
- package/dist/graph/presets/metal.js +8 -8
- package/dist/graph/presets/stockings.d.ts.map +1 -1
- package/dist/graph/presets/stockings.js +4 -11
- package/dist/graph/presets/unlit.d.ts.map +1 -1
- package/dist/graph/presets/unlit.js +9 -13
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +128 -157
- package/dist/graph/render-class.d.ts +5 -2
- package/dist/graph/render-class.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +12 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +23 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +41 -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 +7 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +17 -3
- package/dist/shaders/anchor-table.d.ts +11 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -1
- package/dist/shaders/anchor-table.js +9 -3
- package/dist/shaders/directives.d.ts +42 -5
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +51 -4
- package/dist/shaders/lights.d.ts +11 -7
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +315 -271
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +30 -5
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +191 -271
- 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 +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +121 -77
- package/dist/shaders/passes/ground.js +1 -1
- 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.map +1 -1
- package/dist/shaders/passes/particles.js +9 -3
- package/dist/shaders/passes/probe.d.ts +45 -0
- package/dist/shaders/passes/probe.d.ts.map +1 -0
- package/dist/shaders/passes/probe.js +230 -0
- package/dist/shaders/passes/scene-contract.d.ts +13 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +4 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +1 -0
- package/dist/unity/globals.d.ts +7 -0
- package/dist/unity/globals.d.ts.map +1 -1
- package/dist/unity/globals.js +4 -3
- package/dist/unity/host.d.ts +18 -0
- package/dist/unity/host.d.ts.map +1 -1
- package/dist/unity/host.js +144 -70
- package/dist/unity/looks.d.ts +47 -0
- package/dist/unity/looks.d.ts.map +1 -1
- package/dist/unity/looks.js +150 -6
- package/dist/unity/skin.d.ts.map +1 -1
- package/dist/unity/skin.js +5 -2
- package/dist/unity/stage.d.ts +24 -0
- package/dist/unity/stage.d.ts.map +1 -1
- package/dist/unity/stage.js +59 -7
- package/package.json +1 -1
- package/src/engine.ts +2010 -1033
- package/src/graph/compile.ts +4 -0
- package/src/graph/presets/body.ts +26 -24
- package/src/graph/presets/cloth_rough.ts +8 -8
- package/src/graph/presets/cloth_smooth.ts +8 -8
- package/src/graph/presets/default.ts +4 -4
- package/src/graph/presets/eye.ts +10 -10
- package/src/graph/presets/face.ts +28 -26
- package/src/graph/presets/hair.ts +6 -6
- package/src/graph/presets/metal.ts +8 -8
- package/src/graph/presets/stockings.ts +4 -11
- package/src/graph/presets/unlit.ts +9 -13
- package/src/graph/registry.ts +133 -158
- package/src/graph/render-class.ts +5 -2
- package/src/graph/slots.ts +12 -2
- package/src/index.ts +1 -0
- package/src/model.ts +50 -1
- package/src/outline-normals.ts +110 -0
- package/src/pmx-loader.ts +18 -3
- package/src/shaders/anchor-table.ts +15 -3
- package/src/shaders/directives.ts +88 -6
- package/src/shaders/lights.ts +464 -415
- package/src/shaders/materials/common.ts +30 -5
- package/src/shaders/materials/nodes.ts +191 -271
- package/src/shaders/passes/bloom.ts +59 -87
- package/src/shaders/passes/composite.ts +121 -77
- package/src/shaders/passes/ground.ts +1 -1
- package/src/shaders/passes/mirror.ts +60 -13
- package/src/shaders/passes/outline.ts +71 -14
- package/src/shaders/passes/particles.ts +9 -3
- package/src/shaders/passes/probe.ts +240 -0
- package/src/shaders/passes/scene-contract.ts +17 -0
- package/src/shaders/scene-light-api.ts +1 -0
- package/src/unity/globals.ts +7 -3
- package/src/unity/host.ts +93 -20
- package/src/unity/looks.ts +182 -9
- package/src/unity/skin.ts +5 -2
- package/src/unity/stage.ts +66 -7
- 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/agx-lut.d.ts +0 -11
- package/dist/shaders/agx-lut.d.ts.map +0 -1
- package/dist/shaders/agx-lut.js +0 -29
- package/dist/shaders/dfg_lut.d.ts +0 -3
- package/dist/shaders/dfg_lut.d.ts.map +0 -1
- package/dist/shaders/dfg_lut.js +0 -129
- package/dist/shaders/ltc_mag_lut.d.ts +0 -3
- package/dist/shaders/ltc_mag_lut.d.ts.map +0 -1
- package/dist/shaders/ltc_mag_lut.js +0 -1033
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
- package/src/shaders/agx-lut.ts +0 -30
- package/src/shaders/dfg_lut.ts +0 -131
- package/src/shaders/ltc_mag_lut.ts +0 -1035
|
@@ -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
|
}
|
|
@@ -388,9 +388,15 @@ ${src.orient ? ` // the author's own plane (particleOrient): a card on a surfac
|
|
|
388
388
|
let o = particleOrient(p, ii);
|
|
389
389
|
right = o[0];
|
|
390
390
|
up = o[1];
|
|
391
|
-
` : ""}
|
|
391
|
+
` : ""}${/\bfn\s+particlePivot\s*\(/.test(src.wgsl) ? ` // the card's pivot (particlePivot, in half-size units of the quad): the
|
|
392
|
+
// corners are offset by it BEFORE the turn, so the card turns about its
|
|
393
|
+
// pivot rather than its centre — Unity's renderer pivot, a lighthouse beam
|
|
394
|
+
// whose card starts at the lamp instead of straddling it
|
|
395
|
+
let cp = c + particlePivot(p, ii);
|
|
396
|
+
` : ` let cp = c;
|
|
397
|
+
`} let s = sin(p.rot);
|
|
392
398
|
let k = cos(p.rot);
|
|
393
|
-
var r = vec2f(
|
|
399
|
+
var r = vec2f(cp.x * k - cp.y * s, cp.x * s + cp.y * k);
|
|
394
400
|
// Stretched along the direction of travel ON SCREEN — which is not the world
|
|
395
401
|
// direction once the camera is off-axis. Rain falling straight down is nearly
|
|
396
402
|
// a point when viewed from above and a long streak from the side, and taking
|
|
@@ -402,7 +408,7 @@ ${src.orient ? ` // the author's own plane (particleOrient): a card on a surfac
|
|
|
402
408
|
let vlen = length(vec2f(vr, vu));
|
|
403
409
|
if (vlen > 1e-5) {
|
|
404
410
|
let d = vec2f(vr, vu) / vlen;
|
|
405
|
-
r = vec2f(d.y, -d.x) *
|
|
411
|
+
r = vec2f(d.y, -d.x) * cp.x + d * (cp.y * p.stretch);
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
let world = p.pos + (right * r.x + up * r.y) * p.size;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// The reflection probe's GPU passes, and the sky's prefilter.
|
|
2
|
+
//
|
|
3
|
+
// A game does not reflect the sky alone: it bakes a cube of the stage itself
|
|
4
|
+
// from somewhere in the middle of it, prefilters it down its mips with the
|
|
5
|
+
// specular lobe, and every surface reads it box-projected onto the stage's
|
|
6
|
+
// bounds. The engine does the same once a stage has loaded (Engine:
|
|
7
|
+
// captureReflectionProbe): six renders of the stage into a 128² cube, the
|
|
8
|
+
// sky laid in wherever nothing was drawn, a mip chain, then a GGX convolution
|
|
9
|
+
// into the 64² cube the materials sample (rzProbeSpecular in nodes.ts).
|
|
10
|
+
//
|
|
11
|
+
// The world's sky gets the same convolution down its equirect mips, in place
|
|
12
|
+
// of the box average they were built from.
|
|
13
|
+
|
|
14
|
+
import { NODES_WGSL } from "../materials/nodes"
|
|
15
|
+
import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL } from "../materials/common"
|
|
16
|
+
|
|
17
|
+
/** Face size of the capture, and of the cube the materials read. The game
|
|
18
|
+
* bakes 64²; the capture is twice that so the base level is antialiased. */
|
|
19
|
+
export const PROBE_CAPTURE_SIZE = 128
|
|
20
|
+
export const PROBE_SIZE = 64
|
|
21
|
+
/** 64² down to 1²: seven levels, the game's own count. */
|
|
22
|
+
export const PROBE_LEVELS = 7
|
|
23
|
+
export const PROBE_CAPTURE_LEVELS = 8
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The perceptual roughness level m of the probe was convolved with, on
|
|
27
|
+
* Unity's probe curve: m = r·(1.7 − 0.7r)·6 (UNITY_SPECCUBE_LOD_STEPS), the
|
|
28
|
+
* convention the game's baked probes follow and its lookup (×8, clamped at 6)
|
|
29
|
+
* reads against. Level 0 is the unblurred capture.
|
|
30
|
+
*/
|
|
31
|
+
export function probeLevelRoughness(level: number): number {
|
|
32
|
+
if (level <= 0) return 0
|
|
33
|
+
const m = Math.min(level, 6)
|
|
34
|
+
return Math.min((10.2 - Math.sqrt(Math.max(104.04 - 16.8 * m, 0))) / 8.4, 1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The perceptual roughness level m of the sky's equirect chain was convolved
|
|
39
|
+
* with: the engine's own lookup is lod = sqrt(r)·(levels − 1), so level m is
|
|
40
|
+
* r = (m / (levels − 1))².
|
|
41
|
+
*/
|
|
42
|
+
export function skyLevelRoughness(level: number, levels: number): number {
|
|
43
|
+
if (level <= 0 || levels <= 1) return 0
|
|
44
|
+
const t = level / (levels - 1)
|
|
45
|
+
return Math.min(t * t, 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A cube face's direction for a uv on it (uv.y down), in the convention the
|
|
49
|
+
* GPU samples a cube with — the same one the capture cameras render. */
|
|
50
|
+
const FACE_DIR_WGSL = /* wgsl */ `
|
|
51
|
+
fn rz_face_dir(face: u32, uv: vec2f) -> vec3f {
|
|
52
|
+
let s = uv.x * 2.0 - 1.0;
|
|
53
|
+
let t = uv.y * 2.0 - 1.0;
|
|
54
|
+
switch face {
|
|
55
|
+
case 0u: { return normalize(vec3f(1.0, -t, -s)); }
|
|
56
|
+
case 1u: { return normalize(vec3f(-1.0, -t, s)); }
|
|
57
|
+
case 2u: { return normalize(vec3f(s, 1.0, t)); }
|
|
58
|
+
case 3u: { return normalize(vec3f(s, -1.0, -t)); }
|
|
59
|
+
case 4u: { return normalize(vec3f(s, -t, 1.0)); }
|
|
60
|
+
default: { return normalize(vec3f(-s, -t, -1.0)); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
struct RzFullOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
|
|
65
|
+
|
|
66
|
+
@vertex fn vs_full(@builtin(vertex_index) i: u32) -> RzFullOut {
|
|
67
|
+
var o: RzFullOut;
|
|
68
|
+
let x = f32(i32(i / 2u) * 4 - 1);
|
|
69
|
+
let y = f32(i32(i % 2u) * 4 - 1);
|
|
70
|
+
o.pos = vec4f(x, y, 0.0, 1.0);
|
|
71
|
+
o.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
|
|
72
|
+
return o;
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
/** GGX importance sampling, the filtered kind (Křivánek & Colbert): each
|
|
77
|
+
* sample reads the source at the mip whose texel matches its solid angle, so
|
|
78
|
+
* a few dozen taps integrate a wide lobe without the fireflies a point sample
|
|
79
|
+
* of a bright lamp would leave. */
|
|
80
|
+
const GGX_SAMPLING_WGSL = /* wgsl */ `
|
|
81
|
+
fn rz_hammersley(i: u32, n: u32) -> vec2f {
|
|
82
|
+
var b = (i << 16u) | (i >> 16u);
|
|
83
|
+
b = ((b & 0x55555555u) << 1u) | ((b & 0xAAAAAAAAu) >> 1u);
|
|
84
|
+
b = ((b & 0x33333333u) << 2u) | ((b & 0xCCCCCCCCu) >> 2u);
|
|
85
|
+
b = ((b & 0x0F0F0F0Fu) << 4u) | ((b & 0xF0F0F0F0u) >> 4u);
|
|
86
|
+
b = ((b & 0x00FF00FFu) << 8u) | ((b & 0xFF00FF00u) >> 8u);
|
|
87
|
+
return vec2f(f32(i) / f32(n), f32(b) * 2.3283064365386963e-10);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A half vector about N for the lobe at alpha = a (= perceptual r²). */
|
|
91
|
+
fn rz_ggx_half(xi: vec2f, N: vec3f, a: f32) -> vec3f {
|
|
92
|
+
let phi = 6.283185307 * xi.x;
|
|
93
|
+
let cosT = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
|
|
94
|
+
let sinT = sqrt(max(1.0 - cosT * cosT, 0.0));
|
|
95
|
+
let h = vec3f(sinT * cos(phi), sinT * sin(phi), cosT);
|
|
96
|
+
let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(N.z) < 0.999);
|
|
97
|
+
let tx = normalize(cross(up, N));
|
|
98
|
+
let ty = cross(N, tx);
|
|
99
|
+
return normalize(tx * h.x + ty * h.y + N * h.z);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn rz_ggx_d(nh: f32, a: f32) -> f32 {
|
|
103
|
+
let a2 = a * a;
|
|
104
|
+
let d = nh * nh * (a2 - 1.0) + 1.0;
|
|
105
|
+
return a2 / (3.141592653589793 * d * d);
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The compose step: the stage as captured, with the world's sky laid in
|
|
111
|
+
* behind it wherever the capture's coverage says nothing was drawn — an open
|
|
112
|
+
* stage reflects its sky, an interior its walls. Built on the material
|
|
113
|
+
* prelude for rzWorldSpecularLod, which knows every kind of sky the scene may
|
|
114
|
+
* have (an HDRI, a gradient, one colour). Only the bindings it reads end up in
|
|
115
|
+
* its layout ("auto"): the light uniform, the sampler and the sky at group 0,
|
|
116
|
+
* and its own three past the material set's.
|
|
117
|
+
*/
|
|
118
|
+
export const PROBE_COMPOSE_WGSL =
|
|
119
|
+
NODES_WGSL +
|
|
120
|
+
COMMON_MATERIAL_PRELUDE_WGSL +
|
|
121
|
+
DISSOLVE_WGSL +
|
|
122
|
+
FACE_DIR_WGSL +
|
|
123
|
+
/* wgsl */ `
|
|
124
|
+
struct RzProbeStep { face: u32, lod: f32, roughness: f32, srcSize: f32 };
|
|
125
|
+
@group(0) @binding(20) var rzCaptureColor: texture_2d_array<f32>;
|
|
126
|
+
@group(0) @binding(21) var rzCaptureAux: texture_2d_array<f32>;
|
|
127
|
+
@group(0) @binding(22) var<uniform> rzStep: RzProbeStep;
|
|
128
|
+
|
|
129
|
+
@fragment fn fs_compose(in: RzFullOut) -> @location(0) vec4f {
|
|
130
|
+
let size = textureDimensions(rzCaptureColor);
|
|
131
|
+
let px = vec2u(min(in.pos.xy, vec2f(size) - vec2f(1.0)));
|
|
132
|
+
let c = textureLoad(rzCaptureColor, px, rzStep.face, 0).rgb;
|
|
133
|
+
let cover = saturate(textureLoad(rzCaptureAux, px, rzStep.face, 0).g);
|
|
134
|
+
let sky = rzWorldSpecularLod(rz_face_dir(rzStep.face, in.uv), 0.0, 0.0);
|
|
135
|
+
return vec4f(c + sky * (1.0 - cover), 1.0);
|
|
136
|
+
}
|
|
137
|
+
`
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The cube's own passes: a mip from the level above (mode 0 — one bilinear
|
|
141
|
+
* tap at the child texel's direction is the 2×2 average), and the GGX
|
|
142
|
+
* convolution into the probe the materials read (mode 1).
|
|
143
|
+
*/
|
|
144
|
+
export const PROBE_FILTER_WGSL =
|
|
145
|
+
FACE_DIR_WGSL +
|
|
146
|
+
GGX_SAMPLING_WGSL +
|
|
147
|
+
/* wgsl */ `
|
|
148
|
+
struct RzProbeStep { face: u32, lod: f32, roughness: f32, srcSize: f32, mode: u32 };
|
|
149
|
+
@group(0) @binding(0) var rzSrc: texture_cube<f32>;
|
|
150
|
+
@group(0) @binding(1) var rzSamp: sampler;
|
|
151
|
+
@group(0) @binding(2) var<uniform> rzStep: RzProbeStep;
|
|
152
|
+
|
|
153
|
+
const RZ_PROBE_SAMPLES: u32 = 128u;
|
|
154
|
+
|
|
155
|
+
@fragment fn fs_filter(in: RzFullOut) -> @location(0) vec4f {
|
|
156
|
+
let N = rz_face_dir(rzStep.face, in.uv);
|
|
157
|
+
let r = rzStep.roughness;
|
|
158
|
+
if (rzStep.mode == 0u || r < 0.002) {
|
|
159
|
+
return vec4f(textureSampleLevel(rzSrc, rzSamp, N, rzStep.lod).rgb, 1.0);
|
|
160
|
+
}
|
|
161
|
+
let a = r * r;
|
|
162
|
+
let texelSolid = 4.0 * 3.141592653589793 / (6.0 * rzStep.srcSize * rzStep.srcSize);
|
|
163
|
+
let maxLod = f32(textureNumLevels(rzSrc) - 1u);
|
|
164
|
+
var sum = vec3f(0.0);
|
|
165
|
+
var weight = 0.0;
|
|
166
|
+
for (var i = 0u; i < RZ_PROBE_SAMPLES; i++) {
|
|
167
|
+
let H = rz_ggx_half(rz_hammersley(i, RZ_PROBE_SAMPLES), N, a);
|
|
168
|
+
let L = 2.0 * dot(N, H) * H - N;
|
|
169
|
+
let nl = dot(N, L);
|
|
170
|
+
if (nl <= 0.0) { continue; }
|
|
171
|
+
// N = V: the pdf over L is D·(N·H)/(4·V·H) = D/4.
|
|
172
|
+
let pdf = rz_ggx_d(saturate(dot(N, H)), a) * 0.25;
|
|
173
|
+
let sampleSolid = 1.0 / (f32(RZ_PROBE_SAMPLES) * pdf + 1e-6);
|
|
174
|
+
let lod = clamp(0.5 * log2(sampleSolid / texelSolid) + 1.0, 0.0, maxLod);
|
|
175
|
+
sum += textureSampleLevel(rzSrc, rzSamp, L, lod).rgb * nl;
|
|
176
|
+
weight += nl;
|
|
177
|
+
}
|
|
178
|
+
return vec4f(sum / max(weight, 1e-4), 1.0);
|
|
179
|
+
}
|
|
180
|
+
`
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The sky's mips, convolved: level m of the equirect the materials read
|
|
184
|
+
* (rzWorldSpecularLod) becomes the GGX-filtered sky at the roughness that
|
|
185
|
+
* level stands for, sampled from the box-averaged chain the upload built.
|
|
186
|
+
* Directions follow the composite's dome: u = 0.5 + atan2(x, z)/2π,
|
|
187
|
+
* v = 0.5 − asin(y)/π.
|
|
188
|
+
*/
|
|
189
|
+
export const SKY_PREFILTER_WGSL =
|
|
190
|
+
GGX_SAMPLING_WGSL +
|
|
191
|
+
/* wgsl */ `
|
|
192
|
+
struct RzFullOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
|
|
193
|
+
|
|
194
|
+
@vertex fn vs_full(@builtin(vertex_index) i: u32) -> RzFullOut {
|
|
195
|
+
var o: RzFullOut;
|
|
196
|
+
let x = f32(i32(i / 2u) * 4 - 1);
|
|
197
|
+
let y = f32(i32(i % 2u) * 4 - 1);
|
|
198
|
+
o.pos = vec4f(x, y, 0.0, 1.0);
|
|
199
|
+
o.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
|
|
200
|
+
return o;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
struct RzSkyStep { roughness: f32, srcWidth: f32, srcHeight: f32, spare: f32 };
|
|
204
|
+
@group(0) @binding(0) var rzSky: texture_2d<f32>;
|
|
205
|
+
@group(0) @binding(1) var rzSamp: sampler;
|
|
206
|
+
@group(0) @binding(2) var<uniform> rzStep: RzSkyStep;
|
|
207
|
+
|
|
208
|
+
const RZ_SKY_SAMPLES: u32 = 64u;
|
|
209
|
+
|
|
210
|
+
fn rz_sky_dir(uv: vec2f) -> vec3f {
|
|
211
|
+
let phi = (uv.x - 0.5) * 6.283185307;
|
|
212
|
+
let lat = (0.5 - uv.y) * 3.141592653589793;
|
|
213
|
+
return vec3f(cos(lat) * sin(phi), sin(lat), cos(lat) * cos(phi));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn rz_sky_uv(d: vec3f) -> vec2f {
|
|
217
|
+
return vec2f(0.5 + atan2(d.x, d.z) * 0.15915494309, 0.5 - asin(clamp(d.y, -1.0, 1.0)) * 0.31830988618);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@fragment fn fs_sky(in: RzFullOut) -> @location(0) vec4f {
|
|
221
|
+
let N = rz_sky_dir(in.uv);
|
|
222
|
+
let a = rzStep.roughness * rzStep.roughness;
|
|
223
|
+
let texelSolid = 4.0 * 3.141592653589793 / (rzStep.srcWidth * rzStep.srcHeight);
|
|
224
|
+
let maxLod = f32(textureNumLevels(rzSky) - 1u);
|
|
225
|
+
var sum = vec3f(0.0);
|
|
226
|
+
var weight = 0.0;
|
|
227
|
+
for (var i = 0u; i < RZ_SKY_SAMPLES; i++) {
|
|
228
|
+
let H = rz_ggx_half(rz_hammersley(i, RZ_SKY_SAMPLES), N, a);
|
|
229
|
+
let L = 2.0 * dot(N, H) * H - N;
|
|
230
|
+
let nl = dot(N, L);
|
|
231
|
+
if (nl <= 0.0) { continue; }
|
|
232
|
+
let pdf = rz_ggx_d(saturate(dot(N, H)), a) * 0.25;
|
|
233
|
+
let sampleSolid = 1.0 / (f32(RZ_SKY_SAMPLES) * pdf + 1e-6);
|
|
234
|
+
let lod = clamp(0.5 * log2(sampleSolid / texelSolid) + 1.0, 0.0, maxLod);
|
|
235
|
+
sum += textureSampleLevel(rzSky, rzSamp, rz_sky_uv(L), lod).rgb * nl;
|
|
236
|
+
weight += nl;
|
|
237
|
+
}
|
|
238
|
+
return vec4f(sum / max(weight, 1e-4), 1.0);
|
|
239
|
+
}
|
|
240
|
+
`
|
|
@@ -98,6 +98,12 @@ type SceneRenderClass =
|
|
|
98
98
|
* X309's night sky was. Out of WRITES_ID for the mirror's reason — a sheet of
|
|
99
99
|
* light is not an object anything should be able to pick or outline. */
|
|
100
100
|
| "material-additive"
|
|
101
|
+
/** A material whose colour arrives ALREADY weighted by its coverage — a
|
|
102
|
+
* game's glass (Blend One OneMinusSrcAlpha): the pane's diffuse fades with
|
|
103
|
+
* its alpha while its reflection does not, which a src-alpha blend cannot
|
|
104
|
+
* express. Out of WRITES_ID for the mirror's reason: a window is not what a
|
|
105
|
+
* click on the figure behind it means. */
|
|
106
|
+
| "material-premultiplied"
|
|
101
107
|
/** Particles and ribbons in their default, non-additive mode. */
|
|
102
108
|
| "particle"
|
|
103
109
|
/** Particles declaring `#blend additive` — LIGHT rather than matter, so
|
|
@@ -110,6 +116,13 @@ type SceneRenderClass =
|
|
|
110
116
|
* whole, and an ordinary alpha-over aux so a ribbon's mask does not
|
|
111
117
|
* saturate along every overlap. */
|
|
112
118
|
| "trail"
|
|
119
|
+
/** Ribbons declaring `#blend over` — MATTER rather than light: laid over
|
|
120
|
+
* what is behind them by their own alpha, the way a game's trail material
|
|
121
|
+
* with Blend One OneMinusSrcAlpha draws. A dark band then darkens, which an
|
|
122
|
+
* additive ribbon can never do; it is how a deep-blue smoke tail reads as a
|
|
123
|
+
* tail at all. The fragment writes straight colour, so the colour blend is
|
|
124
|
+
* src-alpha like any other alpha-over class. */
|
|
125
|
+
| "trail-over"
|
|
113
126
|
/** The transparent depth prepass: it exists to write DEPTH after the fabric's
|
|
114
127
|
* colour blended, so an outline drawn later is occluded behind it. It must
|
|
115
128
|
* therefore write no colour at all — the targets exist only to make the
|
|
@@ -183,6 +196,9 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
|
|
|
183
196
|
// The same pair the additive particles use: light into the colour target, and
|
|
184
197
|
// coverage that still accumulates so bloom and the composite can see it.
|
|
185
198
|
"material-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
199
|
+
// The colour premultiplied by the graph (game_pbr's glass); the aux is
|
|
200
|
+
// written straight, as every material's is.
|
|
201
|
+
"material-premultiplied": [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
186
202
|
// PREMULTIPLIED, like the ground and for the same reason: what a mirror
|
|
187
203
|
// writes is a sample of the HDR target, and that target already holds colour
|
|
188
204
|
// premultiplied by its own alpha. Handed to the src-alpha blend it would be
|
|
@@ -201,6 +217,7 @@ const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState,
|
|
|
201
217
|
particle: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
202
218
|
"particle-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
203
219
|
trail: [ADD_PREMULTIPLIED, ALPHA_OVER],
|
|
220
|
+
"trail-over": [ALPHA_OVER, ALPHA_OVER],
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
/**
|
|
@@ -131,6 +131,7 @@ export function sceneLightApi(on: boolean, group: number, binding: number): stri
|
|
|
131
131
|
return /* wgsl */ `
|
|
132
132
|
fn rzShadow(p: vec3f) -> f32 { return 1.0; }
|
|
133
133
|
fn rzWorldAmbient(n: vec3f) -> vec3f { return vec3f(0.0); }
|
|
134
|
+
fn rzWorldAmbientAvg() -> vec3f { return vec3f(0.0); }
|
|
134
135
|
`
|
|
135
136
|
}
|
|
136
137
|
const n = SHADOW_CASCADES.length
|
package/src/unity/globals.ts
CHANGED
|
@@ -71,6 +71,9 @@ export type UnityFrameInput = {
|
|
|
71
71
|
/** The direction the main light travels, unit; null for none. */
|
|
72
72
|
sunDirection: [number, number, number] | null
|
|
73
73
|
sunColor: [number, number, number]
|
|
74
|
+
/** A game stage's own main light as its pipeline packed it (recorded), which
|
|
75
|
+
* its shaders get in place of sunColor; null for the scene sun's. */
|
|
76
|
+
sunUnity?: { color: number[]; simColor: number[]; simColorNoInt: number[] } | null
|
|
74
77
|
sunShadow: number
|
|
75
78
|
lights: UnityLight[]
|
|
76
79
|
/** Ambient as the engine's folded SH9 (27 floats, ibl.ts), or null for flat. */
|
|
@@ -257,12 +260,13 @@ export function unityFrameGlobals(f: UnityFrameInput): Record<string, NativeValu
|
|
|
257
260
|
if (f.sunDirection) {
|
|
258
261
|
const d = gameDir(f.sunDirection)
|
|
259
262
|
g.SimMainLightDir = [-d[0], -d[1], -d[2], 0]
|
|
260
|
-
|
|
261
|
-
g.
|
|
263
|
+
const u = f.sunUnity
|
|
264
|
+
g.SimMainLightColor = u ? u.simColor.slice(0, 4) : [...f.sunColor, 1]
|
|
265
|
+
g.SimMainLightColorNoInt = u ? u.simColorNoInt.slice(0, 4) : [...f.sunColor, 1]
|
|
262
266
|
// As the game's pipeline leaves it (recorded): the way the light travels.
|
|
263
267
|
g._MainLightPosition = [d[0], d[1], d[2], 0]
|
|
264
268
|
g.sim_ShadowLightDirection = [-d[0], -d[1], -d[2], 0]
|
|
265
|
-
g._MainLightColor = [...f.sunColor, 1]
|
|
269
|
+
g._MainLightColor = u ? u.color.slice(0, 4) : [...f.sunColor, 1]
|
|
266
270
|
} else {
|
|
267
271
|
g.SimMainLightDir = [0, 0, 0, 0]
|
|
268
272
|
g.SimMainLightColor = [0, 0, 0, 0]
|
package/src/unity/host.ts
CHANGED
|
@@ -54,6 +54,21 @@ export type NativeValue = number | ArrayLike<number> | ArrayLike<ArrayLike<numbe
|
|
|
54
54
|
/** Name -> value; the first source that has a name wins. */
|
|
55
55
|
export type ValueSource = Record<string, NativeValue | undefined>
|
|
56
56
|
|
|
57
|
+
/** Whether two values carry the same numbers (and the same integer-ness). */
|
|
58
|
+
export function sameValue(a: NativeValue, b: NativeValue): boolean {
|
|
59
|
+
if (typeof a === "number" || typeof b === "number") return a === b
|
|
60
|
+
if (a.length !== b.length) return false
|
|
61
|
+
if (a instanceof Uint32Array !== b instanceof Uint32Array) return false
|
|
62
|
+
for (let i = 0; i < a.length; i++) {
|
|
63
|
+
const x = a[i] as number | ArrayLike<number>
|
|
64
|
+
const y = b[i] as number | ArrayLike<number>
|
|
65
|
+
if (typeof x === "number" || typeof y === "number") {
|
|
66
|
+
if (x !== y) return false
|
|
67
|
+
} else if (!sameValue(x, y)) return false
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
/** One vertex stream a draw binds, by the semantic a shader input names. */
|
|
58
73
|
export type NativeStream = {
|
|
59
74
|
buffer: GPUBuffer
|
|
@@ -87,14 +102,25 @@ export function wrapFragment(frag: string, ids: boolean): string {
|
|
|
87
102
|
const at = frag.lastIndexOf("@fragment")
|
|
88
103
|
if (at < 0) throw new Error("native shader: no fragment entry point")
|
|
89
104
|
const head = frag.slice(at)
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
// A target narrower than four channels (a float3 SV_Target) leaves the rest
|
|
106
|
+
// unwritten: widened here with zeros and an alpha of one.
|
|
107
|
+
const sig = /->\s*@location\(0\)\s*(vec4<f32>|vec3<f32>|vec2<f32>|f32)\s*\{/
|
|
108
|
+
const kind = sig.exec(head)?.[1]
|
|
109
|
+
if (!kind) throw new Error("native shader: the fragment entry does not return one float target")
|
|
92
110
|
let body = head.replace(sig, "-> RzNativeOut {")
|
|
93
111
|
const ret = /return\s+(_e\d+)\s*;\s*\}\s*$/
|
|
94
112
|
const m = ret.exec(body)
|
|
95
113
|
if (!m) throw new Error("native shader: the fragment entry's return is not the translator's")
|
|
96
|
-
const
|
|
97
|
-
|
|
114
|
+
const e = m[1]
|
|
115
|
+
const v =
|
|
116
|
+
kind === "vec4<f32>"
|
|
117
|
+
? e
|
|
118
|
+
: kind === "vec3<f32>"
|
|
119
|
+
? `vec4f(${e}, 1.0)`
|
|
120
|
+
: kind === "vec2<f32>"
|
|
121
|
+
? `vec4f(${e}, 0.0, 1.0)`
|
|
122
|
+
: `vec4f(${e}, 0.0, 0.0, 1.0)`
|
|
123
|
+
body = body.replace(ret, `let rzColor = ${v};\n return RzNativeOut(rzColor, vec4f(1.0, 1.0, 0.0, rzColor.a)${ids ? ", vec2u(0u)" : ""});\n}\n`)
|
|
98
124
|
const struct = `struct RzNativeOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n${ids ? " @location(2) id: vec2u,\n" : ""}};\n`
|
|
99
125
|
// After the module's directives (diagnostic, enable): WGSL wants them first.
|
|
100
126
|
const pre = frag.slice(0, at)
|
|
@@ -173,6 +199,8 @@ export type NativeDraw = {
|
|
|
173
199
|
export class NativeHost {
|
|
174
200
|
private prepared = new Map<string, Prepared>()
|
|
175
201
|
private pipelines = new Map<string, { pipeline: GPURenderPipeline; stencilRef: number } | null>()
|
|
202
|
+
/** Pipelines being compiled (see warm). */
|
|
203
|
+
private pending = new Map<string, Promise<void>>()
|
|
176
204
|
private draws = new Map<string, DrawCache>()
|
|
177
205
|
private shared = new Map<string, Block>()
|
|
178
206
|
private frame = 0
|
|
@@ -188,6 +216,7 @@ export class NativeHost {
|
|
|
188
216
|
white: GPUTextureView
|
|
189
217
|
black: GPUTextureView
|
|
190
218
|
blackCube: GPUTextureView
|
|
219
|
+
emptyVolume: GPUTextureView
|
|
191
220
|
depth: GPUTextureView
|
|
192
221
|
comparison: GPUSampler
|
|
193
222
|
},
|
|
@@ -281,9 +310,58 @@ export class NativeHost {
|
|
|
281
310
|
: []
|
|
282
311
|
}
|
|
283
312
|
|
|
313
|
+
private pipelineKey(p: Prepared, state: PassState, streams: NativeStream[]): string {
|
|
314
|
+
return `${p.shader.name}|${JSON.stringify(state)}|${streams.map((s) => `${s.format}/${s.stride}`).join(",")}`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Compile a draw's pipeline ahead of the draw — what a stage or a look calls
|
|
319
|
+
* for every pass it will draw, the moment it knows the shader, the state and
|
|
320
|
+
* the streams. Async: a synchronous build holds the GPU process for the
|
|
321
|
+
* whole compile, which was a one-to-two-second frozen frame at the first
|
|
322
|
+
* draw of each new shader. Resolves when the pipeline is ready or has
|
|
323
|
+
* failed (into `errors`); never rejects.
|
|
324
|
+
*/
|
|
325
|
+
warm(shader: string, state: PassState, streams: NativeStream[]): Promise<void> {
|
|
326
|
+
const p = this.prepared.get(shader)
|
|
327
|
+
if (!p) return Promise.resolve()
|
|
328
|
+
const key = this.pipelineKey(p, state, streams)
|
|
329
|
+
if (this.pipelines.has(key)) return Promise.resolve()
|
|
330
|
+
let job = this.pending.get(key)
|
|
331
|
+
if (job) return job
|
|
332
|
+
let built: { desc: GPURenderPipelineDescriptor; stencilRef: number }
|
|
333
|
+
try {
|
|
334
|
+
built = this.describe(p, state, streams)
|
|
335
|
+
} catch (e) {
|
|
336
|
+
this.errors.push(`pipeline ${p.shader.name}: ${(e as Error).message}`)
|
|
337
|
+
this.pipelines.set(key, null)
|
|
338
|
+
return Promise.resolve()
|
|
339
|
+
}
|
|
340
|
+
job = this.device.createRenderPipelineAsync(built.desc).then(
|
|
341
|
+
(pipeline) => {
|
|
342
|
+
this.pipelines.set(key, { pipeline, stencilRef: built.stencilRef })
|
|
343
|
+
this.pending.delete(key)
|
|
344
|
+
},
|
|
345
|
+
(e) => {
|
|
346
|
+
this.errors.push(`pipeline ${p.shader.name}: ${(e as Error).message}`)
|
|
347
|
+
this.pipelines.set(key, null)
|
|
348
|
+
this.pending.delete(key)
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
this.pending.set(key, job)
|
|
352
|
+
return job
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** The draw's pipeline if it is ready. Never built here: one that is not
|
|
356
|
+
* ready is started and the draw sits this frame out. */
|
|
284
357
|
private pipeline(p: Prepared, state: PassState, streams: NativeStream[]) {
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
358
|
+
const ready = this.pipelines.get(this.pipelineKey(p, state, streams))
|
|
359
|
+
if (ready !== undefined) return ready
|
|
360
|
+
void this.warm(p.shader.name, state, streams)
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private describe(p: Prepared, state: PassState, streams: NativeStream[]): { desc: GPURenderPipelineDescriptor; stencilRef: number } {
|
|
287
365
|
const t = this.targets
|
|
288
366
|
const stencil = stencilState(state)
|
|
289
367
|
const blend = blendState(state)
|
|
@@ -317,10 +395,8 @@ export class NativeHost {
|
|
|
317
395
|
]
|
|
318
396
|
if (t.colorFormats[2]) targets.push({ format: t.colorFormats[2], writeMask: 0 })
|
|
319
397
|
const offset = state.Offset
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
result = {
|
|
323
|
-
pipeline: this.device.createRenderPipeline({
|
|
398
|
+
return {
|
|
399
|
+
desc: {
|
|
324
400
|
label: `${p.shader.name} pipeline`,
|
|
325
401
|
layout: p.layout,
|
|
326
402
|
vertex: {
|
|
@@ -367,15 +443,9 @@ export class NativeHost {
|
|
|
367
443
|
: {}),
|
|
368
444
|
},
|
|
369
445
|
multisample: { count: t.sampleCount },
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
} catch (e) {
|
|
374
|
-
this.errors.push(`pipeline ${p.shader.name}: ${(e as Error).message}`)
|
|
375
|
-
result = null
|
|
446
|
+
},
|
|
447
|
+
stencilRef: stencil?.ref ?? 0,
|
|
376
448
|
}
|
|
377
|
-
this.pipelines.set(key, result)
|
|
378
|
-
return result
|
|
379
449
|
}
|
|
380
450
|
|
|
381
451
|
private lookup(name: string, sources: ValueSource[]): NativeValue | undefined {
|
|
@@ -555,14 +625,17 @@ export class NativeHost {
|
|
|
555
625
|
if (!samplers) return this.sampler
|
|
556
626
|
const bare = name.replace(/^sampler_?/, "")
|
|
557
627
|
const tex = info.bindings[bare] ? bare : info.bindings[`_${bare}`] ? `_${bare}` : bare
|
|
558
|
-
return samplers[tex] ?? this.sampler
|
|
628
|
+
return samplers[tex] ?? samplers[tex.replace(/_+$/, "")] ?? this.sampler
|
|
559
629
|
}
|
|
560
630
|
|
|
561
631
|
private textureView(name: string, type: string, textures: Record<string, GPUTextureView | undefined>): GPUTextureView {
|
|
562
|
-
|
|
632
|
+
// naga renames a binding that ends in a digit with a trailing "_", as it
|
|
633
|
+
// does a member (lookupFrom): _AlbedoTex_1 is declared _AlbedoTex_1_
|
|
634
|
+
const v = textures[name] ?? (name.endsWith("_") ? textures[name.replace(/_+$/, "")] : undefined)
|
|
563
635
|
if (v) return v
|
|
564
636
|
if (/texture_depth/.test(type)) return this.fallback.depth
|
|
565
637
|
if (/texture_cube/.test(type)) return this.fallback.blackCube
|
|
638
|
+
if (/texture_3d/.test(type)) return this.fallback.emptyVolume
|
|
566
639
|
return this.fallback.white
|
|
567
640
|
}
|
|
568
641
|
|