reze-engine 0.12.1 → 0.12.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 (46) hide show
  1. package/README.md +99 -58
  2. package/dist/shaders/materials/body.d.ts +1 -1
  3. package/dist/shaders/materials/body.d.ts.map +1 -1
  4. package/dist/shaders/materials/body.js +86 -197
  5. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  6. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  7. package/dist/shaders/materials/cloth_rough.js +10 -121
  8. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  9. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  10. package/dist/shaders/materials/cloth_smooth.js +59 -172
  11. package/dist/shaders/materials/common.d.ts +6 -0
  12. package/dist/shaders/materials/common.d.ts.map +1 -0
  13. package/dist/shaders/materials/common.js +144 -0
  14. package/dist/shaders/materials/default.d.ts +1 -1
  15. package/dist/shaders/materials/default.d.ts.map +1 -1
  16. package/dist/shaders/materials/default.js +12 -145
  17. package/dist/shaders/materials/eye.d.ts +1 -1
  18. package/dist/shaders/materials/eye.d.ts.map +1 -1
  19. package/dist/shaders/materials/eye.js +12 -117
  20. package/dist/shaders/materials/face.d.ts +1 -1
  21. package/dist/shaders/materials/face.d.ts.map +1 -1
  22. package/dist/shaders/materials/face.js +85 -197
  23. package/dist/shaders/materials/hair.d.ts +1 -1
  24. package/dist/shaders/materials/hair.d.ts.map +1 -1
  25. package/dist/shaders/materials/hair.js +78 -183
  26. package/dist/shaders/materials/metal.d.ts +1 -1
  27. package/dist/shaders/materials/metal.d.ts.map +1 -1
  28. package/dist/shaders/materials/metal.js +15 -121
  29. package/dist/shaders/materials/nodes.d.ts +1 -1
  30. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  31. package/dist/shaders/materials/nodes.js +77 -0
  32. package/dist/shaders/materials/stockings.d.ts +1 -1
  33. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  34. package/dist/shaders/materials/stockings.js +26 -152
  35. package/package.json +1 -1
  36. package/src/shaders/materials/body.ts +90 -201
  37. package/src/shaders/materials/cloth_rough.ts +10 -121
  38. package/src/shaders/materials/cloth_smooth.ts +63 -176
  39. package/src/shaders/materials/common.ts +155 -0
  40. package/src/shaders/materials/default.ts +12 -145
  41. package/src/shaders/materials/eye.ts +12 -117
  42. package/src/shaders/materials/face.ts +89 -201
  43. package/src/shaders/materials/hair.ts +82 -187
  44. package/src/shaders/materials/metal.ts +15 -121
  45. package/src/shaders/materials/nodes.ts +77 -0
  46. package/src/shaders/materials/stockings.ts +27 -153
@@ -1,77 +1,15 @@
1
- // M_Stockings — 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Stockings".
2
- // NPR mask (bbox gradient × facing rim) drives Mix Shader between an Emission (HSV-boosted texture)
3
- // and a Principled BSDF with sheen. Mapping rotation + Generated-like coord approximated via UV,
4
- // since our Y-up PMX engine has no object bbox; the gradient is a soft mask, not a hard landmark.
1
+ // M_Stockings — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Stockings". A bbox-gradient ×
2
+ // facing-rim mask drives a Mix Shader between an HSV-boosted emission and a sheen
3
+ // Principled BSDF. Wyman hashed-alpha testing replaces the graph's Alpha=0.95 (which
4
+ // would require TAA to hide the dither dots across every pixel).
5
5
  import { NODES_WGSL } from "./nodes";
6
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common";
6
7
  export const STOCKINGS_SHADER_WGSL = /* wgsl */ `
7
8
 
8
9
  ${NODES_WGSL}
10
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
9
11
 
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_S: f32 = 3.141592653589793;
74
- // Principled BSDF params from dump (Alpha=0.95 is intentionally dropped — see alpha-hash note below)
12
+ // Principled params from dump (Alpha=0.95 intentionally dropped — see hash note below).
75
13
  const STOCK_METALLIC: f32 = 0.1;
76
14
  const STOCK_SPECULAR: f32 = 1.0;
77
15
  const STOCK_ROUGHNESS: f32 = 0.5;
@@ -82,12 +20,6 @@ const STOCK_RAMP002_P1: f32 = 0.9565; // EASE [0→black, 0.9565→white]
82
20
  const STOCK_RAMPFACE_P1: f32 = 0.5435; // EASE [0→black, 0.5435→white]
83
21
  const STOCK_LW_BLEND: f32 = 0.4; // Layer Weight Blend
84
22
 
85
- // principled_sheen (gpu_shader_material_principled.glsl:8-14) — empirical NV curve
86
- fn principled_sheen(NV: f32) -> f32 {
87
- let f = 1.0 - NV;
88
- return f * f * f * 0.077 + f * 0.01 + 0.00026;
89
- }
90
-
91
23
  // Wyman & McGuire "Hashed Alpha Testing" (2017) — world-space hash with derivative-aware
92
24
  // pixel-scale selection, matches Blender EEVEE prepass_frag.glsl::hashed_alpha_threshold.
93
25
  // Key property: dither pattern is stable in object/world space (doesn't swim) and stays
@@ -109,7 +41,7 @@ fn hashed_alpha_threshold(co: vec3f) -> f32 {
109
41
  let a_hi = _hash3d_wm(floor(px_hi * co));
110
42
  let fac = fract(pix_scale_log);
111
43
  let x = mix(a_lo, a_hi, fac);
112
- // CDF remap so that discard-probability = (1 - alpha) uniformly across scale transitions
44
+ // CDF remap so discard-probability = (1 - alpha) uniformly across scale transitions
113
45
  let a = min(fac, 1.0 - fac);
114
46
  let one_a = 1.0 - a;
115
47
  let denom = 1.0 / max(2.0 * a * one_a, 1e-6);
@@ -127,39 +59,6 @@ fn ramp_ease_s(f: f32, p0: f32, p1: f32) -> f32 {
127
59
  return t * t * (3.0 - 2.0 * t);
128
60
  }
129
61
 
130
- @vertex fn vs(
131
- @location(0) position: vec3f,
132
- @location(1) normal: vec3f,
133
- @location(2) uv: vec2f,
134
- @location(3) joints0: vec4<u32>,
135
- @location(4) weights0: vec4<f32>
136
- ) -> VertexOutput {
137
- var output: VertexOutput;
138
- let pos4 = vec4f(position, 1.0);
139
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
140
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
141
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
142
- var skinnedPos = vec4f(0.0);
143
- var skinnedNrm = vec3f(0.0);
144
- for (var i = 0u; i < 4u; i++) {
145
- let m = skinMats[joints0[i]];
146
- let w = nw[i];
147
- skinnedPos += (m * pos4) * w;
148
- skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
149
- }
150
- output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
151
- // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
152
- output.normal = skinnedNrm;
153
- output.uv = uv;
154
- output.worldPos = skinnedPos.xyz;
155
- return output;
156
- }
157
-
158
- struct FSOut {
159
- @location(0) color: vec4f,
160
- @location(1) bloom_mask: f32,
161
- };
162
-
163
62
  @fragment fn fs(input: VertexOutput) -> FSOut {
164
63
  let n = normalize(input.normal);
165
64
  let v = normalize(camera.viewPos - input.worldPos);
@@ -170,18 +69,16 @@ struct FSOut {
170
69
 
171
70
  let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
172
71
  let tex_rgb = tex_s.rgb;
173
- // Alpha HASHED (Blender EEVEE "Hashed" blend mode) per preset author's note —
174
- // self-overlap on the stockings produces sort cracks under alpha blend. Wyman-style
175
- // worldPos hash + depth-write is sort-independent. NOTE: Principled.Alpha=0.95 from
176
- // the dump is DROPPED here — it relies on TAA to smooth the resulting 5%-everywhere
177
- // dither, and without TAA it shows as a pervasive dot pattern. Hash now gates only
178
- // on texture/material alpha, so solid stockings regions stay fully opaque.
72
+ // Alpha HASHED (Blender EEVEE "Hashed" blend mode) per preset author's note — self-overlap
73
+ // on the stockings produces sort cracks under alpha blend. Wyman-style worldPos hash +
74
+ // depth-write is sort-independent. NOTE: Principled.Alpha=0.95 from the dump is DROPPED;
75
+ // it relies on TAA to smooth the 5%-everywhere dither, and without TAA it shows as a
76
+ // pervasive dot pattern. Hash now gates only on texture/material alpha.
179
77
  let combined_alpha = material.alpha * tex_s.a;
180
78
  if (combined_alpha < hashed_alpha_threshold(input.worldPos)) { discard; }
181
- let out_alpha = 1.0;
182
79
 
183
- // ═══ NPR MASK: TEX_COORD.Generated → Mapping(Rot=0,π/2,π/2, Loc=(1,1,1)) → Gradient Texture
184
- // The Blender mapping reduces to gradient.x = 1 - input.y (rot swaps axes, loc offsets by 1).
80
+ // ═══ NPR MASK ═══ TEX_COORD.Generated → Mapping(Rot=0,π/2,π/2, Loc=(1,1,1)) → Gradient.
81
+ // The Blender mapping reduces to gradient.x = 1 - input.y (rot swaps axes, loc offsets).
185
82
  // We approximate Generated with UV since Y-up PMX has no object bbox in pipeline state.
186
83
  let gen_coord = vec3f(input.uv, 0.0);
187
84
  let mapped = mapping_point(gen_coord, vec3f(1.0), vec3f(0.0, 1.5708, 1.5708), vec3f(1.0));
@@ -189,55 +86,32 @@ struct FSOut {
189
86
 
190
87
  // Ramp.001 LINEAR [0→black, 0.5→white, 1.0→black] — triangular peak at 0.5
191
88
  let ramp001 = 1.0 - abs(2.0 * gradient - 1.0);
192
- // Ramp.002 EASE [0→black, 0.9565→white]
193
89
  let ramp002 = ramp_ease_s(ramp001, 0.0, STOCK_RAMP002_P1);
194
90
 
195
- // Layer Weight.Facing (Blend=0.4) → Ramp EASE [0→black, 0.5435→white]
196
91
  let facing = layer_weight_facing(STOCK_LW_BLEND, n, v);
197
92
  let ramp_face = ramp_ease_s(facing, 0.0, STOCK_RAMPFACE_P1);
198
93
 
199
- // Mix.001: MIX blend Fac=0.5, A=white, B=ramp_face → (A,B) averaged 50/50
94
+ // Mix.001: MIX blend Fac=0.5, A=white, B=ramp_face
200
95
  let mix001 = mix(1.0, ramp_face, 0.5);
201
- // Mix: LIGHTEN blend Fac=0.5, A=mix001, B=ramp002 → A smoothly lightens toward max(A,B)
96
+ // Mix: LIGHTEN blend Fac=0.5, A=mix001, B=ramp002
202
97
  let lighten = max(mix001, ramp002);
203
98
  let mask = mix(mix001, lighten, 0.5);
204
99
 
205
- // ═══ EMISSION SHADER ═══
206
- // Hue=0.5 (identity rotation), Sat=1.0, Val=5.0 (5× brightness boost), Fac=1; Strength=1
100
+ // ═══ EMISSION SHADER ═══ Hue=0.5 (identity), Sat=1.0, Val=5.0 (5× brightness), Fac=1.
207
101
  let emission = hue_sat_id(1.0, 5.0, 1.0, tex_rgb);
208
102
 
209
- // ═══ PRINCIPLED BSDF (EEVEE port) ═══
210
- // base_color_tint, metallic f0, sheen coarse approx (scales diffuse radiance).
211
- let NL = max(dot(n, l), 0.0);
212
- let NV = max(dot(n, v), 1e-4);
213
-
214
- // f0 = mix((0.08*spec)*dielectric_tint, base, metallic); dielectric_tint=1 since specular_tint=0.
215
- let dielectric_f0 = vec3f(0.08 * STOCK_SPECULAR);
216
- let f0 = mix(dielectric_f0, tex_rgb, STOCK_METALLIC);
217
- let f90 = mix(f0, vec3f(1.0), sqrt(STOCK_SPECULAR));
218
- let brdf_lut = brdf_lut_sample(NV, STOCK_ROUGHNESS);
219
- let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
220
-
221
- let spec_direct = bsdf_ggx(n, l, v, NL, NV, STOCK_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
222
- let spec_indirect = amb;
223
- let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
224
-
225
- // Sheen coarse: diffuse_color += sheen * sheen_color * principled_sheen(NV).
226
- let base_tint = tint_from_color(tex_rgb);
227
- let sheen_color = mix(vec3f(1.0), base_tint, STOCK_SHEEN_TINT);
228
- let diffuse_color = tex_rgb + STOCK_SHEEN * sheen_color * principled_sheen(NV);
229
-
230
- // diffuse_weight = (1 - metallic). Indirect diffuse uses L_w (no π; see closure_eval_surface_lib:302).
231
- let diffuse_weight = 1.0 - STOCK_METALLIC;
232
- let diffuse_radiance = diffuse_color * (sun * NL * shadow / PI_S + amb) * diffuse_weight;
233
- let principled = diffuse_radiance + spec_radiance;
103
+ // ═══ PRINCIPLED BSDF with sheen ═══ metallic=0.1, sheen=0.7, sheen_tint=0.5.
104
+ let principled = eval_principled(
105
+ PrincipledIn(tex_rgb, STOCK_METALLIC, STOCK_SPECULAR, STOCK_ROUGHNESS, 1e30, STOCK_SHEEN, STOCK_SHEEN_TINT),
106
+ n, l, v, sun, amb, shadow
107
+ );
234
108
 
235
- // ═══ MIX SHADER: Shader=Emission, Shader_001=Principled, Fac=mask ═══
109
+ // MIX SHADER: Shader=Emission, Shader_001=Principled, Fac=mask
236
110
  let final_color = mix(emission, principled, mask);
237
111
 
238
112
  var out: FSOut;
239
- out.color = vec4f(final_color, out_alpha);
240
- out.bloom_mask = 1.0;
113
+ out.color = vec4f(final_color, 1.0);
114
+ out.mask = 1.0;
241
115
  return out;
242
116
  }
243
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,201 +1,90 @@
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 BODY_ROUGHNESS: f32 = 0.3;
74
- const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
75
- const BODY_RIM2_POW: f32 = 1.4300000667572021;
76
- const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
77
- const BODY_WARM_STR: f32 = 0.30000001192092896;
78
- const BODY_SPECULAR: f32 = 0.5;
79
- const BODY_MIX_NPR: f32 = 0.5;
80
- // EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
81
- const BODY_SPEC_CLAMP: f32 = 10.0;
82
-
83
- // smoothstep-based ramp: t*t*(3-2*t) between two color stops
84
- fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
85
- let t = saturate((f - p0) / max(p1 - p0, 1e-6));
86
- let ss = t * t * (3.0 - 2.0 * t);
87
- return mix(c0, c1, ss);
88
- }
89
-
90
- @vertex fn vs(
91
- @location(0) position: vec3f,
92
- @location(1) normal: vec3f,
93
- @location(2) uv: vec2f,
94
- @location(3) joints0: vec4<u32>,
95
- @location(4) weights0: vec4<f32>
96
- ) -> VertexOutput {
97
- var output: VertexOutput;
98
- let pos4 = vec4f(position, 1.0);
99
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
100
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
101
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
102
- var skinnedPos = vec4f(0.0);
103
- var skinnedNrm = vec3f(0.0);
104
- for (var i = 0u; i < 4u; i++) {
105
- let m = skinMats[joints0[i]];
106
- let w = nw[i];
107
- skinnedPos += (m * pos4) * w;
108
- skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
109
- }
110
- output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
111
- // Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
112
- output.normal = skinnedNrm;
113
- output.uv = uv;
114
- output.worldPos = skinnedPos.xyz;
115
- return output;
116
- }
117
-
118
- struct FSOut {
119
- @location(0) color: vec4f,
120
- @location(1) mask: f32,
121
- };
122
-
123
- @fragment fn fs(input: VertexOutput) -> FSOut {
124
- let alpha = material.alpha;
125
- if (alpha < 0.001) { discard; }
126
-
127
- let n = normalize(input.normal);
128
- let v = normalize(camera.viewPos - input.worldPos);
129
- let l = -light.lights[0].direction.xyz;
130
- let sun = light.lights[0].color.xyz * light.lights[0].color.w;
131
-
132
- let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
133
- let shadow = sampleShadow(input.worldPos, n);
134
-
135
- let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
136
- let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
137
-
138
- let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
139
- let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
140
- let toon_color = mix_blend(toon, shadow_tint, lit_tint);
141
- let bc = bright_contrast(toon_color, 0.1, 0.2);
142
-
143
- let emission3 = bc * 4.0;
144
-
145
- let warm_input = clamp(toon + 0.5, 0.0, 1.0);
146
- let warm_color = ramp_cardinal(warm_input, 0.2409,
147
- vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
148
- vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
149
- let warm_emission = warm_color * BODY_WARM_STR;
150
-
151
- let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
152
- let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
153
-
154
- let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
155
- let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
156
- let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
157
- let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
158
-
159
- let npr_stack = rim1 + rim2_mixed + warm_emission;
160
-
161
- // Noise bump — Mapping loc=rot=0 folds to a plain scale multiply.
162
- let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
163
- let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
164
- let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
165
-
166
- let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
167
- let p_emission = bc * 0.2;
168
-
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
-
187
- // Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
188
- // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
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;
192
-
193
- let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
194
-
195
- var out: FSOut;
196
- out.color = vec4f(final_color, alpha);
197
- out.mask = 1.0;
198
- return out;
199
- }
200
-
201
- `
1
+ // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
2
+ // stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
3
+
4
+ import { NODES_WGSL } from "./nodes"
5
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
6
+
7
+ export const BODY_SHADER_WGSL = /* wgsl */ `
8
+
9
+ ${NODES_WGSL}
10
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
11
+
12
+ const BODY_ROUGHNESS: f32 = 0.3;
13
+ const BODY_SPECULAR: f32 = 0.5;
14
+ const BODY_MIX_NPR: f32 = 0.5;
15
+ const BODY_SPEC_CLAMP: f32 = 10.0;
16
+ const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
17
+ const BODY_RIM2_POW: f32 = 1.4300000667572021;
18
+ const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
19
+ const BODY_WARM_STR: f32 = 0.30000001192092896;
20
+
21
+ // smoothstep-based ramp: t*t*(3-2*t) between two color stops
22
+ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
23
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
24
+ let ss = t * t * (3.0 - 2.0 * t);
25
+ return mix(c0, c1, ss);
26
+ }
27
+
28
+ @fragment fn fs(input: VertexOutput) -> FSOut {
29
+ let alpha = material.alpha;
30
+ if (alpha < 0.001) { discard; }
31
+
32
+ let n = normalize(input.normal);
33
+ let v = normalize(camera.viewPos - input.worldPos);
34
+ let l = -light.lights[0].direction.xyz;
35
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
36
+ let amb = light.ambientColor.xyz;
37
+ let shadow = sampleShadow(input.worldPos, n);
38
+
39
+ let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
40
+
41
+ // ═══ NPR STACK ═══
42
+ let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
43
+ let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
44
+
45
+ let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
46
+ let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
47
+ let toon_color = mix_blend(toon, shadow_tint, lit_tint);
48
+ let bc = bright_contrast(toon_color, 0.1, 0.2);
49
+
50
+ let emission3 = bc * 4.0;
51
+
52
+ let warm_input = clamp(toon + 0.5, 0.0, 1.0);
53
+ let warm_color = ramp_cardinal(warm_input, 0.2409,
54
+ vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
55
+ vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
56
+ let warm_emission = warm_color * BODY_WARM_STR;
57
+
58
+ let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
59
+ let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
60
+
61
+ let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
62
+ let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
63
+ let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
64
+ let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
65
+
66
+ let npr_stack = rim1 + rim2_mixed + warm_emission;
67
+
68
+ // ═══ PRINCIPLED BSDF with noise bump ═══
69
+ // Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
70
+ let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
71
+ let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
72
+ let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
73
+
74
+ let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
75
+ let p_emission = bc * 0.2;
76
+
77
+ let principled = eval_principled(
78
+ PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
79
+ bumped_n, l, v, sun, amb, shadow
80
+ ) + p_emission;
81
+
82
+ let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
83
+
84
+ var out: FSOut;
85
+ out.color = vec4f(final_color, alpha);
86
+ out.mask = 1.0;
87
+ return out;
88
+ }
89
+
90
+ `