reze-engine 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -13
- package/dist/engine.d.ts +170 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1080 -308
- 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 +209 -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 +172 -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 +171 -0
- package/dist/shaders/default.d.ts +2 -0
- package/dist/shaders/default.d.ts.map +1 -0
- package/dist/shaders/default.js +168 -0
- package/dist/shaders/dfg_lut.d.ts +4 -0
- package/dist/shaders/dfg_lut.d.ts.map +1 -0
- package/dist/shaders/dfg_lut.js +125 -0
- package/dist/shaders/eye.d.ts +2 -0
- package/dist/shaders/eye.d.ts.map +1 -0
- package/dist/shaders/eye.js +142 -0
- package/dist/shaders/face.d.ts +2 -0
- package/dist/shaders/face.d.ts.map +1 -0
- package/dist/shaders/face.js +211 -0
- package/dist/shaders/hair.d.ts +2 -0
- package/dist/shaders/hair.d.ts.map +1 -0
- package/dist/shaders/hair.js +186 -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 +171 -0
- package/dist/shaders/nodes.d.ts +2 -0
- package/dist/shaders/nodes.d.ts.map +1 -0
- package/dist/shaders/nodes.js +423 -0
- package/dist/shaders/stockings.d.ts +2 -0
- package/dist/shaders/stockings.d.ts.map +1 -0
- package/dist/shaders/stockings.js +229 -0
- package/package.json +1 -1
- package/src/engine.ts +1281 -376
- package/src/index.ts +12 -2
- package/src/shaders/body.ts +211 -0
- package/src/shaders/classify.ts +25 -0
- package/src/shaders/cloth_rough.ts +174 -0
- package/src/shaders/cloth_smooth.ts +173 -0
- package/src/shaders/default.ts +169 -0
- package/src/shaders/dfg_lut.ts +127 -0
- package/src/shaders/eye.ts +143 -0
- package/src/shaders/face.ts +213 -0
- package/src/shaders/hair.ts +188 -0
- package/src/shaders/ltc_mag_lut.ts +1035 -0
- package/src/shaders/metal.ts +173 -0
- package/src/shaders/nodes.ts +424 -0
- package/src/shaders/stockings.ts +231 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { Engine, type EngineStats, type LoadModelFromFilesOptions } from "./engine";
|
|
1
|
+
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, } from "./engine";
|
|
2
2
|
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
|
|
3
3
|
export { Model } from "./model";
|
|
4
4
|
export { Vec3, Quat, Mat4 } from "./math";
|
|
5
5
|
export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
|
|
6
6
|
export { FPS } from "./animation";
|
|
7
7
|
export { Physics, type PhysicsOptions } from "./physics";
|
|
8
|
+
export type { MaterialPreset, MaterialPresetMap } from "./shaders/classify";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,GAC/B,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Engine } from "./engine";
|
|
1
|
+
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, } from "./engine";
|
|
2
2
|
export { parsePmxFolderInput, pmxFileAtRelativePath } from "./folder-upload";
|
|
3
3
|
export { Model } from "./model";
|
|
4
4
|
export { Vec3, Quat, Mat4 } from "./math";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const BODY_SHADER_WGSL = "\n\n\n\n// Baked 64\u00D764 rg16float DFG LUT \u2014 created once at engine init by dfg_lut.ts.\n// Paired with group(0) binding(2) diffuseSampler (linear filter, clamp implicit\n// via the half-texel bias inside brdf_lut_baked). Bound by the main per-frame\n// bind group to every material pipeline that includes this module.\n@group(0) @binding(9) var dfgLut: texture_2d<f32>;\n\n// Baked 64\u00D764 rg16float LTC GGX magnitude LUT \u2014 ltc_mag_ggx from Blender eevee_lut.c.\n// Heitz 2016 LTC fit amplitude \u2014 same UV addressing as dfgLut.\n// Used for direct-specular energy compensation (ltc_brdf_scale in closure_eval_glossy_lib.glsl).\n@group(0) @binding(10) var ltcMag: texture_2d<f32>;\n\n// \u2500\u2500\u2500 RGB \u2194 HSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn rgb_to_hsv(rgb: vec3f) -> vec3f {\n let c_max = max(rgb.r, max(rgb.g, rgb.b));\n let c_min = min(rgb.r, min(rgb.g, rgb.b));\n let delta = c_max - c_min;\n\n var h = 0.0;\n if (delta > 1e-6) {\n if (c_max == rgb.r) {\n h = (rgb.g - rgb.b) / delta;\n if (h < 0.0) { h += 6.0; }\n } else if (c_max == rgb.g) {\n h = 2.0 + (rgb.b - rgb.r) / delta;\n } else {\n h = 4.0 + (rgb.r - rgb.g) / delta;\n }\n h /= 6.0;\n }\n let s = select(0.0, delta / c_max, c_max > 1e-6);\n return vec3f(h, s, c_max);\n}\n\nfn hsv_to_rgb(hsv: vec3f) -> vec3f {\n let h = hsv.x;\n let s = hsv.y;\n let v = hsv.z;\n if (s < 1e-6) { return vec3f(v); }\n\n let hh = fract(h) * 6.0;\n let sector = u32(hh);\n let f = hh - f32(sector);\n let p = v * (1.0 - s);\n let q = v * (1.0 - s * f);\n let t = v * (1.0 - s * (1.0 - f));\n\n switch (sector) {\n case 0u: { return vec3f(v, t, p); }\n case 1u: { return vec3f(q, v, p); }\n case 2u: { return vec3f(p, v, t); }\n case 3u: { return vec3f(p, q, v); }\n case 4u: { return vec3f(t, p, v); }\n default: { return vec3f(v, p, q); }\n }\n}\n\n// \u2500\u2500\u2500 HUE_SAT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {\n var hsv = rgb_to_hsv(color);\n hsv.x = fract(hsv.x + hue - 0.5);\n hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);\n hsv.z *= value;\n return mix(color, hsv_to_rgb(hsv), fac);\n}\n\n// \u2500\u2500\u2500 BRIGHTCONTRAST node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {\n let a = 1.0 + contrast;\n let b = bright - contrast * 0.5;\n return max(vec3f(0.0), color * a + vec3f(b));\n}\n\n// \u2500\u2500\u2500 INVERT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn invert(fac: f32, color: vec3f) -> vec3f {\n return mix(color, vec3f(1.0) - color, fac);\n}\n\nfn invert_f(fac: f32, val: f32) -> f32 {\n return mix(val, 1.0 - val, fac);\n}\n\n// \u2500\u2500\u2500 Color ramp (VALTORGB) \u2014 2-stop variants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// All 7 presets use exclusively 2-stop ramps.\n\nfn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n return select(c0, c1, f >= p1);\n}\n\n// CONSTANT ramp with screen-space edge AA \u2014 kills sparkle where fwidth(f) straddles a hard step (NPR terminator)\nfn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {\n let w = max(fwidth(f) * 1.75, 6e-6);\n let t = smoothstep(edge - w, edge + w, f);\n return mix(c0, c1, t);\n}\n\nfn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n return mix(c0, c1, t);\n}\n\nfn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n // cardinal spline with 2 stops degrades to smoothstep\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n// \u2500\u2500\u2500 MATH node operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn math_add(a: f32, b: f32) -> f32 { return a + b; }\nfn math_multiply(a: f32, b: f32) -> f32 { return a * b; }\nfn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }\nfn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }\n\n// \u2500\u2500\u2500 MIX node (blend_type variants) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, b, fac);\n}\n\nfn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n let lo = 2.0 * a * b;\n let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);\n let overlay = select(hi, lo, a < vec3f(0.5));\n return mix(a, overlay, fac);\n}\n\nfn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a * b, fac);\n}\n\nfn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, max(a, b), fac);\n}\n\n// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)\nfn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a + 2.0 * b - vec3f(1.0), fac);\n}\n\n// Luminance for Shader\u2192RGB scalar gates (linear RGB, Rec.709 weights)\nfn luminance_rec709_linear(c: vec3f) -> f32 {\n return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));\n}\n\n// \u2500\u2500\u2500 FRESNEL node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schlick approximation matching Blender's Fresnel node\n\nfn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {\n let f0 = pow((ior - 1.0) / (ior + 1.0), 2.0);\n let cos_theta = clamp(dot(n, v), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\n// \u2500\u2500\u2500 LAYER_WEIGHT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {\n let eta = max(1.0 - blend, 1e-4);\n let f0 = pow((1.0 - eta) / (1.0 + eta), 2.0);\n let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\nfn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {\n var facing = abs(dot(n, v));\n let b = clamp(blend, 0.0, 0.99999);\n if (b != 0.5) {\n let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);\n facing = pow(facing, exponent);\n }\n return 1.0 - facing;\n}\n\n// \u2500\u2500\u2500 SHADER_TO_RGB (white DiffuseBSDF) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Eevee captures lit diffuse: (albedo/\u03C0)*sun*N\u00B7L*shadow + ambient (linear). Albedo=1.\n// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.\n\nfn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {\n const PI_S: f32 = 3.141592653589793;\n let ndotl = max(dot(n, l), 0.0);\n let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;\n return luminance_rec709_linear(rgb);\n}\n\n// \u2500\u2500\u2500 AMBIENT_OCCLUSION node (faked) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Real SSAO is a non-goal. We approximate: use the \"inside\" value from\n// concavity heuristic: 1.0 = fully lit, lower = occluded.\n// For now returns 1.0 (no darkening). Individual presets can override.\n\nfn ao_fake(n: vec3f, v: vec3f) -> f32 {\n return 1.0;\n}\n\n// \u2500\u2500\u2500 BUMP node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Screen-space bump from a scalar height field. Needs dFdx/dFdy which\n// WGSL provides as dpdx/dpdy.\n\nfn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference\nfn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// \u2500\u2500\u2500 NOISE texture (Perlin-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Simplified gradient noise matching Blender's default noise output.\n\nfn _hash33(p: vec3f) -> vec3f {\n var q = vec3f(\n dot(p, vec3f(127.1, 311.7, 74.7)),\n dot(p, vec3f(269.5, 183.3, 246.1)),\n dot(p, vec3f(113.5, 271.9, 124.6))\n );\n return fract(sin(q) * 43758.5453123) * 2.0 - 1.0;\n}\n\nfn _noise3(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return mix(\n mix(\n mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),\n dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),\n dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),\n mix(\n mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),\n dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),\n dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),\n u.z);\n}\n\nfn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {\n var q = p;\n if (abs(distortion) > 1e-6) {\n let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));\n q = p + (w * 2.0 - 1.0) * distortion;\n }\n let coords = q * scale;\n var value = 0.0;\n var amplitude = 1.0;\n var frequency = 1.0;\n var total_amp = 0.0;\n let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;\n for (var i = 0; i < octaves; i++) {\n value += amplitude * _noise3(coords * frequency);\n total_amp += amplitude;\n amplitude *= roughness;\n frequency *= 2.0;\n }\n return value / max(total_amp, 1e-6) * 0.5 + 0.5;\n}\n\n// \u2500\u2500\u2500 TEX_GRADIENT (linear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Stockings preset. Maps the input vector's X to a 0\u20131 gradient.\n\nfn tex_gradient_linear(uv: vec3f) -> f32 {\n return clamp(uv.x, 0.0, 1.0);\n}\n\n// \u2500\u2500\u2500 TEX_VORONOI (distance only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Metal preset. Simplified F1 cell noise.\n\nfn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {\n let coords = p * scale;\n let i = floor(coords);\n let f = fract(coords);\n var min_dist = 1e10;\n for (var z = -1; z <= 1; z++) {\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let neighbor = vec3f(f32(x), f32(y), f32(z));\n let point = _hash33(i + neighbor) * 0.5 + 0.5;\n let diff = neighbor + point - f;\n min_dist = min(min_dist, dot(diff, diff));\n }\n }\n }\n return sqrt(min_dist);\n}\n\n// \u2500\u2500\u2500 SEPXYZ node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn separate_xyz(v: vec3f) -> vec3f { return v; }\n\n// \u2500\u2500\u2500 VECT_MATH (cross product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }\n\n// \u2500\u2500\u2500 MAPPING node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Point-type mapping: scale, rotate (euler XYZ), translate.\n\nfn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {\n var p = v * scl;\n // simplified: skip rotation when all angles are zero (common case)\n if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {\n let cx = cos(rot.x); let sx = sin(rot.x);\n let cy = cos(rot.y); let sy = sin(rot.y);\n let cz = cos(rot.z); let sz = sin(rot.z);\n let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);\n let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);\n p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);\n }\n return p + loc;\n}\n\n// \u2500\u2500\u2500 NORMAL_MAP node (tangent-space) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Applies a tangent-space normal map. Requires TBN from vertex stage.\n\nfn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {\n let ts = map_color * 2.0 - 1.0;\n let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);\n return normalize(mix(normal, perturbed, strength));\n}\n\n// \u2500\u2500\u2500 EEVEE Principled BSDF primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/\n// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.\n// Usage pattern (see material shaders): direct spec = bsdf_ggx \u00D7 sun \u00D7 shadow\n// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with\n// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.\n\nconst EEVEE_PI: f32 = 3.141592653589793;\n\n// Fused analytic GGX specular (direct lights). Returns BRDF \u00D7 NL.\n// 4\u00B7NL\u00B7NV is cancelled via G1_Smith reciprocal form \u2014 see bsdf_common_lib.glsl:115.\nfn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, roughness: f32) -> f32 {\n let a = max(roughness, 1e-4);\n let a2 = a * a;\n let H = normalize(L + V);\n let NH = max(dot(N, H), 1e-8);\n let NL = max(dot(N, L), 1e-8);\n let NV = max(dot(N, V), 1e-8);\n // G1_Smith_GGX_opti reciprocal form \u2014 denominator piece only.\n let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);\n let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);\n let G = G1L * G1V;\n // D_ggx_opti = pi * denom\u00B2 \u2014 reciprocal D \u00D7 a\u00B2.\n let tmp = (NH * a2 - NH) * NH + 1.0;\n let D_opti = EEVEE_PI * tmp * tmp;\n return NL * a2 / (D_opti * G);\n}\n\n// Split-sum DFG LUT \u2014 Karis 2013 curve fit stand-in for the 64\u00D764 baked LUT.\n// Returns (lut.x, lut.y) in Blender convention: tint = f0\u00B7lut.x + f90\u00B7lut.y.\nfn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {\n let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);\n let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);\n let r = roughness * c0 + c1;\n let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;\n return vec2f(-1.04, 1.04) * a004 + r.zw;\n}\n\n// Baked 64\u00D764 EEVEE split-sum LUT \u2014 exact port of bsdf_lut_frag.glsl.\n// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:\n// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.\n// Requires group(0) binding(9) dfgLut + binding(2) diffuseSampler in the host shader.\nfn brdf_lut_baked(NV: f32, roughness: f32) -> vec2f {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n return textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n}\n\nfn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n return lut.y * f90 + lut.x * f0;\n}\n\n// Fdez-Ag\u00FCera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).\nfn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n let FssEss = lut.y * f90 + lut.x * f0;\n let Ess = lut.x + lut.y;\n let Ems = 1.0 - Ess;\n let Favg = f0 + (1.0 - f0) / 21.0;\n let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);\n return FssEss + Fms * Ems;\n}\n\n// EEVEE direct-specular energy compensation factor \u2014 closure_eval_glossy_lib.glsl:79-81:\n// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)\n// Because Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum,\n// direct radiance is rescaled so total-energy matches what the split-sum LUT expects.\n// Sample both LUTs at identical lut_coords and return the ratio.\nfn ltc_brdf_scale(NV: f32, roughness: f32) -> f32 {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n let ltc = textureSampleLevel(ltcMag, diffuseSampler, uv, 0.0).rg;\n let dfg = textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n return (ltc.x + ltc.y) / max(dfg.x + dfg.y, 1e-6);\n}\n\n// Luminance-normalized hue extraction \u2014 Blender tint_from_color (isolates hue+sat).\nfn tint_from_color(color: vec3f) -> vec3f {\n let lum = dot(color, vec3f(0.3, 0.6, 0.1));\n return select(vec3f(1.0), color / lum, lum > 0.0);\n}\n\n\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\nstruct MaterialUniforms {\n diffuseColor: vec3f,\n alpha: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n var vis = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);\n }\n }\n return vis / 9.0;\n}\n\nconst PI_B: f32 = 3.141592653589793;\nconst F0_BODY: f32 = 0.04;\nconst BODY_ROUGHNESS: f32 = 0.3;\n// Dump: \u5C42\u6743\u91CD.002 Blend; \u8FD0\u7B97.007 POWER exponent Value_001; \u80CC\u666F Color; \u8FD0\u7B97.004 after invert\nconst BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;\nconst BODY_RIM2_POW: f32 = 1.4300000667572021;\nconst BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);\nconst BODY_WARM_AO_MUL: f32 = 0.30000001192092896;\nconst BODY_MIX_NPR: f32 = 0.5;\n\nfn ggx_d_body(ndoth: f32, a2: f32) -> f32 {\n let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;\n return a2 / (PI_B * denom * denom);\n}\n\nfn smith_g1_body(ndotx: f32, a2: f32) -> f32 {\n return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));\n}\n\nfn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {\n return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);\n}\n\n// smoothstep-based ramp: t*t*(3-2*t) between two color stops\nfn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = normalize(skinnedNrm);\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n return output;\n}\n\n@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {\n let alpha = material.alpha;\n if (alpha < 0.001) { discard; }\n\n let n = normalize(input.normal);\n let v = normalize(camera.viewPos - input.worldPos);\n let l = -light.lights[0].direction.xyz;\n let sun = light.lights[0].color.xyz * light.lights[0].color.w;\n\n let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;\n let shadow = sampleShadow(input.worldPos, n);\n\n // \u2550\u2550\u2550 TOON MASK: ShaderToRGB \u2192 ramp.008 CONSTANT [0\u2192black, 0.2966\u2192white] \u2550\u2550\u2550\n let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);\n let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;\n\n // \u2550\u2550\u2550 TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) \u2550\u2550\u2550\n let shadow_tint = hue_sat(0.5, 2.0, 0.3499999940395355, 1.0, tex_color);\n let lit_tint = hue_sat(0.5, 1.5, 1.0, 1.0, tex_color);\n let toon_color = mix_blend(toon, shadow_tint, lit_tint);\n let bc = bright_contrast(toon_color, 0.1, 0.2);\n\n // \u2550\u2550\u2550 AO CHAIN: AO \u2192 ramp CONSTANT [0\u2192white, 0.5995\u2192black] \u2192 Mix.003 \u2550\u2550\u2550\n let ao = ao_fake(n, v);\n let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;\n let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));\n\n // \u2550\u2550\u2550 EMISSION.003 (strength=4.0) \u2550\u2550\u2550\n let emission3 = ao_mixed * 4.0;\n\n // \u2550\u2550\u2550 WARM: \u989C\u8272\u6E10\u53D8.008 \u2192 \u8FD0\u7B97.006 ADD +0.5 (m_graphs) \u2192 clamp \u2192 \u989C\u8272\u6E10\u53D8.003 \u2550\u2550\u2550\n let ao_inv = invert_f(1.0, ao_ramp);\n let warm_str = ao_inv * BODY_WARM_AO_MUL;\n let warm_input = clamp(toon + 0.5, 0.0, 1.0);\n let warm_color = ramp_cardinal(warm_input, 0.2409,\n vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,\n vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;\n let warm_emission = warm_color * warm_str;\n\n // \u2550\u2550\u2550 RIM 1: \u83F2\u6D85\u5C14 \u00D7 \u5C42\u6743\u91CD.001 Facing Blend=0.24 \u2192 \u81EA\u53D1\u5149 Strength \u2550\u2550\u2550\n let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);\n let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;\n\n // \u2550\u2550\u2550 RIM 2: \u5C42\u6743\u91CD.002 Facing \u2192 \u8FD0\u7B97.007 POWER \u2192 \u989C\u8272\u6E10\u53D8.010 EASE \u2192 MixShader.002 Fac \u2550\u2550\u2550\n let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);\n let facing_pow = math_power(facing_raw, BODY_RIM2_POW);\n let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;\n let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);\n\n // \u2550\u2550\u2550 NPR STACK: AddShader chain (no bright gate in body) \u2550\u2550\u2550\n let add0 = rim1 + rim2_mixed;\n let npr_stack = add0 + warm_emission;\n\n // \u2550\u2550\u2550 PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO \u2550\u2550\u2550\n let gen = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0, 1.0, 1.5));\n let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);\n let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;\n let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);\n\n let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));\n let p_emission = bc * 0.2;\n\n let ao2 = ao_fake(n, v);\n let sss = ramp_linear(ao2, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;\n\n let p_ndotl = max(dot(bumped_n, l), 0.0);\n let p_ndotv = max(dot(bumped_n, v), 0.001);\n let h = normalize(l + v);\n let p_ndoth = max(dot(bumped_n, h), 0.0);\n let p_vdoth = max(dot(v, h), 0.0);\n let a2 = BODY_ROUGHNESS * BODY_ROUGHNESS;\n let D = ggx_d_body(p_ndoth, a2);\n let G = smith_g1_body(p_ndotl, a2) * smith_g1_body(p_ndotv, a2);\n let F = fresnel_schlick_body(p_vdoth, F0_BODY);\n let spec = (D * G * F) / max(4.0 * p_ndotl * p_ndotv, 0.001) * ltc_brdf_scale(p_ndotv, BODY_ROUGHNESS);\n let kd = (1.0 - F) * principled_base / PI_B;\n let direct = (kd + spec) * sun * p_ndotl * shadow;\n // Indirect diffuse = base_color \u00D7 L_w per Blender closure_eval_surface_lib.glsl line 302;\n // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).\n let ambient = principled_base * light.ambientColor.xyz;\n let principled = ambient + direct + p_emission + vec3f(sss);\n\n // \u6DF7\u5408\u7740\u8272\u5668.001: Shader=\u76F8\u52A0\u7740\u8272\u5668.001, Shader_001=\u539F\u7406\u5316BSDF\n let final_color = mix(npr_stack, principled, BODY_MIX_NPR);\n\n return vec4f(final_color, alpha);\n}\n\n";
|
|
2
|
+
//# sourceMappingURL=body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/shaders/body.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB,004BA8M5B,CAAA"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Body"; VALTORGB / math ops from m_graphs where dump omits them.
|
|
2
|
+
import { NODES_WGSL } from "./nodes";
|
|
3
|
+
export const BODY_SHADER_WGSL = /* wgsl */ `
|
|
4
|
+
|
|
5
|
+
${NODES_WGSL}
|
|
6
|
+
|
|
7
|
+
struct CameraUniforms {
|
|
8
|
+
view: mat4x4f,
|
|
9
|
+
projection: mat4x4f,
|
|
10
|
+
viewPos: vec3f,
|
|
11
|
+
_padding: f32,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
struct Light {
|
|
15
|
+
direction: vec4f,
|
|
16
|
+
color: vec4f,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
struct LightUniforms {
|
|
20
|
+
ambientColor: vec4f,
|
|
21
|
+
lights: array<Light, 4>,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
struct MaterialUniforms {
|
|
25
|
+
diffuseColor: vec3f,
|
|
26
|
+
alpha: f32,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
struct VertexOutput {
|
|
30
|
+
@builtin(position) position: vec4f,
|
|
31
|
+
@location(0) normal: vec3f,
|
|
32
|
+
@location(1) uv: vec2f,
|
|
33
|
+
@location(2) worldPos: vec3f,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
37
|
+
|
|
38
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
39
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
40
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
41
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
42
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
43
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
44
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
45
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
46
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
47
|
+
|
|
48
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
49
|
+
let biasedPos = worldPos + n * 0.08;
|
|
50
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
51
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
52
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
53
|
+
let cmpZ = ndc.z - 0.001;
|
|
54
|
+
let ts = 1.0 / 4096.0;
|
|
55
|
+
var vis = 0.0;
|
|
56
|
+
for (var y = -1; y <= 1; y++) {
|
|
57
|
+
for (var x = -1; x <= 1; x++) {
|
|
58
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return vis / 9.0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PI_B: f32 = 3.141592653589793;
|
|
65
|
+
const F0_BODY: f32 = 0.04;
|
|
66
|
+
const BODY_ROUGHNESS: f32 = 0.3;
|
|
67
|
+
// Dump: 层权重.002 Blend; 运算.007 POWER exponent Value_001; 背景 Color; 运算.004 after invert
|
|
68
|
+
const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
|
|
69
|
+
const BODY_RIM2_POW: f32 = 1.4300000667572021;
|
|
70
|
+
const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
|
|
71
|
+
const BODY_WARM_AO_MUL: f32 = 0.30000001192092896;
|
|
72
|
+
const BODY_MIX_NPR: f32 = 0.5;
|
|
73
|
+
|
|
74
|
+
fn ggx_d_body(ndoth: f32, a2: f32) -> f32 {
|
|
75
|
+
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
76
|
+
return a2 / (PI_B * denom * denom);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn smith_g1_body(ndotx: f32, a2: f32) -> f32 {
|
|
80
|
+
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn fresnel_schlick_body(cosTheta: f32, f0: f32) -> f32 {
|
|
84
|
+
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// smoothstep-based ramp: t*t*(3-2*t) between two color stops
|
|
88
|
+
fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
89
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
90
|
+
let ss = t * t * (3.0 - 2.0 * t);
|
|
91
|
+
return mix(c0, c1, ss);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@vertex fn vs(
|
|
95
|
+
@location(0) position: vec3f,
|
|
96
|
+
@location(1) normal: vec3f,
|
|
97
|
+
@location(2) uv: vec2f,
|
|
98
|
+
@location(3) joints0: vec4<u32>,
|
|
99
|
+
@location(4) weights0: vec4<f32>
|
|
100
|
+
) -> VertexOutput {
|
|
101
|
+
var output: VertexOutput;
|
|
102
|
+
let pos4 = vec4f(position, 1.0);
|
|
103
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
104
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
105
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
106
|
+
var skinnedPos = vec4f(0.0);
|
|
107
|
+
var skinnedNrm = vec3f(0.0);
|
|
108
|
+
for (var i = 0u; i < 4u; i++) {
|
|
109
|
+
let m = skinMats[joints0[i]];
|
|
110
|
+
let w = nw[i];
|
|
111
|
+
skinnedPos += (m * pos4) * w;
|
|
112
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
113
|
+
}
|
|
114
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
115
|
+
output.normal = normalize(skinnedNrm);
|
|
116
|
+
output.uv = uv;
|
|
117
|
+
output.worldPos = skinnedPos.xyz;
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
122
|
+
let alpha = material.alpha;
|
|
123
|
+
if (alpha < 0.001) { discard; }
|
|
124
|
+
|
|
125
|
+
let n = normalize(input.normal);
|
|
126
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
127
|
+
let l = -light.lights[0].direction.xyz;
|
|
128
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
129
|
+
|
|
130
|
+
let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
131
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
132
|
+
|
|
133
|
+
// ═══ TOON MASK: ShaderToRGB → ramp.008 CONSTANT [0→black, 0.2966→white] ═══
|
|
134
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
135
|
+
let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
|
|
136
|
+
|
|
137
|
+
// ═══ TOON COLOR: Mix.004 A=HueSat, B=HueSat.001, Fac=ramp.008 (R) ═══
|
|
138
|
+
let shadow_tint = hue_sat(0.5, 2.0, 0.3499999940395355, 1.0, tex_color);
|
|
139
|
+
let lit_tint = hue_sat(0.5, 1.5, 1.0, 1.0, tex_color);
|
|
140
|
+
let toon_color = mix_blend(toon, shadow_tint, lit_tint);
|
|
141
|
+
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
142
|
+
|
|
143
|
+
// ═══ AO CHAIN: AO → ramp CONSTANT [0→white, 0.5995→black] → Mix.003 ═══
|
|
144
|
+
let ao = ao_fake(n, v);
|
|
145
|
+
let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
|
|
146
|
+
let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8301780223846436, 0.3345769941806793, 0.27946099638938904));
|
|
147
|
+
|
|
148
|
+
// ═══ EMISSION.003 (strength=4.0) ═══
|
|
149
|
+
let emission3 = ao_mixed * 4.0;
|
|
150
|
+
|
|
151
|
+
// ═══ WARM: 颜色渐变.008 → 运算.006 ADD +0.5 (m_graphs) → clamp → 颜色渐变.003 ═══
|
|
152
|
+
let ao_inv = invert_f(1.0, ao_ramp);
|
|
153
|
+
let warm_str = ao_inv * BODY_WARM_AO_MUL;
|
|
154
|
+
let warm_input = clamp(toon + 0.5, 0.0, 1.0);
|
|
155
|
+
let warm_color = ramp_cardinal(warm_input, 0.2409,
|
|
156
|
+
vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
|
|
157
|
+
vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
|
|
158
|
+
let warm_emission = warm_color * warm_str;
|
|
159
|
+
|
|
160
|
+
// ═══ RIM 1: 菲涅尔 × 层权重.001 Facing Blend=0.24 → 自发光 Strength ═══
|
|
161
|
+
let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
|
|
162
|
+
let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
|
|
163
|
+
|
|
164
|
+
// ═══ RIM 2: 层权重.002 Facing → 运算.007 POWER → 颜色渐变.010 EASE → MixShader.002 Fac ═══
|
|
165
|
+
let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
|
|
166
|
+
let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
|
|
167
|
+
let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
|
|
168
|
+
let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
|
|
169
|
+
|
|
170
|
+
// ═══ NPR STACK: AddShader chain (no bright gate in body) ═══
|
|
171
|
+
let add0 = rim1 + rim2_mixed;
|
|
172
|
+
let npr_stack = add0 + warm_emission;
|
|
173
|
+
|
|
174
|
+
// ═══ PRINCIPLED BSDF: noise bump, GGX specular, SSS from AO ═══
|
|
175
|
+
let gen = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0, 1.0, 1.5));
|
|
176
|
+
let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);
|
|
177
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
178
|
+
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
|
|
179
|
+
|
|
180
|
+
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
|
|
181
|
+
let p_emission = bc * 0.2;
|
|
182
|
+
|
|
183
|
+
let ao2 = ao_fake(n, v);
|
|
184
|
+
let sss = ramp_linear(ao2, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
|
|
185
|
+
|
|
186
|
+
let p_ndotl = max(dot(bumped_n, l), 0.0);
|
|
187
|
+
let p_ndotv = max(dot(bumped_n, v), 0.001);
|
|
188
|
+
let h = normalize(l + v);
|
|
189
|
+
let p_ndoth = max(dot(bumped_n, h), 0.0);
|
|
190
|
+
let p_vdoth = max(dot(v, h), 0.0);
|
|
191
|
+
let a2 = BODY_ROUGHNESS * BODY_ROUGHNESS;
|
|
192
|
+
let D = ggx_d_body(p_ndoth, a2);
|
|
193
|
+
let G = smith_g1_body(p_ndotl, a2) * smith_g1_body(p_ndotv, a2);
|
|
194
|
+
let F = fresnel_schlick_body(p_vdoth, F0_BODY);
|
|
195
|
+
let spec = (D * G * F) / max(4.0 * p_ndotl * p_ndotv, 0.001) * ltc_brdf_scale(p_ndotv, BODY_ROUGHNESS);
|
|
196
|
+
let kd = (1.0 - F) * principled_base / PI_B;
|
|
197
|
+
let direct = (kd + spec) * sun * p_ndotl * shadow;
|
|
198
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
199
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
200
|
+
let ambient = principled_base * light.ambientColor.xyz;
|
|
201
|
+
let principled = ambient + direct + p_emission + vec3f(sss);
|
|
202
|
+
|
|
203
|
+
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF
|
|
204
|
+
let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
|
|
205
|
+
|
|
206
|
+
return vec4f(final_color, alpha);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type MaterialPreset = "default" | "face" | "hair" | "body" | "eye" | "stockings" | "metal" | "cloth_smooth" | "cloth_rough";
|
|
2
|
+
export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>;
|
|
3
|
+
export declare function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): MaterialPreset;
|
|
4
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/shaders/classify.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,MAAM,GACN,MAAM,GACN,MAAM,GACN,KAAK,GACL,WAAW,GACX,OAAO,GACP,cAAc,GACd,aAAa,CAAA;AAGjB,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;AAEzE,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,SAAS,GAAG,cAAc,CAMtG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Material preset types for NPR pipeline dispatch.
|
|
2
|
+
// Mapping is explicit — the consumer provides a MaterialPresetMap that assigns
|
|
3
|
+
// material names to presets. Unmapped materials fall back to "default" (Principled BSDF).
|
|
4
|
+
export function resolvePreset(materialName, map) {
|
|
5
|
+
if (!map)
|
|
6
|
+
return "default";
|
|
7
|
+
for (const [preset, names] of Object.entries(map)) {
|
|
8
|
+
if (names && names.includes(materialName))
|
|
9
|
+
return preset;
|
|
10
|
+
}
|
|
11
|
+
return "default";
|
|
12
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const CLOTH_ROUGH_SHADER_WGSL = "\n\n\n\n// Baked 64\u00D764 rg16float DFG LUT \u2014 created once at engine init by dfg_lut.ts.\n// Paired with group(0) binding(2) diffuseSampler (linear filter, clamp implicit\n// via the half-texel bias inside brdf_lut_baked). Bound by the main per-frame\n// bind group to every material pipeline that includes this module.\n@group(0) @binding(9) var dfgLut: texture_2d<f32>;\n\n// Baked 64\u00D764 rg16float LTC GGX magnitude LUT \u2014 ltc_mag_ggx from Blender eevee_lut.c.\n// Heitz 2016 LTC fit amplitude \u2014 same UV addressing as dfgLut.\n// Used for direct-specular energy compensation (ltc_brdf_scale in closure_eval_glossy_lib.glsl).\n@group(0) @binding(10) var ltcMag: texture_2d<f32>;\n\n// \u2500\u2500\u2500 RGB \u2194 HSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn rgb_to_hsv(rgb: vec3f) -> vec3f {\n let c_max = max(rgb.r, max(rgb.g, rgb.b));\n let c_min = min(rgb.r, min(rgb.g, rgb.b));\n let delta = c_max - c_min;\n\n var h = 0.0;\n if (delta > 1e-6) {\n if (c_max == rgb.r) {\n h = (rgb.g - rgb.b) / delta;\n if (h < 0.0) { h += 6.0; }\n } else if (c_max == rgb.g) {\n h = 2.0 + (rgb.b - rgb.r) / delta;\n } else {\n h = 4.0 + (rgb.r - rgb.g) / delta;\n }\n h /= 6.0;\n }\n let s = select(0.0, delta / c_max, c_max > 1e-6);\n return vec3f(h, s, c_max);\n}\n\nfn hsv_to_rgb(hsv: vec3f) -> vec3f {\n let h = hsv.x;\n let s = hsv.y;\n let v = hsv.z;\n if (s < 1e-6) { return vec3f(v); }\n\n let hh = fract(h) * 6.0;\n let sector = u32(hh);\n let f = hh - f32(sector);\n let p = v * (1.0 - s);\n let q = v * (1.0 - s * f);\n let t = v * (1.0 - s * (1.0 - f));\n\n switch (sector) {\n case 0u: { return vec3f(v, t, p); }\n case 1u: { return vec3f(q, v, p); }\n case 2u: { return vec3f(p, v, t); }\n case 3u: { return vec3f(p, q, v); }\n case 4u: { return vec3f(t, p, v); }\n default: { return vec3f(v, p, q); }\n }\n}\n\n// \u2500\u2500\u2500 HUE_SAT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {\n var hsv = rgb_to_hsv(color);\n hsv.x = fract(hsv.x + hue - 0.5);\n hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);\n hsv.z *= value;\n return mix(color, hsv_to_rgb(hsv), fac);\n}\n\n// \u2500\u2500\u2500 BRIGHTCONTRAST node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {\n let a = 1.0 + contrast;\n let b = bright - contrast * 0.5;\n return max(vec3f(0.0), color * a + vec3f(b));\n}\n\n// \u2500\u2500\u2500 INVERT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn invert(fac: f32, color: vec3f) -> vec3f {\n return mix(color, vec3f(1.0) - color, fac);\n}\n\nfn invert_f(fac: f32, val: f32) -> f32 {\n return mix(val, 1.0 - val, fac);\n}\n\n// \u2500\u2500\u2500 Color ramp (VALTORGB) \u2014 2-stop variants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// All 7 presets use exclusively 2-stop ramps.\n\nfn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n return select(c0, c1, f >= p1);\n}\n\n// CONSTANT ramp with screen-space edge AA \u2014 kills sparkle where fwidth(f) straddles a hard step (NPR terminator)\nfn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {\n let w = max(fwidth(f) * 1.75, 6e-6);\n let t = smoothstep(edge - w, edge + w, f);\n return mix(c0, c1, t);\n}\n\nfn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n return mix(c0, c1, t);\n}\n\nfn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n // cardinal spline with 2 stops degrades to smoothstep\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n// \u2500\u2500\u2500 MATH node operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn math_add(a: f32, b: f32) -> f32 { return a + b; }\nfn math_multiply(a: f32, b: f32) -> f32 { return a * b; }\nfn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }\nfn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }\n\n// \u2500\u2500\u2500 MIX node (blend_type variants) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, b, fac);\n}\n\nfn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n let lo = 2.0 * a * b;\n let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);\n let overlay = select(hi, lo, a < vec3f(0.5));\n return mix(a, overlay, fac);\n}\n\nfn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a * b, fac);\n}\n\nfn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, max(a, b), fac);\n}\n\n// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)\nfn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a + 2.0 * b - vec3f(1.0), fac);\n}\n\n// Luminance for Shader\u2192RGB scalar gates (linear RGB, Rec.709 weights)\nfn luminance_rec709_linear(c: vec3f) -> f32 {\n return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));\n}\n\n// \u2500\u2500\u2500 FRESNEL node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schlick approximation matching Blender's Fresnel node\n\nfn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {\n let f0 = pow((ior - 1.0) / (ior + 1.0), 2.0);\n let cos_theta = clamp(dot(n, v), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\n// \u2500\u2500\u2500 LAYER_WEIGHT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {\n let eta = max(1.0 - blend, 1e-4);\n let f0 = pow((1.0 - eta) / (1.0 + eta), 2.0);\n let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\nfn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {\n var facing = abs(dot(n, v));\n let b = clamp(blend, 0.0, 0.99999);\n if (b != 0.5) {\n let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);\n facing = pow(facing, exponent);\n }\n return 1.0 - facing;\n}\n\n// \u2500\u2500\u2500 SHADER_TO_RGB (white DiffuseBSDF) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Eevee captures lit diffuse: (albedo/\u03C0)*sun*N\u00B7L*shadow + ambient (linear). Albedo=1.\n// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.\n\nfn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {\n const PI_S: f32 = 3.141592653589793;\n let ndotl = max(dot(n, l), 0.0);\n let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;\n return luminance_rec709_linear(rgb);\n}\n\n// \u2500\u2500\u2500 AMBIENT_OCCLUSION node (faked) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Real SSAO is a non-goal. We approximate: use the \"inside\" value from\n// concavity heuristic: 1.0 = fully lit, lower = occluded.\n// For now returns 1.0 (no darkening). Individual presets can override.\n\nfn ao_fake(n: vec3f, v: vec3f) -> f32 {\n return 1.0;\n}\n\n// \u2500\u2500\u2500 BUMP node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Screen-space bump from a scalar height field. Needs dFdx/dFdy which\n// WGSL provides as dpdx/dpdy.\n\nfn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference\nfn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// \u2500\u2500\u2500 NOISE texture (Perlin-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Simplified gradient noise matching Blender's default noise output.\n\nfn _hash33(p: vec3f) -> vec3f {\n var q = vec3f(\n dot(p, vec3f(127.1, 311.7, 74.7)),\n dot(p, vec3f(269.5, 183.3, 246.1)),\n dot(p, vec3f(113.5, 271.9, 124.6))\n );\n return fract(sin(q) * 43758.5453123) * 2.0 - 1.0;\n}\n\nfn _noise3(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return mix(\n mix(\n mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),\n dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),\n dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),\n mix(\n mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),\n dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),\n dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),\n u.z);\n}\n\nfn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {\n var q = p;\n if (abs(distortion) > 1e-6) {\n let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));\n q = p + (w * 2.0 - 1.0) * distortion;\n }\n let coords = q * scale;\n var value = 0.0;\n var amplitude = 1.0;\n var frequency = 1.0;\n var total_amp = 0.0;\n let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;\n for (var i = 0; i < octaves; i++) {\n value += amplitude * _noise3(coords * frequency);\n total_amp += amplitude;\n amplitude *= roughness;\n frequency *= 2.0;\n }\n return value / max(total_amp, 1e-6) * 0.5 + 0.5;\n}\n\n// \u2500\u2500\u2500 TEX_GRADIENT (linear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Stockings preset. Maps the input vector's X to a 0\u20131 gradient.\n\nfn tex_gradient_linear(uv: vec3f) -> f32 {\n return clamp(uv.x, 0.0, 1.0);\n}\n\n// \u2500\u2500\u2500 TEX_VORONOI (distance only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Metal preset. Simplified F1 cell noise.\n\nfn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {\n let coords = p * scale;\n let i = floor(coords);\n let f = fract(coords);\n var min_dist = 1e10;\n for (var z = -1; z <= 1; z++) {\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let neighbor = vec3f(f32(x), f32(y), f32(z));\n let point = _hash33(i + neighbor) * 0.5 + 0.5;\n let diff = neighbor + point - f;\n min_dist = min(min_dist, dot(diff, diff));\n }\n }\n }\n return sqrt(min_dist);\n}\n\n// \u2500\u2500\u2500 SEPXYZ node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn separate_xyz(v: vec3f) -> vec3f { return v; }\n\n// \u2500\u2500\u2500 VECT_MATH (cross product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }\n\n// \u2500\u2500\u2500 MAPPING node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Point-type mapping: scale, rotate (euler XYZ), translate.\n\nfn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {\n var p = v * scl;\n // simplified: skip rotation when all angles are zero (common case)\n if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {\n let cx = cos(rot.x); let sx = sin(rot.x);\n let cy = cos(rot.y); let sy = sin(rot.y);\n let cz = cos(rot.z); let sz = sin(rot.z);\n let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);\n let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);\n p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);\n }\n return p + loc;\n}\n\n// \u2500\u2500\u2500 NORMAL_MAP node (tangent-space) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Applies a tangent-space normal map. Requires TBN from vertex stage.\n\nfn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {\n let ts = map_color * 2.0 - 1.0;\n let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);\n return normalize(mix(normal, perturbed, strength));\n}\n\n// \u2500\u2500\u2500 EEVEE Principled BSDF primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/\n// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.\n// Usage pattern (see material shaders): direct spec = bsdf_ggx \u00D7 sun \u00D7 shadow\n// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with\n// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.\n\nconst EEVEE_PI: f32 = 3.141592653589793;\n\n// Fused analytic GGX specular (direct lights). Returns BRDF \u00D7 NL.\n// 4\u00B7NL\u00B7NV is cancelled via G1_Smith reciprocal form \u2014 see bsdf_common_lib.glsl:115.\nfn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, roughness: f32) -> f32 {\n let a = max(roughness, 1e-4);\n let a2 = a * a;\n let H = normalize(L + V);\n let NH = max(dot(N, H), 1e-8);\n let NL = max(dot(N, L), 1e-8);\n let NV = max(dot(N, V), 1e-8);\n // G1_Smith_GGX_opti reciprocal form \u2014 denominator piece only.\n let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);\n let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);\n let G = G1L * G1V;\n // D_ggx_opti = pi * denom\u00B2 \u2014 reciprocal D \u00D7 a\u00B2.\n let tmp = (NH * a2 - NH) * NH + 1.0;\n let D_opti = EEVEE_PI * tmp * tmp;\n return NL * a2 / (D_opti * G);\n}\n\n// Split-sum DFG LUT \u2014 Karis 2013 curve fit stand-in for the 64\u00D764 baked LUT.\n// Returns (lut.x, lut.y) in Blender convention: tint = f0\u00B7lut.x + f90\u00B7lut.y.\nfn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {\n let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);\n let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);\n let r = roughness * c0 + c1;\n let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;\n return vec2f(-1.04, 1.04) * a004 + r.zw;\n}\n\n// Baked 64\u00D764 EEVEE split-sum LUT \u2014 exact port of bsdf_lut_frag.glsl.\n// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:\n// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.\n// Requires group(0) binding(9) dfgLut + binding(2) diffuseSampler in the host shader.\nfn brdf_lut_baked(NV: f32, roughness: f32) -> vec2f {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n return textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n}\n\nfn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n return lut.y * f90 + lut.x * f0;\n}\n\n// Fdez-Ag\u00FCera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).\nfn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n let FssEss = lut.y * f90 + lut.x * f0;\n let Ess = lut.x + lut.y;\n let Ems = 1.0 - Ess;\n let Favg = f0 + (1.0 - f0) / 21.0;\n let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);\n return FssEss + Fms * Ems;\n}\n\n// EEVEE direct-specular energy compensation factor \u2014 closure_eval_glossy_lib.glsl:79-81:\n// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)\n// Because Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum,\n// direct radiance is rescaled so total-energy matches what the split-sum LUT expects.\n// Sample both LUTs at identical lut_coords and return the ratio.\nfn ltc_brdf_scale(NV: f32, roughness: f32) -> f32 {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n let ltc = textureSampleLevel(ltcMag, diffuseSampler, uv, 0.0).rg;\n let dfg = textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n return (ltc.x + ltc.y) / max(dfg.x + dfg.y, 1e-6);\n}\n\n// Luminance-normalized hue extraction \u2014 Blender tint_from_color (isolates hue+sat).\nfn tint_from_color(color: vec3f) -> vec3f {\n let lum = dot(color, vec3f(0.3, 0.6, 0.1));\n return select(vec3f(1.0), color / lum, lum > 0.0);\n}\n\n\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\nstruct MaterialUniforms {\n diffuseColor: vec3f,\n alpha: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n var vis = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);\n }\n }\n return vis / 9.0;\n}\n\nconst PI_CR: f32 = 3.141592653589793;\nconst CLOTH_R_SPECULAR: f32 = 0.8;\nconst CLOTH_R_ROUGHNESS: f32 = 0.8187;\nconst CLOTH_R_TOON_EDGE: f32 = 0.2966;\nconst CLOTH_R_MIX04_MUL: f32 = 0.5;\nconst CLOTH_R_EMIT_STR: f32 = 18.200000762939453;\nconst CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;\nconst CLOTH_R_NOISE_SCALE: f32 = 17.7;\nconst CLOTH_R_BUMP_STR: f32 = 1.0;\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = normalize(skinnedNrm);\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n return output;\n}\n\n@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {\n let n = normalize(input.normal);\n let v = normalize(camera.viewPos - input.worldPos);\n let l = -light.lights[0].direction.xyz;\n let sun = light.lights[0].color.xyz * light.lights[0].color.w;\n let amb = light.ambientColor.xyz;\n let shadow = sampleShadow(input.worldPos, n);\n\n let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);\n let tex_rgb = tex_s.rgb;\n let out_alpha = material.alpha * tex_s.a;\n if (out_alpha < 0.001) { discard; }\n\n // Shader\u2192RGB \u2192 \u989C\u8272\u6E10\u53D8.008 CONSTANT (edge AA terminator)\n let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);\n let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_R_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));\n let toon_r = ramp008.r;\n let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);\n\n // \u6DF7\u5408.004: A=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.002(Hue=0.5 Sat=1.0 Val=0.2), B=\u7EB9\u7406\n let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);\n let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);\n\n // \u5012\u89D2.001.Z \u2192 \u6DF7\u5408.003: A=\u6DF7\u5408.004, B=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.002\n let bevel_z = clamp(n.y, 0.0, 1.0);\n let mix03 = mix_blend(bevel_z, mix04, dark_tex);\n\n // \u73AF\u5883\u5149\u906E\u853D \u2192 \u989C\u8272\u6E10\u53D8.001 LINEAR \u2192 \u6DF7\u5408.001 (white/black) \u2192 \u6DF7\u5408.002 OVERLAY Fac\n let ao = ao_fake(n, v);\n let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));\n let mix01_fac = ao_ramp_c.r;\n let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);\n\n // \u6DF7\u5408.002 OVERLAY: Fac=\u6DF7\u5408.001, A=\u6DF7\u5408.003, B=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.004\n let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);\n let overlay_fac = mix01_rgb.r;\n let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);\n let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;\n\n // \u566A\u6CE2\u2192\u6E10\u53D8\u2192\u51F9\u51F8 (LIVE in M_Rough_Cloth \u2014 unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.\n let noise_uv = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0));\n let noise_val = tex_noise(noise_uv, CLOTH_R_NOISE_SCALE, 2.0, 0.5, 0.0);\n let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;\n let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);\n\n // \u539F\u7406\u5316BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.\n let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);\n let NL = max(dot(bumped_n, l), 0.0);\n let NV = max(dot(bumped_n, v), 1e-4);\n\n let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);\n let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));\n let split_sum = brdf_lut_baked(NV, CLOTH_R_ROUGHNESS);\n let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);\n\n let spec_direct = bsdf_ggx(bumped_n, l, v, CLOTH_R_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_R_ROUGHNESS);\n let spec_indirect = amb;\n let spec_radiance = (spec_direct + spec_indirect) * reflection_color;\n\n // Indirect diffuse = base_color \u00D7 L_w per Blender closure_eval_surface_lib.glsl line 302;\n // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).\n let diffuse_radiance = principled_base * (sun * NL * shadow / PI_CR + amb);\n let principled = diffuse_radiance + spec_radiance;\n\n // \u6DF7\u5408\u7740\u8272\u5668.001 Fac=0.9: Shader=\u81EA\u53D1\u5149.005, Shader_001=\u539F\u7406\u5316BSDF\n let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);\n\n return vec4f(final_color, out_alpha);\n}\n\n";
|
|
2
|
+
//# sourceMappingURL=cloth_rough.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloth_rough.d.ts","sourceRoot":"","sources":["../../src/shaders/cloth_rough.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,uBAAuB,gg1BAwKnC,CAAA"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// M_Rough_Cloth — NPR graph identical to M_Smooth_Cloth but bump chain IS live
|
|
2
|
+
// (噪波→渐变→凹凸.Normal → 原理化BSDF.Normal in m_graphs) and Roughness=0.8187.
|
|
3
|
+
import { NODES_WGSL } from "./nodes";
|
|
4
|
+
export const CLOTH_ROUGH_SHADER_WGSL = /* wgsl */ `
|
|
5
|
+
|
|
6
|
+
${NODES_WGSL}
|
|
7
|
+
|
|
8
|
+
struct CameraUniforms {
|
|
9
|
+
view: mat4x4f,
|
|
10
|
+
projection: mat4x4f,
|
|
11
|
+
viewPos: vec3f,
|
|
12
|
+
_padding: f32,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
struct Light {
|
|
16
|
+
direction: vec4f,
|
|
17
|
+
color: vec4f,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
struct LightUniforms {
|
|
21
|
+
ambientColor: vec4f,
|
|
22
|
+
lights: array<Light, 4>,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
struct MaterialUniforms {
|
|
26
|
+
diffuseColor: vec3f,
|
|
27
|
+
alpha: f32,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
struct VertexOutput {
|
|
31
|
+
@builtin(position) position: vec4f,
|
|
32
|
+
@location(0) normal: vec3f,
|
|
33
|
+
@location(1) uv: vec2f,
|
|
34
|
+
@location(2) worldPos: vec3f,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
38
|
+
|
|
39
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
40
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
41
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
42
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
43
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
44
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
45
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
46
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
47
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
48
|
+
|
|
49
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
50
|
+
let biasedPos = worldPos + n * 0.08;
|
|
51
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
52
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
53
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
54
|
+
let cmpZ = ndc.z - 0.001;
|
|
55
|
+
let ts = 1.0 / 4096.0;
|
|
56
|
+
var vis = 0.0;
|
|
57
|
+
for (var y = -1; y <= 1; y++) {
|
|
58
|
+
for (var x = -1; x <= 1; x++) {
|
|
59
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return vis / 9.0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const PI_CR: f32 = 3.141592653589793;
|
|
66
|
+
const CLOTH_R_SPECULAR: f32 = 0.8;
|
|
67
|
+
const CLOTH_R_ROUGHNESS: f32 = 0.8187;
|
|
68
|
+
const CLOTH_R_TOON_EDGE: f32 = 0.2966;
|
|
69
|
+
const CLOTH_R_MIX04_MUL: f32 = 0.5;
|
|
70
|
+
const CLOTH_R_EMIT_STR: f32 = 18.200000762939453;
|
|
71
|
+
const CLOTH_R_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
72
|
+
const CLOTH_R_NOISE_SCALE: f32 = 17.7;
|
|
73
|
+
const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
74
|
+
|
|
75
|
+
@vertex fn vs(
|
|
76
|
+
@location(0) position: vec3f,
|
|
77
|
+
@location(1) normal: vec3f,
|
|
78
|
+
@location(2) uv: vec2f,
|
|
79
|
+
@location(3) joints0: vec4<u32>,
|
|
80
|
+
@location(4) weights0: vec4<f32>
|
|
81
|
+
) -> VertexOutput {
|
|
82
|
+
var output: VertexOutput;
|
|
83
|
+
let pos4 = vec4f(position, 1.0);
|
|
84
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
85
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
86
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
87
|
+
var skinnedPos = vec4f(0.0);
|
|
88
|
+
var skinnedNrm = vec3f(0.0);
|
|
89
|
+
for (var i = 0u; i < 4u; i++) {
|
|
90
|
+
let m = skinMats[joints0[i]];
|
|
91
|
+
let w = nw[i];
|
|
92
|
+
skinnedPos += (m * pos4) * w;
|
|
93
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
94
|
+
}
|
|
95
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
96
|
+
output.normal = normalize(skinnedNrm);
|
|
97
|
+
output.uv = uv;
|
|
98
|
+
output.worldPos = skinnedPos.xyz;
|
|
99
|
+
return output;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
103
|
+
let n = normalize(input.normal);
|
|
104
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
105
|
+
let l = -light.lights[0].direction.xyz;
|
|
106
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
107
|
+
let amb = light.ambientColor.xyz;
|
|
108
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
109
|
+
|
|
110
|
+
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
111
|
+
let tex_rgb = tex_s.rgb;
|
|
112
|
+
let out_alpha = material.alpha * tex_s.a;
|
|
113
|
+
if (out_alpha < 0.001) { discard; }
|
|
114
|
+
|
|
115
|
+
// Shader→RGB → 颜色渐变.008 CONSTANT (edge AA terminator)
|
|
116
|
+
let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
117
|
+
let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_R_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));
|
|
118
|
+
let toon_r = ramp008.r;
|
|
119
|
+
let mix04_fac = math_multiply(toon_r, CLOTH_R_MIX04_MUL);
|
|
120
|
+
|
|
121
|
+
// 混合.004: A=色相/饱和度/明度.002(Hue=0.5 Sat=1.0 Val=0.2), B=纹理
|
|
122
|
+
let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);
|
|
123
|
+
let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);
|
|
124
|
+
|
|
125
|
+
// 倒角.001.Z → 混合.003: A=混合.004, B=色相/饱和度/明度.002
|
|
126
|
+
let bevel_z = clamp(n.y, 0.0, 1.0);
|
|
127
|
+
let mix03 = mix_blend(bevel_z, mix04, dark_tex);
|
|
128
|
+
|
|
129
|
+
// 环境光遮蔽 → 颜色渐变.001 LINEAR → 混合.001 (white/black) → 混合.002 OVERLAY Fac
|
|
130
|
+
let ao = ao_fake(n, v);
|
|
131
|
+
let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));
|
|
132
|
+
let mix01_fac = ao_ramp_c.r;
|
|
133
|
+
let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);
|
|
134
|
+
|
|
135
|
+
// 混合.002 OVERLAY: Fac=混合.001, A=混合.003, B=色相/饱和度/明度.004
|
|
136
|
+
let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);
|
|
137
|
+
let overlay_fac = mix01_rgb.r;
|
|
138
|
+
let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);
|
|
139
|
+
let npr_emission = npr_rgb * CLOTH_R_EMIT_STR;
|
|
140
|
+
|
|
141
|
+
// 噪波→渐变→凹凸 (LIVE in M_Rough_Cloth — unlike Smooth Cloth): Strength=1.0, noise Scale=17.7.
|
|
142
|
+
let noise_uv = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0));
|
|
143
|
+
let noise_val = tex_noise(noise_uv, CLOTH_R_NOISE_SCALE, 2.0, 0.5, 0.0);
|
|
144
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
145
|
+
let bumped_n = bump_lh(CLOTH_R_BUMP_STR, noise_ramp, n, input.worldPos);
|
|
146
|
+
|
|
147
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.8187, specular_tint=0.
|
|
148
|
+
let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
149
|
+
let NL = max(dot(bumped_n, l), 0.0);
|
|
150
|
+
let NV = max(dot(bumped_n, v), 1e-4);
|
|
151
|
+
|
|
152
|
+
let f0 = vec3f(0.08 * CLOTH_R_SPECULAR);
|
|
153
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_R_SPECULAR));
|
|
154
|
+
let split_sum = brdf_lut_baked(NV, CLOTH_R_ROUGHNESS);
|
|
155
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
|
|
156
|
+
|
|
157
|
+
let spec_direct = bsdf_ggx(bumped_n, l, v, CLOTH_R_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_R_ROUGHNESS);
|
|
158
|
+
let spec_indirect = amb;
|
|
159
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
160
|
+
|
|
161
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
162
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
163
|
+
let diffuse_radiance = principled_base * (sun * NL * shadow / PI_CR + amb);
|
|
164
|
+
let principled = diffuse_radiance + spec_radiance;
|
|
165
|
+
|
|
166
|
+
// 混合着色器.001 Fac=0.9: Shader=自发光.005, Shader_001=原理化BSDF
|
|
167
|
+
let final_color = mix(npr_emission, principled, CLOTH_R_MIX_SHADER_FAC);
|
|
168
|
+
|
|
169
|
+
return vec4f(final_color, out_alpha);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
`;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const CLOTH_SMOOTH_SHADER_WGSL = "\n\n\n\n// Baked 64\u00D764 rg16float DFG LUT \u2014 created once at engine init by dfg_lut.ts.\n// Paired with group(0) binding(2) diffuseSampler (linear filter, clamp implicit\n// via the half-texel bias inside brdf_lut_baked). Bound by the main per-frame\n// bind group to every material pipeline that includes this module.\n@group(0) @binding(9) var dfgLut: texture_2d<f32>;\n\n// Baked 64\u00D764 rg16float LTC GGX magnitude LUT \u2014 ltc_mag_ggx from Blender eevee_lut.c.\n// Heitz 2016 LTC fit amplitude \u2014 same UV addressing as dfgLut.\n// Used for direct-specular energy compensation (ltc_brdf_scale in closure_eval_glossy_lib.glsl).\n@group(0) @binding(10) var ltcMag: texture_2d<f32>;\n\n// \u2500\u2500\u2500 RGB \u2194 HSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn rgb_to_hsv(rgb: vec3f) -> vec3f {\n let c_max = max(rgb.r, max(rgb.g, rgb.b));\n let c_min = min(rgb.r, min(rgb.g, rgb.b));\n let delta = c_max - c_min;\n\n var h = 0.0;\n if (delta > 1e-6) {\n if (c_max == rgb.r) {\n h = (rgb.g - rgb.b) / delta;\n if (h < 0.0) { h += 6.0; }\n } else if (c_max == rgb.g) {\n h = 2.0 + (rgb.b - rgb.r) / delta;\n } else {\n h = 4.0 + (rgb.r - rgb.g) / delta;\n }\n h /= 6.0;\n }\n let s = select(0.0, delta / c_max, c_max > 1e-6);\n return vec3f(h, s, c_max);\n}\n\nfn hsv_to_rgb(hsv: vec3f) -> vec3f {\n let h = hsv.x;\n let s = hsv.y;\n let v = hsv.z;\n if (s < 1e-6) { return vec3f(v); }\n\n let hh = fract(h) * 6.0;\n let sector = u32(hh);\n let f = hh - f32(sector);\n let p = v * (1.0 - s);\n let q = v * (1.0 - s * f);\n let t = v * (1.0 - s * (1.0 - f));\n\n switch (sector) {\n case 0u: { return vec3f(v, t, p); }\n case 1u: { return vec3f(q, v, p); }\n case 2u: { return vec3f(p, v, t); }\n case 3u: { return vec3f(p, q, v); }\n case 4u: { return vec3f(t, p, v); }\n default: { return vec3f(v, p, q); }\n }\n}\n\n// \u2500\u2500\u2500 HUE_SAT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {\n var hsv = rgb_to_hsv(color);\n hsv.x = fract(hsv.x + hue - 0.5);\n hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);\n hsv.z *= value;\n return mix(color, hsv_to_rgb(hsv), fac);\n}\n\n// \u2500\u2500\u2500 BRIGHTCONTRAST node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {\n let a = 1.0 + contrast;\n let b = bright - contrast * 0.5;\n return max(vec3f(0.0), color * a + vec3f(b));\n}\n\n// \u2500\u2500\u2500 INVERT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn invert(fac: f32, color: vec3f) -> vec3f {\n return mix(color, vec3f(1.0) - color, fac);\n}\n\nfn invert_f(fac: f32, val: f32) -> f32 {\n return mix(val, 1.0 - val, fac);\n}\n\n// \u2500\u2500\u2500 Color ramp (VALTORGB) \u2014 2-stop variants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// All 7 presets use exclusively 2-stop ramps.\n\nfn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n return select(c0, c1, f >= p1);\n}\n\n// CONSTANT ramp with screen-space edge AA \u2014 kills sparkle where fwidth(f) straddles a hard step (NPR terminator)\nfn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {\n let w = max(fwidth(f) * 1.75, 6e-6);\n let t = smoothstep(edge - w, edge + w, f);\n return mix(c0, c1, t);\n}\n\nfn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n return mix(c0, c1, t);\n}\n\nfn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n // cardinal spline with 2 stops degrades to smoothstep\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n// \u2500\u2500\u2500 MATH node operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn math_add(a: f32, b: f32) -> f32 { return a + b; }\nfn math_multiply(a: f32, b: f32) -> f32 { return a * b; }\nfn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }\nfn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }\n\n// \u2500\u2500\u2500 MIX node (blend_type variants) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, b, fac);\n}\n\nfn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n let lo = 2.0 * a * b;\n let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);\n let overlay = select(hi, lo, a < vec3f(0.5));\n return mix(a, overlay, fac);\n}\n\nfn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a * b, fac);\n}\n\nfn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, max(a, b), fac);\n}\n\n// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)\nfn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a + 2.0 * b - vec3f(1.0), fac);\n}\n\n// Luminance for Shader\u2192RGB scalar gates (linear RGB, Rec.709 weights)\nfn luminance_rec709_linear(c: vec3f) -> f32 {\n return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));\n}\n\n// \u2500\u2500\u2500 FRESNEL node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schlick approximation matching Blender's Fresnel node\n\nfn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {\n let f0 = pow((ior - 1.0) / (ior + 1.0), 2.0);\n let cos_theta = clamp(dot(n, v), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\n// \u2500\u2500\u2500 LAYER_WEIGHT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {\n let eta = max(1.0 - blend, 1e-4);\n let f0 = pow((1.0 - eta) / (1.0 + eta), 2.0);\n let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);\n return f0 + (1.0 - f0) * pow(1.0 - cos_theta, 5.0);\n}\n\nfn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {\n var facing = abs(dot(n, v));\n let b = clamp(blend, 0.0, 0.99999);\n if (b != 0.5) {\n let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);\n facing = pow(facing, exponent);\n }\n return 1.0 - facing;\n}\n\n// \u2500\u2500\u2500 SHADER_TO_RGB (white DiffuseBSDF) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Eevee captures lit diffuse: (albedo/\u03C0)*sun*N\u00B7L*shadow + ambient (linear). Albedo=1.\n// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.\n\nfn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {\n const PI_S: f32 = 3.141592653589793;\n let ndotl = max(dot(n, l), 0.0);\n let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;\n return luminance_rec709_linear(rgb);\n}\n\n// \u2500\u2500\u2500 AMBIENT_OCCLUSION node (faked) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Real SSAO is a non-goal. We approximate: use the \"inside\" value from\n// concavity heuristic: 1.0 = fully lit, lower = occluded.\n// For now returns 1.0 (no darkening). Individual presets can override.\n\nfn ao_fake(n: vec3f, v: vec3f) -> f32 {\n return 1.0;\n}\n\n// \u2500\u2500\u2500 BUMP node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Screen-space bump from a scalar height field. Needs dFdx/dFdy which\n// WGSL provides as dpdx/dpdy.\n\nfn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference\nfn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// \u2500\u2500\u2500 NOISE texture (Perlin-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Simplified gradient noise matching Blender's default noise output.\n\nfn _hash33(p: vec3f) -> vec3f {\n var q = vec3f(\n dot(p, vec3f(127.1, 311.7, 74.7)),\n dot(p, vec3f(269.5, 183.3, 246.1)),\n dot(p, vec3f(113.5, 271.9, 124.6))\n );\n return fract(sin(q) * 43758.5453123) * 2.0 - 1.0;\n}\n\nfn _noise3(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return mix(\n mix(\n mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),\n dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),\n dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),\n mix(\n mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),\n dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),\n dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),\n u.z);\n}\n\nfn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {\n var q = p;\n if (abs(distortion) > 1e-6) {\n let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));\n q = p + (w * 2.0 - 1.0) * distortion;\n }\n let coords = q * scale;\n var value = 0.0;\n var amplitude = 1.0;\n var frequency = 1.0;\n var total_amp = 0.0;\n let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;\n for (var i = 0; i < octaves; i++) {\n value += amplitude * _noise3(coords * frequency);\n total_amp += amplitude;\n amplitude *= roughness;\n frequency *= 2.0;\n }\n return value / max(total_amp, 1e-6) * 0.5 + 0.5;\n}\n\n// \u2500\u2500\u2500 TEX_GRADIENT (linear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Stockings preset. Maps the input vector's X to a 0\u20131 gradient.\n\nfn tex_gradient_linear(uv: vec3f) -> f32 {\n return clamp(uv.x, 0.0, 1.0);\n}\n\n// \u2500\u2500\u2500 TEX_VORONOI (distance only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Metal preset. Simplified F1 cell noise.\n\nfn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {\n let coords = p * scale;\n let i = floor(coords);\n let f = fract(coords);\n var min_dist = 1e10;\n for (var z = -1; z <= 1; z++) {\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let neighbor = vec3f(f32(x), f32(y), f32(z));\n let point = _hash33(i + neighbor) * 0.5 + 0.5;\n let diff = neighbor + point - f;\n min_dist = min(min_dist, dot(diff, diff));\n }\n }\n }\n return sqrt(min_dist);\n}\n\n// \u2500\u2500\u2500 SEPXYZ node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn separate_xyz(v: vec3f) -> vec3f { return v; }\n\n// \u2500\u2500\u2500 VECT_MATH (cross product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }\n\n// \u2500\u2500\u2500 MAPPING node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Point-type mapping: scale, rotate (euler XYZ), translate.\n\nfn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {\n var p = v * scl;\n // simplified: skip rotation when all angles are zero (common case)\n if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {\n let cx = cos(rot.x); let sx = sin(rot.x);\n let cy = cos(rot.y); let sy = sin(rot.y);\n let cz = cos(rot.z); let sz = sin(rot.z);\n let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);\n let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);\n p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);\n }\n return p + loc;\n}\n\n// \u2500\u2500\u2500 NORMAL_MAP node (tangent-space) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Applies a tangent-space normal map. Requires TBN from vertex stage.\n\nfn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {\n let ts = map_color * 2.0 - 1.0;\n let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);\n return normalize(mix(normal, perturbed, strength));\n}\n\n// \u2500\u2500\u2500 EEVEE Principled BSDF primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/\n// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.\n// Usage pattern (see material shaders): direct spec = bsdf_ggx \u00D7 sun \u00D7 shadow\n// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with\n// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.\n\nconst EEVEE_PI: f32 = 3.141592653589793;\n\n// Fused analytic GGX specular (direct lights). Returns BRDF \u00D7 NL.\n// 4\u00B7NL\u00B7NV is cancelled via G1_Smith reciprocal form \u2014 see bsdf_common_lib.glsl:115.\nfn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, roughness: f32) -> f32 {\n let a = max(roughness, 1e-4);\n let a2 = a * a;\n let H = normalize(L + V);\n let NH = max(dot(N, H), 1e-8);\n let NL = max(dot(N, L), 1e-8);\n let NV = max(dot(N, V), 1e-8);\n // G1_Smith_GGX_opti reciprocal form \u2014 denominator piece only.\n let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);\n let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);\n let G = G1L * G1V;\n // D_ggx_opti = pi * denom\u00B2 \u2014 reciprocal D \u00D7 a\u00B2.\n let tmp = (NH * a2 - NH) * NH + 1.0;\n let D_opti = EEVEE_PI * tmp * tmp;\n return NL * a2 / (D_opti * G);\n}\n\n// Split-sum DFG LUT \u2014 Karis 2013 curve fit stand-in for the 64\u00D764 baked LUT.\n// Returns (lut.x, lut.y) in Blender convention: tint = f0\u00B7lut.x + f90\u00B7lut.y.\nfn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {\n let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);\n let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);\n let r = roughness * c0 + c1;\n let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;\n return vec2f(-1.04, 1.04) * a004 + r.zw;\n}\n\n// Baked 64\u00D764 EEVEE split-sum LUT \u2014 exact port of bsdf_lut_frag.glsl.\n// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:\n// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.\n// Requires group(0) binding(9) dfgLut + binding(2) diffuseSampler in the host shader.\nfn brdf_lut_baked(NV: f32, roughness: f32) -> vec2f {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n return textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n}\n\nfn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n return lut.y * f90 + lut.x * f0;\n}\n\n// Fdez-Ag\u00FCera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).\nfn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n let FssEss = lut.y * f90 + lut.x * f0;\n let Ess = lut.x + lut.y;\n let Ems = 1.0 - Ess;\n let Favg = f0 + (1.0 - f0) / 21.0;\n let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);\n return FssEss + Fms * Ems;\n}\n\n// EEVEE direct-specular energy compensation factor \u2014 closure_eval_glossy_lib.glsl:79-81:\n// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)\n// Because Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum,\n// direct radiance is rescaled so total-energy matches what the split-sum LUT expects.\n// Sample both LUTs at identical lut_coords and return the ratio.\nfn ltc_brdf_scale(NV: f32, roughness: f32) -> f32 {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n let ltc = textureSampleLevel(ltcMag, diffuseSampler, uv, 0.0).rg;\n let dfg = textureSampleLevel(dfgLut, diffuseSampler, uv, 0.0).rg;\n return (ltc.x + ltc.y) / max(dfg.x + dfg.y, 1e-6);\n}\n\n// Luminance-normalized hue extraction \u2014 Blender tint_from_color (isolates hue+sat).\nfn tint_from_color(color: vec3f) -> vec3f {\n let lum = dot(color, vec3f(0.3, 0.6, 0.1));\n return select(vec3f(1.0), color / lum, lum > 0.0);\n}\n\n\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\nstruct MaterialUniforms {\n diffuseColor: vec3f,\n alpha: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n var vis = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);\n }\n }\n return vis / 9.0;\n}\n\nconst PI_C: f32 = 3.141592653589793;\nconst CLOTH_SPECULAR: f32 = 0.8;\nconst CLOTH_ROUGHNESS: f32 = 0.5;\nconst CLOTH_TOON_EDGE: f32 = 0.2966;\nconst CLOTH_MIX04_MUL: f32 = 0.5; // \u8FD0\u7B97.004 MULTIPLY Value_001 (dump)\nconst NPR_EMIT_STR: f32 = 18.200000762939453;\nconst NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = normalize(skinnedNrm);\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n return output;\n}\n\n@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {\n let n = normalize(input.normal);\n let v = normalize(camera.viewPos - input.worldPos);\n let l = -light.lights[0].direction.xyz;\n let sun = light.lights[0].color.xyz * light.lights[0].color.w;\n let amb = light.ambientColor.xyz;\n let shadow = sampleShadow(input.worldPos, n);\n\n let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);\n let tex_rgb = tex_s.rgb;\n let out_alpha = material.alpha * tex_s.a;\n if (out_alpha < 0.001) { discard; }\n\n // Shader\u2192RGB \u2192 \u989C\u8272\u6E10\u53D8.008 CONSTANT \u2014 AA like face (same terminator artifact class)\n let lum_shade = shader_to_rgb_diffuse(n, l, sun, amb, shadow);\n let ramp008 = ramp_constant_edge_aa(lum_shade, CLOTH_TOON_EDGE, vec4f(0,0,0,1), vec4f(1,1,1,1));\n let toon_r = ramp008.r;\n // \u989C\u8272\u6E10\u53D8.008 \u2192 \u8FD0\u7B97.004 MULTIPLY 0.5 \u2192 \u6DF7\u5408.004 Factor\n let mix04_fac = math_multiply(toon_r, CLOTH_MIX04_MUL);\n\n // \u6DF7\u5408.004: A=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.002, B=\u7EB9\u7406\n let dark_tex = hue_sat(0.5, 1.0, 0.19999998807907104, 1.0, tex_rgb);\n let mix04 = mix_blend(mix04_fac, dark_tex, tex_rgb);\n\n // \u5012\u89D2.001\u2192Z \u2192 \u6DF7\u5408.003 Factor; A=\u6DF7\u5408.004, B=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.002\n let bevel_z = clamp(n.y, 0.0, 1.0);\n let mix03 = mix_blend(bevel_z, mix04, dark_tex);\n\n // \u73AF\u5883\u5149\u906E\u853D \u2192 \u989C\u8272\u6E10\u53D8.001 LINEAR \u2192 \u6DF7\u5408.001 (\u767D/\u9ED1) \u2192 \u6DF7\u5408.002 OVERLAY Fac\n let ao = ao_fake(n, v);\n let ao_ramp_c = ramp_linear(ao, 0.0, vec4f(1,1,1,1), 0.8808, vec4f(0,0,0,1));\n let mix01_fac = ao_ramp_c.r;\n let mix01_rgb = mix(vec3f(1.0), vec3f(0.0), mix01_fac);\n\n // \u6DF7\u5408.002 OVERLAY: Fac=\u6DF7\u5408.001, A=\u6DF7\u5408.003, B=\u8272\u76F8/\u9971\u548C\u5EA6/\u660E\u5EA6.004\n let hue004 = hue_sat(0.5, 0.800000011920929, 2.0, 1.0, mix03);\n let overlay_fac = mix01_rgb.r;\n let npr_rgb = mix_overlay(overlay_fac, mix03, hue004);\n let npr_emission = npr_rgb * NPR_EMIT_STR;\n\n // \u539F\u7406\u5316BSDF (EEVEE port): metallic=0, specular=0.8, roughness=0.5, specular_tint=0.\n // Bump subtree is dead in the Blender graph (\u566A\u6CE2\u2192\u51F9\u51F8 not linked to Principled.Normal).\n let principled_base = hue_sat(0.5, 1.0, 0.800000011920929, 1.0, tex_rgb);\n let NL = max(dot(n, l), 0.0);\n let NV = max(dot(n, v), 1e-4);\n\n // f0/f90 per gpu_shader_material_principled.glsl \u2014 specular_tint=0 \u2192 dielectric_f0_color=white.\n let f0 = vec3f(0.08 * CLOTH_SPECULAR);\n let f90 = mix(f0, vec3f(1.0), sqrt(CLOTH_SPECULAR));\n let split_sum = brdf_lut_baked(NV, CLOTH_ROUGHNESS);\n let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);\n\n // Direct glossy \u2014 bsdf_ggx already includes NL; no F applied here (tinted after accum).\n // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.\n let spec_direct = bsdf_ggx(n, l, v, CLOTH_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, CLOTH_ROUGHNESS);\n // Indirect glossy \u2014 flat world probe (solid color). Phase 2 adds cubemap.\n let spec_indirect = amb;\n let spec_radiance = (spec_direct + spec_indirect) * reflection_color;\n\n // Diffuse (Lambert), no (1-F) factor per EEVEE \u2014 it doesn't energy-conserve spec<->diffuse.\n // probe_evaluate_world_diff returns radiance L_w (SH projected, not cosine-convolved); in\n // closure_eval_surface_lib line 302: closure.radiance += diffuse_accum * L_w * diffuse.color.\n // So indirect diffuse = base_color \u00D7 L_w, no \u03C0 factor.\n let diffuse_radiance = principled_base * (sun * NL * shadow / PI_C + amb);\n let principled = diffuse_radiance + spec_radiance;\n\n // \u6DF7\u5408\u7740\u8272\u5668.001: Shader=\u81EA\u53D1\u5149.005, Shader_001=\u539F\u7406\u5316BSDF, Fac=0.9\n let final_color = mix(npr_emission, principled, NPR_MIX_SHADER_FAC);\n\n return vec4f(final_color, out_alpha);\n}\n\n";
|
|
2
|
+
//# sourceMappingURL=cloth_smooth.d.ts.map
|