reze-engine 0.10.2 → 0.11.1

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 (62) hide show
  1. package/README.md +90 -13
  2. package/dist/engine.d.ts +177 -34
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +1204 -318
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/shaders/body.d.ts +2 -0
  9. package/dist/shaders/body.d.ts.map +1 -0
  10. package/dist/shaders/body.js +232 -0
  11. package/dist/shaders/classify.d.ts +4 -0
  12. package/dist/shaders/classify.d.ts.map +1 -0
  13. package/dist/shaders/classify.js +12 -0
  14. package/dist/shaders/cloth_rough.d.ts +2 -0
  15. package/dist/shaders/cloth_rough.d.ts.map +1 -0
  16. package/dist/shaders/cloth_rough.js +190 -0
  17. package/dist/shaders/cloth_smooth.d.ts +2 -0
  18. package/dist/shaders/cloth_smooth.d.ts.map +1 -0
  19. package/dist/shaders/cloth_smooth.js +186 -0
  20. package/dist/shaders/default.d.ts +2 -0
  21. package/dist/shaders/default.d.ts.map +1 -0
  22. package/dist/shaders/default.js +185 -0
  23. package/dist/shaders/dfg_lut.d.ts +3 -0
  24. package/dist/shaders/dfg_lut.d.ts.map +1 -0
  25. package/dist/shaders/dfg_lut.js +129 -0
  26. package/dist/shaders/eye.d.ts +2 -0
  27. package/dist/shaders/eye.d.ts.map +1 -0
  28. package/dist/shaders/eye.js +159 -0
  29. package/dist/shaders/face.d.ts +2 -0
  30. package/dist/shaders/face.d.ts.map +1 -0
  31. package/dist/shaders/face.js +235 -0
  32. package/dist/shaders/hair.d.ts +2 -0
  33. package/dist/shaders/hair.d.ts.map +1 -0
  34. package/dist/shaders/hair.js +196 -0
  35. package/dist/shaders/ltc_mag_lut.d.ts +3 -0
  36. package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
  37. package/dist/shaders/ltc_mag_lut.js +1033 -0
  38. package/dist/shaders/metal.d.ts +2 -0
  39. package/dist/shaders/metal.d.ts.map +1 -0
  40. package/dist/shaders/metal.js +187 -0
  41. package/dist/shaders/nodes.d.ts +2 -0
  42. package/dist/shaders/nodes.d.ts.map +1 -0
  43. package/dist/shaders/nodes.js +465 -0
  44. package/dist/shaders/stockings.d.ts +2 -0
  45. package/dist/shaders/stockings.d.ts.map +1 -0
  46. package/dist/shaders/stockings.js +244 -0
  47. package/package.json +1 -1
  48. package/src/engine.ts +1412 -385
  49. package/src/index.ts +12 -2
  50. package/src/shaders/body.ts +234 -0
  51. package/src/shaders/classify.ts +25 -0
  52. package/src/shaders/cloth_rough.ts +192 -0
  53. package/src/shaders/cloth_smooth.ts +188 -0
  54. package/src/shaders/default.ts +186 -0
  55. package/src/shaders/dfg_lut.ts +131 -0
  56. package/src/shaders/eye.ts +160 -0
  57. package/src/shaders/face.ts +237 -0
  58. package/src/shaders/hair.ts +198 -0
  59. package/src/shaders/ltc_mag_lut.ts +1035 -0
  60. package/src/shaders/metal.ts +189 -0
  61. package/src/shaders/nodes.ts +466 -0
  62. package/src/shaders/stockings.ts +246 -0
package/src/index.ts CHANGED
@@ -1,4 +1,13 @@
1
- export { Engine, type EngineStats, type LoadModelFromFilesOptions } from "./engine"
1
+ export {
2
+ Engine,
3
+ DEFAULT_BLOOM_OPTIONS,
4
+ DEFAULT_VIEW_TRANSFORM,
5
+ type EngineStats,
6
+ type EngineOptions,
7
+ type BloomOptions,
8
+ type ViewTransformOptions,
9
+ type LoadModelFromFilesOptions,
10
+ } from "./engine"
2
11
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
3
12
  export { Model } from "./model"
4
13
  export { Vec3, Quat, Mat4 } from "./math"
@@ -12,4 +21,5 @@ export type {
12
21
  ControlPoint,
13
22
  } from "./animation"
14
23
  export { FPS } from "./animation"
15
- export { Physics, type PhysicsOptions } from "./physics"
24
+ export { Physics, type PhysicsOptions } from "./physics"
25
+ export type { MaterialPreset, MaterialPresetMap } from "./shaders/classify"
@@ -0,0 +1,234 @@
1
+ // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Body"; VALTORGB / math ops from m_graphs where dump omits them.
2
+
3
+ import { NODES_WGSL } from "./nodes"
4
+
5
+ export const BODY_SHADER_WGSL = /* wgsl */ `
6
+
7
+ ${NODES_WGSL}
8
+
9
+ struct CameraUniforms {
10
+ view: mat4x4f,
11
+ projection: mat4x4f,
12
+ viewPos: vec3f,
13
+ _padding: f32,
14
+ };
15
+
16
+ struct Light {
17
+ direction: vec4f,
18
+ color: vec4f,
19
+ };
20
+
21
+ struct LightUniforms {
22
+ ambientColor: vec4f,
23
+ lights: array<Light, 4>,
24
+ };
25
+
26
+ struct MaterialUniforms {
27
+ diffuseColor: vec3f,
28
+ alpha: f32,
29
+ };
30
+
31
+ struct VertexOutput {
32
+ @builtin(position) position: vec4f,
33
+ @location(0) normal: vec3f,
34
+ @location(1) uv: vec2f,
35
+ @location(2) worldPos: vec3f,
36
+ };
37
+
38
+ struct LightVP { viewProj: mat4x4f, };
39
+
40
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
41
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
42
+ @group(0) @binding(2) var diffuseSampler: sampler;
43
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
44
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
45
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
46
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
47
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
48
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
49
+
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; }
53
+ let biasedPos = worldPos + n * 0.08;
54
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
55
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
56
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
57
+ let cmpZ = ndc.z - 0.001;
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);
70
+ }
71
+
72
+ const PI_B: f32 = 3.141592653589793;
73
+ const F0_BODY: f32 = 0.04;
74
+ const BODY_ROUGHNESS: f32 = 0.3;
75
+ // Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
76
+ const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
77
+ const BODY_RIM2_POW: f32 = 1.4300000667572021;
78
+ const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
79
+ const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
80
+ const BODY_MIX_NPR: f32 = 0.5;
81
+ // EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
82
+ const BODY_SPEC_CLAMP: f32 = 10.0;
83
+
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
+ // smoothstep-based ramp: t*t*(3-2*t) between two color stops
100
+ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
101
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
102
+ let ss = t * t * (3.0 - 2.0 * t);
103
+ return mix(c0, c1, ss);
104
+ }
105
+
106
+ @vertex fn vs(
107
+ @location(0) position: vec3f,
108
+ @location(1) normal: vec3f,
109
+ @location(2) uv: vec2f,
110
+ @location(3) joints0: vec4<u32>,
111
+ @location(4) weights0: vec4<f32>
112
+ ) -> VertexOutput {
113
+ var output: VertexOutput;
114
+ let pos4 = vec4f(position, 1.0);
115
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
116
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
117
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
118
+ var skinnedPos = vec4f(0.0);
119
+ var skinnedNrm = vec3f(0.0);
120
+ for (var i = 0u; i < 4u; i++) {
121
+ let m = skinMats[joints0[i]];
122
+ let w = nw[i];
123
+ skinnedPos += (m * pos4) * w;
124
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
125
+ }
126
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
127
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
128
+ output.normal = skinnedNrm;
129
+ output.uv = uv;
130
+ output.worldPos = skinnedPos.xyz;
131
+ return output;
132
+ }
133
+
134
+ struct FSOut {
135
+ @location(0) color: vec4f,
136
+ @location(1) mask: f32,
137
+ };
138
+
139
+ @fragment fn fs(input: VertexOutput) -> FSOut {
140
+ let alpha = material.alpha;
141
+ if (alpha < 0.001) { discard; }
142
+
143
+ let n = normalize(input.normal);
144
+ let v = normalize(camera.viewPos - input.worldPos);
145
+ let l = -light.lights[0].direction.xyz;
146
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
147
+
148
+ let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
149
+ let shadow = sampleShadow(input.worldPos, n);
150
+
151
+ // ═══ TOON MASK: ShaderToRGB → ramp.008 CONSTANT [0→black, 0.2966→white] ═══
152
+ let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
153
+ let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
154
+
155
+ // ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
156
+ let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
157
+ let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
158
+ let toon_color = mix_blend(toon, shadow_tint, lit_tint);
159
+ let bc = bright_contrast(toon_color, 0.1, 0.2);
160
+
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;
168
+
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
+ let warm_input = clamp(toon + 0.5, 0.0, 1.0);
173
+ let warm_color = ramp_cardinal(warm_input, 0.2409,
174
+ vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
175
+ vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
176
+ let warm_emission = warm_color * warm_str;
177
+
178
+ // ═══ RIM 1: 菲涅尔 × 层权重.001 Facing Blend=0.24 → 自发光 Strength ═══
179
+ let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
180
+ let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
181
+
182
+ // ═══ RIM 2: 层权重.002 Facing → 运算.007 POWER → 颜色渐变.010 EASE → MixShader.002 Fac ═══
183
+ let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
184
+ let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
185
+ let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
186
+ let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
187
+
188
+ // ═══ NPR STACK: AddShader chain (no bright gate in body) ═══
189
+ let add0 = rim1 + rim2_mixed;
190
+ let npr_stack = add0 + warm_emission;
191
+
192
+ // ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
193
+ // Mapping loc=rot=0 → plain scale multiply, inline.
194
+ let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
195
+ let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
196
+ let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
197
+
198
+ let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
199
+ let p_emission = bc * 0.2;
200
+
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;
220
+ // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
221
+ // 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);
224
+
225
+ // 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
226
+ let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
227
+
228
+ var out: FSOut;
229
+ out.color = vec4f(final_color, alpha);
230
+ out.mask = 1.0;
231
+ return out;
232
+ }
233
+
234
+ `
@@ -0,0 +1,25 @@
1
+ // Material preset types for NPR pipeline dispatch.
2
+ // Mapping is explicit — the consumer provides a MaterialPresetMap that assigns
3
+ // material names to presets. Unmapped materials fall back to "default" (Principled BSDF).
4
+
5
+ export type MaterialPreset =
6
+ | "default"
7
+ | "face"
8
+ | "hair"
9
+ | "body"
10
+ | "eye"
11
+ | "stockings"
12
+ | "metal"
13
+ | "cloth_smooth"
14
+ | "cloth_rough"
15
+
16
+ // Keys = preset name, values = array of material names that should use that preset.
17
+ export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
18
+
19
+ export function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): MaterialPreset {
20
+ if (!map) return "default"
21
+ for (const [preset, names] of Object.entries(map)) {
22
+ if (names && names.includes(materialName)) return preset as MaterialPreset
23
+ }
24
+ return "default"
25
+ }
@@ -0,0 +1,192 @@
1
+ // M_Rough_Cloth — NPR graph identical to M_Smooth_Cloth but bump chain IS live
2
+ // (噪波→渐变→凹凸.Normal → 原理化BSDF.Normal in m_graphs) and Roughness=0.8187.
3
+
4
+ import { NODES_WGSL } from "./nodes"
5
+
6
+ export const CLOTH_ROUGH_SHADER_WGSL = /* wgsl */ `
7
+
8
+ ${NODES_WGSL}
9
+
10
+ struct CameraUniforms {
11
+ view: mat4x4f,
12
+ projection: mat4x4f,
13
+ viewPos: vec3f,
14
+ _padding: f32,
15
+ };
16
+
17
+ struct Light {
18
+ direction: vec4f,
19
+ color: vec4f,
20
+ };
21
+
22
+ struct LightUniforms {
23
+ ambientColor: vec4f,
24
+ lights: array<Light, 4>,
25
+ };
26
+
27
+ struct MaterialUniforms {
28
+ diffuseColor: vec3f,
29
+ alpha: f32,
30
+ };
31
+
32
+ struct VertexOutput {
33
+ @builtin(position) position: vec4f,
34
+ @location(0) normal: vec3f,
35
+ @location(1) uv: vec2f,
36
+ @location(2) worldPos: vec3f,
37
+ };
38
+
39
+ struct LightVP { viewProj: mat4x4f, };
40
+
41
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
42
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
43
+ @group(0) @binding(2) var diffuseSampler: sampler;
44
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
45
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
46
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
47
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
48
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
49
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
50
+
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; }
54
+ let biasedPos = worldPos + n * 0.08;
55
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
56
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
57
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
58
+ let cmpZ = ndc.z - 0.001;
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);
71
+ }
72
+
73
+ const PI_CR: f32 = 3.141592653589793;
74
+ const CLOTH_R_SPECULAR: f32 = 0.8;
75
+ const CLOTH_R_ROUGHNESS: f32 = 0.8187;
76
+ const CLOTH_R_TOON_EDGE: f32 = 0.2966;
77
+ const CLOTH_R_MIX04_MUL: f32 = 0.5;
78
+ const CLOTH_R_EMIT_STR: f32 = 18.200000762939453;
79
+ const CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;
80
+ const CLOTH_R_NOISE_SCALE: f32 = 17.7;
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;
84
+
85
+ @vertex fn vs(
86
+ @location(0) position: vec3f,
87
+ @location(1) normal: vec3f,
88
+ @location(2) uv: vec2f,
89
+ @location(3) joints0: vec4<u32>,
90
+ @location(4) weights0: vec4<f32>
91
+ ) -> VertexOutput {
92
+ var output: VertexOutput;
93
+ let pos4 = vec4f(position, 1.0);
94
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
95
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
96
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
97
+ var skinnedPos = vec4f(0.0);
98
+ var skinnedNrm = vec3f(0.0);
99
+ for (var i = 0u; i < 4u; i++) {
100
+ let m = skinMats[joints0[i]];
101
+ let w = nw[i];
102
+ skinnedPos += (m * pos4) * w;
103
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
104
+ }
105
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
106
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
107
+ output.normal = skinnedNrm;
108
+ output.uv = uv;
109
+ output.worldPos = skinnedPos.xyz;
110
+ return output;
111
+ }
112
+
113
+ struct FSOut {
114
+ @location(0) color: vec4f,
115
+ @location(1) mask: f32,
116
+ };
117
+
118
+ @fragment fn fs(input: VertexOutput) -> FSOut {
119
+ let n = normalize(input.normal);
120
+ let v = normalize(camera.viewPos - input.worldPos);
121
+ let l = -light.lights[0].direction.xyz;
122
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
123
+ let amb = light.ambientColor.xyz;
124
+ let shadow = sampleShadow(input.worldPos, n);
125
+
126
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
127
+ let tex_rgb = tex_s.rgb;
128
+ let out_alpha = material.alpha * tex_s.a;
129
+ if (out_alpha < 0.001) { discard; }
130
+
131
+ // Shader→RGB → 颜色渐变.008 CONSTANT (edge AA terminator)
132
+ let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
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);
136
+
137
+ // 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
138
+ let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
139
+ let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
140
+
141
+ // 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
142
+ let bevel_z = clamp(n.y, 0.0, 1.0);
143
+ let mix03 = mix_blend(bevel_z, mix04, dark_tex);
144
+
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
+ 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);
155
+ let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
156
+
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.
159
+ let noise_val = tex_noise_d2(input.worldPos, CLOTH_R_NOISE_SCALE);
160
+ let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
161
+ let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
162
+
163
+ // 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.
164
+ let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
165
+ let NL = max(dot(bumped_n, l), 0.0);
166
+ let NV = max(dot(bumped_n, v), 1e-4);
167
+
168
+ let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);
169
+ let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));
170
+ let brdf_lut = brdf_lut_sample(NV, CLOTH_R_ROUGHNESS);
171
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
172
+
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));
175
+ let spec_indirect = amb;
176
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
177
+
178
+ // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
179
+ // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
180
+ let diffuse_radiance = principled_base * (sun * NL * shadow / PI_CR + amb);
181
+ let principled = diffuse_radiance + spec_radiance;
182
+
183
+ // 混合着色器.001 Fac=0.9: Shader=自发光.005, Shader_001=原理化BSDF
184
+ let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);
185
+
186
+ var out: FSOut;
187
+ out.color = vec4f(final_color, out_alpha);
188
+ out.mask = 1.0;
189
+ return out;
190
+ }
191
+
192
+ `
@@ -0,0 +1,188 @@
1
+ // M_Smooth_Cloth — dump socket order + m_graphs ramps/overlay/noise-bump (dump omits 噪波→凹凸 subtree).
2
+
3
+ import { NODES_WGSL } from "./nodes"
4
+
5
+ export const CLOTH_SMOOTH_SHADER_WGSL = /* wgsl */ `
6
+
7
+ ${NODES_WGSL}
8
+
9
+ struct CameraUniforms {
10
+ view: mat4x4f,
11
+ projection: mat4x4f,
12
+ viewPos: vec3f,
13
+ _padding: f32,
14
+ };
15
+
16
+ struct Light {
17
+ direction: vec4f,
18
+ color: vec4f,
19
+ };
20
+
21
+ struct LightUniforms {
22
+ ambientColor: vec4f,
23
+ lights: array<Light, 4>,
24
+ };
25
+
26
+ struct MaterialUniforms {
27
+ diffuseColor: vec3f,
28
+ alpha: f32,
29
+ };
30
+
31
+ struct VertexOutput {
32
+ @builtin(position) position: vec4f,
33
+ @location(0) normal: vec3f,
34
+ @location(1) uv: vec2f,
35
+ @location(2) worldPos: vec3f,
36
+ };
37
+
38
+ struct LightVP { viewProj: mat4x4f, };
39
+
40
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
41
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
42
+ @group(0) @binding(2) var diffuseSampler: sampler;
43
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
44
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
45
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
46
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
47
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
48
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
49
+
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; }
53
+ let biasedPos = worldPos + n * 0.08;
54
+ let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
55
+ let ndc = lclip.xyz / max(lclip.w, 1e-6);
56
+ let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
57
+ let cmpZ = ndc.z - 0.001;
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);
70
+ }
71
+
72
+ const PI_C: f32 = 3.141592653589793;
73
+ const CLOTH_SPECULAR: f32 = 0.8;
74
+ const CLOTH_ROUGHNESS: f32 = 0.5;
75
+ const CLOTH_TOON_EDGE: f32 = 0.2966;
76
+ const CLOTH_MIX04_MUL: f32 = 0.5; // 运算.004 MULTIPLY Value_001 (dump)
77
+ const NPR_EMIT_STR: f32 = 18.200000762939453;
78
+ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
79
+
80
+ @vertex fn vs(
81
+ @location(0) position: vec3f,
82
+ @location(1) normal: vec3f,
83
+ @location(2) uv: vec2f,
84
+ @location(3) joints0: vec4<u32>,
85
+ @location(4) weights0: vec4<f32>
86
+ ) -> VertexOutput {
87
+ var output: VertexOutput;
88
+ let pos4 = vec4f(position, 1.0);
89
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
90
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
91
+ let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
92
+ var skinnedPos = vec4f(0.0);
93
+ var skinnedNrm = vec3f(0.0);
94
+ for (var i = 0u; i < 4u; i++) {
95
+ let m = skinMats[joints0[i]];
96
+ let w = nw[i];
97
+ skinnedPos += (m * pos4) * w;
98
+ skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
99
+ }
100
+ output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
101
+ // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
102
+ output.normal = skinnedNrm;
103
+ output.uv = uv;
104
+ output.worldPos = skinnedPos.xyz;
105
+ return output;
106
+ }
107
+
108
+ struct FSOut {
109
+ @location(0) color: vec4f,
110
+ @location(1) mask: f32,
111
+ };
112
+
113
+ @fragment fn fs(input: VertexOutput) -> FSOut {
114
+ let n = normalize(input.normal);
115
+ let v = normalize(camera.viewPos - input.worldPos);
116
+ let l = -light.lights[0].direction.xyz;
117
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
118
+ let amb = light.ambientColor.xyz;
119
+ let shadow = sampleShadow(input.worldPos, n);
120
+
121
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
122
+ let tex_rgb = tex_s.rgb;
123
+ let out_alpha = material.alpha * tex_s.a;
124
+ if (out_alpha < 0.001) { discard; }
125
+
126
+ // Shader→RGB → 颜色渐变.008 CONSTANT — AA like face (same terminator artifact class)
127
+ let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
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);
132
+
133
+ // 混合.004: A=色相/饱和度/明度.002, B=纹理
134
+ let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
135
+ let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
136
+
137
+ // 倒角.001→Z → 混合.003 Factor; A=混合.004, B=色相/饱和度/明度.002
138
+ let bevel_z = clamp(n.y, 0.0, 1.0);
139
+ let mix03 = mix_blend(bevel_z, mix04, dark_tex);
140
+
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
+ 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);
151
+ let npr_emission = npr_rgb * NPR_EMIT_STR;
152
+
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).
155
+ let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
156
+ let NL = max(dot(n, l), 0.0);
157
+ let NV = max(dot(n, v), 1e-4);
158
+
159
+ // f0/f90 per gpu_shader_material_principled.glsl — specular_tint=0 → dielectric_f0_color=white.
160
+ let f0 = vec3f(0.08 * CLOTH_SPECULAR);
161
+ let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_SPECULAR));
162
+ let brdf_lut = brdf_lut_sample(NV, CLOTH_ROUGHNESS);
163
+ let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
164
+
165
+ // Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after accum).
166
+ // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
167
+ let spec_direct = bsdf_ggx(n, l, v, NL, NV, CLOTH_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
168
+ // Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
169
+ let spec_indirect = amb;
170
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
171
+
172
+ // Diffuse (Lambert), no (1-F) factor per EEVEE — it doesn't energy-conserve spec<->diffuse.
173
+ // probe_evaluate_world_diff returns radiance L_w (SH projected, not cosine-convolved); in
174
+ // closure_eval_surface_lib line 302: closure.radiance += diffuse_accum * L_w * diffuse.color.
175
+ // So indirect diffuse = base_color × L_w, no π factor.
176
+ let diffuse_radiance = principled_base * (sun * NL * shadow / PI_C + amb);
177
+ let principled = diffuse_radiance + spec_radiance;
178
+
179
+ // 混合着色器.001: Shader=自发光.005, Shader_001=原理化BSDF, Fac=0.9
180
+ let final_color = mix(npr_emission, principled, NPR_MIX_SHADER_FAC);
181
+
182
+ var out: FSOut;
183
+ out.color = vec4f(final_color, out_alpha);
184
+ out.mask = 1.0;
185
+ return out;
186
+ }
187
+
188
+ `