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/dfg_lut.ts
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
// One-shot bake pass that
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// One-shot bake pass that produces the combined EEVEE BRDF LUT.
|
|
2
|
+
// Output: 64×64 rgba8unorm — .rg = split-sum DFG (Blender bsdf_lut_frag.glsl,
|
|
3
|
+
// Karis convention: tint = f0·x + f90·y), .ba = Heitz 2016 LTC magnitude
|
|
4
|
+
// (ltc_mag_ggx from eevee_lut.c), sampled from a temp rg16float source texture
|
|
5
|
+
// passed in at bake time.
|
|
4
6
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
7
|
+
// Packing both LUTs into one texture lets runtime shaders do a SINGLE texture
|
|
8
|
+
// fetch per fragment to get everything needed for F_brdf_multi_scatter AND
|
|
9
|
+
// ltc_brdf_scale. Was 3 taps (dfg in brdf_lut_baked + dfg+ltc in ltc_brdf_scale);
|
|
10
|
+
// now 1. Big win on Apple GPUs where fragment-stage texture fetches are the
|
|
11
|
+
// dominant cost with MSAA.
|
|
12
|
+
//
|
|
13
|
+
// rgba8unorm (vs rgba16float) is a deliberate precision drop: DFG values live in
|
|
14
|
+
// [0,1], LTC magnitude in [0,1], 1/255 quantization is below the perceptual
|
|
15
|
+
// threshold for direct-spec energy compensation. Halves bandwidth per sample.
|
|
16
|
+
|
|
17
|
+
export const BRDF_LUT_SIZE = 64
|
|
18
|
+
const BAKE_SAMPLE_COUNT = 32
|
|
19
|
+
|
|
20
|
+
export const BRDF_LUT_BAKE_WGSL = /* wgsl */ `
|
|
21
|
+
const LUT_SIZE: f32 = ${BRDF_LUT_SIZE}.0;
|
|
22
|
+
const SAMPLE_COUNT: u32 = ${BAKE_SAMPLE_COUNT}u;
|
|
17
23
|
const M_2PI: f32 = 6.283185307179586;
|
|
18
24
|
|
|
25
|
+
// Temp LTC magnitude source (rg16float, uploaded from eevee_lut.c ltc_mag_ggx).
|
|
26
|
+
// Sampled 1:1 by pixel — bake coord mapping matches runtime sample coord mapping.
|
|
27
|
+
@group(0) @binding(0) var ltcSrc: texture_2d<f32>;
|
|
28
|
+
|
|
19
29
|
@vertex fn vs(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f {
|
|
20
|
-
// Full-screen triangle covering [-1,1]² in NDC.
|
|
21
30
|
let x = f32((vid << 1u) & 2u) * 2.0 - 1.0;
|
|
22
31
|
let y = f32(vid & 2u) * 2.0 - 1.0;
|
|
23
32
|
return vec4f(x, y, 0.0, 1.0);
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
// common_math_geom_lib.glsl:165 — make_orthonormal_basis.
|
|
27
35
|
fn orthonormal_basis(N: vec3f) -> mat2x3f {
|
|
28
36
|
let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(N.z) < 0.99999);
|
|
29
37
|
let T = normalize(cross(up, N));
|
|
@@ -31,7 +39,6 @@ fn orthonormal_basis(N: vec3f) -> mat2x3f {
|
|
|
31
39
|
return mat2x3f(T, B);
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
// bsdf_sampling_lib.glsl:27 — Heitz 2018 VNDF sampling in tangent space.
|
|
35
42
|
fn sample_ggx_vndf(rand: vec3f, alpha: f32, Vt: vec3f) -> vec3f {
|
|
36
43
|
let Vh = normalize(vec3f(alpha * Vt.xy, Vt.z));
|
|
37
44
|
let tb = orthonormal_basis(Vh);
|
|
@@ -47,12 +54,10 @@ fn sample_ggx_vndf(rand: vec3f, alpha: f32, Vt: vec3f) -> vec3f {
|
|
|
47
54
|
return normalize(vec3f(alpha * Hh.xy, saturate(Hh.z)));
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
// bsdf_common_lib.glsl:105 — G1 Smith GGX (Brian Karis opti form).
|
|
51
57
|
fn G1_Smith_GGX_opti(NX: f32, a2: f32) -> f32 {
|
|
52
58
|
return NX + sqrt(NX * (NX - NX * a2) + a2);
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
// bsdf_common_lib.glsl:50 — exact dielectric Fresnel (monochromatic).
|
|
56
61
|
fn F_eta(eta: f32, cos_theta: f32) -> f32 {
|
|
57
62
|
let c = abs(cos_theta);
|
|
58
63
|
var g = eta * eta - 1.0 + c * c;
|
|
@@ -62,7 +67,7 @@ fn F_eta(eta: f32, cos_theta: f32) -> f32 {
|
|
|
62
67
|
let B = (c * (g + c) - 1.0) / (c * (g - c) + 1.0);
|
|
63
68
|
return 0.5 * A * A * (1.0 + B * B);
|
|
64
69
|
}
|
|
65
|
-
return 1.0;
|
|
70
|
+
return 1.0;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
fn f0_from_ior(eta: f32) -> f32 {
|
|
@@ -70,23 +75,21 @@ fn f0_from_ior(eta: f32) -> f32 {
|
|
|
70
75
|
return A * A;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
// F_color_blend(eta, fresnel, vec3(0)).r — blend factor only.
|
|
74
78
|
fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
|
|
75
79
|
let f0 = f0_from_ior(eta);
|
|
76
80
|
return saturate((fresnel - f0) / (1.0 - f0));
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0)
|
|
83
|
+
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
|
|
80
84
|
let y_uv = floor(frag.y) / (LUT_SIZE - 1.0);
|
|
81
85
|
let x_uv = floor(frag.x) / (LUT_SIZE - 1.0);
|
|
82
86
|
|
|
83
87
|
let NV = clamp(1.0 - y_uv * y_uv, 1e-4, 0.9999);
|
|
84
|
-
let a = x_uv
|
|
88
|
+
let a = max(x_uv, 1e-4);
|
|
85
89
|
let a2 = clamp(a * a, 1e-4, 0.9999);
|
|
86
90
|
|
|
87
91
|
let V = vec3f(sqrt(1.0 - NV * NV), 0.0, NV);
|
|
88
92
|
|
|
89
|
-
// principled specular=1.0 — max value, matches bsdf_lut_frag.glsl:41.
|
|
90
93
|
let eta = (2.0 / (1.0 - sqrt(0.08 * 1.0))) - 1.0;
|
|
91
94
|
|
|
92
95
|
var brdf_accum = 0.0;
|
|
@@ -96,7 +99,6 @@ fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
|
|
|
96
99
|
for (var i: u32 = 0u; i < SAMPLE_COUNT; i = i + 1u) {
|
|
97
100
|
let ix = (f32(i) + 0.5) / sc_f;
|
|
98
101
|
let iy = (f32(j) + 0.5) / sc_f;
|
|
99
|
-
// Xi.x = radial, Xi.yz = (cos, sin) of azimuth — bsdf_lut_frag.glsl:22.
|
|
100
102
|
let Xi = vec3f(ix, cos(iy * M_2PI), sin(iy * M_2PI));
|
|
101
103
|
|
|
102
104
|
let H = sample_ggx_vndf(Xi, a, V);
|
|
@@ -106,7 +108,6 @@ fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
|
|
|
106
108
|
let NH = max(H.z, 0.0);
|
|
107
109
|
let VH = max(dot(V, H), 0.0);
|
|
108
110
|
|
|
109
|
-
// G_smith (divided form): 4·NV·NL / (G1_v·G1_l). See bsdf_common_lib.glsl:105.
|
|
110
111
|
let G1v = G1_Smith_GGX_opti(NV, a2);
|
|
111
112
|
let G1l = G1_Smith_GGX_opti(NL, a2);
|
|
112
113
|
let G_smith = 4.0 * NV * NL / (G1v * G1l);
|
|
@@ -122,6 +123,9 @@ fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
let n2 = sc_f * sc_f;
|
|
125
|
-
|
|
126
|
+
let dfg = vec2f(brdf_accum / n2, fresnel_accum / n2);
|
|
127
|
+
// Pack preloaded LTC magnitude at matching (roughness, sqrt(1-NV)) pixel.
|
|
128
|
+
let ltc = textureLoad(ltcSrc, vec2i(i32(frag.x), i32(frag.y)), 0).rg;
|
|
129
|
+
return vec4f(dfg, ltc);
|
|
126
130
|
}
|
|
127
131
|
`
|
package/src/shaders/eye.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// Eye preset — default Principled BSDF (
|
|
1
|
+
// Eye preset — default Principled BSDF (Specular=0.5, Roughness=0.5) + Emission socket set to albedo × 1.5.
|
|
2
2
|
// Matches the published preset's instruction: "keep eyes in the default nodegraph, add emission 1.5".
|
|
3
3
|
// Blender's Principled BSDF Emission socket is added on top of the shaded output (pre-tonemap, feeds bloom).
|
|
4
4
|
|
|
5
|
+
import { NODES_WGSL } from "./nodes"
|
|
6
|
+
|
|
5
7
|
export const EYE_SHADER_WGSL = /* wgsl */ `
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
9
|
+
${NODES_WGSL}
|
|
10
|
+
|
|
11
|
+
const PI_E: f32 = 3.141592653589793;
|
|
12
|
+
const EYE_SPECULAR: f32 = 0.5;
|
|
13
|
+
const EYE_ROUGHNESS: f32 = 0.5;
|
|
10
14
|
const EYE_EMISSION_STRENGTH: f32 = 1.5;
|
|
11
15
|
|
|
12
16
|
struct CameraUniforms {
|
|
@@ -50,33 +54,26 @@ struct LightVP { viewProj: mat4x4f, };
|
|
|
50
54
|
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
51
55
|
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
52
56
|
|
|
53
|
-
fn ggx_d(ndoth: f32, a2: f32) -> f32 {
|
|
54
|
-
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
55
|
-
return a2 / (PI * denom * denom);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
fn smith_g1(ndotx: f32, a2: f32) -> f32 {
|
|
59
|
-
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
|
|
63
|
-
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
58
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
59
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
67
60
|
let biasedPos = worldPos + n * 0.08;
|
|
68
61
|
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
69
62
|
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
70
63
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
71
64
|
let cmpZ = ndc.z - 0.001;
|
|
72
|
-
let ts = 1.0 /
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
let ts = 1.0 / 2048.0;
|
|
66
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
67
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
68
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
69
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
70
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
71
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
72
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
73
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
74
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
75
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
76
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
@vertex fn vs(
|
|
@@ -100,13 +97,19 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
100
97
|
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
101
98
|
}
|
|
102
99
|
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
103
|
-
|
|
100
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
101
|
+
output.normal = skinnedNrm;
|
|
104
102
|
output.uv = uv;
|
|
105
103
|
output.worldPos = skinnedPos.xyz;
|
|
106
104
|
return output;
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
struct FSOut {
|
|
108
|
+
@location(0) color: vec4f,
|
|
109
|
+
@location(1) mask: f32,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
110
113
|
let alpha = material.alpha;
|
|
111
114
|
if (alpha < 0.001) { discard; }
|
|
112
115
|
|
|
@@ -115,29 +118,31 @@ fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
|
115
118
|
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
116
119
|
|
|
117
120
|
let l = -light.lights[0].direction.xyz;
|
|
118
|
-
let
|
|
119
|
-
let
|
|
121
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
122
|
+
let amb = light.ambientColor.xyz;
|
|
123
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
let
|
|
123
|
-
let
|
|
124
|
-
let vdoth = max(dot(v, h), 0.0);
|
|
125
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.5, specular_tint=0.
|
|
126
|
+
let NL = max(dot(n, l), 0.0);
|
|
127
|
+
let NV = max(dot(n, v), 1e-4);
|
|
125
128
|
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
let
|
|
129
|
-
let
|
|
130
|
-
let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
|
|
129
|
+
let f0 = vec3f(0.08 * EYE_SPECULAR);
|
|
130
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(EYE_SPECULAR));
|
|
131
|
+
let brdf_lut = brdf_lut_sample(NV, EYE_ROUGHNESS);
|
|
132
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
131
133
|
|
|
132
|
-
let
|
|
133
|
-
let
|
|
134
|
-
let
|
|
135
|
-
let ambient = albedo * light.ambientColor.xyz;
|
|
134
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, EYE_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
135
|
+
let spec_indirect = amb;
|
|
136
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
136
137
|
|
|
138
|
+
let diffuse_radiance = albedo * (sun * NL * shadow / PI_E + amb);
|
|
137
139
|
// Principled Emission socket: emissive = emission_color × strength, added on top of shading.
|
|
138
140
|
let emission = albedo * EYE_EMISSION_STRENGTH;
|
|
139
141
|
|
|
140
|
-
|
|
142
|
+
var out: FSOut;
|
|
143
|
+
out.color = vec4f(diffuse_radiance + spec_radiance + emission, alpha);
|
|
144
|
+
out.mask = 1.0;
|
|
145
|
+
return out;
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
`
|
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
|
`
|