reze-engine 0.11.0 → 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.
- package/README.md +40 -22
- package/dist/engine.d.ts +13 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +184 -70
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +44 -21
- package/dist/shaders/cloth_rough.d.ts +1 -1
- package/dist/shaders/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/cloth_rough.js +38 -20
- package/dist/shaders/cloth_smooth.d.ts +1 -1
- package/dist/shaders/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/cloth_smooth.js +33 -18
- package/dist/shaders/default.d.ts +1 -1
- package/dist/shaders/default.d.ts.map +1 -1
- package/dist/shaders/default.js +29 -12
- package/dist/shaders/dfg_lut.d.ts +2 -3
- package/dist/shaders/dfg_lut.d.ts.map +1 -1
- package/dist/shaders/dfg_lut.js +29 -25
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +29 -12
- package/dist/shaders/face.d.ts +1 -1
- package/dist/shaders/face.d.ts.map +1 -1
- package/dist/shaders/face.js +47 -23
- package/dist/shaders/hair.d.ts +1 -1
- package/dist/shaders/hair.d.ts.map +1 -1
- package/dist/shaders/hair.js +42 -32
- package/dist/shaders/metal.d.ts +1 -1
- package/dist/shaders/metal.d.ts.map +1 -1
- package/dist/shaders/metal.js +35 -19
- package/dist/shaders/nodes.d.ts +1 -1
- package/dist/shaders/nodes.d.ts.map +1 -1
- package/dist/shaders/nodes.js +79 -37
- package/dist/shaders/stockings.d.ts +1 -1
- package/dist/shaders/stockings.d.ts.map +1 -1
- package/dist/shaders/stockings.js +30 -15
- package/package.json +1 -1
- package/src/engine.ts +200 -78
- package/src/shaders/body.ts +44 -21
- package/src/shaders/cloth_rough.ts +38 -20
- package/src/shaders/cloth_smooth.ts +33 -18
- package/src/shaders/default.ts +29 -12
- package/src/shaders/dfg_lut.ts +31 -27
- package/src/shaders/eye.ts +29 -12
- package/src/shaders/face.ts +47 -23
- package/src/shaders/hair.ts +42 -32
- package/src/shaders/metal.ts +35 -19
- package/src/shaders/nodes.ts +79 -37
- package/src/shaders/stockings.ts +30 -15
package/src/shaders/eye.ts
CHANGED
|
@@ -60,23 +60,31 @@ fn smith_g1(ndotx: f32, a2: f32) -> f32 {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
|
|
63
|
-
|
|
63
|
+
let m = 1.0 - cosTheta;
|
|
64
|
+
let m2 = m * m;
|
|
65
|
+
return f0 + (1.0 - f0) * (m2 * m2 * m);
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
69
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
70
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
67
71
|
let biasedPos = worldPos + n * 0.08;
|
|
68
72
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
69
73
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
70
74
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
71
75
|
let cmpZ = ndc.z - 0.001;
|
|
72
|
-
let ts = 1.0 /
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
let ts = 1.0 / 2048.0;
|
|
77
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
78
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
79
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
80
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
81
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
82
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
83
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
84
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
85
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
86
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
87
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
@vertex fn vs(
|
|
@@ -100,13 +108,19 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
100
108
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
101
109
|
}
|
|
102
110
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
103
|
-
|
|
111
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
112
|
+
output.normal = skinnedNrm;
|
|
104
113
|
output.uv = uv;
|
|
105
114
|
output.worldPos = skinnedPos.xyz;
|
|
106
115
|
return output;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
struct FSOut {
|
|
119
|
+
@location(0) color: vec4f,
|
|
120
|
+
@location(1) mask: f32,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
110
124
|
let alpha = material.alpha;
|
|
111
125
|
if (alpha < 0.001) { discard; }
|
|
112
126
|
|
|
@@ -137,7 +151,10 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
137
151
|
// Principled Emission socket: emissive = emission_color × strength, added on top of shading.
|
|
138
152
|
let emission = albedo * EYE_EMISSION_STRENGTH;
|
|
139
153
|
|
|
140
|
-
|
|
154
|
+
var out: FSOut;
|
|
155
|
+
out.color = vec4f(ambient + direct + emission, alpha);
|
|
156
|
+
out.mask = 1.0;
|
|
157
|
+
return out;
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
`
|
package/src/shaders/face.ts
CHANGED
|
@@ -47,21 +47,27 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
47
47
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
48
48
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
49
|
|
|
50
|
-
// 3x3 PCF shadow sampling,
|
|
50
|
+
// 3x3 PCF shadow sampling, 2048 map, normal-bias 0.08, depth-bias 0.001
|
|
51
51
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
53
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
52
54
|
let biasedPos = worldPos + n * 0.08;
|
|
53
55
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
54
56
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
55
57
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
56
58
|
let cmpZ = ndc.z - 0.001;
|
|
57
|
-
let ts = 1.0 /
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
let ts = 1.0 / 2048.0;
|
|
60
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
61
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
62
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
63
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
64
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
65
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
66
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
67
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
68
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
69
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
70
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const PI_F: f32 = 3.141592653589793;
|
|
@@ -73,6 +79,10 @@ const FACE_RIM2_BG: vec3f = vec3f(1.0, 0.4684903025627136, 0.3698573112487793);
|
|
|
73
79
|
const FACE_WARM_AO_MUL: f32 = 0.30000001192092896; // 运算.004 MULTIPLY after invert (was 0.5 in older trace)
|
|
74
80
|
const FACE_BRIGHT_TEX_THRESH: f32 = 0.9300000071525574; // 运算.005 GREATER_THAN Value_001
|
|
75
81
|
const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
82
|
+
// EEVEE Light Clamp equivalent (Render Props → Sampling → Clamping). Caps direct
|
|
83
|
+
// specular firefly from the noise-bumped normal's NDF aliasing — Blender hides this
|
|
84
|
+
// via TAA, which we don't have. Value mirrors EEVEE's default Clamp Indirect=10.0.
|
|
85
|
+
const FACE_SPEC_CLAMP: f32 = 10.0;
|
|
76
86
|
|
|
77
87
|
@vertex fn vs(
|
|
78
88
|
@location(0) position: vec3f,
|
|
@@ -95,7 +105,8 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
95
105
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
96
106
|
}
|
|
97
107
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
98
|
-
|
|
108
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
109
|
+
output.normal = skinnedNrm;
|
|
99
110
|
output.uv = uv;
|
|
100
111
|
output.worldPos = skinnedPos.xyz;
|
|
101
112
|
return output;
|
|
@@ -105,7 +116,12 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
105
116
|
// TEX → HueSat shadow/lit → toon gate → BrightContrast → AO chain → emission stack
|
|
106
117
|
// Fresnel rims, warm AO emission, bright-texture gate, noise-bumped Principled
|
|
107
118
|
// Final = mix(Principled, NPR, 0.5)
|
|
108
|
-
|
|
119
|
+
struct FSOut {
|
|
120
|
+
@location(0) color: vec4f,
|
|
121
|
+
@location(1) mask: f32,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
109
125
|
let alpha = material.alpha;
|
|
110
126
|
if (alpha < 0.001) { discard; }
|
|
111
127
|
|
|
@@ -123,7 +139,7 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
123
139
|
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
124
140
|
// ramp.008 CONSTANT — edge AA avoids binary fac shimmer / white specks on terminator (fwidth + smoothstep)
|
|
125
141
|
let toon = ramp_constant_edge_aa(ndotl_raw, 0.2966, vec4f(0,0,0,1), vec4f(1,1,1,1)).r;
|
|
126
|
-
let ao = ao_fake(n, v);
|
|
142
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
127
143
|
|
|
128
144
|
// ═══ TOON COLOR ═══
|
|
129
145
|
let shadow_tint = hue_sat(0.46000000834465027, 2.0, 0.3499999940395355, 1.0, tex_color); // HueSat.002
|
|
@@ -163,7 +179,12 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
163
179
|
let rim2_mixed = mix(emission3, FACE_RIM2_BG, rim2_fac);
|
|
164
180
|
|
|
165
181
|
// 转接点.005(tex) → 运算.005 GREATER_THAN Value_001
|
|
166
|
-
|
|
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).
|
|
187
|
+
let tex_gate = math_greater_than(color_to_value(tex_color), FACE_BRIGHT_TEX_THRESH);
|
|
167
188
|
let bright_emit = vec3f(tex_gate) * 3.0; // Emission.002(Strength=3.0)
|
|
168
189
|
|
|
169
190
|
// ═══ NPR STACK (AddShader chain) ═══
|
|
@@ -172,9 +193,8 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
172
193
|
let npr_stack = add0 + warm_emission; // AddShader.001
|
|
173
194
|
|
|
174
195
|
// ═══ PRINCIPLED BSDF ═══
|
|
175
|
-
// Noise-based bump normal
|
|
176
|
-
let
|
|
177
|
-
let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);
|
|
196
|
+
// Noise-based bump normal. Mapping loc=rot=0 → plain scale multiply, inline.
|
|
197
|
+
let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
|
|
178
198
|
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
179
199
|
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos); // 凹凸 Strength; LH bump
|
|
180
200
|
|
|
@@ -182,9 +202,9 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
182
202
|
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6832, 0.1947, 0.1373));
|
|
183
203
|
// Emission input from reroute.011 (bc), Strength=0.2
|
|
184
204
|
let p_emission = bc * 0.2;
|
|
185
|
-
// AO.002 → ramp.005 LINEAR [0.003→black, 1.0→gray] for subsurface approx
|
|
186
|
-
|
|
187
|
-
let sss = ramp_linear(
|
|
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;
|
|
188
208
|
|
|
189
209
|
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
|
|
190
210
|
let NL = max(dot(bumped_n, l), 0.0);
|
|
@@ -192,10 +212,11 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
192
212
|
|
|
193
213
|
let f0 = vec3f(0.08 * FACE_SPECULAR);
|
|
194
214
|
let f90 = mix(f0, vec3f(1.0), sqrt(FACE_SPECULAR));
|
|
195
|
-
let
|
|
196
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
215
|
+
let brdf_lut = brdf_lut_sample(NV, FACE_ROUGHNESS);
|
|
216
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
197
217
|
|
|
198
|
-
let
|
|
218
|
+
let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, FACE_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
219
|
+
let spec_direct = min(spec_direct_raw, vec3f(FACE_SPEC_CLAMP));
|
|
199
220
|
let spec_indirect = light.ambientColor.xyz;
|
|
200
221
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
201
222
|
|
|
@@ -207,7 +228,10 @@ const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
|
207
228
|
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF — Fac blends toward second
|
|
208
229
|
let final_color = mix(npr_stack, principled, FACE_MIX_NPR);
|
|
209
230
|
|
|
210
|
-
|
|
231
|
+
var out: FSOut;
|
|
232
|
+
out.color = vec4f(final_color, alpha);
|
|
233
|
+
out.mask = 1.0;
|
|
234
|
+
return out;
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
`
|
package/src/shaders/hair.ts
CHANGED
|
@@ -49,19 +49,25 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
49
49
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
50
50
|
|
|
51
51
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
53
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
52
54
|
let biasedPos = worldPos + n * 0.08;
|
|
53
55
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
54
56
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
55
57
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
56
58
|
let cmpZ = ndc.z - 0.001;
|
|
57
|
-
let ts = 1.0 /
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
let ts = 1.0 / 2048.0;
|
|
60
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
61
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
62
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
63
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
64
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
65
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
66
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
67
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
68
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
69
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
70
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const PI_H: f32 = 3.141592653589793;
|
|
@@ -93,13 +99,19 @@ const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
|
93
99
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
94
100
|
}
|
|
95
101
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
96
|
-
|
|
102
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
103
|
+
output.normal = skinnedNrm;
|
|
97
104
|
output.uv = uv;
|
|
98
105
|
output.worldPos = skinnedPos.xyz;
|
|
99
106
|
return output;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
struct FSOut {
|
|
110
|
+
@location(0) color: vec4f,
|
|
111
|
+
@location(1) mask: f32,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
103
115
|
let alpha = material.alpha;
|
|
104
116
|
if (alpha < 0.001) { discard; }
|
|
105
117
|
|
|
@@ -113,11 +125,11 @@ const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
|
113
125
|
let shadow = sampleShadow(input.worldPos, n);
|
|
114
126
|
|
|
115
127
|
// 色相/饱和度/明度 (Hue=0.5 Sat=1.2 Val=0.5 Fac=1) ← reroute from image
|
|
116
|
-
let hue_sat_shadow =
|
|
128
|
+
let hue_sat_shadow = hue_sat_id(1.2, 0.5, 1.0, tex_color);
|
|
117
129
|
// 色相/饱和度/明度.002 (0.48, 1.2, 0.7, 1) ← previous
|
|
118
130
|
let hue_sat_002 = hue_sat(0.48, 1.2, 0.7, 1.0, hue_sat_shadow);
|
|
119
131
|
// 色相/饱和度/明度.001 (0.5, 1.5, 1.0, 1) ← image reroute (lit path)
|
|
120
|
-
let hue_sat_001 =
|
|
132
|
+
let hue_sat_001 = hue_sat_id(1.5, 1.0, 1.0, tex_color);
|
|
121
133
|
|
|
122
134
|
// 漫射 BSDF.002 → Shader --> RGB → 颜色渐变.008 CONSTANT [0→0, 0.2966→1]
|
|
123
135
|
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
@@ -133,19 +145,10 @@ const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
|
133
145
|
let bevel_z = clamp(n.y, 0.0, 1.0);
|
|
134
146
|
let mix_003 = mix_blend(bevel_z, bc, hue_sat_002);
|
|
135
147
|
|
|
136
|
-
// 环境光遮蔽 (AO).001 → 颜色渐变.001
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
// 色相/饱和度/明度.004 (0.5, 0.8, 0.1, 1) ← mix_003
|
|
142
|
-
let hue_sat_004 = hue_sat(0.5, 0.8, 0.1, 1.0, mix_003);
|
|
143
|
-
|
|
144
|
-
// 混合.002 MIX Fac=ao_factor, A=hue_sat_004, B=mix_003
|
|
145
|
-
let mix_002 = mix_blend(ao_factor, hue_sat_004, mix_003);
|
|
146
|
-
|
|
147
|
-
// 自发光(发射).003 Strength=1.0 ← mix_002
|
|
148
|
-
let emission3 = mix_002 * 1.0;
|
|
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)
|
|
149
152
|
|
|
150
153
|
// 菲涅尔.001 × 层权重.002 → 运算.003 MULTIPLY → 运算.007 POWER(exponent Value_001) → MixShader.002 Fac
|
|
151
154
|
let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
|
|
@@ -153,24 +156,28 @@ const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
|
153
156
|
// MixShader.002: Shader=Emission.003, Shader_001=背景 — (1-Fac)*emission + Fac*bg
|
|
154
157
|
let mix_shader_002 = mix(emission3, HAIR_MIX_BG, rim2_fac);
|
|
155
158
|
|
|
156
|
-
// 运算.004 GREATER_THAN: 图像→Value, threshold Value_001
|
|
157
|
-
|
|
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.
|
|
161
|
+
let tex_gate = math_greater_than(color_to_value(tex_color), HAIR_TEX_GATE_THRESH);
|
|
158
162
|
let gate_emit = vec3f(tex_gate) * 0.1;
|
|
159
163
|
|
|
160
164
|
// 相加着色器: MixShader.002 + gate emission (color sum in linear space)
|
|
161
165
|
let add_shader = mix_shader_002 + gate_emit;
|
|
162
166
|
|
|
163
167
|
// 原理化BSDF (EEVEE port): metallic=0, specular=1.0, roughness=0.3, specular_tint=0.
|
|
164
|
-
//
|
|
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.
|
|
165
172
|
let NL = max(dot(n, l), 0.0);
|
|
166
173
|
let NV = max(dot(n, v), 1e-4);
|
|
167
174
|
|
|
168
175
|
let f0 = vec3f(0.08 * HAIR_SPECULAR);
|
|
169
176
|
let f90 = mix(f0, vec3f(1.0), sqrt(HAIR_SPECULAR));
|
|
170
|
-
let
|
|
171
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
177
|
+
let brdf_lut = brdf_lut_sample(NV, HAIR_ROUGHNESS);
|
|
178
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
172
179
|
|
|
173
|
-
let spec_direct = bsdf_ggx(n, l, v, HAIR_ROUGHNESS) * sun * shadow *
|
|
180
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, HAIR_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
174
181
|
let spec_indirect = light.ambientColor.xyz;
|
|
175
182
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
176
183
|
|
|
@@ -182,7 +189,10 @@ const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
|
182
189
|
// 混合着色器.001 Fac=0.2: first socket=相加着色器, second=原理化BSDF
|
|
183
190
|
let final_color = mix(add_shader, principled, 0.2);
|
|
184
191
|
|
|
185
|
-
|
|
192
|
+
var out: FSOut;
|
|
193
|
+
out.color = vec4f(final_color, alpha);
|
|
194
|
+
out.mask = 1.0;
|
|
195
|
+
return out;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
`
|
package/src/shaders/metal.ts
CHANGED
|
@@ -51,19 +51,25 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
51
51
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
52
52
|
|
|
53
53
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
54
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
55
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
54
56
|
let biasedPos = worldPos + n * 0.08;
|
|
55
57
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
56
58
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
57
59
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
58
60
|
let cmpZ = ndc.z - 0.001;
|
|
59
|
-
let ts = 1.0 /
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
let ts = 1.0 / 2048.0;
|
|
62
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
63
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
64
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
65
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
66
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
67
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
68
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
69
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
70
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
71
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
72
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
const PI_M: f32 = 3.141592653589793;
|
|
@@ -98,13 +104,19 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
98
104
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
99
105
|
}
|
|
100
106
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
101
|
-
|
|
107
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
108
|
+
output.normal = skinnedNrm;
|
|
102
109
|
output.uv = uv;
|
|
103
110
|
output.worldPos = skinnedPos.xyz;
|
|
104
111
|
return output;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
struct FSOut {
|
|
115
|
+
@location(0) color: vec4f,
|
|
116
|
+
@location(1) mask: f32,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
108
120
|
let n = normalize(input.normal);
|
|
109
121
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
110
122
|
let l = -light.lights[0].direction.xyz;
|
|
@@ -118,23 +130,23 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
118
130
|
if (out_alpha < 0.001) { discard; }
|
|
119
131
|
|
|
120
132
|
// ═══ NPR toon stack (图像 → HSV.007 Val=0.8 → 转接点.001) ═══
|
|
121
|
-
let tex_tint =
|
|
133
|
+
let tex_tint = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
122
134
|
let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
123
135
|
let ramp008 = ramp_constant_edge_aa(lum_shade, METAL_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
|
|
124
136
|
let mix04_fac = math_multiply(ramp008.r, METAL_MIX04_MUL);
|
|
125
137
|
|
|
126
138
|
// 混合.004: A=HSV.002(Val=0.2 dark), B=tex_tint
|
|
127
|
-
let dark_tex =
|
|
139
|
+
let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_tint);
|
|
128
140
|
let mix04 = mix_blend(mix04_fac, dark_tex, tex_tint);
|
|
129
141
|
|
|
130
142
|
// AO white/black ramp → 混合.002 factor
|
|
131
|
-
let ao = ao_fake(n, v);
|
|
143
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
132
144
|
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
133
145
|
let overlay_fac = mix(1.0, 0.0, ao_ramp_c.r);
|
|
134
146
|
|
|
135
147
|
// 混合.002 OVERLAY: A=HSV.008(Val=1.0 identity) ← mix04, B=HSV.004(Val=2.0 bright) ← mix04
|
|
136
148
|
let hue008 = mix04; // identity HSV
|
|
137
|
-
let hue004 =
|
|
149
|
+
let hue004 = hue_sat_id(1.0, 2.0, 1.0, mix04);
|
|
138
150
|
let npr_rgb = mix_overlay(overlay_fac, hue008, hue004);
|
|
139
151
|
let npr_emission = npr_rgb * METAL_EMIT_STR;
|
|
140
152
|
|
|
@@ -145,7 +157,7 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
145
157
|
let voro = tex_voronoi_f1(refl_dir, METAL_VORONOI_SCALE);
|
|
146
158
|
let voro_ramp = ramp_linear(voro, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
147
159
|
// 混合.005: Fac=voro_ramp, A=voro_color(grayscale), B=HSV.006(Hue=0.5 Sat=1.5 Val=1.3)
|
|
148
|
-
let hue006 =
|
|
160
|
+
let hue006 = hue_sat_id(1.5, 1.2999999523162842, 1.0, tex_tint);
|
|
149
161
|
let albedo = mix_blend(voro_ramp, vec3f(voro_ramp), hue006);
|
|
150
162
|
|
|
151
163
|
// 原理化BSDF (EEVEE port): metallic=1.0, specular=1.0, roughness=0.3.
|
|
@@ -153,11 +165,12 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
153
165
|
// metallic=1 this is just albedo (specular_tint is dielectric-only and ignored here).
|
|
154
166
|
let f0 = albedo;
|
|
155
167
|
let f90 = mix(f0, vec3f(1.0), sqrt(METAL_SPECULAR));
|
|
168
|
+
let NL = max(dot(n, l), 0.0);
|
|
156
169
|
let NV = max(dot(n, v), 1e-4);
|
|
157
|
-
let
|
|
158
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
170
|
+
let brdf_lut = brdf_lut_sample(NV, METAL_ROUGHNESS);
|
|
171
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
159
172
|
|
|
160
|
-
let spec_direct = bsdf_ggx(n, l, v, METAL_ROUGHNESS) * sun * shadow *
|
|
173
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, METAL_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
161
174
|
let spec_indirect = amb;
|
|
162
175
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
163
176
|
|
|
@@ -167,7 +180,10 @@ const METAL_VORONOI_SCALE: f32 = 4.3;
|
|
|
167
180
|
// 混合着色器.001 Fac=0.6967: Shader=npr_emission, Shader_001=principled
|
|
168
181
|
let final_color = mix(npr_emission, principled, METAL_MIX_SHADER_FAC);
|
|
169
182
|
|
|
170
|
-
|
|
183
|
+
var out: FSOut;
|
|
184
|
+
out.color = vec4f(final_color, out_alpha);
|
|
185
|
+
out.mask = 1.0;
|
|
186
|
+
return out;
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
`
|