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.
Files changed (50) hide show
  1. package/README.md +40 -22
  2. package/dist/engine.d.ts +14 -7
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +206 -77
  5. package/dist/shaders/body.d.ts +1 -1
  6. package/dist/shaders/body.d.ts.map +1 -1
  7. package/dist/shaders/body.js +58 -47
  8. package/dist/shaders/cloth_rough.d.ts +1 -1
  9. package/dist/shaders/cloth_rough.d.ts.map +1 -1
  10. package/dist/shaders/cloth_rough.js +38 -20
  11. package/dist/shaders/cloth_smooth.d.ts +1 -1
  12. package/dist/shaders/cloth_smooth.d.ts.map +1 -1
  13. package/dist/shaders/cloth_smooth.js +33 -18
  14. package/dist/shaders/default.d.ts +1 -1
  15. package/dist/shaders/default.d.ts.map +1 -1
  16. package/dist/shaders/default.js +45 -42
  17. package/dist/shaders/dfg_lut.d.ts +2 -3
  18. package/dist/shaders/dfg_lut.d.ts.map +1 -1
  19. package/dist/shaders/dfg_lut.js +30 -26
  20. package/dist/shaders/eye.d.ts +1 -1
  21. package/dist/shaders/eye.d.ts.map +1 -1
  22. package/dist/shaders/eye.js +47 -43
  23. package/dist/shaders/face.d.ts +1 -1
  24. package/dist/shaders/face.d.ts.map +1 -1
  25. package/dist/shaders/face.js +47 -23
  26. package/dist/shaders/hair.d.ts +1 -1
  27. package/dist/shaders/hair.d.ts.map +1 -1
  28. package/dist/shaders/hair.js +42 -32
  29. package/dist/shaders/metal.d.ts +1 -1
  30. package/dist/shaders/metal.d.ts.map +1 -1
  31. package/dist/shaders/metal.js +35 -19
  32. package/dist/shaders/nodes.d.ts +1 -1
  33. package/dist/shaders/nodes.d.ts.map +1 -1
  34. package/dist/shaders/nodes.js +79 -37
  35. package/dist/shaders/stockings.d.ts +1 -1
  36. package/dist/shaders/stockings.d.ts.map +1 -1
  37. package/dist/shaders/stockings.js +30 -15
  38. package/package.json +2 -2
  39. package/src/engine.ts +227 -97
  40. package/src/shaders/body.ts +58 -47
  41. package/src/shaders/cloth_rough.ts +38 -20
  42. package/src/shaders/cloth_smooth.ts +33 -18
  43. package/src/shaders/default.ts +46 -42
  44. package/src/shaders/dfg_lut.ts +32 -28
  45. package/src/shaders/eye.ts +48 -43
  46. package/src/shaders/face.ts +47 -23
  47. package/src/shaders/hair.ts +42 -32
  48. package/src/shaders/metal.ts +35 -19
  49. package/src/shaders/nodes.ts +79 -37
  50. package/src/shaders/stockings.ts +30 -15
@@ -48,43 +48,38 @@ struct LightVP { viewProj: mat4x4f, };
48
48
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
49
49
 
50
50
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
51
+ // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
52
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
51
53
  let biasedPos = worldPos + n * 0.08;
52
54
  let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
53
55
  let ndc = lclip.xyz / max(lclip.w, 1e-6);
54
56
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
55
57
  let cmpZ = ndc.z - 0.001;
56
- let ts = 1.0 / 4096.0;
57
- var vis = 0.0;
58
- for (var y = -1; y <= 1; y++) {
59
- for (var x = -1; x <= 1; x++) {
60
- vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
61
- }
62
- }
63
- return vis / 9.0;
58
+ let ts = 1.0 / 2048.0;
59
+ // 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
60
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
61
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
62
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
63
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
64
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
65
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
66
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
67
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
68
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
69
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
64
70
  }
65
71
 
66
72
  const PI_B: f32 = 3.141592653589793;
67
- const F0_BODY: f32 = 0.04;
68
73
  const BODY_ROUGHNESS: f32 = 0.3;
69
74
  // Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
70
75
  const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
71
76
  const BODY_RIM2_POW: f32 = 1.4300000667572021;
72
77
  const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
73
78
  const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
79
+ const BODY_SPECULAR: f32 = 0.5;
74
80
  const BODY_MIX_NPR: f32 = 0.5;
75
-
76
- fn ggx_d_body(ndoth: f32, a2: f32) -> f32 {
77
- let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
78
- return a2 / (PI_B * denom * denom);
79
- }
80
-
81
- fn smith_g1_body(ndotx: f32, a2: f32) -> f32 {
82
- return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
83
- }
84
-
85
- fn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {
86
- return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
87
- }
81
+ // EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
82
+ const BODY_SPEC_CLAMP: f32 = 10.0;
88
83
 
89
84
  // smoothstep-based ramp: t*t*(3-2*t) between two color stops
90
85
  fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
@@ -114,13 +109,19 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
114
109
  skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
115
110
  }
116
111
  output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
117
- output.normal = normalize(skinnedNrm);
112
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
113
+ output.normal = skinnedNrm;
118
114
  output.uv = uv;
119
115
  output.worldPos = skinnedPos.xyz;
120
116
  return output;
121
117
  }
122
118
 
123
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
119
+ struct FSOut {
120
+ @location(0) color: vec4f,
121
+ @location(1) mask: f32,
122
+ };
123
+
124
+ @fragment fn fs(input: VertexOutput) -> FSOut {
124
125
  let alpha = material.alpha;
125
126
  if (alpha < 0.001) { discard; }
126
127
 
@@ -137,13 +138,13 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
137
138
  let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
138
139
 
139
140
  // ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
140
- let shadow_tint = hue_sat(0.5, 2.0, 0.3499999940395355, 1.0, tex_color);
141
- let lit_tint = hue_sat(0.5, 1.5, 1.0, 1.0, tex_color);
141
+ let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
142
+ let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
142
143
  let toon_color = mix_blend(toon, shadow_tint, lit_tint);
143
144
  let bc = bright_contrast(toon_color, 0.1, 0.2);
144
145
 
145
146
  // ═══ AO CHAIN: AO → ramp CONSTANT [0→white, 0.5995→black] → Mix.003 ═══
146
- let ao = ao_fake(n, v);
147
+ let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
147
148
  let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
148
149
  let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));
149
150
 
@@ -174,38 +175,48 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
174
175
  let npr_stack = add0 + warm_emission;
175
176
 
176
177
  // ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
177
- let gen = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0, 1.0, 1.5));
178
- let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);
178
+ // Mapping loc=rot=0 plain scale multiply, inline.
179
+ let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
179
180
  let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
180
181
  let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
181
182
 
182
183
  let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
183
184
  let p_emission = bc * 0.2;
184
185
 
185
- let ao2 = ao_fake(n, v);
186
- let sss = ramp_linear(ao2, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
187
-
188
- let p_ndotl = max(dot(bumped_n, l), 0.0);
189
- let p_ndotv = max(dot(bumped_n, v), 0.001);
190
- let h = normalize(l + v);
191
- let p_ndoth = max(dot(bumped_n, h), 0.0);
192
- let p_vdoth = max(dot(v, h), 0.0);
193
- let a2 = BODY_ROUGHNESS * BODY_ROUGHNESS;
194
- let D = ggx_d_body(p_ndoth, a2);
195
- let G = smith_g1_body(p_ndotl, a2) * smith_g1_body(p_ndotv, a2);
196
- let F = fresnel_schlick_body(p_vdoth, F0_BODY);
197
- let spec = (D * G * F) / max(4.0 * p_ndotl * p_ndotv, 0.001) * ltc_brdf_scale(p_ndotv, BODY_ROUGHNESS);
198
- let kd = (1.0 - F) * principled_base / PI_B;
199
- let direct = (kd + spec) * sun * p_ndotl * shadow;
186
+ // Reuse 'ao' (ao_fake(n, v) above) — identical inputs, avoid a second procedural AO pass.
187
+ let sss = ramp_linear(ao, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
188
+
189
+ // 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
190
+ let NL = max(dot(bumped_n, l), 0.0);
191
+ let NV = max(dot(bumped_n, v), 1e-4);
192
+
193
+ // f0/f90 per gpu_shader_material_principled.glsl specular_tint=0 → dielectric_f0_color=white.
194
+ let f0 = vec3f(0.08 * BODY_SPECULAR);
195
+ let f90 = mix(f0, vec3f(1.0), sqrt(BODY_SPECULAR));
196
+ let brdf_lut = brdf_lut_sample(NV, BODY_ROUGHNESS);
197
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
198
+
199
+ // Direct glossy bsdf_ggx already includes NL; no F applied here (tinted after accum).
200
+ // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
201
+ let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, BODY_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
202
+ let spec_direct = min(spec_direct_raw, vec3f(BODY_SPEC_CLAMP));
203
+ // Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
204
+ let spec_indirect = light.ambientColor.xyz;
205
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
206
+
200
207
  // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
201
208
  // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
202
- let ambient = principled_base * light.ambientColor.xyz;
203
- let principled = ambient + direct + p_emission + vec3f(sss);
209
+ // No (1-F) factor per EEVEE — it doesn't energy-conserve spec<->diffuse.
210
+ let diffuse_radiance = principled_base * (sun * NL * shadow / PI_B + light.ambientColor.xyz);
211
+ let principled = diffuse_radiance + spec_radiance + p_emission + vec3f(sss);
204
212
 
205
213
  // 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
206
214
  let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
207
215
 
208
- return vec4f(final_color, alpha);
216
+ var out: FSOut;
217
+ out.color = vec4f(final_color, alpha);
218
+ out.mask = 1.0;
219
+ return out;
209
220
  }
210
221
 
211
222
  `
@@ -49,19 +49,25 @@ struct LightVP { viewProj: mat4x4f, };
49
49
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
50
50
 
51
51
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
52
+ // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
53
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
52
54
  let biasedPos = worldPos + n * 0.08;
53
55
  let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
54
56
  let ndc = lclip.xyz / max(lclip.w, 1e-6);
55
57
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
56
58
  let cmpZ = ndc.z - 0.001;
57
- let ts = 1.0 / 4096.0;
58
- var vis = 0.0;
59
- for (var y = -1; y <= 1; y++) {
60
- for (var x = -1; x <= 1; x++) {
61
- vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
62
- }
63
- }
64
- return vis / 9.0;
59
+ let ts = 1.0 / 2048.0;
60
+ // 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
61
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
62
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
63
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
64
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
65
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
66
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
67
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
68
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
69
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
70
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
65
71
  }
66
72
 
67
73
  const PI_CR: f32 = 3.141592653589793;
@@ -73,6 +79,8 @@ const CLOTH_R_EMIT_STR: f32 = 18.200000762939453;
73
79
  const CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;
74
80
  const CLOTH_R_NOISE_SCALE: f32 = 17.7;
75
81
  const CLOTH_R_BUMP_STR: f32 = 1.0;
82
+ // EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
83
+ const CLOTH_R_SPEC_CLAMP: f32 = 10.0;
76
84
 
77
85
  @vertex fn vs(
78
86
  @location(0) position: vec3f,
@@ -95,13 +103,19 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
95
103
  skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
96
104
  }
97
105
  output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
98
- output.normal = normalize(skinnedNrm);
106
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
107
+ output.normal = skinnedNrm;
99
108
  output.uv = uv;
100
109
  output.worldPos = skinnedPos.xyz;
101
110
  return output;
102
111
  }
103
112
 
104
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
113
+ struct FSOut {
114
+ @location(0) color: vec4f,
115
+ @location(1) mask: f32,
116
+ };
117
+
118
+ @fragment fn fs(input: VertexOutput) -> FSOut {
105
119
  let n = normalize(input.normal);
106
120
  let v = normalize(camera.viewPos - input.worldPos);
107
121
  let l = -light.lights[0].direction.xyz;
@@ -121,7 +135,7 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
121
135
  let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);
122
136
 
123
137
  // 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
124
- let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);
138
+ let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
125
139
  let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
126
140
 
127
141
  // 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
@@ -129,34 +143,35 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
129
143
  let mix03 = mix_blend(bevel_z, mix04, dark_tex);
130
144
 
131
145
  // 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (white/black) → 混合.002 OVERLAY Fac
132
- let ao = ao_fake(n, v);
146
+ let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
133
147
  let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
134
148
  let mix01_fac = ao_ramp_c.r;
135
149
  let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
136
150
 
137
151
  // 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
138
- let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);
152
+ let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
139
153
  let overlay_fac = mix01_rgb.r;
140
154
  let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
141
155
  let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
142
156
 
143
157
  // 噪波→渐变→凹凸 (LIVE in M_Rough_Cloth — unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.
144
- let noise_uv = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0));
145
- let noise_val = tex_noise(noise_uv, CLOTH_R_NOISE_SCALE, 2.0, 0.5, 0.0);
158
+ // mapping scale=(1,1,1), loc=rot=0 → identity; use worldPos directly.
159
+ let noise_val = tex_noise_d2(input.worldPos, CLOTH_R_NOISE_SCALE);
146
160
  let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
147
161
  let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
148
162
 
149
163
  // 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.
150
- let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);
164
+ let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
151
165
  let NL = max(dot(bumped_n, l), 0.0);
152
166
  let NV = max(dot(bumped_n, v), 1e-4);
153
167
 
154
168
  let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);
155
169
  let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));
156
- let split_sum = brdf_lut_baked(NV, CLOTH_R_ROUGHNESS);
157
- let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
170
+ let brdf_lut = brdf_lut_sample(NV, CLOTH_R_ROUGHNESS);
171
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
158
172
 
159
- let spec_direct = bsdf_ggx(bumped_n, l, v, CLOTH_R_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_R_ROUGHNESS);
173
+ let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, CLOTH_R_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
174
+ let spec_direct = min(spec_direct_raw, vec3f(CLOTH_R_SPEC_CLAMP));
160
175
  let spec_indirect = amb;
161
176
  let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
162
177
 
@@ -168,7 +183,10 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
168
183
  // 混合着色器.001 Fac=0.9: Shader=自发光.005, Shader_001=原理化BSDF
169
184
  let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);
170
185
 
171
- return vec4f(final_color, out_alpha);
186
+ var out: FSOut;
187
+ out.color = vec4f(final_color, out_alpha);
188
+ out.mask = 1.0;
189
+ return out;
172
190
  }
173
191
 
174
192
  `
@@ -48,19 +48,25 @@ struct LightVP { viewProj: mat4x4f, };
48
48
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
49
49
 
50
50
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
51
+ // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
52
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
51
53
  let biasedPos = worldPos + n * 0.08;
52
54
  let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
53
55
  let ndc = lclip.xyz / max(lclip.w, 1e-6);
54
56
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
55
57
  let cmpZ = ndc.z - 0.001;
56
- let ts = 1.0 / 4096.0;
57
- var vis = 0.0;
58
- for (var y = -1; y <= 1; y++) {
59
- for (var x = -1; x <= 1; x++) {
60
- vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
61
- }
62
- }
63
- return vis / 9.0;
58
+ let ts = 1.0 / 2048.0;
59
+ // 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
60
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
61
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
62
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
63
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
64
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
65
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
66
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
67
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
68
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
69
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
64
70
  }
65
71
 
66
72
  const PI_C: f32 = 3.141592653589793;
@@ -92,13 +98,19 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
92
98
  skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
93
99
  }
94
100
  output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
95
- output.normal = normalize(skinnedNrm);
101
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
102
+ output.normal = skinnedNrm;
96
103
  output.uv = uv;
97
104
  output.worldPos = skinnedPos.xyz;
98
105
  return output;
99
106
  }
100
107
 
101
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
108
+ struct FSOut {
109
+ @location(0) color: vec4f,
110
+ @location(1) mask: f32,
111
+ };
112
+
113
+ @fragment fn fs(input: VertexOutput) -> FSOut {
102
114
  let n = normalize(input.normal);
103
115
  let v = normalize(camera.viewPos - input.worldPos);
104
116
  let l = -light.lights[0].direction.xyz;
@@ -119,7 +131,7 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
119
131
  let mix04_fac = math_multiply(toon_r, CLOTH_MIX04_MUL);
120
132
 
121
133
  // 混合.004: A=色相/饱和度/明度.002, B=纹理
122
- let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);
134
+ let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
123
135
  let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
124
136
 
125
137
  // 倒角.001→Z → 混合.003 Factor; A=混合.004, B=色相/饱和度/明度.002
@@ -127,32 +139,32 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
127
139
  let mix03 = mix_blend(bevel_z, mix04, dark_tex);
128
140
 
129
141
  // 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (白/黑) → 混合.002 OVERLAY Fac
130
- let ao = ao_fake(n, v);
142
+ let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
131
143
  let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
132
144
  let mix01_fac = ao_ramp_c.r;
133
145
  let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
134
146
 
135
147
  // 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
136
- let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);
148
+ let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
137
149
  let overlay_fac = mix01_rgb.r;
138
150
  let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
139
151
  let npr_emission = npr_rgb * NPR_EMIT_STR;
140
152
 
141
153
  // 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.5, specular_tint=0.
142
154
  // Bump subtree is dead in the Blender graph (噪波→凹凸 not linked to Principled.Normal).
143
- let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);
155
+ let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
144
156
  let NL = max(dot(n, l), 0.0);
145
157
  let NV = max(dot(n, v), 1e-4);
146
158
 
147
159
  // f0/f90 per gpu_shader_material_principled.glsl — specular_tint=0 → dielectric_f0_color=white.
148
160
  let f0 = vec3f(0.08 * CLOTH_SPECULAR);
149
161
  let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_SPECULAR));
150
- let split_sum = brdf_lut_baked(NV, CLOTH_ROUGHNESS);
151
- let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
162
+ let brdf_lut = brdf_lut_sample(NV, CLOTH_ROUGHNESS);
163
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
152
164
 
153
165
  // Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after accum).
154
166
  // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
155
- let spec_direct = bsdf_ggx(n, l, v, CLOTH_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_ROUGHNESS);
167
+ let spec_direct = bsdf_ggx(n, l, v, NL, NV, CLOTH_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
156
168
  // Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
157
169
  let spec_indirect = amb;
158
170
  let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
@@ -167,7 +179,10 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
167
179
  // 混合着色器.001: Shader=自发光.005, Shader_001=原理化BSDF, Fac=0.9
168
180
  let final_color = mix(npr_emission, principled, NPR_MIX_SHADER_FAC);
169
181
 
170
- return vec4f(final_color, out_alpha);
182
+ var out: FSOut;
183
+ out.color = vec4f(final_color, out_alpha);
184
+ out.mask = 1.0;
185
+ return out;
171
186
  }
172
187
 
173
188
  `
@@ -2,10 +2,14 @@
2
2
  // Metallic=0, Specular=0.5 (F0=0.04), Roughness=0.5.
3
3
  // Tone mapping via LUT sampled from Blender's OCIO pipeline (exposure -0.3 baked in).
4
4
 
5
+ import { NODES_WGSL } from "./nodes"
6
+
5
7
  export const DEFAULT_SHADER_WGSL = /* wgsl */ `
6
8
 
9
+ ${NODES_WGSL}
10
+
7
11
  const PI: f32 = 3.141592653589793;
8
- const F0_DIELECTRIC: f32 = 0.04;
12
+ const DEFAULT_SPECULAR: f32 = 0.5;
9
13
  const ROUGHNESS: f32 = 0.5;
10
14
 
11
15
  struct CameraUniforms {
@@ -52,21 +56,6 @@ struct LightVP { viewProj: mat4x4f, };
52
56
  @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
53
57
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
54
58
 
55
- // ─── GGX specular helpers ───────────────────────────────────────────
56
-
57
- fn ggx_d(ndoth: f32, a2: f32) -> f32 {
58
- let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
59
- return a2 / (PI * denom * denom);
60
- }
61
-
62
- fn smith_g1(ndotx: f32, a2: f32) -> f32 {
63
- return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
64
- }
65
-
66
- fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
67
- return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
68
- }
69
-
70
59
  // ─── Filmic tone mapping (LUT extracted from Blender 3.6 OCIO) ─────
71
60
  // View transform = Filmic, Look = Medium High Contrast, Exposure = -0.3.
72
61
  // 14 samples at integer log2 stops from -10 to +3 (inclusive).
@@ -91,19 +80,25 @@ fn tonemap(hdr: vec3f) -> vec3f {
91
80
  // ─── Shadow sampling (3×3 PCF) ──────────────────────────────────────
92
81
 
93
82
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
83
+ // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
84
+ if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
94
85
  let biasedPos = worldPos + n * 0.08;
95
86
  let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
96
87
  let ndc = lclip.xyz / max(lclip.w, 1e-6);
97
88
  let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
98
89
  let cmpZ = ndc.z - 0.001;
99
- let ts = 1.0 / 4096.0;
100
- var vis = 0.0;
101
- for (var y = -1; y <= 1; y++) {
102
- for (var x = -1; x <= 1; x++) {
103
- vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
104
- }
105
- }
106
- return vis / 9.0;
90
+ let ts = 1.0 / 2048.0;
91
+ // 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
92
+ let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
93
+ let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
94
+ let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
95
+ let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
96
+ let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
97
+ let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
98
+ let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
99
+ let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
100
+ let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
101
+ return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
107
102
  }
108
103
 
109
104
  // ─── Vertex / Fragment ──────────────────────────────────────────────
@@ -129,13 +124,19 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
129
124
  skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
130
125
  }
131
126
  output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
132
- output.normal = normalize(skinnedNrm);
127
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
128
+ output.normal = skinnedNrm;
133
129
  output.uv = uv;
134
130
  output.worldPos = skinnedPos.xyz;
135
131
  return output;
136
132
  }
137
133
 
138
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
134
+ struct FSOut {
135
+ @location(0) color: vec4f,
136
+ @location(1) mask: f32,
137
+ };
138
+
139
+ @fragment fn fs(input: VertexOutput) -> FSOut {
139
140
  let alpha = material.alpha;
140
141
  if (alpha < 0.001) { discard; }
141
142
 
@@ -144,26 +145,29 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
144
145
  let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
145
146
 
146
147
  let l = -light.lights[0].direction.xyz;
147
- let sunColor = light.lights[0].color.xyz * light.lights[0].color.w;
148
- let h = normalize(l + v);
148
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
149
+ let amb = light.ambientColor.xyz;
150
+ let shadow = sampleShadow(input.worldPos, n);
149
151
 
150
- let ndotl = max(dot(n, l), 0.0);
151
- let ndotv = max(dot(n, v), 0.001);
152
- let ndoth = max(dot(n, h), 0.0);
153
- let vdoth = max(dot(v, h), 0.0);
152
+ // 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.5, specular_tint=0.
153
+ let NL = max(dot(n, l), 0.0);
154
+ let NV = max(dot(n, v), 1e-4);
154
155
 
155
- let a2 = ROUGHNESS * ROUGHNESS;
156
- let D = ggx_d(ndoth, a2);
157
- let G = smith_g1(ndotl, a2) * smith_g1(ndotv, a2);
158
- let F = fresnel_schlick(vdoth, F0_DIELECTRIC);
159
- let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
156
+ let f0 = vec3f(0.08 * DEFAULT_SPECULAR);
157
+ let f90 = mix(f0, vec3f(1.0), sqrt(DEFAULT_SPECULAR));
158
+ let brdf_lut = brdf_lut_sample(NV, ROUGHNESS);
159
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
160
160
 
161
- let shadow = sampleShadow(input.worldPos, n);
162
- let kd = (1.0 - F) * albedo / PI;
163
- let direct = (kd + spec) * sunColor * ndotl * shadow;
164
- let ambient = albedo * light.ambientColor.xyz;
161
+ let spec_direct = bsdf_ggx(n, l, v, NL, NV, ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
162
+ let spec_indirect = amb;
163
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
164
+
165
+ let diffuse_radiance = albedo * (sun * NL * shadow / PI + amb);
165
166
 
166
- return vec4f(ambient + direct, alpha);
167
+ var out: FSOut;
168
+ out.color = vec4f(diffuse_radiance + spec_radiance, alpha);
169
+ out.mask = 1.0;
170
+ return out;
167
171
  }
168
172
 
169
173
  `