reze-engine 0.11.0 → 0.11.2
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 +40 -22
- package/dist/engine.d.ts +14 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +206 -77
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +58 -47
- 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 +38 -20
- 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 +33 -18
- package/dist/shaders/default.d.ts +1 -1
- package/dist/shaders/default.d.ts.map +1 -1
- package/dist/shaders/default.js +45 -42
- package/dist/shaders/dfg_lut.d.ts +2 -3
- package/dist/shaders/dfg_lut.d.ts.map +1 -1
- package/dist/shaders/dfg_lut.js +30 -26
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +47 -43
- package/dist/shaders/face.d.ts +1 -1
- package/dist/shaders/face.d.ts.map +1 -1
- package/dist/shaders/face.js +47 -23
- package/dist/shaders/hair.d.ts +1 -1
- package/dist/shaders/hair.d.ts.map +1 -1
- package/dist/shaders/hair.js +42 -32
- package/dist/shaders/metal.d.ts +1 -1
- package/dist/shaders/metal.d.ts.map +1 -1
- package/dist/shaders/metal.js +35 -19
- package/dist/shaders/nodes.d.ts +1 -1
- package/dist/shaders/nodes.d.ts.map +1 -1
- package/dist/shaders/nodes.js +79 -37
- package/dist/shaders/stockings.d.ts +1 -1
- package/dist/shaders/stockings.d.ts.map +1 -1
- package/dist/shaders/stockings.js +30 -15
- package/package.json +2 -2
- package/src/engine.ts +227 -97
- package/src/shaders/body.ts +58 -47
- package/src/shaders/cloth_rough.ts +38 -20
- package/src/shaders/cloth_smooth.ts +33 -18
- package/src/shaders/default.ts +46 -42
- package/src/shaders/dfg_lut.ts +32 -28
- package/src/shaders/eye.ts +48 -43
- package/src/shaders/face.ts +47 -23
- package/src/shaders/hair.ts +42 -32
- package/src/shaders/metal.ts +35 -19
- package/src/shaders/nodes.ts +79 -37
- package/src/shaders/stockings.ts +30 -15
package/src/shaders/metal.ts
CHANGED
|
@@ -51,19 +51,25 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
51
51
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
52
52
|
|
|
53
53
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
54
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
55
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
54
56
|
let biasedPos = worldPos + n * 0.08;
|
|
55
57
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
56
58
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
57
59
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
58
60
|
let cmpZ = ndc.z - 0.001;
|
|
59
|
-
let ts = 1.0 /
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
let ts = 1.0 / 2048.0;
|
|
62
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
63
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
64
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
65
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
66
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
67
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
68
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
69
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
70
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
71
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
72
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const PI_M: f32 = 3.141592653589793;
|
|
@@ -98,13 +104,19 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
98
104
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
99
105
|
}
|
|
100
106
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
101
|
-
|
|
107
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
108
|
+
output.normal = skinnedNrm;
|
|
102
109
|
output.uv = uv;
|
|
103
110
|
output.worldPos = skinnedPos.xyz;
|
|
104
111
|
return output;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
struct FSOut {
|
|
115
|
+
@location(0) color: vec4f,
|
|
116
|
+
@location(1) mask: f32,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
108
120
|
let n = normalize(input.normal);
|
|
109
121
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
110
122
|
let l = -light.lights[0].direction.xyz;
|
|
@@ -118,23 +130,23 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
118
130
|
if (out_alpha < 0.001) { discard; }
|
|
119
131
|
|
|
120
132
|
// ═══ NPR toon stack (图像 → HSV.007 Val=0.8 → 转接点.001) ═══
|
|
121
|
-
let tex_tint =
|
|
133
|
+
let tex_tint = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
122
134
|
let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
123
135
|
let ramp008 = ramp_constant_edge_aa(lum_shade, METAL_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
|
|
124
136
|
let mix04_fac = math_multiply(ramp008.r, METAL_MIX04_MUL);
|
|
125
137
|
|
|
126
138
|
// 混合.004: A=HSV.002(Val=0.2 dark), B=tex_tint
|
|
127
|
-
let dark_tex =
|
|
139
|
+
let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_tint);
|
|
128
140
|
let mix04 = mix_blend(mix04_fac, dark_tex, tex_tint);
|
|
129
141
|
|
|
130
142
|
// AO white/black ramp → 混合.002 factor
|
|
131
|
-
let ao = ao_fake(n, v);
|
|
143
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
132
144
|
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
133
145
|
let overlay_fac = mix(1.0, 0.0, ao_ramp_c.r);
|
|
134
146
|
|
|
135
147
|
// 混合.002 OVERLAY: A=HSV.008(Val=1.0 identity) ← mix04, B=HSV.004(Val=2.0 bright) ← mix04
|
|
136
148
|
let hue008 = mix04; // identity HSV
|
|
137
|
-
let hue004 =
|
|
149
|
+
let hue004 = hue_sat_id(1.0, 2.0, 1.0, mix04);
|
|
138
150
|
let npr_rgb = mix_overlay(overlay_fac, hue008, hue004);
|
|
139
151
|
let npr_emission = npr_rgb * METAL_EMIT_STR;
|
|
140
152
|
|
|
@@ -145,7 +157,7 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
145
157
|
let voro = tex_voronoi_f1(refl_dir, METAL_VORONOI_SCALE);
|
|
146
158
|
let voro_ramp = ramp_linear(voro, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
147
159
|
// 混合.005: Fac=voro_ramp, A=voro_color(grayscale), B=HSV.006(Hue=0.5 Sat=1.5 Val=1.3)
|
|
148
|
-
let hue006 =
|
|
160
|
+
let hue006 = hue_sat_id(1.5, 1.2999999523162842, 1.0, tex_tint);
|
|
149
161
|
let albedo = mix_blend(voro_ramp, vec3f(voro_ramp), hue006);
|
|
150
162
|
|
|
151
163
|
// 原理化BSDF (EEVEE port): metallic=1.0, specular=1.0, roughness=0.3.
|
|
@@ -153,11 +165,12 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
153
165
|
// metallic=1 this is just albedo (specular_tint is dielectric-only and ignored here).
|
|
154
166
|
let f0 = albedo;
|
|
155
167
|
let f90 = mix(f0, vec3f(1.0), sqrt(METAL_SPECULAR));
|
|
168
|
+
let NL = max(dot(n, l), 0.0);
|
|
156
169
|
let NV = max(dot(n, v), 1e-4);
|
|
157
|
-
let
|
|
158
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
170
|
+
let brdf_lut = brdf_lut_sample(NV, METAL_ROUGHNESS);
|
|
171
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
159
172
|
|
|
160
|
-
let spec_direct = bsdf_ggx(n, l, v, METAL_ROUGHNESS) * sun * shadow *
|
|
173
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, METAL_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
161
174
|
let spec_indirect = amb;
|
|
162
175
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
163
176
|
|
|
@@ -167,7 +180,10 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
167
180
|
// 混合着色器.001 Fac=0.6967: Shader=npr_emission, Shader_001=principled
|
|
168
181
|
let final_color = mix(npr_emission, principled, METAL_MIX_SHADER_FAC);
|
|
169
182
|
|
|
170
|
-
|
|
183
|
+
var out: FSOut;
|
|
184
|
+
out.color = vec4f(final_color, out_alpha);
|
|
185
|
+
out.mask = 1.0;
|
|
186
|
+
return out;
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
`
|
package/src/shaders/nodes.ts
CHANGED
|
@@ -4,16 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
export const NODES_WGSL = /* wgsl */ `
|
|
6
6
|
|
|
7
|
-
// Baked 64×64
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Heitz 2016 LTC fit amplitude — same UV addressing as dfgLut.
|
|
15
|
-
// Used for direct-specular energy compensation (ltc_brdf_scale in closure_eval_glossy_lib.glsl).
|
|
16
|
-
@group(0) @binding(10) var ltcMag: texture_2d<f32>;
|
|
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>;
|
|
17
14
|
|
|
18
15
|
// ─── RGB ↔ HSV ──────────────────────────────────────────────────────
|
|
19
16
|
|
|
@@ -71,6 +68,23 @@ fn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec
|
|
|
71
68
|
return mix(color, hsv_to_rgb(hsv), fac);
|
|
72
69
|
}
|
|
73
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
|
+
|
|
74
88
|
// ─── BRIGHTCONTRAST node ────────────────────────────────────────────
|
|
75
89
|
|
|
76
90
|
fn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {
|
|
@@ -122,6 +136,13 @@ fn math_multiply(a: f32, b: f32) -> f32 { return a * b; }
|
|
|
122
136
|
fn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }
|
|
123
137
|
fn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }
|
|
124
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
|
+
|
|
125
146
|
// ─── MIX node (blend_type variants) ────────────────────────────────
|
|
126
147
|
|
|
127
148
|
fn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
@@ -157,18 +178,26 @@ fn luminance_rec709_linear(c: vec3f) -> f32 {
|
|
|
157
178
|
// Schlick approximation matching Blender's Fresnel node
|
|
158
179
|
|
|
159
180
|
fn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {
|
|
160
|
-
let
|
|
181
|
+
let r = (ior - 1.0) / (ior + 1.0);
|
|
182
|
+
let f0 = r * r;
|
|
161
183
|
let cos_theta = clamp(dot(n, v), 0.0, 1.0);
|
|
162
|
-
|
|
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;
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
// ─── LAYER_WEIGHT node ──────────────────────────────────────────────
|
|
166
191
|
|
|
167
192
|
fn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
168
193
|
let eta = max(1.0 - blend, 1e-4);
|
|
169
|
-
let
|
|
194
|
+
let r = (1.0 - eta) / (1.0 + eta);
|
|
195
|
+
let f0 = r * r;
|
|
170
196
|
let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);
|
|
171
|
-
|
|
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;
|
|
172
201
|
}
|
|
173
202
|
|
|
174
203
|
fn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
@@ -227,13 +256,18 @@ fn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f
|
|
|
227
256
|
// ─── NOISE texture (Perlin-style) ───────────────────────────────────
|
|
228
257
|
// Simplified gradient noise matching Blender's default noise output.
|
|
229
258
|
|
|
259
|
+
// PCG-style integer hash. Replaces the classic 'fract(sin(q) * LARGE)' trick because
|
|
260
|
+
// WebKit's Metal backend compiles 'sin' to a full transcendental op (slow), while
|
|
261
|
+
// Safari's Apple-GPU scalar ALU handles int muls/xors near free. Inputs arrive as
|
|
262
|
+
// integer-valued floats (floor(p) + unit offsets) from _noise3, so vec3i cast is exact.
|
|
230
263
|
fn _hash33(p: vec3f) -> vec3f {
|
|
231
|
-
var
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
var h = vec3u(vec3i(p) + vec3i(32768));
|
|
265
|
+
h = h * vec3u(1664525u, 1013904223u, 2654435761u);
|
|
266
|
+
h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
|
|
267
|
+
h = h ^ (h >> vec3u(16u));
|
|
268
|
+
// Mask to 24 bits — above that f32 loses precision on the u32→f32 convert.
|
|
269
|
+
let hm = h & vec3u(16777215u);
|
|
270
|
+
return vec3f(hm) * (2.0 / 16777216.0) - 1.0;
|
|
237
271
|
}
|
|
238
272
|
|
|
239
273
|
fn _noise3(p: vec3f) -> f32 {
|
|
@@ -276,6 +310,15 @@ fn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32)
|
|
|
276
310
|
return value / max(total_amp, 1e-6) * 0.5 + 0.5;
|
|
277
311
|
}
|
|
278
312
|
|
|
313
|
+
// tex_noise specialization: detail=2.0 (3 octaves), roughness=0.5, distortion=0.
|
|
314
|
+
// WebKit can't unroll tex_noise's for-loop because 'octaves' is a runtime value;
|
|
315
|
+
// this variant is fully unrolled with constants folded (total_amp = 1.75).
|
|
316
|
+
fn tex_noise_d2(p: vec3f, scale: f32) -> f32 {
|
|
317
|
+
let c = p * scale;
|
|
318
|
+
let v = _noise3(c) + 0.5 * _noise3(c * 2.0) + 0.25 * _noise3(c * 4.0);
|
|
319
|
+
return v * (1.0 / 1.75) * 0.5 + 0.5;
|
|
320
|
+
}
|
|
321
|
+
|
|
279
322
|
// ─── TEX_GRADIENT (linear) ──────────────────────────────────────────
|
|
280
323
|
// Used by Stockings preset. Maps the input vector's X to a 0–1 gradient.
|
|
281
324
|
|
|
@@ -349,13 +392,15 @@ const EEVEE_PI: f32 = 3.141592653589793;
|
|
|
349
392
|
|
|
350
393
|
// Fused analytic GGX specular (direct lights). Returns BRDF × NL.
|
|
351
394
|
// 4·NL·NV is cancelled via G1_Smith reciprocal form — see bsdf_common_lib.glsl:115.
|
|
352
|
-
|
|
395
|
+
// Caller passes NL, NV (already computed for diffuse + brdf_lut_sample) so WebKit
|
|
396
|
+
// can reuse them instead of recomputing dot products across the function boundary.
|
|
397
|
+
fn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, NL_in: f32, NV_in: f32, roughness: f32) -> f32 {
|
|
353
398
|
let a = max(roughness, 1e-4);
|
|
354
399
|
let a2 = a * a;
|
|
355
400
|
let H = normalize(L + V);
|
|
356
401
|
let NH = max(dot(N, H), 1e-8);
|
|
357
|
-
let NL = max(
|
|
358
|
-
let NV = max(
|
|
402
|
+
let NL = max(NL_in, 1e-8);
|
|
403
|
+
let NV = max(NV_in, 1e-8);
|
|
359
404
|
// G1_Smith_GGX_opti reciprocal form — denominator piece only.
|
|
360
405
|
let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);
|
|
361
406
|
let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);
|
|
@@ -376,15 +421,16 @@ fn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {
|
|
|
376
421
|
return vec2f(-1.04, 1.04) * a004 + r.zw;
|
|
377
422
|
}
|
|
378
423
|
|
|
379
|
-
// Baked
|
|
424
|
+
// Baked combined BRDF LUT — exact port of Blender bsdf_lut_frag.glsl packed with
|
|
425
|
+
// ltc_mag_ggx from eevee_lut.c. Single sample returns DFG (.rg) and LTC mag (.ba).
|
|
380
426
|
// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:
|
|
381
427
|
// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.
|
|
382
|
-
// Requires group(0) binding(9)
|
|
383
|
-
fn
|
|
428
|
+
// Requires group(0) binding(9) brdfLut + binding(2) diffuseSampler in the host shader.
|
|
429
|
+
fn brdf_lut_sample(NV: f32, roughness: f32) -> vec4f {
|
|
384
430
|
let LUT_SIZE: f32 = 64.0;
|
|
385
431
|
var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
|
|
386
432
|
uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
|
|
387
|
-
return textureSampleLevel(
|
|
433
|
+
return textureSampleLevel(brdfLut, diffuseSampler, uv, 0.0);
|
|
388
434
|
}
|
|
389
435
|
|
|
390
436
|
fn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
@@ -403,16 +449,12 @@ fn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
|
403
449
|
|
|
404
450
|
// EEVEE direct-specular energy compensation factor — closure_eval_glossy_lib.glsl:79-81:
|
|
405
451
|
// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)
|
|
406
|
-
//
|
|
407
|
-
// direct radiance is rescaled so total-energy matches
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
|
|
413
|
-
let ltc = textureSampleLevel(ltcMag, diffuseSampler, uv, 0.0).rg;
|
|
414
|
-
let dfg = textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;
|
|
415
|
-
return (ltc.x + ltc.y) / max(dfg.x + dfg.y, 1e-6);
|
|
452
|
+
// Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum;
|
|
453
|
+
// direct radiance is rescaled so total-energy matches the split-sum LUT.
|
|
454
|
+
// Takes a pre-sampled vec4f from brdf_lut_sample() to share the fetch with
|
|
455
|
+
// F_brdf_multi_scatter on the same fragment.
|
|
456
|
+
fn ltc_brdf_scale_from_lut(lut: vec4f) -> f32 {
|
|
457
|
+
return (lut.z + lut.w) / max(lut.x + lut.y, 1e-6);
|
|
416
458
|
}
|
|
417
459
|
|
|
418
460
|
// Luminance-normalized hue extraction — Blender tint_from_color (isolates hue+sat).
|
package/src/shaders/stockings.ts
CHANGED
|
@@ -51,19 +51,25 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
51
51
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
52
52
|
|
|
53
53
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
54
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
55
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
54
56
|
let biasedPos = worldPos + n * 0.08;
|
|
55
57
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
56
58
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
57
59
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
58
60
|
let cmpZ = ndc.z - 0.001;
|
|
59
|
-
let ts = 1.0 /
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
let ts = 1.0 / 2048.0;
|
|
62
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
63
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
64
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
65
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
66
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
67
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
68
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
69
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
70
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
71
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
72
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const PI_S: f32 = 3.141592653589793;
|
|
@@ -144,13 +150,19 @@ fn ramp_ease_s(f: f32, p0: f32, p1: f32) -> f32 {
|
|
|
144
150
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
145
151
|
}
|
|
146
152
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
147
|
-
|
|
153
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
154
|
+
output.normal = skinnedNrm;
|
|
148
155
|
output.uv = uv;
|
|
149
156
|
output.worldPos = skinnedPos.xyz;
|
|
150
157
|
return output;
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
struct FSOut {
|
|
161
|
+
@location(0) color: vec4f,
|
|
162
|
+
@location(1) bloom_mask: f32,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
154
166
|
let n = normalize(input.normal);
|
|
155
167
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
156
168
|
let l = -light.lights[0].direction.xyz;
|
|
@@ -194,7 +206,7 @@ fn ramp_ease_s(f: f32, p0: f32, p1: f32) -> f32 {
|
|
|
194
206
|
|
|
195
207
|
// ═══ EMISSION SHADER ═══
|
|
196
208
|
// Hue=0.5 (identity rotation), Sat=1.0, Val=5.0 (5× brightness boost), Fac=1; Strength=1
|
|
197
|
-
let emission =
|
|
209
|
+
let emission = hue_sat_id(1.0, 5.0, 1.0, tex_rgb);
|
|
198
210
|
|
|
199
211
|
// ═══ PRINCIPLED BSDF (EEVEE port) ═══
|
|
200
212
|
// base_color_tint, metallic f0, sheen coarse approx (scales diffuse radiance).
|
|
@@ -205,10 +217,10 @@ fn ramp_ease_s(f: f32, p0: f32, p1: f32) -> f32 {
|
|
|
205
217
|
let dielectric_f0 = vec3f(0.08 * STOCK_SPECULAR);
|
|
206
218
|
let f0 = mix(dielectric_f0, tex_rgb, STOCK_METALLIC);
|
|
207
219
|
let f90 = mix(f0, vec3f(1.0), sqrt(STOCK_SPECULAR));
|
|
208
|
-
let
|
|
209
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
220
|
+
let brdf_lut = brdf_lut_sample(NV, STOCK_ROUGHNESS);
|
|
221
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
210
222
|
|
|
211
|
-
let spec_direct = bsdf_ggx(n, l, v, STOCK_ROUGHNESS) * sun * shadow *
|
|
223
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, STOCK_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
212
224
|
let spec_indirect = amb;
|
|
213
225
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
214
226
|
|
|
@@ -225,7 +237,10 @@ fn ramp_ease_s(f: f32, p0: f32, p1: f32) -> f32 {
|
|
|
225
237
|
// ═══ MIX SHADER: Shader=Emission, Shader_001=Principled, Fac=mask ═══
|
|
226
238
|
let final_color = mix(emission, principled, mask);
|
|
227
239
|
|
|
228
|
-
|
|
240
|
+
var out: FSOut;
|
|
241
|
+
out.color = vec4f(final_color, out_alpha);
|
|
242
|
+
out.bloom_mask = 1.0;
|
|
243
|
+
return out;
|
|
229
244
|
}
|
|
230
245
|
|
|
231
246
|
`
|