reze-engine 0.11.1 → 0.11.3
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 +5 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +72 -425
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +27 -60
- package/dist/shaders/cloth_rough.d.ts +1 -1
- package/dist/shaders/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/cloth_rough.js +4 -16
- package/dist/shaders/cloth_smooth.d.ts +1 -1
- package/dist/shaders/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/cloth_smooth.js +5 -17
- package/dist/shaders/default.d.ts +1 -1
- package/dist/shaders/default.d.ts.map +1 -1
- package/dist/shaders/default.js +20 -34
- package/dist/shaders/dfg_lut.d.ts +1 -1
- package/dist/shaders/dfg_lut.d.ts.map +1 -1
- package/dist/shaders/dfg_lut.js +1 -1
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +22 -35
- package/dist/shaders/face.d.ts +1 -1
- package/dist/shaders/face.d.ts.map +1 -1
- package/dist/shaders/face.js +21 -57
- package/dist/shaders/hair.d.ts +1 -1
- package/dist/shaders/hair.d.ts.map +1 -1
- package/dist/shaders/hair.js +7 -27
- package/dist/shaders/materials/body.d.ts +2 -0
- package/dist/shaders/materials/body.d.ts.map +1 -0
- package/dist/shaders/materials/body.js +199 -0
- package/dist/shaders/materials/cloth_rough.d.ts +2 -0
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_rough.js +178 -0
- package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_smooth.js +174 -0
- package/dist/shaders/materials/default.d.ts +2 -0
- package/dist/shaders/materials/default.d.ts.map +1 -0
- package/dist/shaders/materials/default.js +171 -0
- package/dist/shaders/materials/eye.d.ts +2 -0
- package/dist/shaders/materials/eye.d.ts.map +1 -0
- package/dist/shaders/materials/eye.js +146 -0
- package/dist/shaders/materials/face.d.ts +2 -0
- package/dist/shaders/materials/face.d.ts.map +1 -0
- package/dist/shaders/materials/face.js +199 -0
- package/dist/shaders/materials/hair.d.ts +2 -0
- package/dist/shaders/materials/hair.d.ts.map +1 -0
- package/dist/shaders/materials/hair.js +176 -0
- package/dist/shaders/materials/metal.d.ts +2 -0
- package/dist/shaders/materials/metal.d.ts.map +1 -0
- package/dist/shaders/materials/metal.js +183 -0
- package/dist/shaders/materials/nodes.d.ts +2 -0
- package/dist/shaders/materials/nodes.d.ts.map +1 -0
- package/{src/shaders/nodes.ts → dist/shaders/materials/nodes.js} +32 -16
- package/dist/shaders/materials/stockings.d.ts +2 -0
- package/dist/shaders/materials/stockings.d.ts.map +1 -0
- package/dist/shaders/materials/stockings.js +244 -0
- package/dist/shaders/metal.d.ts +1 -1
- package/dist/shaders/metal.d.ts.map +1 -1
- package/dist/shaders/metal.js +4 -17
- package/dist/shaders/nodes.d.ts +1 -1
- package/dist/shaders/nodes.d.ts.map +1 -1
- package/dist/shaders/nodes.js +0 -9
- package/dist/shaders/passes/bloom.d.ts +4 -0
- package/dist/shaders/passes/bloom.d.ts.map +1 -0
- package/dist/shaders/passes/bloom.js +117 -0
- package/dist/shaders/passes/composite.d.ts +2 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -0
- package/dist/shaders/passes/composite.js +61 -0
- package/dist/shaders/passes/ground.d.ts +2 -0
- package/dist/shaders/passes/ground.d.ts.map +1 -0
- package/dist/shaders/passes/ground.js +93 -0
- package/dist/shaders/passes/mipmap.d.ts +2 -0
- package/dist/shaders/passes/mipmap.d.ts.map +1 -0
- package/dist/shaders/passes/mipmap.js +16 -0
- package/dist/shaders/passes/outline.d.ts +2 -0
- package/dist/shaders/passes/outline.d.ts.map +1 -0
- package/dist/shaders/passes/outline.js +83 -0
- package/dist/shaders/passes/pick.d.ts +2 -0
- package/dist/shaders/passes/pick.d.ts.map +1 -0
- package/dist/shaders/passes/pick.js +39 -0
- package/dist/shaders/passes/shadow.d.ts +2 -0
- package/dist/shaders/passes/shadow.d.ts.map +1 -0
- package/dist/shaders/passes/shadow.js +16 -0
- package/dist/shaders/stockings.d.ts +1 -1
- package/dist/shaders/stockings.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/engine.ts +112 -449
- package/src/index.ts +3 -2
- package/src/shaders/dfg_lut.ts +1 -1
- package/src/shaders/{body.ts → materials/body.ts} +27 -60
- package/src/shaders/{cloth_rough.ts → materials/cloth_rough.ts} +4 -16
- package/src/shaders/{cloth_smooth.ts → materials/cloth_smooth.ts} +5 -17
- package/src/shaders/{default.ts → materials/default.ts} +21 -34
- package/src/shaders/{eye.ts → materials/eye.ts} +23 -35
- package/src/shaders/{face.ts → materials/face.ts} +21 -57
- package/src/shaders/{hair.ts → materials/hair.ts} +7 -27
- package/src/shaders/{metal.ts → materials/metal.ts} +15 -19
- package/src/shaders/materials/nodes.ts +483 -0
- package/src/shaders/passes/bloom.ts +121 -0
- package/src/shaders/passes/composite.ts +62 -0
- package/src/shaders/passes/ground.ts +94 -0
- package/src/shaders/passes/mipmap.ts +17 -0
- package/src/shaders/passes/outline.ts +84 -0
- package/src/shaders/passes/pick.ts +40 -0
- package/src/shaders/passes/shadow.ts +17 -0
- package/src/shaders/classify.ts +0 -25
- /package/src/shaders/{stockings.ts → materials/stockings.ts} +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// + NPR toon/AO emission stack (Strength=8.1), MixShader Fac=0.6967.
|
|
3
3
|
// Base color uses a Voronoi pattern sampled in reflection-coord space (Blender 纹理坐标.Reflection)
|
|
4
4
|
// to add subtle metallic sparkle variation. No Normal link in the graph.
|
|
5
|
+
//
|
|
6
|
+
// Graph's base color chain is: 纹理坐标.Reflection → 矢量运算.007(CROSS, Vec2=(0,1,0)) →
|
|
7
|
+
// 沃罗诺伊纹理(F1, Color out) → 颜色渐变(linear) → 混合.005. The dumper did not capture the
|
|
8
|
+
// VectorMath operation — CROSS is the assumed op based on the hardcoded (0,1,0) Vector_001
|
|
9
|
+
// constant (MULTIPLY would zero X/Z producing 1D bands; CROSS produces horizontal ring
|
|
10
|
+
// patterns consistent with metallic anisotropy).
|
|
5
11
|
|
|
6
12
|
import { NODES_WGSL } from "./nodes"
|
|
7
13
|
|
|
@@ -129,40 +135,30 @@ struct FSOut {
|
|
|
129
135
|
let out_alpha = material.alpha * tex_s.a;
|
|
130
136
|
if (out_alpha < 0.001) { discard; }
|
|
131
137
|
|
|
132
|
-
// ═══ NPR toon stack (图像 → HSV.007 Val=0.8 → 转接点.001) ═══
|
|
133
138
|
let tex_tint = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
134
139
|
let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
135
140
|
let ramp008 = ramp_constant_edge_aa(lum_shade, METAL_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
|
|
136
141
|
let mix04_fac = math_multiply(ramp008.r, METAL_MIX04_MUL);
|
|
137
142
|
|
|
138
|
-
// 混合.004: A=HSV.002(Val=0.2 dark), B=tex_tint
|
|
139
143
|
let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_tint);
|
|
140
144
|
let mix04 = mix_blend(mix04_fac, dark_tex, tex_tint);
|
|
141
145
|
|
|
142
|
-
// AO white/black ramp → 混合.002 factor
|
|
143
|
-
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
144
|
-
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
145
|
-
let overlay_fac = mix(1.0, 0.0, ao_ramp_c.r);
|
|
146
|
-
|
|
147
|
-
// 混合.002 OVERLAY: A=HSV.008(Val=1.0 identity) ← mix04, B=HSV.004(Val=2.0 bright) ← mix04
|
|
148
|
-
let hue008 = mix04; // identity HSV
|
|
149
146
|
let hue004 = hue_sat_id(1.0, 2.0, 1.0, mix04);
|
|
150
|
-
let npr_rgb = mix_overlay(
|
|
147
|
+
let npr_rgb = mix_overlay(1.0, mix04, hue004);
|
|
151
148
|
let npr_emission = npr_rgb * METAL_EMIT_STR;
|
|
152
149
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// 纹理坐标.Reflection → 矢量运算 → 沃罗诺伊(Scale=4.3) → 颜色渐变 → 混合.005
|
|
150
|
+
// Reflection-coord Voronoi produces the metallic sparkle variation.
|
|
151
|
+
// VALTORGB takes Color → Fac via Blender's BT.601 implicit color_to_value.
|
|
156
152
|
let refl_dir = reflect(-v, n);
|
|
157
|
-
let
|
|
158
|
-
let
|
|
159
|
-
|
|
153
|
+
let voro_input = cross(refl_dir, vec3f(0.0, 1.0, 0.0));
|
|
154
|
+
let voro_rgb = tex_voronoi_color(voro_input, METAL_VORONOI_SCALE);
|
|
155
|
+
let voro_scalar = color_to_value(voro_rgb);
|
|
156
|
+
let voro_ramp = ramp_linear(voro_scalar, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
160
157
|
let hue006 = hue_sat_id(1.5, 1.2999999523162842, 1.0, tex_tint);
|
|
161
158
|
let albedo = mix_blend(voro_ramp, vec3f(voro_ramp), hue006);
|
|
162
159
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// metallic=1 this is just albedo (specular_tint is dielectric-only and ignored here).
|
|
160
|
+
// Principled BSDF (EEVEE port): metallic=1 collapses f0 = mix(dielectric, albedo, 1) = albedo;
|
|
161
|
+
// specular_tint is dielectric-only and ignored here.
|
|
166
162
|
let f0 = albedo;
|
|
167
163
|
let f90 = mix(f0, vec3f(1.0), sqrt(METAL_SPECULAR));
|
|
168
164
|
let NL = max(dot(n, l), 0.0);
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// Shared WGSL primitives for Blender-style NPR material nodes.
|
|
2
|
+
// Every function here maps 1:1 to a Blender shader node type used in the preset JSONs.
|
|
3
|
+
// Hand-ported material shaders concatenate this block before their own code.
|
|
4
|
+
|
|
5
|
+
export const NODES_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
// Baked 64×64 rgba8unorm combined BRDF LUT — created once at engine init by dfg_lut.ts.
|
|
8
|
+
// .rg = split-sum DFG (Karis: tint = f0·x + f90·y) → F_brdf_*_scatter
|
|
9
|
+
// .ba = Heitz 2016 LTC magnitude (ltc_mag_ggx) → ltc_brdf_scale_from_lut
|
|
10
|
+
// Paired with group(0) binding(2) diffuseSampler (linear filter). Sample once per
|
|
11
|
+
// fragment via brdf_lut_sample() — callers feed .rg and the whole vec4 into the
|
|
12
|
+
// helpers below, halving LUT taps on the default Principled path.
|
|
13
|
+
@group(0) @binding(9) var brdfLut: texture_2d<f32>;
|
|
14
|
+
|
|
15
|
+
// ─── RGB ↔ HSV ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
fn rgb_to_hsv(rgb: vec3f) -> vec3f {
|
|
18
|
+
let c_max = max(rgb.r, max(rgb.g, rgb.b));
|
|
19
|
+
let c_min = min(rgb.r, min(rgb.g, rgb.b));
|
|
20
|
+
let delta = c_max - c_min;
|
|
21
|
+
|
|
22
|
+
var h = 0.0;
|
|
23
|
+
if (delta > 1e-6) {
|
|
24
|
+
if (c_max == rgb.r) {
|
|
25
|
+
h = (rgb.g - rgb.b) / delta;
|
|
26
|
+
if (h < 0.0) { h += 6.0; }
|
|
27
|
+
} else if (c_max == rgb.g) {
|
|
28
|
+
h = 2.0 + (rgb.b - rgb.r) / delta;
|
|
29
|
+
} else {
|
|
30
|
+
h = 4.0 + (rgb.r - rgb.g) / delta;
|
|
31
|
+
}
|
|
32
|
+
h /= 6.0;
|
|
33
|
+
}
|
|
34
|
+
let s = select(0.0, delta / c_max, c_max > 1e-6);
|
|
35
|
+
return vec3f(h, s, c_max);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn hsv_to_rgb(hsv: vec3f) -> vec3f {
|
|
39
|
+
let h = hsv.x;
|
|
40
|
+
let s = hsv.y;
|
|
41
|
+
let v = hsv.z;
|
|
42
|
+
if (s < 1e-6) { return vec3f(v); }
|
|
43
|
+
|
|
44
|
+
let hh = fract(h) * 6.0;
|
|
45
|
+
let sector = u32(hh);
|
|
46
|
+
let f = hh - f32(sector);
|
|
47
|
+
let p = v * (1.0 - s);
|
|
48
|
+
let q = v * (1.0 - s * f);
|
|
49
|
+
let t = v * (1.0 - s * (1.0 - f));
|
|
50
|
+
|
|
51
|
+
switch (sector) {
|
|
52
|
+
case 0u: { return vec3f(v, t, p); }
|
|
53
|
+
case 1u: { return vec3f(q, v, p); }
|
|
54
|
+
case 2u: { return vec3f(p, v, t); }
|
|
55
|
+
case 3u: { return vec3f(p, q, v); }
|
|
56
|
+
case 4u: { return vec3f(t, p, v); }
|
|
57
|
+
default: { return vec3f(v, p, q); }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── HUE_SAT node ───────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
fn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
64
|
+
var hsv = rgb_to_hsv(color);
|
|
65
|
+
hsv.x = fract(hsv.x + hue - 0.5);
|
|
66
|
+
hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);
|
|
67
|
+
hsv.z *= value;
|
|
68
|
+
return mix(color, hsv_to_rgb(hsv), fac);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// hue_sat specialization for hue=0.5 (identity hue shift — fract(h + 0.5 - 0.5) = h).
|
|
72
|
+
// Branchless equivalent that skips the rgb_to_hsv → hsv_to_rgb roundtrip: WebKit's
|
|
73
|
+
// Metal backend serializes the 3-way if chain in rgb_to_hsv and the 6-way switch in
|
|
74
|
+
// hsv_to_rgb, where this form compiles to linear SIMD ops + a single select.
|
|
75
|
+
fn hue_sat_id(saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
76
|
+
let m = max(max(color.r, color.g), color.b);
|
|
77
|
+
let n = min(min(color.r, color.g), color.b);
|
|
78
|
+
// Unclamped (sat*old_s ≤ 1): reproj = mix(vec3f(m), color, saturation).
|
|
79
|
+
// Clamped (saturated to 1): reproj = (color - n) * m / (m - n).
|
|
80
|
+
let range = max(m - n, 1e-6);
|
|
81
|
+
let unclamped = mix(vec3f(m), color, saturation);
|
|
82
|
+
let clamped = (color - vec3f(n)) * m / range;
|
|
83
|
+
let needs_clamp = (m - n) * saturation >= m;
|
|
84
|
+
let reproj = select(unclamped, clamped, needs_clamp);
|
|
85
|
+
return mix(color, reproj * value, fac);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── BRIGHTCONTRAST node ────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
fn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {
|
|
91
|
+
let a = 1.0 + contrast;
|
|
92
|
+
let b = bright - contrast * 0.5;
|
|
93
|
+
return max(vec3f(0.0), color * a + vec3f(b));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── INVERT node ────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
fn invert(fac: f32, color: vec3f) -> vec3f {
|
|
99
|
+
return mix(color, vec3f(1.0) - color, fac);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn invert_f(fac: f32, val: f32) -> f32 {
|
|
103
|
+
return mix(val, 1.0 - val, fac);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Color ramp (VALTORGB) — 2-stop variants ───────────────────────
|
|
107
|
+
// All 7 presets use exclusively 2-stop ramps.
|
|
108
|
+
|
|
109
|
+
fn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
110
|
+
return select(c0, c1, f >= p1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// CONSTANT ramp with screen-space edge AA — kills sparkle where fwidth(f) straddles a hard step (NPR terminator)
|
|
114
|
+
fn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {
|
|
115
|
+
let w = max(fwidth(f) * 1.75, 6e-6);
|
|
116
|
+
let t = smoothstep(edge - w, edge + w, f);
|
|
117
|
+
return mix(c0, c1, t);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
121
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
122
|
+
return mix(c0, c1, t);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
126
|
+
// cardinal spline with 2 stops degrades to smoothstep
|
|
127
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
128
|
+
let ss = t * t * (3.0 - 2.0 * t);
|
|
129
|
+
return mix(c0, c1, ss);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── MATH node operations ───────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
fn math_add(a: f32, b: f32) -> f32 { return a + b; }
|
|
135
|
+
fn math_multiply(a: f32, b: f32) -> f32 { return a * b; }
|
|
136
|
+
fn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }
|
|
137
|
+
fn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }
|
|
138
|
+
|
|
139
|
+
// Blender's implicit Color → Float socket conversion uses BT.601 grayscale
|
|
140
|
+
// (rgb_to_grayscale in blenkernel/intern/node.cc). When a material graph plugs a
|
|
141
|
+
// Color output into a Math node's Value input, this is the scalar it actually sees.
|
|
142
|
+
fn color_to_value(c: vec3f) -> f32 {
|
|
143
|
+
return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── MIX node (blend_type variants) ────────────────────────────────
|
|
147
|
+
|
|
148
|
+
fn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
149
|
+
return mix(a, b, fac);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
153
|
+
let lo = 2.0 * a * b;
|
|
154
|
+
let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);
|
|
155
|
+
let overlay = select(hi, lo, a < vec3f(0.5));
|
|
156
|
+
return mix(a, overlay, fac);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
160
|
+
return mix(a, a * b, fac);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
164
|
+
return mix(a, max(a, b), fac);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)
|
|
168
|
+
fn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
169
|
+
return mix(a, a + 2.0 * b - vec3f(1.0), fac);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Luminance for Shader→RGB scalar gates (linear RGB, Rec.709 weights)
|
|
173
|
+
fn luminance_rec709_linear(c: vec3f) -> f32 {
|
|
174
|
+
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── FRESNEL node ───────────────────────────────────────────────────
|
|
178
|
+
// Schlick approximation matching Blender's Fresnel node
|
|
179
|
+
|
|
180
|
+
fn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {
|
|
181
|
+
let r = (ior - 1.0) / (ior + 1.0);
|
|
182
|
+
let f0 = r * r;
|
|
183
|
+
let cos_theta = clamp(dot(n, v), 0.0, 1.0);
|
|
184
|
+
let m = 1.0 - cos_theta;
|
|
185
|
+
let m2 = m * m;
|
|
186
|
+
let m5 = m2 * m2 * m;
|
|
187
|
+
return f0 + (1.0 - f0) * m5;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── LAYER_WEIGHT node ──────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
fn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
193
|
+
let eta = max(1.0 - blend, 1e-4);
|
|
194
|
+
let r = (1.0 - eta) / (1.0 + eta);
|
|
195
|
+
let f0 = r * r;
|
|
196
|
+
let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);
|
|
197
|
+
let m = 1.0 - cos_theta;
|
|
198
|
+
let m2 = m * m;
|
|
199
|
+
let m5 = m2 * m2 * m;
|
|
200
|
+
return f0 + (1.0 - f0) * m5;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
204
|
+
var facing = abs(dot(n, v));
|
|
205
|
+
let b = clamp(blend, 0.0, 0.99999);
|
|
206
|
+
if (b != 0.5) {
|
|
207
|
+
let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);
|
|
208
|
+
facing = pow(facing, exponent);
|
|
209
|
+
}
|
|
210
|
+
return 1.0 - facing;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── SHADER_TO_RGB (white DiffuseBSDF) ──────────────────────────────
|
|
214
|
+
// Eevee captures lit diffuse: (albedo/π)*sun*N·L*shadow + ambient (linear). Albedo=1.
|
|
215
|
+
// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.
|
|
216
|
+
|
|
217
|
+
fn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {
|
|
218
|
+
const PI_S: f32 = 3.141592653589793;
|
|
219
|
+
let ndotl = max(dot(n, l), 0.0);
|
|
220
|
+
let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;
|
|
221
|
+
return luminance_rec709_linear(rgb);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── BUMP node ──────────────────────────────────────────────────────
|
|
225
|
+
// Screen-space bump from a scalar height field. Needs dFdx/dFdy which
|
|
226
|
+
// WGSL provides as dpdx/dpdy.
|
|
227
|
+
|
|
228
|
+
fn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
|
|
229
|
+
let dhdx = dpdx(height);
|
|
230
|
+
let dhdy = dpdy(height);
|
|
231
|
+
let dpdx_pos = dpdx(world_pos);
|
|
232
|
+
let dpdy_pos = dpdy(world_pos);
|
|
233
|
+
let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));
|
|
234
|
+
return normalize(perturbed);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference
|
|
238
|
+
fn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
|
|
239
|
+
let dhdx = dpdx(height);
|
|
240
|
+
let dhdy = dpdy(height);
|
|
241
|
+
let dpdx_pos = dpdx(world_pos);
|
|
242
|
+
let dpdy_pos = dpdy(world_pos);
|
|
243
|
+
let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));
|
|
244
|
+
return normalize(perturbed);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── NOISE texture (Perlin-style) ───────────────────────────────────
|
|
248
|
+
// Simplified gradient noise matching Blender's default noise output.
|
|
249
|
+
|
|
250
|
+
// PCG-style integer hash. Replaces the classic 'fract(sin(q) * LARGE)' trick because
|
|
251
|
+
// WebKit's Metal backend compiles 'sin' to a full transcendental op (slow), while
|
|
252
|
+
// Safari's Apple-GPU scalar ALU handles int muls/xors near free. Inputs arrive as
|
|
253
|
+
// integer-valued floats (floor(p) + unit offsets) from _noise3, so vec3i cast is exact.
|
|
254
|
+
fn _hash33(p: vec3f) -> vec3f {
|
|
255
|
+
var h = vec3u(vec3i(p) + vec3i(32768));
|
|
256
|
+
h = h * vec3u(1664525u, 1013904223u, 2654435761u);
|
|
257
|
+
h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
|
|
258
|
+
h = h ^ (h >> vec3u(16u));
|
|
259
|
+
// Mask to 24 bits — above that f32 loses precision on the u32→f32 convert.
|
|
260
|
+
let hm = h & vec3u(16777215u);
|
|
261
|
+
return vec3f(hm) * (2.0 / 16777216.0) - 1.0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn _noise3(p: vec3f) -> f32 {
|
|
265
|
+
let i = floor(p);
|
|
266
|
+
let f = fract(p);
|
|
267
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
268
|
+
|
|
269
|
+
return mix(
|
|
270
|
+
mix(
|
|
271
|
+
mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),
|
|
272
|
+
dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),
|
|
273
|
+
mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),
|
|
274
|
+
dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),
|
|
275
|
+
mix(
|
|
276
|
+
mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),
|
|
277
|
+
dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),
|
|
278
|
+
mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),
|
|
279
|
+
dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),
|
|
280
|
+
u.z);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {
|
|
284
|
+
var q = p;
|
|
285
|
+
if (abs(distortion) > 1e-6) {
|
|
286
|
+
let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));
|
|
287
|
+
q = p + (w * 2.0 - 1.0) * distortion;
|
|
288
|
+
}
|
|
289
|
+
let coords = q * scale;
|
|
290
|
+
var value = 0.0;
|
|
291
|
+
var amplitude = 1.0;
|
|
292
|
+
var frequency = 1.0;
|
|
293
|
+
var total_amp = 0.0;
|
|
294
|
+
let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;
|
|
295
|
+
for (var i = 0; i < octaves; i++) {
|
|
296
|
+
value += amplitude * _noise3(coords * frequency);
|
|
297
|
+
total_amp += amplitude;
|
|
298
|
+
amplitude *= roughness;
|
|
299
|
+
frequency *= 2.0;
|
|
300
|
+
}
|
|
301
|
+
return value / max(total_amp, 1e-6) * 0.5 + 0.5;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// tex_noise specialization: detail=2.0 (3 octaves), roughness=0.5, distortion=0.
|
|
305
|
+
// WebKit can't unroll tex_noise's for-loop because 'octaves' is a runtime value;
|
|
306
|
+
// this variant is fully unrolled with constants folded (total_amp = 1.75).
|
|
307
|
+
fn tex_noise_d2(p: vec3f, scale: f32) -> f32 {
|
|
308
|
+
let c = p * scale;
|
|
309
|
+
let v = _noise3(c) + 0.5 * _noise3(c * 2.0) + 0.25 * _noise3(c * 4.0);
|
|
310
|
+
return v * (1.0 / 1.75) * 0.5 + 0.5;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── TEX_GRADIENT (linear) ──────────────────────────────────────────
|
|
314
|
+
// Used by Stockings preset. Maps the input vector's X to a 0–1 gradient.
|
|
315
|
+
|
|
316
|
+
fn tex_gradient_linear(uv: vec3f) -> f32 {
|
|
317
|
+
return clamp(uv.x, 0.0, 1.0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── TEX_VORONOI ────────────────────────────────────────────────────
|
|
321
|
+
// 3D F1 voronoi. _f1 returns Distance; _color returns per-cell hash color
|
|
322
|
+
// (matches Blender voronoi.cc: outColor = hash_int3_to_float3(cell + targetOffset)).
|
|
323
|
+
|
|
324
|
+
fn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {
|
|
325
|
+
let coords = p * scale;
|
|
326
|
+
let i = floor(coords);
|
|
327
|
+
let f = fract(coords);
|
|
328
|
+
var min_dist = 1e10;
|
|
329
|
+
for (var z = -1; z <= 1; z++) {
|
|
330
|
+
for (var y = -1; y <= 1; y++) {
|
|
331
|
+
for (var x = -1; x <= 1; x++) {
|
|
332
|
+
let neighbor = vec3f(f32(x), f32(y), f32(z));
|
|
333
|
+
let point = _hash33(i + neighbor) * 0.5 + 0.5;
|
|
334
|
+
let diff = neighbor + point - f;
|
|
335
|
+
min_dist = min(min_dist, dot(diff, diff));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return sqrt(min_dist);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// The per-cell jitter hash IS the Color output in Blender — reuse the same hash
|
|
343
|
+
// tap for jitter + color instead of computing two.
|
|
344
|
+
fn tex_voronoi_color(p: vec3f, scale: f32) -> vec3f {
|
|
345
|
+
let coords = p * scale;
|
|
346
|
+
let i = floor(coords);
|
|
347
|
+
let f = fract(coords);
|
|
348
|
+
var min_dist = 1e10;
|
|
349
|
+
var min_hash = vec3f(0.5);
|
|
350
|
+
for (var z = -1; z <= 1; z++) {
|
|
351
|
+
for (var y = -1; y <= 1; y++) {
|
|
352
|
+
for (var x = -1; x <= 1; x++) {
|
|
353
|
+
let neighbor = vec3f(f32(x), f32(y), f32(z));
|
|
354
|
+
let h = _hash33(i + neighbor) * 0.5 + 0.5;
|
|
355
|
+
let diff = neighbor + h - f;
|
|
356
|
+
let d = dot(diff, diff);
|
|
357
|
+
if (d < min_dist) {
|
|
358
|
+
min_dist = d;
|
|
359
|
+
min_hash = h;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return min_hash;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── SEPXYZ node ────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
fn separate_xyz(v: vec3f) -> vec3f { return v; }
|
|
370
|
+
|
|
371
|
+
// ─── VECT_MATH (cross product) ──────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
fn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }
|
|
374
|
+
|
|
375
|
+
// ─── MAPPING node ───────────────────────────────────────────────────
|
|
376
|
+
// Point-type mapping: scale, rotate (euler XYZ), translate.
|
|
377
|
+
|
|
378
|
+
fn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {
|
|
379
|
+
var p = v * scl;
|
|
380
|
+
// simplified: skip rotation when all angles are zero (common case)
|
|
381
|
+
if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {
|
|
382
|
+
let cx = cos(rot.x); let sx = sin(rot.x);
|
|
383
|
+
let cy = cos(rot.y); let sy = sin(rot.y);
|
|
384
|
+
let cz = cos(rot.z); let sz = sin(rot.z);
|
|
385
|
+
let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);
|
|
386
|
+
let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);
|
|
387
|
+
p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);
|
|
388
|
+
}
|
|
389
|
+
return p + loc;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── NORMAL_MAP node (tangent-space) ────────────────────────────────
|
|
393
|
+
// Applies a tangent-space normal map. Requires TBN from vertex stage.
|
|
394
|
+
|
|
395
|
+
fn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {
|
|
396
|
+
let ts = map_color * 2.0 - 1.0;
|
|
397
|
+
let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);
|
|
398
|
+
return normalize(mix(normal, perturbed, strength));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── EEVEE Principled BSDF primitives ───────────────────────────────
|
|
402
|
+
// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/
|
|
403
|
+
// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.
|
|
404
|
+
// Usage pattern (see material shaders): direct spec = bsdf_ggx × sun × shadow
|
|
405
|
+
// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with
|
|
406
|
+
// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.
|
|
407
|
+
|
|
408
|
+
const EEVEE_PI: f32 = 3.141592653589793;
|
|
409
|
+
|
|
410
|
+
// Fused analytic GGX specular (direct lights). Returns BRDF × NL.
|
|
411
|
+
// 4·NL·NV is cancelled via G1_Smith reciprocal form — see bsdf_common_lib.glsl:115.
|
|
412
|
+
// Caller passes NL, NV (already computed for diffuse + brdf_lut_sample) so WebKit
|
|
413
|
+
// can reuse them instead of recomputing dot products across the function boundary.
|
|
414
|
+
fn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, NL_in: f32, NV_in: f32, roughness: f32) -> f32 {
|
|
415
|
+
let a = max(roughness, 1e-4);
|
|
416
|
+
let a2 = a * a;
|
|
417
|
+
let H = normalize(L + V);
|
|
418
|
+
let NH = max(dot(N, H), 1e-8);
|
|
419
|
+
let NL = max(NL_in, 1e-8);
|
|
420
|
+
let NV = max(NV_in, 1e-8);
|
|
421
|
+
// G1_Smith_GGX_opti reciprocal form — denominator piece only.
|
|
422
|
+
let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);
|
|
423
|
+
let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);
|
|
424
|
+
let G = G1L * G1V;
|
|
425
|
+
// D_ggx_opti = pi * denom² — reciprocal D × a².
|
|
426
|
+
let tmp = (NH * a2 - NH) * NH + 1.0;
|
|
427
|
+
let D_opti = EEVEE_PI * tmp * tmp;
|
|
428
|
+
return NL * a2 / (D_opti * G);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Split-sum DFG LUT — Karis 2013 curve fit stand-in for the 64×64 baked LUT.
|
|
432
|
+
// Returns (lut.x, lut.y) in Blender convention: tint = f0·lut.x + f90·lut.y.
|
|
433
|
+
fn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {
|
|
434
|
+
let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);
|
|
435
|
+
let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);
|
|
436
|
+
let r = roughness * c0 + c1;
|
|
437
|
+
let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;
|
|
438
|
+
return vec2f(-1.04, 1.04) * a004 + r.zw;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Baked combined BRDF LUT — exact port of Blender bsdf_lut_frag.glsl packed with
|
|
442
|
+
// ltc_mag_ggx from eevee_lut.c. Single sample returns DFG (.rg) and LTC mag (.ba).
|
|
443
|
+
// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:
|
|
444
|
+
// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.
|
|
445
|
+
// Requires group(0) binding(9) brdfLut + binding(2) diffuseSampler in the host shader.
|
|
446
|
+
fn brdf_lut_sample(NV: f32, roughness: f32) -> vec4f {
|
|
447
|
+
let LUT_SIZE: f32 = 64.0;
|
|
448
|
+
var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
|
|
449
|
+
uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
|
|
450
|
+
return textureSampleLevel(brdfLut, diffuseSampler, uv, 0.0);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
454
|
+
return lut.y * f90 + lut.x * f0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Fdez-Agüera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).
|
|
458
|
+
fn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
459
|
+
let FssEss = lut.y * f90 + lut.x * f0;
|
|
460
|
+
let Ess = lut.x + lut.y;
|
|
461
|
+
let Ems = 1.0 - Ess;
|
|
462
|
+
let Favg = f0 + (1.0 - f0) / 21.0;
|
|
463
|
+
let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);
|
|
464
|
+
return FssEss + Fms * Ems;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// EEVEE direct-specular energy compensation factor — closure_eval_glossy_lib.glsl:79-81:
|
|
468
|
+
// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)
|
|
469
|
+
// Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum;
|
|
470
|
+
// direct radiance is rescaled so total-energy matches the split-sum LUT.
|
|
471
|
+
// Takes a pre-sampled vec4f from brdf_lut_sample() to share the fetch with
|
|
472
|
+
// F_brdf_multi_scatter on the same fragment.
|
|
473
|
+
fn ltc_brdf_scale_from_lut(lut: vec4f) -> f32 {
|
|
474
|
+
return (lut.z + lut.w) / max(lut.x + lut.y, 1e-6);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Luminance-normalized hue extraction — Blender tint_from_color (isolates hue+sat).
|
|
478
|
+
fn tint_from_color(color: vec3f) -> vec3f {
|
|
479
|
+
let lum = dot(color, vec3f(0.3, 0.6, 0.1));
|
|
480
|
+
return select(vec3f(1.0), color / lum, lum > 0.0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
`;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// EEVEE 3.6 bloom pyramid: blit (Karis prefilter) → 13-tap downsamples → 9-tap tent upsamples.
|
|
2
|
+
// Mirrors source/blender/draw/engines/eevee/shaders/effect_bloom_frag.glsl. Firefly suppression
|
|
3
|
+
// lives in the blit (Karis luminance-weighted 4-tap average). A single-pass Gaussian cannot
|
|
4
|
+
// reproduce this — hot pixels dominate and produce the sparkle halo.
|
|
5
|
+
|
|
6
|
+
const FULLSCREEN_VS = /* wgsl */ `
|
|
7
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
8
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
9
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
10
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
11
|
+
}
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
// Full-res HDR → half-res. Karis 4-tap firefly average + EEVEE quadratic knee threshold + clamp.
|
|
15
|
+
export const BLOOM_BLIT_SHADER_WGSL = `${FULLSCREEN_VS}
|
|
16
|
+
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
17
|
+
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
18
|
+
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
19
|
+
|
|
20
|
+
fn luminance(c: vec3f) -> f32 {
|
|
21
|
+
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
22
|
+
}
|
|
23
|
+
fn fetch(c: vec2<i32>, clampV: f32) -> vec3f {
|
|
24
|
+
let d = vec2<i32>(textureDimensions(hdrTex));
|
|
25
|
+
let cc = clamp(c, vec2<i32>(0), d - vec2<i32>(1));
|
|
26
|
+
let s = textureLoad(hdrTex, cc, 0);
|
|
27
|
+
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
28
|
+
let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
|
|
29
|
+
// Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
|
|
30
|
+
let mask = textureLoad(maskTex, cc, 0).r;
|
|
31
|
+
let masked = rgb * mask;
|
|
32
|
+
// Blender clamps each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
33
|
+
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
37
|
+
let dst = vec2<i32>(p.xy - vec2f(0.5));
|
|
38
|
+
let base = dst * 2;
|
|
39
|
+
let clampV = prefilter.z;
|
|
40
|
+
let a = fetch(base + vec2<i32>(0, 0), clampV);
|
|
41
|
+
let b = fetch(base + vec2<i32>(1, 0), clampV);
|
|
42
|
+
let c = fetch(base + vec2<i32>(0, 1), clampV);
|
|
43
|
+
let d = fetch(base + vec2<i32>(1, 1), clampV);
|
|
44
|
+
// Karis partial average: weight each tap by 1/(1+luma) — suppresses fireflies.
|
|
45
|
+
let wa = 1.0 / (1.0 + luminance(a));
|
|
46
|
+
let wb = 1.0 / (1.0 + luminance(b));
|
|
47
|
+
let wc = 1.0 / (1.0 + luminance(c));
|
|
48
|
+
let wd = 1.0 / (1.0 + luminance(d));
|
|
49
|
+
let avg = (a * wa + b * wb + c * wc + d * wd) / max(wa + wb + wc + wd, 1e-6);
|
|
50
|
+
// EEVEE quadratic threshold (brightness = max-channel, then soft-knee curve).
|
|
51
|
+
let bright = max(avg.r, max(avg.g, avg.b));
|
|
52
|
+
let soft = clamp(bright - prefilter.x + prefilter.y, 0.0, 2.0 * prefilter.y);
|
|
53
|
+
let q = (soft * soft) / (4.0 * max(prefilter.y, 1e-4) + 1e-6);
|
|
54
|
+
let contrib = max(q, bright - prefilter.x) / max(bright, 1e-4);
|
|
55
|
+
return vec4f(max(avg * contrib, vec3f(0.0)), 1.0);
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
// Jimenez/COD 13-tap dual-box — 5 weighted 2×2 averages, rejects nyquist ringing.
|
|
60
|
+
export const BLOOM_DOWNSAMPLE_SHADER_WGSL = `${FULLSCREEN_VS}
|
|
61
|
+
@group(0) @binding(0) var srcTex: texture_2d<f32>;
|
|
62
|
+
@group(0) @binding(1) var srcSamp: sampler;
|
|
63
|
+
|
|
64
|
+
fn samp(uv: vec2f, off: vec2f) -> vec3f {
|
|
65
|
+
return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
69
|
+
let srcDims = vec2f(textureDimensions(srcTex));
|
|
70
|
+
let t = 1.0 / srcDims;
|
|
71
|
+
// fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
|
|
72
|
+
let dstDims = srcDims * 0.5;
|
|
73
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
74
|
+
let A = samp(uv, t * vec2f(-2.0, -2.0));
|
|
75
|
+
let B = samp(uv, t * vec2f( 0.0, -2.0));
|
|
76
|
+
let C = samp(uv, t * vec2f( 2.0, -2.0));
|
|
77
|
+
let D = samp(uv, t * vec2f(-1.0, -1.0));
|
|
78
|
+
let E = samp(uv, t * vec2f( 1.0, -1.0));
|
|
79
|
+
let F = samp(uv, t * vec2f(-2.0, 0.0));
|
|
80
|
+
let G = samp(uv, t * vec2f( 0.0, 0.0));
|
|
81
|
+
let H = samp(uv, t * vec2f( 2.0, 0.0));
|
|
82
|
+
let I = samp(uv, t * vec2f(-1.0, 1.0));
|
|
83
|
+
let J = samp(uv, t * vec2f( 1.0, 1.0));
|
|
84
|
+
let K = samp(uv, t * vec2f(-2.0, 2.0));
|
|
85
|
+
let L = samp(uv, t * vec2f( 0.0, 2.0));
|
|
86
|
+
let M = samp(uv, t * vec2f( 2.0, 2.0));
|
|
87
|
+
var o = (D + E + I + J) * (0.5 / 4.0);
|
|
88
|
+
o = o + (A + B + G + F) * (0.125 / 4.0);
|
|
89
|
+
o = o + (B + C + H + G) * (0.125 / 4.0);
|
|
90
|
+
o = o + (F + G + L + K) * (0.125 / 4.0);
|
|
91
|
+
o = o + (G + H + M + L) * (0.125 / 4.0);
|
|
92
|
+
return vec4f(o, 1.0);
|
|
93
|
+
}
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
// 9-tap tent, progressively added to matching downsample mip. Blender radius = sample scale.
|
|
97
|
+
export const BLOOM_UPSAMPLE_SHADER_WGSL = `${FULLSCREEN_VS}
|
|
98
|
+
@group(0) @binding(0) var srcTex: texture_2d<f32>; // coarser accumulator
|
|
99
|
+
@group(0) @binding(1) var baseTex: texture_2d<f32>; // matching downsample mip
|
|
100
|
+
@group(0) @binding(2) var srcSamp: sampler;
|
|
101
|
+
@group(0) @binding(3) var<uniform> upU: vec4<f32>; // sampleScale, _, _, _
|
|
102
|
+
|
|
103
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
104
|
+
let srcDims = vec2f(textureDimensions(srcTex));
|
|
105
|
+
let baseDims = vec2f(textureDimensions(baseTex));
|
|
106
|
+
let uv = p.xy / max(baseDims, vec2f(1.0));
|
|
107
|
+
let t = upU.x / srcDims;
|
|
108
|
+
var o = textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, -1.0), 0.0).rgb * 1.0;
|
|
109
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, -1.0), 0.0).rgb * 2.0;
|
|
110
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, -1.0), 0.0).rgb * 1.0;
|
|
111
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 0.0), 0.0).rgb * 2.0;
|
|
112
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 0.0), 0.0).rgb * 4.0;
|
|
113
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 0.0), 0.0).rgb * 2.0;
|
|
114
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 1.0), 0.0).rgb * 1.0;
|
|
115
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 1.0), 0.0).rgb * 2.0;
|
|
116
|
+
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 1.0), 0.0).rgb * 1.0;
|
|
117
|
+
o = o * (1.0 / 16.0);
|
|
118
|
+
let base = textureSampleLevel(baseTex, srcSamp, uv, 0.0).rgb;
|
|
119
|
+
return vec4f(o + base, 1.0);
|
|
120
|
+
}
|
|
121
|
+
`
|