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.
Files changed (110) hide show
  1. package/dist/engine.d.ts +5 -3
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +72 -425
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/shaders/body.d.ts +1 -1
  7. package/dist/shaders/body.d.ts.map +1 -1
  8. package/dist/shaders/body.js +27 -60
  9. package/dist/shaders/cloth_rough.d.ts +1 -1
  10. package/dist/shaders/cloth_rough.d.ts.map +1 -1
  11. package/dist/shaders/cloth_rough.js +4 -16
  12. package/dist/shaders/cloth_smooth.d.ts +1 -1
  13. package/dist/shaders/cloth_smooth.d.ts.map +1 -1
  14. package/dist/shaders/cloth_smooth.js +5 -17
  15. package/dist/shaders/default.d.ts +1 -1
  16. package/dist/shaders/default.d.ts.map +1 -1
  17. package/dist/shaders/default.js +20 -34
  18. package/dist/shaders/dfg_lut.d.ts +1 -1
  19. package/dist/shaders/dfg_lut.d.ts.map +1 -1
  20. package/dist/shaders/dfg_lut.js +1 -1
  21. package/dist/shaders/eye.d.ts +1 -1
  22. package/dist/shaders/eye.d.ts.map +1 -1
  23. package/dist/shaders/eye.js +22 -35
  24. package/dist/shaders/face.d.ts +1 -1
  25. package/dist/shaders/face.d.ts.map +1 -1
  26. package/dist/shaders/face.js +21 -57
  27. package/dist/shaders/hair.d.ts +1 -1
  28. package/dist/shaders/hair.d.ts.map +1 -1
  29. package/dist/shaders/hair.js +7 -27
  30. package/dist/shaders/materials/body.d.ts +2 -0
  31. package/dist/shaders/materials/body.d.ts.map +1 -0
  32. package/dist/shaders/materials/body.js +199 -0
  33. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  34. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  35. package/dist/shaders/materials/cloth_rough.js +178 -0
  36. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  37. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  38. package/dist/shaders/materials/cloth_smooth.js +174 -0
  39. package/dist/shaders/materials/default.d.ts +2 -0
  40. package/dist/shaders/materials/default.d.ts.map +1 -0
  41. package/dist/shaders/materials/default.js +171 -0
  42. package/dist/shaders/materials/eye.d.ts +2 -0
  43. package/dist/shaders/materials/eye.d.ts.map +1 -0
  44. package/dist/shaders/materials/eye.js +146 -0
  45. package/dist/shaders/materials/face.d.ts +2 -0
  46. package/dist/shaders/materials/face.d.ts.map +1 -0
  47. package/dist/shaders/materials/face.js +199 -0
  48. package/dist/shaders/materials/hair.d.ts +2 -0
  49. package/dist/shaders/materials/hair.d.ts.map +1 -0
  50. package/dist/shaders/materials/hair.js +176 -0
  51. package/dist/shaders/materials/metal.d.ts +2 -0
  52. package/dist/shaders/materials/metal.d.ts.map +1 -0
  53. package/dist/shaders/materials/metal.js +183 -0
  54. package/dist/shaders/materials/nodes.d.ts +2 -0
  55. package/dist/shaders/materials/nodes.d.ts.map +1 -0
  56. package/{src/shaders/nodes.ts → dist/shaders/materials/nodes.js} +32 -16
  57. package/dist/shaders/materials/stockings.d.ts +2 -0
  58. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  59. package/dist/shaders/materials/stockings.js +244 -0
  60. package/dist/shaders/metal.d.ts +1 -1
  61. package/dist/shaders/metal.d.ts.map +1 -1
  62. package/dist/shaders/metal.js +4 -17
  63. package/dist/shaders/nodes.d.ts +1 -1
  64. package/dist/shaders/nodes.d.ts.map +1 -1
  65. package/dist/shaders/nodes.js +0 -9
  66. package/dist/shaders/passes/bloom.d.ts +4 -0
  67. package/dist/shaders/passes/bloom.d.ts.map +1 -0
  68. package/dist/shaders/passes/bloom.js +117 -0
  69. package/dist/shaders/passes/composite.d.ts +2 -0
  70. package/dist/shaders/passes/composite.d.ts.map +1 -0
  71. package/dist/shaders/passes/composite.js +61 -0
  72. package/dist/shaders/passes/ground.d.ts +2 -0
  73. package/dist/shaders/passes/ground.d.ts.map +1 -0
  74. package/dist/shaders/passes/ground.js +93 -0
  75. package/dist/shaders/passes/mipmap.d.ts +2 -0
  76. package/dist/shaders/passes/mipmap.d.ts.map +1 -0
  77. package/dist/shaders/passes/mipmap.js +16 -0
  78. package/dist/shaders/passes/outline.d.ts +2 -0
  79. package/dist/shaders/passes/outline.d.ts.map +1 -0
  80. package/dist/shaders/passes/outline.js +83 -0
  81. package/dist/shaders/passes/pick.d.ts +2 -0
  82. package/dist/shaders/passes/pick.d.ts.map +1 -0
  83. package/dist/shaders/passes/pick.js +39 -0
  84. package/dist/shaders/passes/shadow.d.ts +2 -0
  85. package/dist/shaders/passes/shadow.d.ts.map +1 -0
  86. package/dist/shaders/passes/shadow.js +16 -0
  87. package/dist/shaders/stockings.d.ts +1 -1
  88. package/dist/shaders/stockings.d.ts.map +1 -1
  89. package/package.json +2 -2
  90. package/src/engine.ts +112 -449
  91. package/src/index.ts +3 -2
  92. package/src/shaders/dfg_lut.ts +1 -1
  93. package/src/shaders/{body.ts → materials/body.ts} +27 -60
  94. package/src/shaders/{cloth_rough.ts → materials/cloth_rough.ts} +4 -16
  95. package/src/shaders/{cloth_smooth.ts → materials/cloth_smooth.ts} +5 -17
  96. package/src/shaders/{default.ts → materials/default.ts} +21 -34
  97. package/src/shaders/{eye.ts → materials/eye.ts} +23 -35
  98. package/src/shaders/{face.ts → materials/face.ts} +21 -57
  99. package/src/shaders/{hair.ts → materials/hair.ts} +7 -27
  100. package/src/shaders/{metal.ts → materials/metal.ts} +15 -19
  101. package/src/shaders/materials/nodes.ts +483 -0
  102. package/src/shaders/passes/bloom.ts +121 -0
  103. package/src/shaders/passes/composite.ts +62 -0
  104. package/src/shaders/passes/ground.ts +94 -0
  105. package/src/shaders/passes/mipmap.ts +17 -0
  106. package/src/shaders/passes/outline.ts +84 -0
  107. package/src/shaders/passes/pick.ts +40 -0
  108. package/src/shaders/passes/shadow.ts +17 -0
  109. package/src/shaders/classify.ts +0 -25
  110. /package/src/shaders/{stockings.ts → materials/stockings.ts} +0 -0
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export {
7
7
  type BloomOptions,
8
8
  type ViewTransformOptions,
9
9
  type LoadModelFromFilesOptions,
10
+ type MaterialPreset,
11
+ type MaterialPresetMap,
10
12
  } from "./engine"
11
13
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
12
14
  export { Model } from "./model"
@@ -21,5 +23,4 @@ export type {
21
23
  ControlPoint,
22
24
  } from "./animation"
23
25
  export { FPS } from "./animation"
24
- export { Physics, type PhysicsOptions } from "./physics"
25
- export type { MaterialPreset, MaterialPresetMap } from "./shaders/classify"
26
+ export { Physics, type PhysicsOptions } from "./physics"
@@ -85,7 +85,7 @@ fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
85
85
  let x_uv = floor(frag.x) / (LUT_SIZE - 1.0);
86
86
 
87
87
  let NV = clamp(1.0 - y_uv * y_uv, 1e-4, 0.9999);
88
- let a = x_uv * x_uv;
88
+ let a = max(x_uv, 1e-4);
89
89
  let a2 = clamp(a * a, 1e-4, 0.9999);
90
90
 
91
91
  let V = vec3f(sqrt(1.0 - NV * NV), 0.0, NV);
@@ -70,32 +70,16 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
70
70
  }
71
71
 
72
72
  const PI_B: f32 = 3.141592653589793;
73
- const F0_BODY: f32 = 0.04;
74
73
  const BODY_ROUGHNESS: f32 = 0.3;
75
- // Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
76
74
  const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
77
75
  const BODY_RIM2_POW: f32 = 1.4300000667572021;
78
76
  const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
79
- const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
77
+ const BODY_WARM_STR: f32 = 0.30000001192092896;
78
+ const BODY_SPECULAR: f32 = 0.5;
80
79
  const BODY_MIX_NPR: f32 = 0.5;
81
80
  // EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
82
81
  const BODY_SPEC_CLAMP: f32 = 10.0;
83
82
 
84
- fn ggx_d_body(ndoth: f32, a2: f32) -> f32 {
85
- let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
86
- return a2 / (PI_B * denom * denom);
87
- }
88
-
89
- fn smith_g1_body(ndotx: f32, a2: f32) -> f32 {
90
- return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
91
- }
92
-
93
- fn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {
94
- let m = 1.0 - cosTheta;
95
- let m2 = m * m;
96
- return f0 + (1.0 - f0) * (m2 * m2 * m);
97
- }
98
-
99
83
  // smoothstep-based ramp: t*t*(3-2*t) between two color stops
100
84
  fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
101
85
  let t = saturate((f - p0) / max(p1 - p0, 1e-6));
@@ -148,49 +132,33 @@ struct FSOut {
148
132
  let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
149
133
  let shadow = sampleShadow(input.worldPos, n);
150
134
 
151
- // ═══ TOON MASK: ShaderToRGB → ramp.008 CONSTANT [0→black, 0.2966→white] ═══
152
135
  let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
153
136
  let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
154
137
 
155
- // ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
156
138
  let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
157
139
  let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
158
140
  let toon_color = mix_blend(toon, shadow_tint, lit_tint);
159
141
  let bc = bright_contrast(toon_color, 0.1, 0.2);
160
142
 
161
- // ═══ AO CHAIN: AO → ramp CONSTANT [0→white, 0.5995→black] → Mix.003 ═══
162
- let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
163
- let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
164
- let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));
165
-
166
- // ═══ EMISSION.003 (strength=4.0) ═══
167
- let emission3 = ao_mixed * 4.0;
143
+ let emission3 = bc * 4.0;
168
144
 
169
- // ═══ WARM: 颜色渐变.008 → 运算.006 ADD +0.5 (m_graphs) → clamp → 颜色渐变.003 ═══
170
- let ao_inv = invert_f(1.0, ao_ramp);
171
- let warm_str = ao_inv * BODY_WARM_AO_MUL;
172
145
  let warm_input = clamp(toon + 0.5, 0.0, 1.0);
173
146
  let warm_color = ramp_cardinal(warm_input, 0.2409,
174
147
  vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
175
148
  vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
176
- let warm_emission = warm_color * warm_str;
149
+ let warm_emission = warm_color * BODY_WARM_STR;
177
150
 
178
- // ═══ RIM 1: 菲涅尔 × 层权重.001 Facing Blend=0.24 → 自发光 Strength ═══
179
151
  let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
180
152
  let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
181
153
 
182
- // ═══ RIM 2: 层权重.002 Facing → 运算.007 POWER → 颜色渐变.010 EASE → MixShader.002 Fac ═══
183
154
  let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
184
155
  let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
185
156
  let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
186
157
  let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
187
158
 
188
- // ═══ NPR STACK: AddShader chain (no bright gate in body) ═══
189
- let add0 = rim1 + rim2_mixed;
190
- let npr_stack = add0 + warm_emission;
159
+ let npr_stack = rim1 + rim2_mixed + warm_emission;
191
160
 
192
- // ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
193
- // Mapping loc=rot=0 → plain scale multiply, inline.
161
+ // Noise bump Mapping loc=rot=0 folds to a plain scale multiply.
194
162
  let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
195
163
  let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
196
164
  let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
@@ -198,31 +166,30 @@ struct FSOut {
198
166
  let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
199
167
  let p_emission = bc * 0.2;
200
168
 
201
- // Reuse 'ao' (ao_fake(n, v) above) identical inputs, avoid a second procedural AO pass.
202
- let sss = ramp_linear(ao, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
203
-
204
- let p_ndotl = max(dot(bumped_n, l), 0.0);
205
- let p_ndotv = max(dot(bumped_n, v), 0.001);
206
- let h = normalize(l + v);
207
- let p_ndoth = max(dot(bumped_n, h), 0.0);
208
- let p_vdoth = max(dot(v, h), 0.0);
209
- let a2 = BODY_ROUGHNESS * BODY_ROUGHNESS;
210
- let D = ggx_d_body(p_ndoth, a2);
211
- let G = smith_g1_body(p_ndotl, a2) * smith_g1_body(p_ndotv, a2);
212
- let F = fresnel_schlick_body(p_vdoth, F0_BODY);
213
- let brdf_lut = brdf_lut_sample(p_ndotv, BODY_ROUGHNESS);
214
- let spec = (D * G * F) / max(4.0 * p_ndotl * p_ndotv, 0.001) * ltc_brdf_scale_from_lut(brdf_lut);
215
- let kd = (1.0 - F) * principled_base / PI_B;
216
- // Split so we can clamp only the spec firefly contribution (EEVEE Light Clamp).
217
- let spec_radiance = vec3f(spec) * sun * p_ndotl * shadow;
218
- let spec_clamped = min(spec_radiance, vec3f(BODY_SPEC_CLAMP));
219
- let direct = kd * sun * p_ndotl * shadow + spec_clamped;
169
+ // Principled BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
170
+ let NL = max(dot(bumped_n, l), 0.0);
171
+ let NV = max(dot(bumped_n, v), 1e-4);
172
+
173
+ // f0/f90 per gpu_shader_material_principled.glsl specular_tint=0 → dielectric_f0_color=white.
174
+ let f0 = vec3f(0.08 * BODY_SPECULAR);
175
+ let f90 = mix(f0, vec3f(1.0), sqrt(BODY_SPECULAR));
176
+ let brdf_lut = brdf_lut_sample(NV, BODY_ROUGHNESS);
177
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
178
+
179
+ // Direct glossy bsdf_ggx already includes NL; no F applied here (tinted after accum).
180
+ // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
181
+ let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, BODY_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
182
+ let spec_direct = min(spec_direct_raw, vec3f(BODY_SPEC_CLAMP));
183
+ // Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
184
+ let spec_indirect = light.ambientColor.xyz;
185
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
186
+
220
187
  // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
221
188
  // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
222
- let ambient = principled_base * light.ambientColor.xyz;
223
- let principled = ambient + direct + p_emission + vec3f(sss);
189
+ // No (1-F) factor per EEVEE — it doesn't energy-conserve spec<->diffuse.
190
+ let diffuse_radiance = principled_base * (sun * NL * shadow / PI_B + light.ambientColor.xyz);
191
+ let principled = diffuse_radiance + spec_radiance + p_emission;
224
192
 
225
- // 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
226
193
  let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
227
194
 
228
195
  var out: FSOut;
@@ -128,34 +128,22 @@ struct FSOut {
128
128
  let out_alpha = material.alpha * tex_s.a;
129
129
  if (out_alpha < 0.001) { discard; }
130
130
 
131
- // Shader→RGB → 颜色渐变.008 CONSTANT (edge AA terminator)
132
131
  let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
132
+ // ramp_constant_edge_aa: avoids binary fac shimmer on terminator (fwidth + smoothstep).
133
133
  let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_R_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
134
- let toon_r = ramp008.r;
135
- let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);
134
+ let mix04_fac = math_multiply(ramp008.r, CLOTH_R_MIX04_MUL);
136
135
 
137
- // 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
138
136
  let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
139
137
  let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
140
138
 
141
- // 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
142
139
  let bevel_z = clamp(n.y, 0.0, 1.0);
143
140
  let mix03 = mix_blend(bevel_z, mix04, dark_tex);
144
141
 
145
- // 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (white/black) → 混合.002 OVERLAY Fac
146
- let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
147
- let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
148
- let mix01_fac = ao_ramp_c.r;
149
- let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
150
-
151
- // 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
152
142
  let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
153
- let overlay_fac = mix01_rgb.r;
154
- let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
143
+ let npr_rgb = mix_overlay(1.0, mix03, hue004);
155
144
  let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
156
145
 
157
- // 噪波→渐变→凹凸 (LIVE in M_Rough_Cloth — unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.
158
- // mapping scale=(1,1,1), loc=rot=0 → identity; use worldPos directly.
146
+ // Noise bump is LIVE in M_Rough_Cloth — unlike Smooth Cloth where the subtree is dead.
159
147
  let noise_val = tex_noise_d2(input.worldPos, CLOTH_R_NOISE_SCALE);
160
148
  let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
161
149
  let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
@@ -123,35 +123,23 @@ struct FSOut {
123
123
  let out_alpha = material.alpha * tex_s.a;
124
124
  if (out_alpha < 0.001) { discard; }
125
125
 
126
- // Shader→RGB → 颜色渐变.008 CONSTANT — AA like face (same terminator artifact class)
127
126
  let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
127
+ // ramp_constant_edge_aa: avoids binary fac shimmer on terminator (fwidth + smoothstep).
128
128
  let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
129
- let toon_r = ramp008.r;
130
- // 颜色渐变.008 → 运算.004 MULTIPLY 0.5 → 混合.004 Factor
131
- let mix04_fac = math_multiply(toon_r, CLOTH_MIX04_MUL);
129
+ let mix04_fac = math_multiply(ramp008.r, CLOTH_MIX04_MUL);
132
130
 
133
- // 混合.004: A=色相/饱和度/明度.002, B=纹理
134
131
  let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
135
132
  let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
136
133
 
137
- // 倒角.001→Z → 混合.003 Factor; A=混合.004, B=色相/饱和度/明度.002
138
134
  let bevel_z = clamp(n.y, 0.0, 1.0);
139
135
  let mix03 = mix_blend(bevel_z, mix04, dark_tex);
140
136
 
141
- // 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (白/黑) → 混合.002 OVERLAY Fac
142
- let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
143
- let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
144
- let mix01_fac = ao_ramp_c.r;
145
- let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
146
-
147
- // 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
148
137
  let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
149
- let overlay_fac = mix01_rgb.r;
150
- let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
138
+ let npr_rgb = mix_overlay(1.0, mix03, hue004);
151
139
  let npr_emission = npr_rgb * NPR_EMIT_STR;
152
140
 
153
- // 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.5, specular_tint=0.
154
- // Bump subtree is dead in the Blender graph (噪波→凹凸 not linked to Principled.Normal).
141
+ // Principled BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.5, specular_tint=0.
142
+ // Bump subtree is dead in the Blender graph (noise→bump not linked to Principled.Normal).
155
143
  let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
156
144
  let NL = max(dot(n, l), 0.0);
157
145
  let NV = max(dot(n, v), 1e-4);
@@ -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,23 +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
- let m = 1.0 - cosTheta;
68
- let m2 = m * m;
69
- return f0 + (1.0 - f0) * (m2 * m2 * m);
70
- }
71
-
72
59
  // ─── Filmic tone mapping (LUT extracted from Blender 3.6 OCIO) ─────
73
60
  // View transform = Filmic, Look = Medium High Contrast, Exposure = -0.3.
74
61
  // 14 samples at integer log2 stops from -10 to +3 (inclusive).
@@ -158,27 +145,27 @@ struct FSOut {
158
145
  let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
159
146
 
160
147
  let l = -light.lights[0].direction.xyz;
161
- let sunColor = light.lights[0].color.xyz * light.lights[0].color.w;
162
- 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);
163
151
 
164
- let ndotl = max(dot(n, l), 0.0);
165
- let ndotv = max(dot(n, v), 0.001);
166
- let ndoth = max(dot(n, h), 0.0);
167
- 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);
168
155
 
169
- let a2 = ROUGHNESS * ROUGHNESS;
170
- let D = ggx_d(ndoth, a2);
171
- let G = smith_g1(ndotl, a2) * smith_g1(ndotv, a2);
172
- let F = fresnel_schlick(vdoth, F0_DIELECTRIC);
173
- 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);
174
160
 
175
- let shadow = sampleShadow(input.worldPos, n);
176
- let kd = (1.0 - F) * albedo / PI;
177
- let direct = (kd + spec) * sunColor * ndotl * shadow;
178
- 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);
179
166
 
180
167
  var out: FSOut;
181
- out.color = vec4f(ambient + direct, alpha);
168
+ out.color = vec4f(diffuse_radiance + spec_radiance, alpha);
182
169
  out.mask = 1.0;
183
170
  return out;
184
171
  }
@@ -1,12 +1,16 @@
1
- // Eye preset — default Principled BSDF (F0=0.04, Roughness=0.5) + Emission socket set to albedo × 1.5.
1
+ // Eye preset — default Principled BSDF (Specular=0.5, Roughness=0.5) + Emission socket set to albedo × 1.5.
2
2
  // Matches the published preset's instruction: "keep eyes in the default nodegraph, add emission 1.5".
3
3
  // Blender's Principled BSDF Emission socket is added on top of the shaded output (pre-tonemap, feeds bloom).
4
4
 
5
+ import { NODES_WGSL } from "./nodes"
6
+
5
7
  export const EYE_SHADER_WGSL = /* wgsl */ `
6
8
 
7
- const PI: f32 = 3.141592653589793;
8
- const F0_DIELECTRIC: f32 = 0.04;
9
- const ROUGHNESS: f32 = 0.5;
9
+ ${NODES_WGSL}
10
+
11
+ const PI_E: f32 = 3.141592653589793;
12
+ const EYE_SPECULAR: f32 = 0.5;
13
+ const EYE_ROUGHNESS: f32 = 0.5;
10
14
  const EYE_EMISSION_STRENGTH: f32 = 1.5;
11
15
 
12
16
  struct CameraUniforms {
@@ -50,21 +54,6 @@ struct LightVP { viewProj: mat4x4f, };
50
54
  @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
51
55
  @group(2) @binding(1) var<uniform> material: MaterialUniforms;
52
56
 
53
- fn ggx_d(ndoth: f32, a2: f32) -> f32 {
54
- let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
55
- return a2 / (PI * denom * denom);
56
- }
57
-
58
- fn smith_g1(ndotx: f32, a2: f32) -> f32 {
59
- return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
60
- }
61
-
62
- fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
63
- let m = 1.0 - cosTheta;
64
- let m2 = m * m;
65
- return f0 + (1.0 - f0) * (m2 * m2 * m);
66
- }
67
-
68
57
  fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
69
58
  // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
70
59
  if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
@@ -129,30 +118,29 @@ struct FSOut {
129
118
  let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
130
119
 
131
120
  let l = -light.lights[0].direction.xyz;
132
- let sunColor = light.lights[0].color.xyz * light.lights[0].color.w;
133
- let h = normalize(l + v);
121
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
122
+ let amb = light.ambientColor.xyz;
123
+ let shadow = sampleShadow(input.worldPos, n);
134
124
 
135
- let ndotl = max(dot(n, l), 0.0);
136
- let ndotv = max(dot(n, v), 0.001);
137
- let ndoth = max(dot(n, h), 0.0);
138
- let vdoth = max(dot(v, h), 0.0);
125
+ // 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.5, specular_tint=0.
126
+ let NL = max(dot(n, l), 0.0);
127
+ let NV = max(dot(n, v), 1e-4);
139
128
 
140
- let a2 = ROUGHNESS * ROUGHNESS;
141
- let D = ggx_d(ndoth, a2);
142
- let G = smith_g1(ndotl, a2) * smith_g1(ndotv, a2);
143
- let F = fresnel_schlick(vdoth, F0_DIELECTRIC);
144
- let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
129
+ let f0 = vec3f(0.08 * EYE_SPECULAR);
130
+ let f90 = mix(f0, vec3f(1.0), sqrt(EYE_SPECULAR));
131
+ let brdf_lut = brdf_lut_sample(NV, EYE_ROUGHNESS);
132
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
145
133
 
146
- let shadow = sampleShadow(input.worldPos, n);
147
- let kd = (1.0 - F) * albedo / PI;
148
- let direct = (kd + spec) * sunColor * ndotl * shadow;
149
- let ambient = albedo * light.ambientColor.xyz;
134
+ let spec_direct = bsdf_ggx(n, l, v, NL, NV, EYE_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
135
+ let spec_indirect = amb;
136
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
150
137
 
138
+ let diffuse_radiance = albedo * (sun * NL * shadow / PI_E + amb);
151
139
  // Principled Emission socket: emissive = emission_color × strength, added on top of shading.
152
140
  let emission = albedo * EYE_EMISSION_STRENGTH;
153
141
 
154
142
  var out: FSOut;
155
- out.color = vec4f(ambient + direct + emission, alpha);
143
+ out.color = vec4f(diffuse_radiance + spec_radiance + emission, alpha);
156
144
  out.mask = 1.0;
157
145
  return out;
158
146
  }
@@ -73,12 +73,11 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
73
73
  const PI_F: f32 = 3.141592653589793;
74
74
  const FACE_SPECULAR: f32 = 0.5;
75
75
  const FACE_ROUGHNESS: f32 = 0.3;
76
- // Dump M_Face unlinked defaults (math op enum not serialized — warm clamp chain still from m_graphs)
77
76
  const FACE_RIM2_POW: f32 = 0.6300000548362732;
78
77
  const FACE_RIM2_BG: vec3f = vec3f(1.0, 0.4684903025627136, 0.3698573112487793);
79
- const FACE_WARM_AO_MUL: f32 = 0.30000001192092896; // 运算.004 MULTIPLY after invert (was 0.5 in older trace)
80
- const FACE_BRIGHT_TEX_THRESH: f32 = 0.9300000071525574; // 运算.005 GREATER_THAN Value_001
81
- const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
78
+ const FACE_WARM_STR: f32 = 0.30000001192092896;
79
+ const FACE_BRIGHT_TEX_THRESH: f32 = 0.9300000071525574;
80
+ const FACE_MIX_NPR: f32 = 0.5;
82
81
  // EEVEE Light Clamp equivalent (Render Props → Sampling → Clamping). Caps direct
83
82
  // specular firefly from the noise-bumped normal's NDF aliasing — Blender hides this
84
83
  // via TAA, which we don't have. Value mirrors EEVEE's default Clamp Indirect=10.0.
@@ -112,10 +111,6 @@ const FACE_SPEC_CLAMP: f32 = 10.0;
112
111
  return output;
113
112
  }
114
113
 
115
- // Fragment: M_Face NPR + Principled hybrid
116
- // TEX → HueSat shadow/lit → toon gate → BrightContrast → AO chain → emission stack
117
- // Fresnel rims, warm AO emission, bright-texture gate, noise-bumped Principled
118
- // Final = mix(Principled, NPR, 0.5)
119
114
  struct FSOut {
120
115
  @location(0) color: vec4f,
121
116
  @location(1) mask: f32,
@@ -134,79 +129,49 @@ struct FSOut {
134
129
  let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
135
130
  let shadow = sampleShadow(input.worldPos, n);
136
131
 
137
- // ═══ SOURCES ═══
138
- // DiffuseBSDF(white) → ShaderToRGB (energy-matched); shadow on direct only
139
132
  let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
140
- // ramp.008 CONSTANT — edge AA avoids binary fac shimmer / white specks on terminator (fwidth + smoothstep)
133
+ // ramp_constant_edge_aa: avoids binary fac shimmer on terminator (fwidth + smoothstep).
141
134
  let toon = ramp_constant_edge_aa(ndotl_raw, 0.2966, vec4f(0,0,0,1), vec4f(1,1,1,1)).r;
142
- let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
143
135
 
144
- // ═══ TOON COLOR ═══
145
- let shadow_tint = hue_sat(0.46000000834465027, 2.0, 0.3499999940395355, 1.0, tex_color); // HueSat.002
146
- let lit_tint = hue_sat(0.46000000834465027, 1.600000023841858, 1.5, 1.0, tex_color); // HueSat.001
147
- let toon_color = mix_blend(toon, shadow_tint, lit_tint); // Mix.004
136
+ let shadow_tint = hue_sat(0.46000000834465027, 2.0, 0.3499999940395355, 1.0, tex_color);
137
+ let lit_tint = hue_sat(0.46000000834465027, 1.600000023841858, 1.5, 1.0, tex_color);
138
+ let toon_color = mix_blend(toon, shadow_tint, lit_tint);
148
139
  let bc = bright_contrast(toon_color, 0.1, 0.2);
149
140
 
150
- // ═══ AO CHAIN ═══
151
- // ramp CONSTANT [0→white, 0.5995→black]
152
- let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
153
- // Mix.003(Factor=ao_ramp, A=bc, B=reddish tint)
154
- let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8302, 0.3346, 0.2795));
141
+ let emission3 = bc * 2.5;
155
142
 
156
- // ═══ EMISSION 3 ═══
157
- let emission3 = ao_mixed * 2.5; // Emission.003(Strength=2.5)
158
-
159
- // ═══ WARM EMISSION ═══
160
- let ao_inv = invert_f(1.0, ao_ramp);
161
- let warm_str = ao_inv * FACE_WARM_AO_MUL; // 反转 → 运算.004 MULTIPLY Value_001
162
- let warm_input = clamp(toon * 0.5 + 0.5, 0.0, 1.0); // 运算.001→运算.006→Clamp
163
- // ramp.003 CARDINAL [0.2409→warm dark, 0.4663→warm light]
143
+ let warm_input = clamp(toon * 0.5 + 0.5, 0.0, 1.0);
164
144
  let warm_color = ramp_cardinal(warm_input, 0.2409,
165
145
  vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
166
146
  vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
167
- let warm_emission = warm_color * warm_str; // Emission.001
147
+ let warm_emission = warm_color * FACE_WARM_STR;
168
148
 
169
- // ═══ RIM 1 ═══
170
- // Fresnel(IOR=2.0) × LayerWeight.001(Facing, Blend=0.24)
171
149
  let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24, n, v);
172
150
  let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
173
151
 
174
- // ═══ RIM 2 ═══
175
- // Fresnel.001(IOR=1.45) × LayerWeight.002(Fresnel output, Blend=0.61)
176
152
  let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
177
153
  let rim2_fac = math_power(rim2_raw, FACE_RIM2_POW);
178
- // MixShader.002: Shader=Emission.003, Shader_001=背景
179
154
  let rim2_mixed = mix(emission3, FACE_RIM2_BG, rim2_fac);
180
155
 
181
- // 转接点.005(tex)运算.005 GREATER_THAN Value_001
182
- // Blender implicitly converts Color Float via BT.601 grayscale when plugging a
183
- // color output into a Math node's Value input. Our earlier trace used tex_color.r,
184
- // which fires aggressively on R-dominant skin single near-white R pixels produced
185
- // firefly speckles. color_to_value matches the actual Blender socket semantic and
186
- // only fires on genuinely near-white painted features (the author's intent).
156
+ // Blender implicitly converts Color Float via BT.601 grayscale when plugging a color
157
+ // output into a Math node's Value input. An earlier trace used tex_color.r, which fires
158
+ // aggressively on R-dominant skin single near-white R pixels produced firefly speckles.
159
+ // color_to_value matches the Blender socket semantic and only fires on genuinely near-
160
+ // white painted features (the author's intent).
187
161
  let tex_gate = math_greater_than(color_to_value(tex_color), FACE_BRIGHT_TEX_THRESH);
188
- let bright_emit = vec3f(tex_gate) * 3.0; // Emission.002(Strength=3.0)
162
+ let bright_emit = vec3f(tex_gate) * 3.0;
189
163
 
190
- // ═══ NPR STACK (AddShader chain) ═══
191
- let add2 = rim2_mixed + bright_emit; // AddShader.002
192
- let add0 = rim1 + add2; // AddShader
193
- let npr_stack = add0 + warm_emission; // AddShader.001
164
+ let npr_stack = rim1 + rim2_mixed + bright_emit + warm_emission;
194
165
 
195
- // ═══ PRINCIPLED BSDF ═══
196
- // Noise-based bump normal. Mapping loc=rot=0 → plain scale multiply, inline.
166
+ // Noise bump Mapping loc=rot=0 folds to a plain scale multiply.
197
167
  let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
198
168
  let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
199
- let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos); // 凹凸 Strength; LH bump
169
+ let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
200
170
 
201
- // Mix.001(Factor=noise_ramp, A=bc, B=dark red)
202
171
  let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6832, 0.1947, 0.1373));
203
- // Emission input from reroute.011 (bc), Strength=0.2
204
172
  let p_emission = bc * 0.2;
205
- // AO.002 → ramp.005 LINEAR [0.003→black, 1.0→gray] for subsurface approx.
206
- // Reuse 'ao' (ao_fake(n, v) above) — identical inputs, avoid a second procedural AO pass.
207
- let sss = ramp_linear(ao, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
208
173
 
209
- // 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
174
+ // Principled BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
210
175
  let NL = max(dot(bumped_n, l), 0.0);
211
176
  let NV = max(dot(bumped_n, v), 1e-4);
212
177
 
@@ -223,9 +188,8 @@ struct FSOut {
223
188
  // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
224
189
  // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
225
190
  let diffuse_radiance = principled_base * (sun * NL * shadow / PI_F + light.ambientColor.xyz);
226
- let principled = diffuse_radiance + spec_radiance + p_emission + vec3f(sss);
191
+ let principled = diffuse_radiance + spec_radiance + p_emission;
227
192
 
228
- // 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF — Fac blends toward second
229
193
  let final_color = mix(npr_stack, principled, FACE_MIX_NPR);
230
194
 
231
195
  var out: FSOut;
@@ -73,7 +73,6 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
73
73
  const PI_H: f32 = 3.141592653589793;
74
74
  const HAIR_SPECULAR: f32 = 1.0;
75
75
  const HAIR_ROUGHNESS: f32 = 0.3;
76
- // Dump M_Hair: 运算.004 GREATER_THAN second operand Value_001; 运算.007 POWER exponent Value_001; 背景 Color
77
76
  const HAIR_TEX_GATE_THRESH: f32 = 0.15000000596046448;
78
77
  const HAIR_RIM2_POW: f32 = 0.6300000548362732;
79
78
  const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
@@ -120,55 +119,37 @@ struct FSOut {
120
119
  let l = -light.lights[0].direction.xyz;
121
120
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
122
121
 
123
- // 图像纹理 ← 纹理坐标.UV → 映射 (default 1,1,1 scale per JSON)
124
122
  let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
125
123
  let shadow = sampleShadow(input.worldPos, n);
126
124
 
127
- // 色相/饱和度/明度 (Hue=0.5 Sat=1.2 Val=0.5 Fac=1) ← reroute from image
128
125
  let hue_sat_shadow = hue_sat_id(1.2, 0.5, 1.0, tex_color);
129
- // 色相/饱和度/明度.002 (0.48, 1.2, 0.7, 1) ← previous
130
126
  let hue_sat_002 = hue_sat(0.48, 1.2, 0.7, 1.0, hue_sat_shadow);
131
- // 色相/饱和度/明度.001 (0.5, 1.5, 1.0, 1) ← image reroute (lit path)
132
127
  let hue_sat_001 = hue_sat_id(1.5, 1.0, 1.0, tex_color);
133
128
 
134
- // 漫射 BSDF.002 → Shader --> RGB → 颜色渐变.008 CONSTANT [0→0, 0.2966→1]
135
129
  let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
136
130
  let ramp_008 = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
137
131
 
138
- // 混合.004 MIX Fac=ramp_008, A=hue_sat_002, B=hue_sat_001
139
132
  let mix_004 = mix_blend(ramp_008, hue_sat_002, hue_sat_001);
140
-
141
- // 亮度/对比度 (Bright=0.1 Contrast=0.2) ← mix_004 only (links: not bevel path)
142
133
  let bc = bright_contrast(mix_004, 0.1, 0.2);
143
134
 
144
- // 倒角.001 → 分离 XYZ.001 → Z → 混合.003 Factor; A=bc, B=hue_sat_002
145
135
  let bevel_z = clamp(n.y, 0.0, 1.0);
146
136
  let mix_003 = mix_blend(bevel_z, bc, hue_sat_002);
147
137
 
148
- // 环境光遮蔽 (AO).001 → 颜色渐变.001 → 混合.001 → 混合.002 chain collapses with fake AO=1:
149
- // ramp_constant(1, 0→white, 0.3756→black).r = 0 → ao_factor = mix(1,0,0) = 1 → mix_002 = mix_003.
150
- // hue_sat_004 becomes unreachable. When real SSAO lands, restore the original 5-line port.
151
- let emission3 = mix_003; // Emission.003 Strength=1.0 (×1 omitted)
152
-
153
- // 菲涅尔.001 × 层权重.002 → 运算.003 MULTIPLY → 运算.007 POWER(exponent Value_001) → MixShader.002 Fac
154
138
  let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
155
139
  let rim2_fac = math_power(rim2_raw, HAIR_RIM2_POW);
156
- // MixShader.002: Shader=Emission.003, Shader_001=背景 — (1-Fac)*emission + Fac*bg
157
- let mix_shader_002 = mix(emission3, HAIR_MIX_BG, rim2_fac);
140
+ let mix_shader_002 = mix(mix_003, HAIR_MIX_BG, rim2_fac);
158
141
 
159
- // 运算.004 GREATER_THAN: 图像→Value, threshold Value_001. Blender converts Color→Float
160
- // via BT.601 luminance, not raw R — same socket-semantic fix as M_Face.
142
+ // Blender's GREATER_THAN converts Color→Float via BT.601 luminance, not raw R — same
143
+ // socket-semantic fix as M_Face.
161
144
  let tex_gate = math_greater_than(color_to_value(tex_color), HAIR_TEX_GATE_THRESH);
162
145
  let gate_emit = vec3f(tex_gate) * 0.1;
163
146
 
164
- // 相加着色器: MixShader.002 + gate emission (color sum in linear space)
165
147
  let add_shader = mix_shader_002 + gate_emit;
166
148
 
167
- // 原理化BSDF (EEVEE port): metallic=0, specular=1.0, roughness=0.3, specular_tint=0.
168
- // Blender graph has 噪波→法线贴图 Strength=0.1 on Principled.Normal, but MixShader.001
169
- // weights Principled at only 0.2; spec contribution × that weight is imperceptible in
170
- // A/B with the noise-bump port enabled, so we drop it and keep plain n saves a full
171
- // tex_noise + bump_lh per hair fragment.
149
+ // Principled BSDF (EEVEE port): metallic=0, specular=1.0, roughness=0.3, specular_tint=0.
150
+ // Graph has a noise→normal_map bump (Strength=0.1) on Principled.Normal, but MixShader.001
151
+ // weights Principled at only 0.2 — the bumped spec × that weight is imperceptible, so we
152
+ // drop the subtree and keep plain n (saves a tex_noise + bump_lh per hair fragment).
172
153
  let NL = max(dot(n, l), 0.0);
173
154
  let NV = max(dot(n, v), 1e-4);
174
155
 
@@ -186,7 +167,6 @@ struct FSOut {
186
167
  let diffuse_radiance = bc * (sun * NL * shadow / PI_H + light.ambientColor.xyz);
187
168
  let principled = diffuse_radiance + spec_radiance;
188
169
 
189
- // 混合着色器.001 Fac=0.2: first socket=相加着色器, second=原理化BSDF
190
170
  let final_color = mix(add_shader, principled, 0.2);
191
171
 
192
172
  var out: FSOut;