reze-engine 0.25.1 → 0.26.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/README.md +4 -2
- package/dist/engine.d.ts +49 -18
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +190 -104
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +46 -4
- package/dist/shaders/passes/outline.d.ts +1 -1
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +50 -23
- package/package.json +1 -1
- package/src/engine.ts +214 -106
- package/src/index.ts +2 -0
- package/src/shaders/passes/composite.ts +51 -4
- package/src/shaders/passes/outline.ts +50 -23
|
@@ -39,7 +39,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
39
39
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
40
40
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
41
41
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
42
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
42
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 10>;
|
|
43
43
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
44
44
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
45
45
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -62,6 +62,8 @@ override APPLY_GAMMA: bool = true;
|
|
|
62
62
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
63
63
|
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
64
64
|
// viewU[6] = (time seconds, effect on/off, canvas width, canvas height).
|
|
65
|
+
// viewU[7] = (grade offset.rgb, contrast); viewU[8] = (grade power.rgb, saturation);
|
|
66
|
+
// viewU[9] = (grade slope.rgb, grade on/off) — see grade() below.
|
|
65
67
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
66
68
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
67
69
|
|
|
@@ -81,6 +83,24 @@ fn filmic(x: f32) -> f32 {
|
|
|
81
83
|
|
|
82
84
|
/** Canvas size in pixels — for user background effects (aspect correction). */
|
|
83
85
|
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
86
|
+
|
|
87
|
+
/** Color grading, applied to the tonemapped SCENE (not the background — see the
|
|
88
|
+
* call site). The core is ASC CDL, the film-industry interchange standard:
|
|
89
|
+
*
|
|
90
|
+
* out = (in · slope + offset) ^ power then saturation (SOP → SAT)
|
|
91
|
+
*
|
|
92
|
+
* Using the real standard rather than invented controls means a look authored
|
|
93
|
+
* here maps onto any grading tool. slope/offset/power are derived on the CPU
|
|
94
|
+
* from the UI's shadow/midtone/highlight colors (see setColorGrading), so the
|
|
95
|
+
* per-pixel cost is one mul-add, one pow, one lerp. */
|
|
96
|
+
fn grade(c: vec3f) -> vec3f {
|
|
97
|
+
var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);
|
|
98
|
+
// Contrast pivots on 0.5 — display-referred midpoint, since we grade post-Filmic.
|
|
99
|
+
x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);
|
|
100
|
+
// Rec.709 luma, matching the ASC SAT node.
|
|
101
|
+
let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));
|
|
102
|
+
return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));
|
|
103
|
+
}
|
|
84
104
|
`
|
|
85
105
|
|
|
86
106
|
const COMPOSITE_BODY = /* wgsl */ `
|
|
@@ -107,6 +127,13 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
107
127
|
let exposed = combined * exp2(viewU[0].x);
|
|
108
128
|
let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
|
|
109
129
|
var disp = max(tm, vec3f(0.0));
|
|
130
|
+
// Grade the SCENE only, before the display gamma. Deliberately not applied to
|
|
131
|
+
// the background: it keeps a picked background color exactly as picked, and —
|
|
132
|
+
// load-bearing — leaves green-screen mode's key color unshifted so chroma
|
|
133
|
+
// keying still works. Skipped entirely when the grade is neutral.
|
|
134
|
+
if (viewU[9].w > 0.5) {
|
|
135
|
+
disp = grade(disp);
|
|
136
|
+
}
|
|
110
137
|
if (APPLY_GAMMA) {
|
|
111
138
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
112
139
|
}
|
|
@@ -117,7 +144,7 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
117
144
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
118
145
|
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
119
146
|
let fxOn = viewU[6].y > 0.5;
|
|
120
|
-
if (bg.w > 1.5 || fxOn) {
|
|
147
|
+
if ((bg.w > 1.5 || fxOn) COVERAGE_GATE) {
|
|
121
148
|
// The equirect and any effect both need this pixel's world-space view ray,
|
|
122
149
|
// rebuilt from the camera basis. The dome sits at infinity (no parallax) —
|
|
123
150
|
// PhotoDome-style, display-only.
|
|
@@ -152,8 +179,28 @@ const EFFECT_CALL = /* wgsl */ `
|
|
|
152
179
|
}
|
|
153
180
|
`
|
|
154
181
|
|
|
182
|
+
// Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
|
|
183
|
+
// analysis rejects the pipeline), so the coverage gate below can only wrap
|
|
184
|
+
// effect code that doesn't use them. Checked textually at build time.
|
|
185
|
+
const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/
|
|
186
|
+
|
|
187
|
+
/** Skip the whole background block (equirect sample + effect) behind pixels the
|
|
188
|
+
* model fully covers — the composite multiplies the result by (1 - alpha) = 0
|
|
189
|
+
* there anyway, and on a full-screen effect that's a third or more of the frame
|
|
190
|
+
* (the cost Safari feels most). The equirect uses explicit-LOD sampling, which
|
|
191
|
+
* is always legal in non-uniform flow; only derivative-using effects must keep
|
|
192
|
+
* uniform control flow and forgo the gate. */
|
|
193
|
+
function coverageGate(effect?: CompositeEffectSource | null): string {
|
|
194
|
+
const gated = !effect || !USES_DERIVATIVES.test(effect.wgsl)
|
|
195
|
+
return gated ? "&& alpha < 0.999" : ""
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
export function buildCompositeShader(effect?: CompositeEffectSource | null): string {
|
|
156
|
-
if (!effect)
|
|
199
|
+
if (!effect)
|
|
200
|
+
return (
|
|
201
|
+
COMPOSITE_HEAD +
|
|
202
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL).replace("COVERAGE_GATE", coverageGate(null))
|
|
203
|
+
)
|
|
157
204
|
return (
|
|
158
205
|
COMPOSITE_HEAD +
|
|
159
206
|
"\n// ── user background effect (setBackgroundEffect) ──\n" +
|
|
@@ -161,7 +208,7 @@ export function buildCompositeShader(effect?: CompositeEffectSource | null): str
|
|
|
161
208
|
"\n" +
|
|
162
209
|
effect.wgsl +
|
|
163
210
|
"\n" +
|
|
164
|
-
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim())
|
|
211
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()).replace("COVERAGE_GATE", coverageGate(effect))
|
|
165
212
|
)
|
|
166
213
|
}
|
|
167
214
|
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
// MMD-style
|
|
2
|
-
//
|
|
1
|
+
// MMD-style inverted-hull outline, ported from babylon-mmd's mmdOutline shader
|
|
2
|
+
// (the reference implementation whose output matches MMD):
|
|
3
|
+
// 1. Extrude along the VIEW-SPACE normal's XY, normalized — a pure screen
|
|
4
|
+
// direction, so rims never smear toward the camera at grazing angles.
|
|
5
|
+
// 2. Offset in clip space by edgeSize · 4/viewport · w. The ×w cancels the
|
|
6
|
+
// perspective divide → CONSTANT screen thickness of exactly
|
|
7
|
+
// 2·edgeSize device pixels (babylon: `screenNormal / (viewport*0.25) *
|
|
8
|
+
// offset * projectedPosition.w`). PMX edgeSize ~0.3–1.0 ⇒ fine 0.6–2px
|
|
9
|
+
// rims, matching MMD instead of our former chunky constants.
|
|
10
|
+
// 3. The FRAGMENT stage samples the material's own diffuse texture and
|
|
11
|
+
// MODULATES the rim's alpha by it (discarding only near-zero cut-out
|
|
12
|
+
// margins) — sheer fabric gets a proportional rim, never a solid black
|
|
13
|
+
// hull, without dropping the author's edge flag.
|
|
3
14
|
|
|
4
15
|
export const OUTLINE_SHADER_WGSL = /* wgsl */ `
|
|
5
16
|
struct CameraUniforms {
|
|
6
17
|
view: mat4x4f,
|
|
7
18
|
projection: mat4x4f,
|
|
8
19
|
viewPos: vec3f,
|
|
9
|
-
|
|
20
|
+
// Render-target height in device pixels (engine writes it each frame);
|
|
21
|
+
// width is recovered via the projection matrix's aspect.
|
|
22
|
+
viewportHeight: f32,
|
|
10
23
|
};
|
|
11
24
|
|
|
12
25
|
struct MaterialUniforms {
|
|
@@ -18,16 +31,20 @@ struct MaterialUniforms {
|
|
|
18
31
|
};
|
|
19
32
|
|
|
20
33
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
34
|
+
@group(0) @binding(1) var edgeSampler: sampler;
|
|
21
35
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
22
36
|
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
37
|
+
@group(2) @binding(1) var diffuseTexture: texture_2d<f32>;
|
|
23
38
|
|
|
24
39
|
struct VertexOutput {
|
|
25
40
|
@builtin(position) position: vec4f,
|
|
41
|
+
@location(0) uv: vec2f,
|
|
26
42
|
};
|
|
27
43
|
|
|
28
44
|
@vertex fn vs(
|
|
29
45
|
@location(0) position: vec3f,
|
|
30
46
|
@location(1) normal: vec3f,
|
|
47
|
+
@location(2) uv: vec2f,
|
|
31
48
|
@location(3) joints0: vec4<u32>,
|
|
32
49
|
@location(4) weights0: vec4<f32>
|
|
33
50
|
) -> VertexOutput {
|
|
@@ -51,33 +68,43 @@ struct VertexOutput {
|
|
|
51
68
|
let worldPos = skinnedPos.xyz;
|
|
52
69
|
let worldNormal = normalize(skinnedNrm);
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// projection is column-major: proj[0][0] = 1/(aspect·tan(fov/2)), proj[1][1] = 1/tan(fov/2).
|
|
67
|
-
// Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).
|
|
71
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
72
|
+
|
|
73
|
+
// babylon-mmd: screenNormal = normalize((view * worldNormal).xy)
|
|
74
|
+
let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;
|
|
75
|
+
let snLen = length(viewNormal.xy);
|
|
76
|
+
let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);
|
|
77
|
+
|
|
78
|
+
// Reference-height normalization (babylon-mmd ships this variant commented
|
|
79
|
+
// out as \`renderHeight = 1080\`): thickness is a constant FRACTION of the
|
|
80
|
+
// frame — 2·edgeSize px at 1080p — so retina DPR and 4K export don't thin
|
|
81
|
+
// the rims to sub-pixel. Width follows the projection aspect.
|
|
82
|
+
// projection[1][1]/projection[0][0] = width/height for a symmetric frustum.
|
|
68
83
|
let aspect = camera.projection[1][1] / camera.projection[0][0];
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
let viewport = vec2f(1080.0 * aspect, 1080.0);
|
|
85
|
+
|
|
86
|
+
// NDC offset = edgeSize · 4/viewport, ×w so the perspective divide cancels:
|
|
87
|
+
// constant screen thickness at any distance (babylon-mmd parity).
|
|
88
|
+
let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;
|
|
73
89
|
output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
|
|
90
|
+
output.uv = uv;
|
|
74
91
|
return output;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
78
|
-
@fragment fn fs() -> FSOut {
|
|
95
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
96
|
+
// Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:
|
|
97
|
+
// MMD draws blend-material edges solid (only cutout materials alpha-test), so
|
|
98
|
+
// a 0.4 discard erased the whole hull on semi-transparent cloth — stockinged
|
|
99
|
+
// legs crossing lost their outline entirely. Modulating instead keeps a
|
|
100
|
+
// proportional rim on sheer weave (never a solid black hull) and still
|
|
101
|
+
// discards true cut-out margins like hair-card borders.
|
|
102
|
+
let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
|
|
103
|
+
if (texA < 0.05) {
|
|
104
|
+
discard;
|
|
105
|
+
}
|
|
79
106
|
var out: FSOut;
|
|
80
|
-
out.color = material.edgeColor;
|
|
107
|
+
out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);
|
|
81
108
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
82
109
|
return out;
|
|
83
110
|
}
|