reze-engine 0.10.1 → 0.11.0

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 (78) hide show
  1. package/README.md +113 -20
  2. package/dist/asset-reader.d.ts +16 -0
  3. package/dist/asset-reader.d.ts.map +1 -0
  4. package/dist/asset-reader.js +74 -0
  5. package/dist/engine.d.ts +179 -36
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +1133 -321
  8. package/dist/folder-upload.d.ts +24 -0
  9. package/dist/folder-upload.d.ts.map +1 -0
  10. package/dist/folder-upload.js +50 -0
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/model.d.ts +6 -1
  15. package/dist/model.d.ts.map +1 -1
  16. package/dist/model.js +34 -2
  17. package/dist/pmx-loader.d.ts +3 -0
  18. package/dist/pmx-loader.d.ts.map +1 -1
  19. package/dist/pmx-loader.js +9 -2
  20. package/dist/shaders/body.d.ts +2 -0
  21. package/dist/shaders/body.d.ts.map +1 -0
  22. package/dist/shaders/body.js +209 -0
  23. package/dist/shaders/classify.d.ts +4 -0
  24. package/dist/shaders/classify.d.ts.map +1 -0
  25. package/dist/shaders/classify.js +12 -0
  26. package/dist/shaders/cloth_rough.d.ts +2 -0
  27. package/dist/shaders/cloth_rough.d.ts.map +1 -0
  28. package/dist/shaders/cloth_rough.js +172 -0
  29. package/dist/shaders/cloth_smooth.d.ts +2 -0
  30. package/dist/shaders/cloth_smooth.d.ts.map +1 -0
  31. package/dist/shaders/cloth_smooth.js +171 -0
  32. package/dist/shaders/default.d.ts +2 -0
  33. package/dist/shaders/default.d.ts.map +1 -0
  34. package/dist/shaders/default.js +168 -0
  35. package/dist/shaders/dfg_lut.d.ts +4 -0
  36. package/dist/shaders/dfg_lut.d.ts.map +1 -0
  37. package/dist/shaders/dfg_lut.js +125 -0
  38. package/dist/shaders/eye.d.ts +2 -0
  39. package/dist/shaders/eye.d.ts.map +1 -0
  40. package/dist/shaders/eye.js +142 -0
  41. package/dist/shaders/face.d.ts +2 -0
  42. package/dist/shaders/face.d.ts.map +1 -0
  43. package/dist/shaders/face.js +211 -0
  44. package/dist/shaders/hair.d.ts +2 -0
  45. package/dist/shaders/hair.d.ts.map +1 -0
  46. package/dist/shaders/hair.js +186 -0
  47. package/dist/shaders/ltc_mag_lut.d.ts +3 -0
  48. package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
  49. package/dist/shaders/ltc_mag_lut.js +1033 -0
  50. package/dist/shaders/metal.d.ts +2 -0
  51. package/dist/shaders/metal.d.ts.map +1 -0
  52. package/dist/shaders/metal.js +171 -0
  53. package/dist/shaders/nodes.d.ts +2 -0
  54. package/dist/shaders/nodes.d.ts.map +1 -0
  55. package/dist/shaders/nodes.js +423 -0
  56. package/dist/shaders/stockings.d.ts +2 -0
  57. package/dist/shaders/stockings.d.ts.map +1 -0
  58. package/dist/shaders/stockings.js +229 -0
  59. package/package.json +1 -1
  60. package/src/asset-reader.ts +79 -0
  61. package/src/engine.ts +1352 -383
  62. package/src/folder-upload.ts +59 -0
  63. package/src/index.ts +12 -2
  64. package/src/model.ts +34 -2
  65. package/src/pmx-loader.ts +11 -2
  66. package/src/shaders/body.ts +211 -0
  67. package/src/shaders/classify.ts +25 -0
  68. package/src/shaders/cloth_rough.ts +174 -0
  69. package/src/shaders/cloth_smooth.ts +173 -0
  70. package/src/shaders/default.ts +169 -0
  71. package/src/shaders/dfg_lut.ts +127 -0
  72. package/src/shaders/eye.ts +143 -0
  73. package/src/shaders/face.ts +213 -0
  74. package/src/shaders/hair.ts +188 -0
  75. package/src/shaders/ltc_mag_lut.ts +1035 -0
  76. package/src/shaders/metal.ts +173 -0
  77. package/src/shaders/nodes.ts +424 -0
  78. package/src/shaders/stockings.ts +231 -0
@@ -0,0 +1,173 @@
1
+ // M_Metal — Metallic Principled (Metallic=1.0, Specular=1.0, Specular Tint=0.114, Roughness=0.3)
2
+ // + NPR toon/AO emission stack (Strength=8.1), MixShader Fac=0.6967.
3
+ // Base color uses a Voronoi pattern sampled in reflection-coord space (Blender 纹理坐标.Reflection)
4
+ // to add subtle metallic sparkle variation. No Normal link in the graph.
5
+
6
+ import { NODES_WGSL } from "./nodes"
7
+
8
+ export const METAL_SHADER_WGSL = /* wgsl */ `
9
+
10
+ ${NODES_WGSL}
11
+
12
+ struct CameraUniforms {
13
+ view: mat4x4f,
14
+ projection: mat4x4f,
15
+ viewPos: vec3f,
16
+ _padding: f32,
17
+ };
18
+
19
+ struct Light {
20
+ direction: vec4f,
21
+ color: vec4f,
22
+ };
23
+
24
+ struct LightUniforms {
25
+ ambientColor: vec4f,
26
+ lights: array<Light, 4>,
27
+ };
28
+
29
+ struct MaterialUniforms {
30
+ diffuseColor: vec3f,
31
+ alpha: f32,
32
+ };
33
+
34
+ struct VertexOutput {
35
+ @builtin(position) position: vec4f,
36
+ @location(0) normal: vec3f,
37
+ @location(1) uv: vec2f,
38
+ @location(2) worldPos: vec3f,
39
+ };
40
+
41
+ struct LightVP { viewProj: mat4x4f, };
42
+
43
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
44
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
45
+ @group(0) @binding(2) var diffuseSampler: sampler;
46
+ @group(0) @binding(3) var shadowMap: texture_depth_2d;
47
+ @group(0) @binding(4) var shadowSampler: sampler_comparison;
48
+ @group(0) @binding(5) var<uniform> lightVP: LightVP;
49
+ @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
50
+ @group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
51
+ @group(2) @binding(1) var<uniform> material: MaterialUniforms;
52
+
53
+ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
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 / 4096.0;
60
+ var vis = 0.0;
61
+ for (var y = -1; y <= 1; y++) {
62
+ for (var x = -1; x <= 1; x++) {
63
+ vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
64
+ }
65
+ }
66
+ return vis / 9.0;
67
+ }
68
+
69
+ const PI_M: f32 = 3.141592653589793;
70
+ const METAL_SPECULAR: f32 = 1.0;
71
+ const METAL_METALLIC: f32 = 1.0;
72
+ const METAL_ROUGHNESS: f32 = 0.3;
73
+ const METAL_SPECULAR_TINT: f32 = 0.114;
74
+ const METAL_TOON_EDGE: f32 = 0.2966;
75
+ const METAL_MIX04_MUL: f32 = 0.5;
76
+ const METAL_EMIT_STR: f32 = 8.100000381469727;
77
+ const METAL_MIX_SHADER_FAC: f32 = 0.6967;
78
+ const METAL_VORONOI_SCALE: f32 = 4.3;
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
+ output.normal = normalize(skinnedNrm);
102
+ output.uv = uv;
103
+ output.worldPos = skinnedPos.xyz;
104
+ return output;
105
+ }
106
+
107
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
108
+ let n = normalize(input.normal);
109
+ let v = normalize(camera.viewPos - input.worldPos);
110
+ let l = -light.lights[0].direction.xyz;
111
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
112
+ let amb = light.ambientColor.xyz;
113
+ let shadow = sampleShadow(input.worldPos, n);
114
+
115
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
116
+ let tex_rgb = tex_s.rgb;
117
+ let out_alpha = material.alpha * tex_s.a;
118
+ if (out_alpha < 0.001) { discard; }
119
+
120
+ // ═══ NPR toon stack (图像 → HSV.007 Val=0.8 → 转接点.001) ═══
121
+ let tex_tint = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);
122
+ let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
123
+ let ramp008 = ramp_constant_edge_aa(lum_shade, METAL_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
124
+ let mix04_fac = math_multiply(ramp008.r, METAL_MIX04_MUL);
125
+
126
+ // 混合.004: A=HSV.002(Val=0.2 dark), B=tex_tint
127
+ let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_tint);
128
+ let mix04 = mix_blend(mix04_fac, dark_tex, tex_tint);
129
+
130
+ // AO white/black ramp → 混合.002 factor
131
+ let ao = ao_fake(n, v);
132
+ let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
133
+ let overlay_fac = mix(1.0, 0.0, ao_ramp_c.r);
134
+
135
+ // 混合.002 OVERLAY: A=HSV.008(Val=1.0 identity) ← mix04, B=HSV.004(Val=2.0 bright) ← mix04
136
+ let hue008 = mix04; // identity HSV
137
+ let hue004 = hue_sat(0.5, 1.0, 2.0, 1.0, mix04);
138
+ let npr_rgb = mix_overlay(overlay_fac, hue008, hue004);
139
+ let npr_emission = npr_rgb * METAL_EMIT_STR;
140
+
141
+ // ═══ Metallic Principled base color ═══
142
+ // Reflection-coord Voronoi for metallic sparkle:
143
+ // 纹理坐标.Reflection → 矢量运算 → 沃罗诺伊(Scale=4.3) → 颜色渐变 → 混合.005
144
+ let refl_dir = reflect(-v, n);
145
+ let voro = tex_voronoi_f1(refl_dir, METAL_VORONOI_SCALE);
146
+ let voro_ramp = ramp_linear(voro, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
147
+ // 混合.005: Fac=voro_ramp, A=voro_color(grayscale), B=HSV.006(Hue=0.5 Sat=1.5 Val=1.3)
148
+ let hue006 = hue_sat(0.5, 1.5, 1.2999999523162842, 1.0, tex_tint);
149
+ let albedo = mix_blend(voro_ramp, vec3f(voro_ramp), hue006);
150
+
151
+ // 原理化BSDF (EEVEE port): metallic=1.0, specular=1.0, roughness=0.3.
152
+ // Per Blender principled wiring: f0 = mix((0.08*spec)*dielectric, albedo, metallic) → with
153
+ // metallic=1 this is just albedo (specular_tint is dielectric-only and ignored here).
154
+ let f0 = albedo;
155
+ let f90 = mix(f0, vec3f(1.0), sqrt(METAL_SPECULAR));
156
+ let NV = max(dot(n, v), 1e-4);
157
+ let split_sum = brdf_lut_baked(NV, METAL_ROUGHNESS);
158
+ let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
159
+
160
+ let spec_direct = bsdf_ggx(n, l, v, METAL_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, METAL_ROUGHNESS);
161
+ let spec_indirect = amb;
162
+ let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
163
+
164
+ // Pure metal — no diffuse lobe (diffuse_weight = (1 - metallic) = 0).
165
+ let principled = spec_radiance;
166
+
167
+ // 混合着色器.001 Fac=0.6967: Shader=npr_emission, Shader_001=principled
168
+ let final_color = mix(npr_emission, principled, METAL_MIX_SHADER_FAC);
169
+
170
+ return vec4f(final_color, out_alpha);
171
+ }
172
+
173
+ `
@@ -0,0 +1,424 @@
1
+ // Shared WGSL primitives for Blender-style NPR material nodes.
2
+ // Every function here maps 1:1 to a Blender shader node type used in the preset JSONs.
3
+ // Hand-ported material shaders concatenate this block before their own code.
4
+
5
+ export const NODES_WGSL = /* wgsl */ `
6
+
7
+ // Baked 64×64 rg16float DFG LUT — created once at engine init by dfg_lut.ts.
8
+ // Paired with group(0) binding(2) diffuseSampler (linear filter, clamp implicit
9
+ // via the half-texel bias inside brdf_lut_baked). Bound by the main per-frame
10
+ // bind group to every material pipeline that includes this module.
11
+ @group(0) @binding(9) var dfgLut: texture_2d<f32>;
12
+
13
+ // Baked 64×64 rg16float LTC GGX magnitude LUT — ltc_mag_ggx from Blender eevee_lut.c.
14
+ // Heitz 2016 LTC fit amplitude — same UV addressing as dfgLut.
15
+ // Used for direct-specular energy compensation (ltc_brdf_scale in closure_eval_glossy_lib.glsl).
16
+ @group(0) @binding(10) var ltcMag: texture_2d<f32>;
17
+
18
+ // ─── RGB ↔ HSV ──────────────────────────────────────────────────────
19
+
20
+ fn rgb_to_hsv(rgb: vec3f) -> vec3f {
21
+ let c_max = max(rgb.r, max(rgb.g, rgb.b));
22
+ let c_min = min(rgb.r, min(rgb.g, rgb.b));
23
+ let delta = c_max - c_min;
24
+
25
+ var h = 0.0;
26
+ if (delta > 1e-6) {
27
+ if (c_max == rgb.r) {
28
+ h = (rgb.g - rgb.b) / delta;
29
+ if (h < 0.0) { h += 6.0; }
30
+ } else if (c_max == rgb.g) {
31
+ h = 2.0 + (rgb.b - rgb.r) / delta;
32
+ } else {
33
+ h = 4.0 + (rgb.r - rgb.g) / delta;
34
+ }
35
+ h /= 6.0;
36
+ }
37
+ let s = select(0.0, delta / c_max, c_max > 1e-6);
38
+ return vec3f(h, s, c_max);
39
+ }
40
+
41
+ fn hsv_to_rgb(hsv: vec3f) -> vec3f {
42
+ let h = hsv.x;
43
+ let s = hsv.y;
44
+ let v = hsv.z;
45
+ if (s < 1e-6) { return vec3f(v); }
46
+
47
+ let hh = fract(h) * 6.0;
48
+ let sector = u32(hh);
49
+ let f = hh - f32(sector);
50
+ let p = v * (1.0 - s);
51
+ let q = v * (1.0 - s * f);
52
+ let t = v * (1.0 - s * (1.0 - f));
53
+
54
+ switch (sector) {
55
+ case 0u: { return vec3f(v, t, p); }
56
+ case 1u: { return vec3f(q, v, p); }
57
+ case 2u: { return vec3f(p, v, t); }
58
+ case 3u: { return vec3f(p, q, v); }
59
+ case 4u: { return vec3f(t, p, v); }
60
+ default: { return vec3f(v, p, q); }
61
+ }
62
+ }
63
+
64
+ // ─── HUE_SAT node ───────────────────────────────────────────────────
65
+
66
+ fn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
67
+ var hsv = rgb_to_hsv(color);
68
+ hsv.x = fract(hsv.x + hue - 0.5);
69
+ hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);
70
+ hsv.z *= value;
71
+ return mix(color, hsv_to_rgb(hsv), fac);
72
+ }
73
+
74
+ // ─── BRIGHTCONTRAST node ────────────────────────────────────────────
75
+
76
+ fn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {
77
+ let a = 1.0 + contrast;
78
+ let b = bright - contrast * 0.5;
79
+ return max(vec3f(0.0), color * a + vec3f(b));
80
+ }
81
+
82
+ // ─── INVERT node ────────────────────────────────────────────────────
83
+
84
+ fn invert(fac: f32, color: vec3f) -> vec3f {
85
+ return mix(color, vec3f(1.0) - color, fac);
86
+ }
87
+
88
+ fn invert_f(fac: f32, val: f32) -> f32 {
89
+ return mix(val, 1.0 - val, fac);
90
+ }
91
+
92
+ // ─── Color ramp (VALTORGB) — 2-stop variants ───────────────────────
93
+ // All 7 presets use exclusively 2-stop ramps.
94
+
95
+ fn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
96
+ return select(c0, c1, f >= p1);
97
+ }
98
+
99
+ // CONSTANT ramp with screen-space edge AA — kills sparkle where fwidth(f) straddles a hard step (NPR terminator)
100
+ fn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {
101
+ let w = max(fwidth(f) * 1.75, 6e-6);
102
+ let t = smoothstep(edge - w, edge + w, f);
103
+ return mix(c0, c1, t);
104
+ }
105
+
106
+ fn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
107
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
108
+ return mix(c0, c1, t);
109
+ }
110
+
111
+ fn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
112
+ // cardinal spline with 2 stops degrades to smoothstep
113
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
114
+ let ss = t * t * (3.0 - 2.0 * t);
115
+ return mix(c0, c1, ss);
116
+ }
117
+
118
+ // ─── MATH node operations ───────────────────────────────────────────
119
+
120
+ fn math_add(a: f32, b: f32) -> f32 { return a + b; }
121
+ fn math_multiply(a: f32, b: f32) -> f32 { return a * b; }
122
+ fn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }
123
+ fn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }
124
+
125
+ // ─── MIX node (blend_type variants) ────────────────────────────────
126
+
127
+ fn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {
128
+ return mix(a, b, fac);
129
+ }
130
+
131
+ fn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {
132
+ let lo = 2.0 * a * b;
133
+ let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);
134
+ let overlay = select(hi, lo, a < vec3f(0.5));
135
+ return mix(a, overlay, fac);
136
+ }
137
+
138
+ fn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {
139
+ return mix(a, a * b, fac);
140
+ }
141
+
142
+ fn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {
143
+ return mix(a, max(a, b), fac);
144
+ }
145
+
146
+ // Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)
147
+ fn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {
148
+ return mix(a, a + 2.0 * b - vec3f(1.0), fac);
149
+ }
150
+
151
+ // Luminance for Shader→RGB scalar gates (linear RGB, Rec.709 weights)
152
+ fn luminance_rec709_linear(c: vec3f) -> f32 {
153
+ return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
154
+ }
155
+
156
+ // ─── FRESNEL node ───────────────────────────────────────────────────
157
+ // Schlick approximation matching Blender's Fresnel node
158
+
159
+ fn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {
160
+ let f0 = pow((ior - 1.0) / (ior + 1.0), 2.0);
161
+ let cos_theta = clamp(dot(n, v), 0.0, 1.0);
162
+ return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);
163
+ }
164
+
165
+ // ─── LAYER_WEIGHT node ──────────────────────────────────────────────
166
+
167
+ fn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {
168
+ let eta = max(1.0 - blend, 1e-4);
169
+ let f0 = pow((1.0 - eta) / (1.0 + eta), 2.0);
170
+ let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);
171
+ return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);
172
+ }
173
+
174
+ fn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {
175
+ var facing = abs(dot(n, v));
176
+ let b = clamp(blend, 0.0, 0.99999);
177
+ if (b != 0.5) {
178
+ let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);
179
+ facing = pow(facing, exponent);
180
+ }
181
+ return 1.0 - facing;
182
+ }
183
+
184
+ // ─── SHADER_TO_RGB (white DiffuseBSDF) ──────────────────────────────
185
+ // Eevee captures lit diffuse: (albedo/π)*sun*N·L*shadow + ambient (linear). Albedo=1.
186
+ // Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.
187
+
188
+ fn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {
189
+ const PI_S: f32 = 3.141592653589793;
190
+ let ndotl = max(dot(n, l), 0.0);
191
+ let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;
192
+ return luminance_rec709_linear(rgb);
193
+ }
194
+
195
+ // ─── AMBIENT_OCCLUSION node (faked) ─────────────────────────────────
196
+ // Real SSAO is a non-goal. We approximate: use the "inside" value from
197
+ // concavity heuristic: 1.0 = fully lit, lower = occluded.
198
+ // For now returns 1.0 (no darkening). Individual presets can override.
199
+
200
+ fn ao_fake(n: vec3f, v: vec3f) -> f32 {
201
+ return 1.0;
202
+ }
203
+
204
+ // ─── BUMP node ──────────────────────────────────────────────────────
205
+ // Screen-space bump from a scalar height field. Needs dFdx/dFdy which
206
+ // WGSL provides as dpdx/dpdy.
207
+
208
+ fn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
209
+ let dhdx = dpdx(height);
210
+ let dhdy = dpdy(height);
211
+ let dpdx_pos = dpdx(world_pos);
212
+ let dpdy_pos = dpdy(world_pos);
213
+ let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));
214
+ return normalize(perturbed);
215
+ }
216
+
217
+ // LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference
218
+ fn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
219
+ let dhdx = dpdx(height);
220
+ let dhdy = dpdy(height);
221
+ let dpdx_pos = dpdx(world_pos);
222
+ let dpdy_pos = dpdy(world_pos);
223
+ let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));
224
+ return normalize(perturbed);
225
+ }
226
+
227
+ // ─── NOISE texture (Perlin-style) ───────────────────────────────────
228
+ // Simplified gradient noise matching Blender's default noise output.
229
+
230
+ fn _hash33(p: vec3f) -> vec3f {
231
+ var q = vec3f(
232
+ dot(p, vec3f(127.1, 311.7, 74.7)),
233
+ dot(p, vec3f(269.5, 183.3, 246.1)),
234
+ dot(p, vec3f(113.5, 271.9, 124.6))
235
+ );
236
+ return fract(sin(q) * 43758.5453123) * 2.0 - 1.0;
237
+ }
238
+
239
+ fn _noise3(p: vec3f) -> f32 {
240
+ let i = floor(p);
241
+ let f = fract(p);
242
+ let u = f * f * (3.0 - 2.0 * f);
243
+
244
+ return mix(
245
+ mix(
246
+ mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),
247
+ dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),
248
+ mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),
249
+ dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),
250
+ mix(
251
+ mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),
252
+ dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),
253
+ mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),
254
+ dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),
255
+ u.z);
256
+ }
257
+
258
+ fn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {
259
+ var q = p;
260
+ if (abs(distortion) > 1e-6) {
261
+ let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));
262
+ q = p + (w * 2.0 - 1.0) * distortion;
263
+ }
264
+ let coords = q * scale;
265
+ var value = 0.0;
266
+ var amplitude = 1.0;
267
+ var frequency = 1.0;
268
+ var total_amp = 0.0;
269
+ let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;
270
+ for (var i = 0; i < octaves; i++) {
271
+ value += amplitude * _noise3(coords * frequency);
272
+ total_amp += amplitude;
273
+ amplitude *= roughness;
274
+ frequency *= 2.0;
275
+ }
276
+ return value / max(total_amp, 1e-6) * 0.5 + 0.5;
277
+ }
278
+
279
+ // ─── TEX_GRADIENT (linear) ──────────────────────────────────────────
280
+ // Used by Stockings preset. Maps the input vector's X to a 0–1 gradient.
281
+
282
+ fn tex_gradient_linear(uv: vec3f) -> f32 {
283
+ return clamp(uv.x, 0.0, 1.0);
284
+ }
285
+
286
+ // ─── TEX_VORONOI (distance only) ────────────────────────────────────
287
+ // Used by Metal preset. Simplified F1 cell noise.
288
+
289
+ fn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {
290
+ let coords = p * scale;
291
+ let i = floor(coords);
292
+ let f = fract(coords);
293
+ var min_dist = 1e10;
294
+ for (var z = -1; z <= 1; z++) {
295
+ for (var y = -1; y <= 1; y++) {
296
+ for (var x = -1; x <= 1; x++) {
297
+ let neighbor = vec3f(f32(x), f32(y), f32(z));
298
+ let point = _hash33(i + neighbor) * 0.5 + 0.5;
299
+ let diff = neighbor + point - f;
300
+ min_dist = min(min_dist, dot(diff, diff));
301
+ }
302
+ }
303
+ }
304
+ return sqrt(min_dist);
305
+ }
306
+
307
+ // ─── SEPXYZ node ────────────────────────────────────────────────────
308
+
309
+ fn separate_xyz(v: vec3f) -> vec3f { return v; }
310
+
311
+ // ─── VECT_MATH (cross product) ──────────────────────────────────────
312
+
313
+ fn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }
314
+
315
+ // ─── MAPPING node ───────────────────────────────────────────────────
316
+ // Point-type mapping: scale, rotate (euler XYZ), translate.
317
+
318
+ fn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {
319
+ var p = v * scl;
320
+ // simplified: skip rotation when all angles are zero (common case)
321
+ if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {
322
+ let cx = cos(rot.x); let sx = sin(rot.x);
323
+ let cy = cos(rot.y); let sy = sin(rot.y);
324
+ let cz = cos(rot.z); let sz = sin(rot.z);
325
+ let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);
326
+ let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);
327
+ p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);
328
+ }
329
+ return p + loc;
330
+ }
331
+
332
+ // ─── NORMAL_MAP node (tangent-space) ────────────────────────────────
333
+ // Applies a tangent-space normal map. Requires TBN from vertex stage.
334
+
335
+ fn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {
336
+ let ts = map_color * 2.0 - 1.0;
337
+ let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);
338
+ return normalize(mix(normal, perturbed, strength));
339
+ }
340
+
341
+ // ─── EEVEE Principled BSDF primitives ───────────────────────────────
342
+ // Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/
343
+ // bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.
344
+ // Usage pattern (see material shaders): direct spec = bsdf_ggx × sun × shadow
345
+ // (NL baked in, no F yet); ambient spec = probe_radiance; tint both with
346
+ // reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.
347
+
348
+ const EEVEE_PI: f32 = 3.141592653589793;
349
+
350
+ // Fused analytic GGX specular (direct lights). Returns BRDF × NL.
351
+ // 4·NL·NV is cancelled via G1_Smith reciprocal form — see bsdf_common_lib.glsl:115.
352
+ fn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, roughness: f32) -> f32 {
353
+ let a = max(roughness, 1e-4);
354
+ let a2 = a * a;
355
+ let H = normalize(L + V);
356
+ let NH = max(dot(N, H), 1e-8);
357
+ let NL = max(dot(N, L), 1e-8);
358
+ let NV = max(dot(N, V), 1e-8);
359
+ // G1_Smith_GGX_opti reciprocal form — denominator piece only.
360
+ let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);
361
+ let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);
362
+ let G = G1L * G1V;
363
+ // D_ggx_opti = pi * denom² — reciprocal D × a².
364
+ let tmp = (NH * a2 - NH) * NH + 1.0;
365
+ let D_opti = EEVEE_PI * tmp * tmp;
366
+ return NL * a2 / (D_opti * G);
367
+ }
368
+
369
+ // Split-sum DFG LUT — Karis 2013 curve fit stand-in for the 64×64 baked LUT.
370
+ // Returns (lut.x, lut.y) in Blender convention: tint = f0·lut.x + f90·lut.y.
371
+ fn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {
372
+ let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);
373
+ let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);
374
+ let r = roughness * c0 + c1;
375
+ let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;
376
+ return vec2f(-1.04, 1.04) * a004 + r.zw;
377
+ }
378
+
379
+ // Baked 64×64 EEVEE split-sum LUT — exact port of bsdf_lut_frag.glsl.
380
+ // Addressed as Blender's common_utiltex_lib.glsl:lut_coords:
381
+ // coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.
382
+ // Requires group(0) binding(9) dfgLut + binding(2) diffuseSampler in the host shader.
383
+ fn brdf_lut_baked(NV: f32, roughness: f32) -> vec2f {
384
+ let LUT_SIZE: f32 = 64.0;
385
+ var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
386
+ uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
387
+ return textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;
388
+ }
389
+
390
+ fn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
391
+ return lut.y * f90 + lut.x * f0;
392
+ }
393
+
394
+ // Fdez-Agüera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).
395
+ fn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
396
+ let FssEss = lut.y * f90 + lut.x * f0;
397
+ let Ess = lut.x + lut.y;
398
+ let Ems = 1.0 - Ess;
399
+ let Favg = f0 + (1.0 - f0) / 21.0;
400
+ let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);
401
+ return FssEss + Fms * Ems;
402
+ }
403
+
404
+ // EEVEE direct-specular energy compensation factor — closure_eval_glossy_lib.glsl:79-81:
405
+ // ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)
406
+ // Because Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum,
407
+ // direct radiance is rescaled so total-energy matches what the split-sum LUT expects.
408
+ // Sample both LUTs at identical lut_coords and return the ratio.
409
+ fn ltc_brdf_scale(NV: f32, roughness: f32) -> f32 {
410
+ let LUT_SIZE: f32 = 64.0;
411
+ var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
412
+ uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
413
+ let ltc = textureSampleLevel(ltcMag, diffuseSampler, uv, 0.0).rg;
414
+ let dfg = textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;
415
+ return (ltc.x + ltc.y) / max(dfg.x + dfg.y, 1e-6);
416
+ }
417
+
418
+ // Luminance-normalized hue extraction — Blender tint_from_color (isolates hue+sat).
419
+ fn tint_from_color(color: vec3f) -> vec3f {
420
+ let lum = dot(color, vec3f(0.3, 0.6, 0.1));
421
+ return select(vec3f(1.0), color / lum, lum > 0.0);
422
+ }
423
+
424
+ `;