reze-engine 0.10.2 → 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 +90 -13
- package/dist/engine.d.ts +177 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1204 -318
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/body.d.ts +2 -0
- package/dist/shaders/body.d.ts.map +1 -0
- package/dist/shaders/body.js +232 -0
- package/dist/shaders/classify.d.ts +4 -0
- package/dist/shaders/classify.d.ts.map +1 -0
- package/dist/shaders/classify.js +12 -0
- package/dist/shaders/cloth_rough.d.ts +2 -0
- package/dist/shaders/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/cloth_rough.js +190 -0
- package/dist/shaders/cloth_smooth.d.ts +2 -0
- package/dist/shaders/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/cloth_smooth.js +186 -0
- package/dist/shaders/default.d.ts +2 -0
- package/dist/shaders/default.d.ts.map +1 -0
- package/dist/shaders/default.js +185 -0
- package/dist/shaders/dfg_lut.d.ts +3 -0
- package/dist/shaders/dfg_lut.d.ts.map +1 -0
- package/dist/shaders/dfg_lut.js +129 -0
- package/dist/shaders/eye.d.ts +2 -0
- package/dist/shaders/eye.d.ts.map +1 -0
- package/dist/shaders/eye.js +159 -0
- package/dist/shaders/face.d.ts +2 -0
- package/dist/shaders/face.d.ts.map +1 -0
- package/dist/shaders/face.js +235 -0
- package/dist/shaders/hair.d.ts +2 -0
- package/dist/shaders/hair.d.ts.map +1 -0
- package/dist/shaders/hair.js +196 -0
- package/dist/shaders/ltc_mag_lut.d.ts +3 -0
- package/dist/shaders/ltc_mag_lut.d.ts.map +1 -0
- package/dist/shaders/ltc_mag_lut.js +1033 -0
- package/dist/shaders/metal.d.ts +2 -0
- package/dist/shaders/metal.d.ts.map +1 -0
- package/dist/shaders/metal.js +187 -0
- package/dist/shaders/nodes.d.ts +2 -0
- package/dist/shaders/nodes.d.ts.map +1 -0
- package/dist/shaders/nodes.js +465 -0
- package/dist/shaders/stockings.d.ts +2 -0
- package/dist/shaders/stockings.d.ts.map +1 -0
- package/dist/shaders/stockings.js +244 -0
- package/package.json +1 -1
- package/src/engine.ts +1412 -385
- package/src/index.ts +12 -2
- package/src/shaders/body.ts +234 -0
- package/src/shaders/classify.ts +25 -0
- package/src/shaders/cloth_rough.ts +192 -0
- package/src/shaders/cloth_smooth.ts +188 -0
- package/src/shaders/default.ts +186 -0
- package/src/shaders/dfg_lut.ts +131 -0
- package/src/shaders/eye.ts +160 -0
- package/src/shaders/face.ts +237 -0
- package/src/shaders/hair.ts +198 -0
- package/src/shaders/ltc_mag_lut.ts +1035 -0
- package/src/shaders/metal.ts +189 -0
- package/src/shaders/nodes.ts +466 -0
- package/src/shaders/stockings.ts +246 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// M_Face — WGSL trace of 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Face"; VALTORGB stops from m_graphs (dump omits curve keys).
|
|
2
|
+
|
|
3
|
+
import { NODES_WGSL } from "./nodes"
|
|
4
|
+
|
|
5
|
+
export const FACE_SHADER_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
${NODES_WGSL}
|
|
8
|
+
|
|
9
|
+
struct CameraUniforms {
|
|
10
|
+
view: mat4x4f,
|
|
11
|
+
projection: mat4x4f,
|
|
12
|
+
viewPos: vec3f,
|
|
13
|
+
_padding: f32,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
struct Light {
|
|
17
|
+
direction: vec4f,
|
|
18
|
+
color: vec4f,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
struct LightUniforms {
|
|
22
|
+
ambientColor: vec4f,
|
|
23
|
+
lights: array<Light, 4>,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
struct MaterialUniforms {
|
|
27
|
+
diffuseColor: vec3f,
|
|
28
|
+
alpha: f32,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct VertexOutput {
|
|
32
|
+
@builtin(position) position: vec4f,
|
|
33
|
+
@location(0) normal: vec3f,
|
|
34
|
+
@location(1) uv: vec2f,
|
|
35
|
+
@location(2) worldPos: vec3f,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
39
|
+
|
|
40
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
41
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
42
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
43
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
44
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
45
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
46
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
47
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
48
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
|
+
|
|
50
|
+
// 3x3 PCF shadow sampling, 2048 map, normal-bias 0.08, depth-bias 0.001
|
|
51
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
53
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
54
|
+
let biasedPos = worldPos + n * 0.08;
|
|
55
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
56
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
57
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
58
|
+
let cmpZ = ndc.z - 0.001;
|
|
59
|
+
let ts = 1.0 / 2048.0;
|
|
60
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
61
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
62
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
63
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
64
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
65
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
66
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
67
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
68
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
69
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
70
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PI_F: f32 = 3.141592653589793;
|
|
74
|
+
const FACE_SPECULAR: f32 = 0.5;
|
|
75
|
+
const FACE_ROUGHNESS: f32 = 0.3;
|
|
76
|
+
// Dump M_Face unlinked defaults (math op enum not serialized — warm clamp chain still from m_graphs)
|
|
77
|
+
const FACE_RIM2_POW: f32 = 0.6300000548362732;
|
|
78
|
+
const FACE_RIM2_BG: vec3f = vec3f(1.0, 0.4684903025627136, 0.3698573112487793);
|
|
79
|
+
const FACE_WARM_AO_MUL: f32 = 0.30000001192092896; // 运算.004 MULTIPLY after invert (was 0.5 in older trace)
|
|
80
|
+
const FACE_BRIGHT_TEX_THRESH: f32 = 0.9300000071525574; // 运算.005 GREATER_THAN Value_001
|
|
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;
|
|
86
|
+
|
|
87
|
+
@vertex fn vs(
|
|
88
|
+
@location(0) position: vec3f,
|
|
89
|
+
@location(1) normal: vec3f,
|
|
90
|
+
@location(2) uv: vec2f,
|
|
91
|
+
@location(3) joints0: vec4<u32>,
|
|
92
|
+
@location(4) weights0: vec4<f32>
|
|
93
|
+
) -> VertexOutput {
|
|
94
|
+
var output: VertexOutput;
|
|
95
|
+
let pos4 = vec4f(position, 1.0);
|
|
96
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
97
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
98
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
99
|
+
var skinnedPos = vec4f(0.0);
|
|
100
|
+
var skinnedNrm = vec3f(0.0);
|
|
101
|
+
for (var i = 0u; i < 4u; i++) {
|
|
102
|
+
let m = skinMats[joints0[i]];
|
|
103
|
+
let w = nw[i];
|
|
104
|
+
skinnedPos += (m * pos4) * w;
|
|
105
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
106
|
+
}
|
|
107
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
108
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
109
|
+
output.normal = skinnedNrm;
|
|
110
|
+
output.uv = uv;
|
|
111
|
+
output.worldPos = skinnedPos.xyz;
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fragment: M_Face NPR + Principled hybrid
|
|
116
|
+
// TEX → HueSat shadow/lit → toon gate → BrightContrast → AO chain → emission stack
|
|
117
|
+
// Fresnel rims, warm AO emission, bright-texture gate, noise-bumped Principled
|
|
118
|
+
// Final = mix(Principled, NPR, 0.5)
|
|
119
|
+
struct FSOut {
|
|
120
|
+
@location(0) color: vec4f,
|
|
121
|
+
@location(1) mask: f32,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
125
|
+
let alpha = material.alpha;
|
|
126
|
+
if (alpha < 0.001) { discard; }
|
|
127
|
+
|
|
128
|
+
let n = normalize(input.normal);
|
|
129
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
130
|
+
let l = -light.lights[0].direction.xyz;
|
|
131
|
+
let intensity = light.lights[0].color.w;
|
|
132
|
+
let sun = light.lights[0].color.xyz * intensity;
|
|
133
|
+
|
|
134
|
+
let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
135
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
136
|
+
|
|
137
|
+
// ═══ SOURCES ═══
|
|
138
|
+
// DiffuseBSDF(white) → ShaderToRGB (energy-matched); shadow on direct only
|
|
139
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
140
|
+
// ramp.008 CONSTANT — edge AA avoids binary fac shimmer / white specks on terminator (fwidth + smoothstep)
|
|
141
|
+
let toon = ramp_constant_edge_aa(ndotl_raw, 0.2966, vec4f(0,0,0,1), vec4f(1,1,1,1)).r;
|
|
142
|
+
let ao = 1.0; // ao_fake(n, v) — no SSAO yet; inline 1.0 so the ramp/mix chain folds at compile time.
|
|
143
|
+
|
|
144
|
+
// ═══ TOON COLOR ═══
|
|
145
|
+
let shadow_tint = hue_sat(0.46000000834465027, 2.0, 0.3499999940395355, 1.0, tex_color); // HueSat.002
|
|
146
|
+
let lit_tint = hue_sat(0.46000000834465027, 1.600000023841858, 1.5, 1.0, tex_color); // HueSat.001
|
|
147
|
+
let toon_color = mix_blend(toon, shadow_tint, lit_tint); // Mix.004
|
|
148
|
+
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
149
|
+
|
|
150
|
+
// ═══ AO CHAIN ═══
|
|
151
|
+
// ramp CONSTANT [0→white, 0.5995→black]
|
|
152
|
+
let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
|
|
153
|
+
// Mix.003(Factor=ao_ramp, A=bc, B=reddish tint)
|
|
154
|
+
let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8302, 0.3346, 0.2795));
|
|
155
|
+
|
|
156
|
+
// ═══ EMISSION 3 ═══
|
|
157
|
+
let emission3 = ao_mixed * 2.5; // Emission.003(Strength=2.5)
|
|
158
|
+
|
|
159
|
+
// ═══ WARM EMISSION ═══
|
|
160
|
+
let ao_inv = invert_f(1.0, ao_ramp);
|
|
161
|
+
let warm_str = ao_inv * FACE_WARM_AO_MUL; // 反转 → 运算.004 MULTIPLY Value_001
|
|
162
|
+
let warm_input = clamp(toon * 0.5 + 0.5, 0.0, 1.0); // 运算.001→运算.006→Clamp
|
|
163
|
+
// ramp.003 CARDINAL [0.2409→warm dark, 0.4663→warm light]
|
|
164
|
+
let warm_color = ramp_cardinal(warm_input, 0.2409,
|
|
165
|
+
vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
|
|
166
|
+
vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
|
|
167
|
+
let warm_emission = warm_color * warm_str; // Emission.001
|
|
168
|
+
|
|
169
|
+
// ═══ RIM 1 ═══
|
|
170
|
+
// Fresnel(IOR=2.0) × LayerWeight.001(Facing, Blend=0.24)
|
|
171
|
+
let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24, n, v);
|
|
172
|
+
let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
|
|
173
|
+
|
|
174
|
+
// ═══ RIM 2 ═══
|
|
175
|
+
// Fresnel.001(IOR=1.45) × LayerWeight.002(Fresnel output, Blend=0.61)
|
|
176
|
+
let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
|
|
177
|
+
let rim2_fac = math_power(rim2_raw, FACE_RIM2_POW);
|
|
178
|
+
// MixShader.002: Shader=Emission.003, Shader_001=背景
|
|
179
|
+
let rim2_mixed = mix(emission3, FACE_RIM2_BG, rim2_fac);
|
|
180
|
+
|
|
181
|
+
// 转接点.005(tex) → 运算.005 GREATER_THAN Value_001
|
|
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);
|
|
188
|
+
let bright_emit = vec3f(tex_gate) * 3.0; // Emission.002(Strength=3.0)
|
|
189
|
+
|
|
190
|
+
// ═══ NPR STACK (AddShader chain) ═══
|
|
191
|
+
let add2 = rim2_mixed + bright_emit; // AddShader.002
|
|
192
|
+
let add0 = rim1 + add2; // AddShader
|
|
193
|
+
let npr_stack = add0 + warm_emission; // AddShader.001
|
|
194
|
+
|
|
195
|
+
// ═══ PRINCIPLED BSDF ═══
|
|
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);
|
|
198
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
199
|
+
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos); // 凹凸 Strength; LH bump
|
|
200
|
+
|
|
201
|
+
// Mix.001(Factor=noise_ramp, A=bc, B=dark red)
|
|
202
|
+
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6832, 0.1947, 0.1373));
|
|
203
|
+
// Emission input from reroute.011 (bc), Strength=0.2
|
|
204
|
+
let p_emission = bc * 0.2;
|
|
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;
|
|
208
|
+
|
|
209
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
|
|
210
|
+
let NL = max(dot(bumped_n, l), 0.0);
|
|
211
|
+
let NV = max(dot(bumped_n, v), 1e-4);
|
|
212
|
+
|
|
213
|
+
let f0 = vec3f(0.08 * FACE_SPECULAR);
|
|
214
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(FACE_SPECULAR));
|
|
215
|
+
let brdf_lut = brdf_lut_sample(NV, FACE_ROUGHNESS);
|
|
216
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
217
|
+
|
|
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));
|
|
220
|
+
let spec_indirect = light.ambientColor.xyz;
|
|
221
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
222
|
+
|
|
223
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
224
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
225
|
+
let diffuse_radiance = principled_base * (sun * NL * shadow / PI_F + light.ambientColor.xyz);
|
|
226
|
+
let principled = diffuse_radiance + spec_radiance + p_emission + vec3f(sss);
|
|
227
|
+
|
|
228
|
+
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF — Fac blends toward second
|
|
229
|
+
let final_color = mix(npr_stack, principled, FACE_MIX_NPR);
|
|
230
|
+
|
|
231
|
+
var out: FSOut;
|
|
232
|
+
out.color = vec4f(final_color, alpha);
|
|
233
|
+
out.mask = 1.0;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
`
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// M_Hair — WGSL trace of 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Hair" (socket ids + defaults).
|
|
2
|
+
// MixShader.001: Add→Shader (first), Principled→Shader_001 (second) → out = mix(first, second, Fac).
|
|
3
|
+
|
|
4
|
+
import { NODES_WGSL } from "./nodes"
|
|
5
|
+
|
|
6
|
+
export const HAIR_SHADER_WGSL = /* wgsl */ `
|
|
7
|
+
|
|
8
|
+
${NODES_WGSL}
|
|
9
|
+
|
|
10
|
+
struct CameraUniforms {
|
|
11
|
+
view: mat4x4f,
|
|
12
|
+
projection: mat4x4f,
|
|
13
|
+
viewPos: vec3f,
|
|
14
|
+
_padding: f32,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct Light {
|
|
18
|
+
direction: vec4f,
|
|
19
|
+
color: vec4f,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct LightUniforms {
|
|
23
|
+
ambientColor: vec4f,
|
|
24
|
+
lights: array<Light, 4>,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
struct MaterialUniforms {
|
|
28
|
+
diffuseColor: vec3f,
|
|
29
|
+
alpha: f32,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
struct VertexOutput {
|
|
33
|
+
@builtin(position) position: vec4f,
|
|
34
|
+
@location(0) normal: vec3f,
|
|
35
|
+
@location(1) uv: vec2f,
|
|
36
|
+
@location(2) worldPos: vec3f,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
40
|
+
|
|
41
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
42
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
43
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
44
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
45
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
46
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
47
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
48
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
49
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
50
|
+
|
|
51
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
// Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.
|
|
53
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
54
|
+
let biasedPos = worldPos + n * 0.08;
|
|
55
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
56
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
57
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
58
|
+
let cmpZ = ndc.z - 0.001;
|
|
59
|
+
let ts = 1.0 / 2048.0;
|
|
60
|
+
// 3x3 PCF unrolled — Safari's Metal backend doesn't unroll nested shadow loops reliably.
|
|
61
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
62
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
63
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
64
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
65
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
66
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
67
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
68
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
69
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
70
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PI_H: f32 = 3.141592653589793;
|
|
74
|
+
const HAIR_SPECULAR: f32 = 1.0;
|
|
75
|
+
const HAIR_ROUGHNESS: f32 = 0.3;
|
|
76
|
+
// Dump M_Hair: 运算.004 GREATER_THAN second operand Value_001; 运算.007 POWER exponent Value_001; 背景 Color
|
|
77
|
+
const HAIR_TEX_GATE_THRESH: f32 = 0.15000000596046448;
|
|
78
|
+
const HAIR_RIM2_POW: f32 = 0.6300000548362732;
|
|
79
|
+
const HAIR_MIX_BG: vec3f = vec3f(0.1673291176557541);
|
|
80
|
+
|
|
81
|
+
@vertex fn vs(
|
|
82
|
+
@location(0) position: vec3f,
|
|
83
|
+
@location(1) normal: vec3f,
|
|
84
|
+
@location(2) uv: vec2f,
|
|
85
|
+
@location(3) joints0: vec4<u32>,
|
|
86
|
+
@location(4) weights0: vec4<f32>
|
|
87
|
+
) -> VertexOutput {
|
|
88
|
+
var output: VertexOutput;
|
|
89
|
+
let pos4 = vec4f(position, 1.0);
|
|
90
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
91
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
92
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
93
|
+
var skinnedPos = vec4f(0.0);
|
|
94
|
+
var skinnedNrm = vec3f(0.0);
|
|
95
|
+
for (var i = 0u; i < 4u; i++) {
|
|
96
|
+
let m = skinMats[joints0[i]];
|
|
97
|
+
let w = nw[i];
|
|
98
|
+
skinnedPos += (m * pos4) * w;
|
|
99
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
100
|
+
}
|
|
101
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
102
|
+
// Skip VS normalize — interpolation denormalizes anyway, and FS always does normalize(input.normal).
|
|
103
|
+
output.normal = skinnedNrm;
|
|
104
|
+
output.uv = uv;
|
|
105
|
+
output.worldPos = skinnedPos.xyz;
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
struct FSOut {
|
|
110
|
+
@location(0) color: vec4f,
|
|
111
|
+
@location(1) mask: f32,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
115
|
+
let alpha = material.alpha;
|
|
116
|
+
if (alpha < 0.001) { discard; }
|
|
117
|
+
|
|
118
|
+
let n = normalize(input.normal);
|
|
119
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
120
|
+
let l = -light.lights[0].direction.xyz;
|
|
121
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
122
|
+
|
|
123
|
+
// 图像纹理 ← 纹理坐标.UV → 映射 (default 1,1,1 scale per JSON)
|
|
124
|
+
let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
125
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
126
|
+
|
|
127
|
+
// 色相/饱和度/明度 (Hue=0.5 Sat=1.2 Val=0.5 Fac=1) ← reroute from image
|
|
128
|
+
let hue_sat_shadow = hue_sat_id(1.2, 0.5, 1.0, tex_color);
|
|
129
|
+
// 色相/饱和度/明度.002 (0.48, 1.2, 0.7, 1) ← previous
|
|
130
|
+
let hue_sat_002 = hue_sat(0.48, 1.2, 0.7, 1.0, hue_sat_shadow);
|
|
131
|
+
// 色相/饱和度/明度.001 (0.5, 1.5, 1.0, 1) ← image reroute (lit path)
|
|
132
|
+
let hue_sat_001 = hue_sat_id(1.5, 1.0, 1.0, tex_color);
|
|
133
|
+
|
|
134
|
+
// 漫射 BSDF.002 → Shader --> RGB → 颜色渐变.008 CONSTANT [0→0, 0.2966→1]
|
|
135
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
136
|
+
let ramp_008 = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
|
|
137
|
+
|
|
138
|
+
// 混合.004 MIX Fac=ramp_008, A=hue_sat_002, B=hue_sat_001
|
|
139
|
+
let mix_004 = mix_blend(ramp_008, hue_sat_002, hue_sat_001);
|
|
140
|
+
|
|
141
|
+
// 亮度/对比度 (Bright=0.1 Contrast=0.2) ← mix_004 only (links: not bevel path)
|
|
142
|
+
let bc = bright_contrast(mix_004, 0.1, 0.2);
|
|
143
|
+
|
|
144
|
+
// 倒角.001 → 分离 XYZ.001 → Z → 混合.003 Factor; A=bc, B=hue_sat_002
|
|
145
|
+
let bevel_z = clamp(n.y, 0.0, 1.0);
|
|
146
|
+
let mix_003 = mix_blend(bevel_z, bc, hue_sat_002);
|
|
147
|
+
|
|
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)
|
|
152
|
+
|
|
153
|
+
// 菲涅尔.001 × 层权重.002 → 运算.003 MULTIPLY → 运算.007 POWER(exponent Value_001) → MixShader.002 Fac
|
|
154
|
+
let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
|
|
155
|
+
let rim2_fac = math_power(rim2_raw, HAIR_RIM2_POW);
|
|
156
|
+
// MixShader.002: Shader=Emission.003, Shader_001=背景 — (1-Fac)*emission + Fac*bg
|
|
157
|
+
let mix_shader_002 = mix(emission3, HAIR_MIX_BG, rim2_fac);
|
|
158
|
+
|
|
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);
|
|
162
|
+
let gate_emit = vec3f(tex_gate) * 0.1;
|
|
163
|
+
|
|
164
|
+
// 相加着色器: MixShader.002 + gate emission (color sum in linear space)
|
|
165
|
+
let add_shader = mix_shader_002 + gate_emit;
|
|
166
|
+
|
|
167
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=1.0, roughness=0.3, specular_tint=0.
|
|
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.
|
|
172
|
+
let NL = max(dot(n, l), 0.0);
|
|
173
|
+
let NV = max(dot(n, v), 1e-4);
|
|
174
|
+
|
|
175
|
+
let f0 = vec3f(0.08 * HAIR_SPECULAR);
|
|
176
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(HAIR_SPECULAR));
|
|
177
|
+
let brdf_lut = brdf_lut_sample(NV, HAIR_ROUGHNESS);
|
|
178
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);
|
|
179
|
+
|
|
180
|
+
let spec_direct = bsdf_ggx(n, l, v, NL, NV, HAIR_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);
|
|
181
|
+
let spec_indirect = light.ambientColor.xyz;
|
|
182
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
183
|
+
|
|
184
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
185
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
186
|
+
let diffuse_radiance = bc * (sun * NL * shadow / PI_H + light.ambientColor.xyz);
|
|
187
|
+
let principled = diffuse_radiance + spec_radiance;
|
|
188
|
+
|
|
189
|
+
// 混合着色器.001 Fac=0.2: first socket=相加着色器, second=原理化BSDF
|
|
190
|
+
let final_color = mix(add_shader, principled, 0.2);
|
|
191
|
+
|
|
192
|
+
var out: FSOut;
|
|
193
|
+
out.color = vec4f(final_color, alpha);
|
|
194
|
+
out.mask = 1.0;
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
`
|