reze-engine 0.11.0 → 0.11.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.
- package/README.md +40 -22
- package/dist/engine.d.ts +14 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +206 -77
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +58 -47
- 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 +45 -42
- 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 +30 -26
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +47 -43
- 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 +2 -2
- package/src/engine.ts +227 -97
- package/src/shaders/body.ts +58 -47
- package/src/shaders/cloth_rough.ts +38 -20
- package/src/shaders/cloth_smooth.ts +33 -18
- package/src/shaders/default.ts +46 -42
- package/src/shaders/dfg_lut.ts +32 -28
- package/src/shaders/eye.ts +48 -43
- 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/body.ts
CHANGED
|
@@ -48,43 +48,38 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
48
48
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
49
|
|
|
50
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; }
|
|
51
53
|
let biasedPos = worldPos + n * 0.08;
|
|
52
54
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
53
55
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
54
56
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
55
57
|
let cmpZ = ndc.z - 0.001;
|
|
56
|
-
let ts = 1.0 /
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const PI_B: f32 = 3.141592653589793;
|
|
67
|
-
const F0_BODY: f32 = 0.04;
|
|
68
73
|
const BODY_ROUGHNESS: f32 = 0.3;
|
|
69
74
|
// Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
|
|
70
75
|
const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
|
|
71
76
|
const BODY_RIM2_POW: f32 = 1.4300000667572021;
|
|
72
77
|
const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
|
|
73
78
|
const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
|
|
79
|
+
const BODY_SPECULAR: f32 = 0.5;
|
|
74
80
|
const BODY_MIX_NPR: f32 = 0.5;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
78
|
-
return a2 / (PI_B * denom * denom);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
fn smith_g1_body(ndotx: f32, a2: f32) -> f32 {
|
|
82
|
-
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {
|
|
86
|
-
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
87
|
-
}
|
|
81
|
+
// EEVEE Light Clamp equivalent — caps firefly specular from noise-bumped NDF aliasing.
|
|
82
|
+
const BODY_SPEC_CLAMP: f32 = 10.0;
|
|
88
83
|
|
|
89
84
|
// smoothstep-based ramp: t*t*(3-2*t) between two color stops
|
|
90
85
|
fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
@@ -114,13 +109,19 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
|
114
109
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
115
110
|
}
|
|
116
111
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
117
|
-
|
|
112
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
113
|
+
output.normal = skinnedNrm;
|
|
118
114
|
output.uv = uv;
|
|
119
115
|
output.worldPos = skinnedPos.xyz;
|
|
120
116
|
return output;
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
|
|
119
|
+
struct FSOut {
|
|
120
|
+
@location(0) color: vec4f,
|
|
121
|
+
@location(1) mask: f32,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
124
125
|
let alpha = material.alpha;
|
|
125
126
|
if (alpha < 0.001) { discard; }
|
|
126
127
|
|
|
@@ -137,13 +138,13 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
|
137
138
|
let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
|
|
138
139
|
|
|
139
140
|
// ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
|
|
140
|
-
let shadow_tint =
|
|
141
|
-
let lit_tint =
|
|
141
|
+
let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
|
|
142
|
+
let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
|
|
142
143
|
let toon_color = mix_blend(toon, shadow_tint, lit_tint);
|
|
143
144
|
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
144
145
|
|
|
145
146
|
// ═══ AO CHAIN: AO → ramp CONSTANT [0→white, 0.5995→black] → Mix.003 ═══
|
|
146
|
-
let ao = ao_fake(n, v);
|
|
147
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
147
148
|
let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
|
|
148
149
|
let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));
|
|
149
150
|
|
|
@@ -174,38 +175,48 @@ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
|
174
175
|
let npr_stack = add0 + warm_emission;
|
|
175
176
|
|
|
176
177
|
// ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
|
|
177
|
-
|
|
178
|
-
let noise_val =
|
|
178
|
+
// Mapping loc=rot=0 → plain scale multiply, inline.
|
|
179
|
+
let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);
|
|
179
180
|
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
180
181
|
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
|
|
181
182
|
|
|
182
183
|
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
|
|
183
184
|
let p_emission = bc * 0.2;
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
let sss = ramp_linear(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let
|
|
194
|
-
let
|
|
195
|
-
let
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
// Reuse 'ao' (ao_fake(n, v) above) — identical inputs, avoid a second procedural AO pass.
|
|
187
|
+
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
|
+
|
|
189
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
|
|
190
|
+
let NL = max(dot(bumped_n, l), 0.0);
|
|
191
|
+
let NV = max(dot(bumped_n, v), 1e-4);
|
|
192
|
+
|
|
193
|
+
// f0/f90 per gpu_shader_material_principled.glsl — specular_tint=0 → dielectric_f0_color=white.
|
|
194
|
+
let f0 = vec3f(0.08 * BODY_SPECULAR);
|
|
195
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(BODY_SPECULAR));
|
|
196
|
+
let brdf_lut = brdf_lut_sample(NV, BODY_ROUGHNESS);
|
|
197
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
198
|
+
|
|
199
|
+
// Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after accum).
|
|
200
|
+
// ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
|
|
201
|
+
let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, BODY_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
202
|
+
let spec_direct = min(spec_direct_raw, vec3f(BODY_SPEC_CLAMP));
|
|
203
|
+
// Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
|
|
204
|
+
let spec_indirect = light.ambientColor.xyz;
|
|
205
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
206
|
+
|
|
200
207
|
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
201
208
|
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
202
|
-
|
|
203
|
-
let
|
|
209
|
+
// No (1-F) factor per EEVEE — it doesn't energy-conserve spec<->diffuse.
|
|
210
|
+
let diffuse_radiance = principled_base * (sun * NL * shadow / PI_B + light.ambientColor.xyz);
|
|
211
|
+
let principled = diffuse_radiance + spec_radiance + p_emission + vec3f(sss);
|
|
204
212
|
|
|
205
213
|
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
|
|
206
214
|
let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
|
|
207
215
|
|
|
208
|
-
|
|
216
|
+
var out: FSOut;
|
|
217
|
+
out.color = vec4f(final_color, alpha);
|
|
218
|
+
out.mask = 1.0;
|
|
219
|
+
return out;
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
`
|
|
@@ -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_CR: f32 = 3.141592653589793;
|
|
@@ -73,6 +79,8 @@ const CLOTH_R_EMIT_STR: f32 = 18.200000762939453;
|
|
|
73
79
|
const CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
74
80
|
const CLOTH_R_NOISE_SCALE: f32 = 17.7;
|
|
75
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;
|
|
76
84
|
|
|
77
85
|
@vertex fn vs(
|
|
78
86
|
@location(0) position: vec3f,
|
|
@@ -95,13 +103,19 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
|
95
103
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
96
104
|
}
|
|
97
105
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
98
|
-
|
|
106
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
107
|
+
output.normal = skinnedNrm;
|
|
99
108
|
output.uv = uv;
|
|
100
109
|
output.worldPos = skinnedPos.xyz;
|
|
101
110
|
return output;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
struct FSOut {
|
|
114
|
+
@location(0) color: vec4f,
|
|
115
|
+
@location(1) mask: f32,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
105
119
|
let n = normalize(input.normal);
|
|
106
120
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
107
121
|
let l = -light.lights[0].direction.xyz;
|
|
@@ -121,7 +135,7 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
|
121
135
|
let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);
|
|
122
136
|
|
|
123
137
|
// 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
|
|
124
|
-
let dark_tex =
|
|
138
|
+
let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
|
|
125
139
|
let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
|
|
126
140
|
|
|
127
141
|
// 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
|
|
@@ -129,34 +143,35 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
|
129
143
|
let mix03 = mix_blend(bevel_z, mix04, dark_tex);
|
|
130
144
|
|
|
131
145
|
// 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (white/black) → 混合.002 OVERLAY Fac
|
|
132
|
-
let ao = ao_fake(n, v);
|
|
146
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
133
147
|
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
134
148
|
let mix01_fac = ao_ramp_c.r;
|
|
135
149
|
let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
|
|
136
150
|
|
|
137
151
|
// 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
|
|
138
|
-
let hue004 =
|
|
152
|
+
let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
|
|
139
153
|
let overlay_fac = mix01_rgb.r;
|
|
140
154
|
let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
|
|
141
155
|
let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
|
|
142
156
|
|
|
143
157
|
// 噪波→渐变→凹凸 (LIVE in M_Rough_Cloth — unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.
|
|
144
|
-
|
|
145
|
-
let noise_val =
|
|
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);
|
|
146
160
|
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
147
161
|
let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
|
|
148
162
|
|
|
149
163
|
// 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.
|
|
150
|
-
let principled_base =
|
|
164
|
+
let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
151
165
|
let NL = max(dot(bumped_n, l), 0.0);
|
|
152
166
|
let NV = max(dot(bumped_n, v), 1e-4);
|
|
153
167
|
|
|
154
168
|
let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);
|
|
155
169
|
let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));
|
|
156
|
-
let
|
|
157
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
170
|
+
let brdf_lut = brdf_lut_sample(NV, CLOTH_R_ROUGHNESS);
|
|
171
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
158
172
|
|
|
159
|
-
let
|
|
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));
|
|
160
175
|
let spec_indirect = amb;
|
|
161
176
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
162
177
|
|
|
@@ -168,7 +183,10 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
|
168
183
|
// 混合着色器.001 Fac=0.9: Shader=自发光.005, Shader_001=原理化BSDF
|
|
169
184
|
let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);
|
|
170
185
|
|
|
171
|
-
|
|
186
|
+
var out: FSOut;
|
|
187
|
+
out.color = vec4f(final_color, out_alpha);
|
|
188
|
+
out.mask = 1.0;
|
|
189
|
+
return out;
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
`
|
|
@@ -48,19 +48,25 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
48
48
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
49
|
|
|
50
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; }
|
|
51
53
|
let biasedPos = worldPos + n * 0.08;
|
|
52
54
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
53
55
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
54
56
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
55
57
|
let cmpZ = ndc.z - 0.001;
|
|
56
|
-
let ts = 1.0 /
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const PI_C: f32 = 3.141592653589793;
|
|
@@ -92,13 +98,19 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
|
92
98
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
93
99
|
}
|
|
94
100
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
95
|
-
|
|
101
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
102
|
+
output.normal = skinnedNrm;
|
|
96
103
|
output.uv = uv;
|
|
97
104
|
output.worldPos = skinnedPos.xyz;
|
|
98
105
|
return output;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
struct FSOut {
|
|
109
|
+
@location(0) color: vec4f,
|
|
110
|
+
@location(1) mask: f32,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
102
114
|
let n = normalize(input.normal);
|
|
103
115
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
104
116
|
let l = -light.lights[0].direction.xyz;
|
|
@@ -119,7 +131,7 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
|
119
131
|
let mix04_fac = math_multiply(toon_r, CLOTH_MIX04_MUL);
|
|
120
132
|
|
|
121
133
|
// 混合.004: A=色相/饱和度/明度.002, B=纹理
|
|
122
|
-
let dark_tex =
|
|
134
|
+
let dark_tex = hue_sat_id(1.0, 0.19999998807907104, 1.0, tex_rgb);
|
|
123
135
|
let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
|
|
124
136
|
|
|
125
137
|
// 倒角.001→Z → 混合.003 Factor; A=混合.004, B=色相/饱和度/明度.002
|
|
@@ -127,32 +139,32 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
|
127
139
|
let mix03 = mix_blend(bevel_z, mix04, dark_tex);
|
|
128
140
|
|
|
129
141
|
// 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (白/黑) → 混合.002 OVERLAY Fac
|
|
130
|
-
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.
|
|
131
143
|
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
132
144
|
let mix01_fac = ao_ramp_c.r;
|
|
133
145
|
let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
|
|
134
146
|
|
|
135
147
|
// 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
|
|
136
|
-
let hue004 =
|
|
148
|
+
let hue004 = hue_sat_id(0.800000011920929, 2.0, 1.0, mix03);
|
|
137
149
|
let overlay_fac = mix01_rgb.r;
|
|
138
150
|
let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
|
|
139
151
|
let npr_emission = npr_rgb * NPR_EMIT_STR;
|
|
140
152
|
|
|
141
153
|
// 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.5, specular_tint=0.
|
|
142
154
|
// Bump subtree is dead in the Blender graph (噪波→凹凸 not linked to Principled.Normal).
|
|
143
|
-
let principled_base =
|
|
155
|
+
let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
144
156
|
let NL = max(dot(n, l), 0.0);
|
|
145
157
|
let NV = max(dot(n, v), 1e-4);
|
|
146
158
|
|
|
147
159
|
// f0/f90 per gpu_shader_material_principled.glsl — specular_tint=0 → dielectric_f0_color=white.
|
|
148
160
|
let f0 = vec3f(0.08 * CLOTH_SPECULAR);
|
|
149
161
|
let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_SPECULAR));
|
|
150
|
-
let
|
|
151
|
-
let reflection_color = F_brdf_multi_scatter(f0, f90,
|
|
162
|
+
let brdf_lut = brdf_lut_sample(NV, CLOTH_ROUGHNESS);
|
|
163
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
152
164
|
|
|
153
165
|
// Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after accum).
|
|
154
166
|
// ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.
|
|
155
|
-
let spec_direct = bsdf_ggx(n, l, v, CLOTH_ROUGHNESS) * sun * shadow *
|
|
167
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, CLOTH_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
156
168
|
// Indirect glossy — flat world probe (solid color). Phase 2 adds cubemap.
|
|
157
169
|
let spec_indirect = amb;
|
|
158
170
|
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
@@ -167,7 +179,10 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
|
167
179
|
// 混合着色器.001: Shader=自发光.005, Shader_001=原理化BSDF, Fac=0.9
|
|
168
180
|
let final_color = mix(npr_emission, principled, NPR_MIX_SHADER_FAC);
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
var out: FSOut;
|
|
183
|
+
out.color = vec4f(final_color, out_alpha);
|
|
184
|
+
out.mask = 1.0;
|
|
185
|
+
return out;
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
`
|
package/src/shaders/default.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
// Metallic=0, Specular=0.5 (F0=0.04), Roughness=0.5.
|
|
3
3
|
// Tone mapping via LUT sampled from Blender's OCIO pipeline (exposure -0.3 baked in).
|
|
4
4
|
|
|
5
|
+
import { NODES_WGSL } from "./nodes"
|
|
6
|
+
|
|
5
7
|
export const DEFAULT_SHADER_WGSL = /* wgsl */ `
|
|
6
8
|
|
|
9
|
+
${NODES_WGSL}
|
|
10
|
+
|
|
7
11
|
const PI: f32 = 3.141592653589793;
|
|
8
|
-
const
|
|
12
|
+
const DEFAULT_SPECULAR: f32 = 0.5;
|
|
9
13
|
const ROUGHNESS: f32 = 0.5;
|
|
10
14
|
|
|
11
15
|
struct CameraUniforms {
|
|
@@ -52,21 +56,6 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
52
56
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
53
57
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
54
58
|
|
|
55
|
-
// ─── GGX specular helpers ───────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
fn ggx_d(ndoth: f32, a2: f32) -> f32 {
|
|
58
|
-
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
59
|
-
return a2 / (PI * denom * denom);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
fn smith_g1(ndotx: f32, a2: f32) -> f32 {
|
|
63
|
-
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
|
|
67
|
-
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
59
|
// ─── Filmic tone mapping (LUT extracted from Blender 3.6 OCIO) ─────
|
|
71
60
|
// View transform = Filmic, Look = Medium High Contrast, Exposure = -0.3.
|
|
72
61
|
// 14 samples at integer log2 stops from -10 to +3 (inclusive).
|
|
@@ -91,19 +80,25 @@ fn tonemap(hdr: vec3f) -> vec3f {
|
|
|
91
80
|
// ─── Shadow sampling (3×3 PCF) ──────────────────────────────────────
|
|
92
81
|
|
|
93
82
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
83
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
84
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
94
85
|
let biasedPos = worldPos + n * 0.08;
|
|
95
86
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
96
87
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
97
88
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
98
89
|
let cmpZ = ndc.z - 0.001;
|
|
99
|
-
let ts = 1.0 /
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
let ts = 1.0 / 2048.0;
|
|
91
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
92
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
93
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
94
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
95
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
96
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
97
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
98
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
99
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
100
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
101
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
107
102
|
}
|
|
108
103
|
|
|
109
104
|
// ─── Vertex / Fragment ──────────────────────────────────────────────
|
|
@@ -129,13 +124,19 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
129
124
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
130
125
|
}
|
|
131
126
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
132
|
-
|
|
127
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
128
|
+
output.normal = skinnedNrm;
|
|
133
129
|
output.uv = uv;
|
|
134
130
|
output.worldPos = skinnedPos.xyz;
|
|
135
131
|
return output;
|
|
136
132
|
}
|
|
137
133
|
|
|
138
|
-
|
|
134
|
+
struct FSOut {
|
|
135
|
+
@location(0) color: vec4f,
|
|
136
|
+
@location(1) mask: f32,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
139
140
|
let alpha = material.alpha;
|
|
140
141
|
if (alpha < 0.001) { discard; }
|
|
141
142
|
|
|
@@ -144,26 +145,29 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
144
145
|
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
145
146
|
|
|
146
147
|
let l = -light.lights[0].direction.xyz;
|
|
147
|
-
let
|
|
148
|
-
let
|
|
148
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
149
|
+
let amb = light.ambientColor.xyz;
|
|
150
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
let
|
|
153
|
-
let vdoth = max(dot(v, h), 0.0);
|
|
152
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.5, specular_tint=0.
|
|
153
|
+
let NL = max(dot(n, l), 0.0);
|
|
154
|
+
let NV = max(dot(n, v), 1e-4);
|
|
154
155
|
|
|
155
|
-
let
|
|
156
|
-
let
|
|
157
|
-
let
|
|
158
|
-
let
|
|
159
|
-
let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
|
|
156
|
+
let f0 = vec3f(0.08 * DEFAULT_SPECULAR);
|
|
157
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(DEFAULT_SPECULAR));
|
|
158
|
+
let brdf_lut = brdf_lut_sample(NV, ROUGHNESS);
|
|
159
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
160
160
|
|
|
161
|
-
let
|
|
162
|
-
let
|
|
163
|
-
let
|
|
164
|
-
|
|
161
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
162
|
+
let spec_indirect = amb;
|
|
163
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
164
|
+
|
|
165
|
+
let diffuse_radiance = albedo * (sun * NL * shadow / PI + amb);
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
var out: FSOut;
|
|
168
|
+
out.color = vec4f(diffuse_radiance + spec_radiance, alpha);
|
|
169
|
+
out.mask = 1.0;
|
|
170
|
+
return out;
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
`
|