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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Blender 3.6 Principled BSDF defaults + Filmic "Medium High Contrast" tone mapping.
|
|
2
|
+
// Metallic=0, Specular=0.5 (F0=0.04), Roughness=0.5.
|
|
3
|
+
// Tone mapping via LUT sampled from Blender's OCIO pipeline (exposure -0.3 baked in).
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SHADER_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
const PI: f32 = 3.141592653589793;
|
|
8
|
+
const F0_DIELECTRIC: f32 = 0.04;
|
|
9
|
+
const ROUGHNESS: f32 = 0.5;
|
|
10
|
+
|
|
11
|
+
struct CameraUniforms {
|
|
12
|
+
view: mat4x4f,
|
|
13
|
+
projection: mat4x4f,
|
|
14
|
+
viewPos: vec3f,
|
|
15
|
+
_padding: f32,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
struct Light {
|
|
19
|
+
direction: vec4f,
|
|
20
|
+
color: vec4f,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
struct LightUniforms {
|
|
24
|
+
ambientColor: vec4f,
|
|
25
|
+
lights: array<Light, 4>,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Per-material uniforms. Add fields here only when a shader actually reads them;
|
|
29
|
+
// preset-specific shaders (face.ts, future hair.ts) share this struct so the
|
|
30
|
+
// engine can use one material bind-group layout.
|
|
31
|
+
struct MaterialUniforms {
|
|
32
|
+
diffuseColor: vec3f, // tint; multiplies sampled albedo (unused by current fs, reserved)
|
|
33
|
+
alpha: f32, // 0 → discard; <1 → transparent draw call
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
struct VertexOutput {
|
|
37
|
+
@builtin(position) position: vec4f,
|
|
38
|
+
@location(0) normal: vec3f,
|
|
39
|
+
@location(1) uv: vec2f,
|
|
40
|
+
@location(2) worldPos: vec3f,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
44
|
+
|
|
45
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
46
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
47
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
48
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
49
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
50
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
51
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
52
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
53
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
54
|
+
|
|
55
|
+
// ─── GGX specular helpers ───────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
fn ggx_d(ndoth: f32, a2: f32) -> f32 {
|
|
58
|
+
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
59
|
+
return a2 / (PI * denom * denom);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn smith_g1(ndotx: f32, a2: f32) -> f32 {
|
|
63
|
+
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
|
|
67
|
+
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Filmic tone mapping (LUT extracted from Blender 3.6 OCIO) ─────
|
|
71
|
+
// View transform = Filmic, Look = Medium High Contrast, Exposure = -0.3.
|
|
72
|
+
// 14 samples at integer log2 stops from -10 to +3 (inclusive).
|
|
73
|
+
// Extracted via scripts/extract_filmic_lut.py → probe image through scene
|
|
74
|
+
// color management. Input: linear scene-referred. Output: sRGB display.
|
|
75
|
+
|
|
76
|
+
fn filmic(x: f32) -> f32 {
|
|
77
|
+
var lut = array<f32, 14>(
|
|
78
|
+
0.0067, 0.0141, 0.0272, 0.0499, 0.0885, 0.1512, 0.2462,
|
|
79
|
+
0.3753, 0.5273, 0.6776, 0.8031, 0.8929, 0.9495, 0.9814
|
|
80
|
+
);
|
|
81
|
+
let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
|
|
82
|
+
let i = u32(t);
|
|
83
|
+
let j = min(i + 1u, 13u);
|
|
84
|
+
return mix(lut[i], lut[j], t - f32(i));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn tonemap(hdr: vec3f) -> vec3f {
|
|
88
|
+
return vec3f(filmic(hdr.x), filmic(hdr.y), filmic(hdr.z));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Shadow sampling (3×3 PCF) ──────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
94
|
+
let biasedPos = worldPos + n * 0.08;
|
|
95
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
96
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
97
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
98
|
+
let cmpZ = ndc.z - 0.001;
|
|
99
|
+
let ts = 1.0 / 4096.0;
|
|
100
|
+
var vis = 0.0;
|
|
101
|
+
for (var y = -1; y <= 1; y++) {
|
|
102
|
+
for (var x = -1; x <= 1; x++) {
|
|
103
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return vis / 9.0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Vertex / Fragment ──────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
@vertex fn vs(
|
|
112
|
+
@location(0) position: vec3f,
|
|
113
|
+
@location(1) normal: vec3f,
|
|
114
|
+
@location(2) uv: vec2f,
|
|
115
|
+
@location(3) joints0: vec4<u32>,
|
|
116
|
+
@location(4) weights0: vec4<f32>
|
|
117
|
+
) -> VertexOutput {
|
|
118
|
+
var output: VertexOutput;
|
|
119
|
+
let pos4 = vec4f(position, 1.0);
|
|
120
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
121
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
122
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
123
|
+
var skinnedPos = vec4f(0.0);
|
|
124
|
+
var skinnedNrm = vec3f(0.0);
|
|
125
|
+
for (var i = 0u; i < 4u; i++) {
|
|
126
|
+
let m = skinMats[joints0[i]];
|
|
127
|
+
let w = nw[i];
|
|
128
|
+
skinnedPos += (m * pos4) * w;
|
|
129
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
130
|
+
}
|
|
131
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
132
|
+
output.normal = normalize(skinnedNrm);
|
|
133
|
+
output.uv = uv;
|
|
134
|
+
output.worldPos = skinnedPos.xyz;
|
|
135
|
+
return output;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
139
|
+
let alpha = material.alpha;
|
|
140
|
+
if (alpha < 0.001) { discard; }
|
|
141
|
+
|
|
142
|
+
let n = normalize(input.normal);
|
|
143
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
144
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
145
|
+
|
|
146
|
+
let l = -light.lights[0].direction.xyz;
|
|
147
|
+
let sunColor = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
148
|
+
let h = normalize(l + v);
|
|
149
|
+
|
|
150
|
+
let ndotl = max(dot(n, l), 0.0);
|
|
151
|
+
let ndotv = max(dot(n, v), 0.001);
|
|
152
|
+
let ndoth = max(dot(n, h), 0.0);
|
|
153
|
+
let vdoth = max(dot(v, h), 0.0);
|
|
154
|
+
|
|
155
|
+
let a2 = ROUGHNESS * ROUGHNESS;
|
|
156
|
+
let D = ggx_d(ndoth, a2);
|
|
157
|
+
let G = smith_g1(ndotl, a2) * smith_g1(ndotv, a2);
|
|
158
|
+
let F = fresnel_schlick(vdoth, F0_DIELECTRIC);
|
|
159
|
+
let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
|
|
160
|
+
|
|
161
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
162
|
+
let kd = (1.0 - F) * albedo / PI;
|
|
163
|
+
let direct = (kd + spec) * sunColor * ndotl * shadow;
|
|
164
|
+
let ambient = albedo * light.ambientColor.xyz;
|
|
165
|
+
|
|
166
|
+
return vec4f(ambient + direct, alpha);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
`
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// One-shot bake pass that precomputes EEVEE's BRDF split-sum DFG LUT.
|
|
2
|
+
// Direct WGSL port of Blender 3.6 source/blender/draw/engines/eevee/shaders/
|
|
3
|
+
// bsdf_lut_frag.glsl + bsdf_sampling_lib.glsl (VNDF GGX branch).
|
|
4
|
+
//
|
|
5
|
+
// Output texture: 64×64 rg16float, written once at engine init.
|
|
6
|
+
// R = (1 - Fc) × BRDF integrated over GGX VNDF — scales f0
|
|
7
|
+
// G = Fc × BRDF integrated over GGX VNDF — scales f90
|
|
8
|
+
// Runtime sampler: see brdf_lut_baked() in nodes.ts. Plug into
|
|
9
|
+
// F_brdf_single_scatter / F_brdf_multi_scatter verbatim.
|
|
10
|
+
|
|
11
|
+
export const DFG_LUT_SIZE = 64
|
|
12
|
+
export const DFG_LUT_SAMPLE_COUNT = 32
|
|
13
|
+
|
|
14
|
+
export const DFG_LUT_WGSL = /* wgsl */ `
|
|
15
|
+
const LUT_SIZE: f32 = ${DFG_LUT_SIZE}.0;
|
|
16
|
+
const SAMPLE_COUNT: u32 = ${DFG_LUT_SAMPLE_COUNT}u;
|
|
17
|
+
const M_2PI: f32 = 6.283185307179586;
|
|
18
|
+
|
|
19
|
+
@vertex fn vs(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f {
|
|
20
|
+
// Full-screen triangle covering [-1,1]² in NDC.
|
|
21
|
+
let x = f32((vid << 1u) & 2u) * 2.0 - 1.0;
|
|
22
|
+
let y = f32(vid & 2u) * 2.0 - 1.0;
|
|
23
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// common_math_geom_lib.glsl:165 — make_orthonormal_basis.
|
|
27
|
+
fn orthonormal_basis(N: vec3f) -> mat2x3f {
|
|
28
|
+
let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(N.z) < 0.99999);
|
|
29
|
+
let T = normalize(cross(up, N));
|
|
30
|
+
let B = cross(N, T);
|
|
31
|
+
return mat2x3f(T, B);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// bsdf_sampling_lib.glsl:27 — Heitz 2018 VNDF sampling in tangent space.
|
|
35
|
+
fn sample_ggx_vndf(rand: vec3f, alpha: f32, Vt: vec3f) -> vec3f {
|
|
36
|
+
let Vh = normalize(vec3f(alpha * Vt.xy, Vt.z));
|
|
37
|
+
let tb = orthonormal_basis(Vh);
|
|
38
|
+
let Th = tb[0];
|
|
39
|
+
let Bh = tb[1];
|
|
40
|
+
let r = sqrt(rand.x);
|
|
41
|
+
let x = r * rand.y;
|
|
42
|
+
var y = r * rand.z;
|
|
43
|
+
let s = 0.5 * (1.0 + Vh.z);
|
|
44
|
+
y = (1.0 - s) * sqrt(1.0 - x * x) + s * y;
|
|
45
|
+
let z = sqrt(saturate(1.0 - x * x - y * y));
|
|
46
|
+
let Hh = x * Th + y * Bh + z * Vh;
|
|
47
|
+
return normalize(vec3f(alpha * Hh.xy, saturate(Hh.z)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// bsdf_common_lib.glsl:105 — G1 Smith GGX (Brian Karis opti form).
|
|
51
|
+
fn G1_Smith_GGX_opti(NX: f32, a2: f32) -> f32 {
|
|
52
|
+
return NX + sqrt(NX * (NX - NX * a2) + a2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// bsdf_common_lib.glsl:50 — exact dielectric Fresnel (monochromatic).
|
|
56
|
+
fn F_eta(eta: f32, cos_theta: f32) -> f32 {
|
|
57
|
+
let c = abs(cos_theta);
|
|
58
|
+
var g = eta * eta - 1.0 + c * c;
|
|
59
|
+
if (g > 0.0) {
|
|
60
|
+
g = sqrt(g);
|
|
61
|
+
let A = (g - c) / (g + c);
|
|
62
|
+
let B = (c * (g + c) - 1.0) / (c * (g - c) + 1.0);
|
|
63
|
+
return 0.5 * A * A * (1.0 + B * B);
|
|
64
|
+
}
|
|
65
|
+
return 1.0; // total internal reflection
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn f0_from_ior(eta: f32) -> f32 {
|
|
69
|
+
let A = (eta - 1.0) / (eta + 1.0);
|
|
70
|
+
return A * A;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// F_color_blend(eta, fresnel, vec3(0)).r — blend factor only.
|
|
74
|
+
fn F_color_blend_zero(eta: f32, fresnel: f32) -> f32 {
|
|
75
|
+
let f0 = f0_from_ior(eta);
|
|
76
|
+
return saturate((fresnel - f0) / (1.0 - f0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec2f {
|
|
80
|
+
let y_uv = floor(frag.y) / (LUT_SIZE - 1.0);
|
|
81
|
+
let x_uv = floor(frag.x) / (LUT_SIZE - 1.0);
|
|
82
|
+
|
|
83
|
+
let NV = clamp(1.0 - y_uv * y_uv, 1e-4, 0.9999);
|
|
84
|
+
let a = x_uv * x_uv;
|
|
85
|
+
let a2 = clamp(a * a, 1e-4, 0.9999);
|
|
86
|
+
|
|
87
|
+
let V = vec3f(sqrt(1.0 - NV * NV), 0.0, NV);
|
|
88
|
+
|
|
89
|
+
// principled specular=1.0 — max value, matches bsdf_lut_frag.glsl:41.
|
|
90
|
+
let eta = (2.0 / (1.0 - sqrt(0.08 * 1.0))) - 1.0;
|
|
91
|
+
|
|
92
|
+
var brdf_accum = 0.0;
|
|
93
|
+
var fresnel_accum = 0.0;
|
|
94
|
+
let sc_f = f32(SAMPLE_COUNT);
|
|
95
|
+
for (var j: u32 = 0u; j < SAMPLE_COUNT; j = j + 1u) {
|
|
96
|
+
for (var i: u32 = 0u; i < SAMPLE_COUNT; i = i + 1u) {
|
|
97
|
+
let ix = (f32(i) + 0.5) / sc_f;
|
|
98
|
+
let iy = (f32(j) + 0.5) / sc_f;
|
|
99
|
+
// Xi.x = radial, Xi.yz = (cos, sin) of azimuth — bsdf_lut_frag.glsl:22.
|
|
100
|
+
let Xi = vec3f(ix, cos(iy * M_2PI), sin(iy * M_2PI));
|
|
101
|
+
|
|
102
|
+
let H = sample_ggx_vndf(Xi, a, V);
|
|
103
|
+
let L = -reflect(V, H);
|
|
104
|
+
let NL = L.z;
|
|
105
|
+
if (NL > 0.0) {
|
|
106
|
+
let NH = max(H.z, 0.0);
|
|
107
|
+
let VH = max(dot(V, H), 0.0);
|
|
108
|
+
|
|
109
|
+
// G_smith (divided form): 4·NV·NL / (G1_v·G1_l). See bsdf_common_lib.glsl:105.
|
|
110
|
+
let G1v = G1_Smith_GGX_opti(NV, a2);
|
|
111
|
+
let G1l = G1_Smith_GGX_opti(NL, a2);
|
|
112
|
+
let G_smith = 4.0 * NV * NL / (G1v * G1l);
|
|
113
|
+
|
|
114
|
+
let brdf = (G_smith * VH) / (NH * NV);
|
|
115
|
+
|
|
116
|
+
let fresnel = F_eta(eta, VH);
|
|
117
|
+
let Fc = F_color_blend_zero(eta, fresnel);
|
|
118
|
+
|
|
119
|
+
brdf_accum = brdf_accum + (1.0 - Fc) * brdf;
|
|
120
|
+
fresnel_accum = fresnel_accum + Fc * brdf;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let n2 = sc_f * sc_f;
|
|
125
|
+
return vec2f(brdf_accum / n2, fresnel_accum / n2);
|
|
126
|
+
}
|
|
127
|
+
`
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Eye preset — default Principled BSDF (F0=0.04, Roughness=0.5) + Emission socket set to albedo × 1.5.
|
|
2
|
+
// Matches the published preset's instruction: "keep eyes in the default nodegraph, add emission 1.5".
|
|
3
|
+
// Blender's Principled BSDF Emission socket is added on top of the shaded output (pre-tonemap, feeds bloom).
|
|
4
|
+
|
|
5
|
+
export const EYE_SHADER_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
const PI: f32 = 3.141592653589793;
|
|
8
|
+
const F0_DIELECTRIC: f32 = 0.04;
|
|
9
|
+
const ROUGHNESS: f32 = 0.5;
|
|
10
|
+
const EYE_EMISSION_STRENGTH: f32 = 1.5;
|
|
11
|
+
|
|
12
|
+
struct CameraUniforms {
|
|
13
|
+
view: mat4x4f,
|
|
14
|
+
projection: mat4x4f,
|
|
15
|
+
viewPos: vec3f,
|
|
16
|
+
_padding: f32,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
struct Light {
|
|
20
|
+
direction: vec4f,
|
|
21
|
+
color: vec4f,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
struct LightUniforms {
|
|
25
|
+
ambientColor: vec4f,
|
|
26
|
+
lights: array<Light, 4>,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
struct MaterialUniforms {
|
|
30
|
+
diffuseColor: vec3f,
|
|
31
|
+
alpha: f32,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
struct VertexOutput {
|
|
35
|
+
@builtin(position) position: vec4f,
|
|
36
|
+
@location(0) normal: vec3f,
|
|
37
|
+
@location(1) uv: vec2f,
|
|
38
|
+
@location(2) worldPos: vec3f,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
42
|
+
|
|
43
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
44
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
45
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
46
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
47
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
48
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
49
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
50
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
51
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
52
|
+
|
|
53
|
+
fn ggx_d(ndoth: f32, a2: f32) -> f32 {
|
|
54
|
+
let denom = ndoth * ndoth * (a2 - 1.0) + 1.0;
|
|
55
|
+
return a2 / (PI * denom * denom);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn smith_g1(ndotx: f32, a2: f32) -> f32 {
|
|
59
|
+
return 2.0 * ndotx / (ndotx + sqrt(a2 + (1.0 - a2) * ndotx * ndotx));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn fresnel_schlick(cosTheta: f32, f0: f32) -> f32 {
|
|
63
|
+
return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
67
|
+
let biasedPos = worldPos + n * 0.08;
|
|
68
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
69
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
70
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
71
|
+
let cmpZ = ndc.z - 0.001;
|
|
72
|
+
let ts = 1.0 / 4096.0;
|
|
73
|
+
var vis = 0.0;
|
|
74
|
+
for (var y = -1; y <= 1; y++) {
|
|
75
|
+
for (var x = -1; x <= 1; x++) {
|
|
76
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return vis / 9.0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@vertex fn vs(
|
|
83
|
+
@location(0) position: vec3f,
|
|
84
|
+
@location(1) normal: vec3f,
|
|
85
|
+
@location(2) uv: vec2f,
|
|
86
|
+
@location(3) joints0: vec4<u32>,
|
|
87
|
+
@location(4) weights0: vec4<f32>
|
|
88
|
+
) -> VertexOutput {
|
|
89
|
+
var output: VertexOutput;
|
|
90
|
+
let pos4 = vec4f(position, 1.0);
|
|
91
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
92
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
93
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
94
|
+
var skinnedPos = vec4f(0.0);
|
|
95
|
+
var skinnedNrm = vec3f(0.0);
|
|
96
|
+
for (var i = 0u; i < 4u; i++) {
|
|
97
|
+
let m = skinMats[joints0[i]];
|
|
98
|
+
let w = nw[i];
|
|
99
|
+
skinnedPos += (m * pos4) * w;
|
|
100
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
101
|
+
}
|
|
102
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
103
|
+
output.normal = normalize(skinnedNrm);
|
|
104
|
+
output.uv = uv;
|
|
105
|
+
output.worldPos = skinnedPos.xyz;
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
110
|
+
let alpha = material.alpha;
|
|
111
|
+
if (alpha < 0.001) { discard; }
|
|
112
|
+
|
|
113
|
+
let n = normalize(input.normal);
|
|
114
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
115
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
116
|
+
|
|
117
|
+
let l = -light.lights[0].direction.xyz;
|
|
118
|
+
let sunColor = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
119
|
+
let h = normalize(l + v);
|
|
120
|
+
|
|
121
|
+
let ndotl = max(dot(n, l), 0.0);
|
|
122
|
+
let ndotv = max(dot(n, v), 0.001);
|
|
123
|
+
let ndoth = max(dot(n, h), 0.0);
|
|
124
|
+
let vdoth = max(dot(v, h), 0.0);
|
|
125
|
+
|
|
126
|
+
let a2 = ROUGHNESS * ROUGHNESS;
|
|
127
|
+
let D = ggx_d(ndoth, a2);
|
|
128
|
+
let G = smith_g1(ndotl, a2) * smith_g1(ndotv, a2);
|
|
129
|
+
let F = fresnel_schlick(vdoth, F0_DIELECTRIC);
|
|
130
|
+
let spec = (D * G * F) / max(4.0 * ndotl * ndotv, 0.001);
|
|
131
|
+
|
|
132
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
133
|
+
let kd = (1.0 - F) * albedo / PI;
|
|
134
|
+
let direct = (kd + spec) * sunColor * ndotl * shadow;
|
|
135
|
+
let ambient = albedo * light.ambientColor.xyz;
|
|
136
|
+
|
|
137
|
+
// Principled Emission socket: emissive = emission_color × strength, added on top of shading.
|
|
138
|
+
let emission = albedo * EYE_EMISSION_STRENGTH;
|
|
139
|
+
|
|
140
|
+
return vec4f(ambient + direct + emission, alpha);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
`
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// M_Face — WGSL trace of 仿深空之眼渲染预设v1.0_by_小绿毛猫_material_graph_dump.json "M_Face"; VALTORGB stops from m_graphs (dump omits curve keys).
|
|
2
|
+
|
|
3
|
+
import { NODES_WGSL } from "./nodes"
|
|
4
|
+
|
|
5
|
+
export const FACE_SHADER_WGSL = /* wgsl */ `
|
|
6
|
+
|
|
7
|
+
${NODES_WGSL}
|
|
8
|
+
|
|
9
|
+
struct CameraUniforms {
|
|
10
|
+
view: mat4x4f,
|
|
11
|
+
projection: mat4x4f,
|
|
12
|
+
viewPos: vec3f,
|
|
13
|
+
_padding: f32,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
struct Light {
|
|
17
|
+
direction: vec4f,
|
|
18
|
+
color: vec4f,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
struct LightUniforms {
|
|
22
|
+
ambientColor: vec4f,
|
|
23
|
+
lights: array<Light, 4>,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
struct MaterialUniforms {
|
|
27
|
+
diffuseColor: vec3f,
|
|
28
|
+
alpha: f32,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct VertexOutput {
|
|
32
|
+
@builtin(position) position: vec4f,
|
|
33
|
+
@location(0) normal: vec3f,
|
|
34
|
+
@location(1) uv: vec2f,
|
|
35
|
+
@location(2) worldPos: vec3f,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
39
|
+
|
|
40
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
41
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
42
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
43
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
44
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
45
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
46
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
47
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
48
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
49
|
+
|
|
50
|
+
// 3x3 PCF shadow sampling, 4096 map, normal-bias 0.08, depth-bias 0.001
|
|
51
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
52
|
+
let biasedPos = worldPos + n * 0.08;
|
|
53
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
54
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
55
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
56
|
+
let cmpZ = ndc.z - 0.001;
|
|
57
|
+
let ts = 1.0 / 4096.0;
|
|
58
|
+
var vis = 0.0;
|
|
59
|
+
for (var y = -1; y <= 1; y++) {
|
|
60
|
+
for (var x = -1; x <= 1; x++) {
|
|
61
|
+
vis += textureSampleCompare(shadowMap, shadowSampler, suv + vec2f(f32(x), f32(y)) * ts, cmpZ);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return vis / 9.0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PI_F: f32 = 3.141592653589793;
|
|
68
|
+
const FACE_SPECULAR: f32 = 0.5;
|
|
69
|
+
const FACE_ROUGHNESS: f32 = 0.3;
|
|
70
|
+
// Dump M_Face unlinked defaults (math op enum not serialized — warm clamp chain still from m_graphs)
|
|
71
|
+
const FACE_RIM2_POW: f32 = 0.6300000548362732;
|
|
72
|
+
const FACE_RIM2_BG: vec3f = vec3f(1.0, 0.4684903025627136, 0.3698573112487793);
|
|
73
|
+
const FACE_WARM_AO_MUL: f32 = 0.30000001192092896; // 运算.004 MULTIPLY after invert (was 0.5 in older trace)
|
|
74
|
+
const FACE_BRIGHT_TEX_THRESH: f32 = 0.9300000071525574; // 运算.005 GREATER_THAN Value_001
|
|
75
|
+
const FACE_MIX_NPR: f32 = 0.5; // 混合着色器.001 Fac
|
|
76
|
+
|
|
77
|
+
@vertex fn vs(
|
|
78
|
+
@location(0) position: vec3f,
|
|
79
|
+
@location(1) normal: vec3f,
|
|
80
|
+
@location(2) uv: vec2f,
|
|
81
|
+
@location(3) joints0: vec4<u32>,
|
|
82
|
+
@location(4) weights0: vec4<f32>
|
|
83
|
+
) -> VertexOutput {
|
|
84
|
+
var output: VertexOutput;
|
|
85
|
+
let pos4 = vec4f(position, 1.0);
|
|
86
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
87
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
88
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
89
|
+
var skinnedPos = vec4f(0.0);
|
|
90
|
+
var skinnedNrm = vec3f(0.0);
|
|
91
|
+
for (var i = 0u; i < 4u; i++) {
|
|
92
|
+
let m = skinMats[joints0[i]];
|
|
93
|
+
let w = nw[i];
|
|
94
|
+
skinnedPos += (m * pos4) * w;
|
|
95
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
96
|
+
}
|
|
97
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
98
|
+
output.normal = normalize(skinnedNrm);
|
|
99
|
+
output.uv = uv;
|
|
100
|
+
output.worldPos = skinnedPos.xyz;
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fragment: M_Face NPR + Principled hybrid
|
|
105
|
+
// TEX → HueSat shadow/lit → toon gate → BrightContrast → AO chain → emission stack
|
|
106
|
+
// Fresnel rims, warm AO emission, bright-texture gate, noise-bumped Principled
|
|
107
|
+
// Final = mix(Principled, NPR, 0.5)
|
|
108
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
109
|
+
let alpha = material.alpha;
|
|
110
|
+
if (alpha < 0.001) { discard; }
|
|
111
|
+
|
|
112
|
+
let n = normalize(input.normal);
|
|
113
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
114
|
+
let l = -light.lights[0].direction.xyz;
|
|
115
|
+
let intensity = light.lights[0].color.w;
|
|
116
|
+
let sun = light.lights[0].color.xyz * intensity;
|
|
117
|
+
|
|
118
|
+
let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
119
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
120
|
+
|
|
121
|
+
// ═══ SOURCES ═══
|
|
122
|
+
// DiffuseBSDF(white) → ShaderToRGB (energy-matched); shadow on direct only
|
|
123
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);
|
|
124
|
+
// ramp.008 CONSTANT — edge AA avoids binary fac shimmer / white specks on terminator (fwidth + smoothstep)
|
|
125
|
+
let toon = ramp_constant_edge_aa(ndotl_raw, 0.2966, vec4f(0,0,0,1), vec4f(1,1,1,1)).r;
|
|
126
|
+
let ao = ao_fake(n, v);
|
|
127
|
+
|
|
128
|
+
// ═══ TOON COLOR ═══
|
|
129
|
+
let shadow_tint = hue_sat(0.46000000834465027, 2.0, 0.3499999940395355, 1.0, tex_color); // HueSat.002
|
|
130
|
+
let lit_tint = hue_sat(0.46000000834465027, 1.600000023841858, 1.5, 1.0, tex_color); // HueSat.001
|
|
131
|
+
let toon_color = mix_blend(toon, shadow_tint, lit_tint); // Mix.004
|
|
132
|
+
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
133
|
+
|
|
134
|
+
// ═══ AO CHAIN ═══
|
|
135
|
+
// ramp CONSTANT [0→white, 0.5995→black]
|
|
136
|
+
let ao_ramp = ramp_constant(ao, 0.0, vec4f(1,1,1,1), 0.5995, vec4f(0,0,0,1)).r;
|
|
137
|
+
// Mix.003(Factor=ao_ramp, A=bc, B=reddish tint)
|
|
138
|
+
let ao_mixed = mix_blend(ao_ramp, bc, vec3f(0.8302, 0.3346, 0.2795));
|
|
139
|
+
|
|
140
|
+
// ═══ EMISSION 3 ═══
|
|
141
|
+
let emission3 = ao_mixed * 2.5; // Emission.003(Strength=2.5)
|
|
142
|
+
|
|
143
|
+
// ═══ WARM EMISSION ═══
|
|
144
|
+
let ao_inv = invert_f(1.0, ao_ramp);
|
|
145
|
+
let warm_str = ao_inv * FACE_WARM_AO_MUL; // 反转 → 运算.004 MULTIPLY Value_001
|
|
146
|
+
let warm_input = clamp(toon * 0.5 + 0.5, 0.0, 1.0); // 运算.001→运算.006→Clamp
|
|
147
|
+
// ramp.003 CARDINAL [0.2409→warm dark, 0.4663→warm light]
|
|
148
|
+
let warm_color = ramp_cardinal(warm_input, 0.2409,
|
|
149
|
+
vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
|
|
150
|
+
vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
|
|
151
|
+
let warm_emission = warm_color * warm_str; // Emission.001
|
|
152
|
+
|
|
153
|
+
// ═══ RIM 1 ═══
|
|
154
|
+
// Fresnel(IOR=2.0) × LayerWeight.001(Facing, Blend=0.24)
|
|
155
|
+
let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24, n, v);
|
|
156
|
+
let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
|
|
157
|
+
|
|
158
|
+
// ═══ RIM 2 ═══
|
|
159
|
+
// Fresnel.001(IOR=1.45) × LayerWeight.002(Fresnel output, Blend=0.61)
|
|
160
|
+
let rim2_raw = fresnel(1.45, n, v) * layer_weight_fresnel(0.61, n, v);
|
|
161
|
+
let rim2_fac = math_power(rim2_raw, FACE_RIM2_POW);
|
|
162
|
+
// MixShader.002: Shader=Emission.003, Shader_001=背景
|
|
163
|
+
let rim2_mixed = mix(emission3, FACE_RIM2_BG, rim2_fac);
|
|
164
|
+
|
|
165
|
+
// 转接点.005(tex) → 运算.005 GREATER_THAN Value_001
|
|
166
|
+
let tex_gate = math_greater_than(tex_color.r, FACE_BRIGHT_TEX_THRESH);
|
|
167
|
+
let bright_emit = vec3f(tex_gate) * 3.0; // Emission.002(Strength=3.0)
|
|
168
|
+
|
|
169
|
+
// ═══ NPR STACK (AddShader chain) ═══
|
|
170
|
+
let add2 = rim2_mixed + bright_emit; // AddShader.002
|
|
171
|
+
let add0 = rim1 + add2; // AddShader
|
|
172
|
+
let npr_stack = add0 + warm_emission; // AddShader.001
|
|
173
|
+
|
|
174
|
+
// ═══ PRINCIPLED BSDF ═══
|
|
175
|
+
// Noise-based bump normal
|
|
176
|
+
let gen = mapping_point(input.worldPos, vec3f(0.0), vec3f(0.0), vec3f(1.0, 1.0, 1.5));
|
|
177
|
+
let noise_val = tex_noise(gen, 1.0, 2.0, 0.5, 0.0);
|
|
178
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
179
|
+
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos); // 凹凸 Strength; LH bump
|
|
180
|
+
|
|
181
|
+
// Mix.001(Factor=noise_ramp, A=bc, B=dark red)
|
|
182
|
+
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6832, 0.1947, 0.1373));
|
|
183
|
+
// Emission input from reroute.011 (bc), Strength=0.2
|
|
184
|
+
let p_emission = bc * 0.2;
|
|
185
|
+
// AO.002 → ramp.005 LINEAR [0.003→black, 1.0→gray] for subsurface approx
|
|
186
|
+
let ao2 = ao_fake(n, v);
|
|
187
|
+
let sss = ramp_linear(ao2, 0.003, vec4f(0,0,0,1), 1.0, vec4f(0.0786, 0.0786, 0.0786, 1.0)).r;
|
|
188
|
+
|
|
189
|
+
// 原理化BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.
|
|
190
|
+
let NL = max(dot(bumped_n, l), 0.0);
|
|
191
|
+
let NV = max(dot(bumped_n, v), 1e-4);
|
|
192
|
+
|
|
193
|
+
let f0 = vec3f(0.08 * FACE_SPECULAR);
|
|
194
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(FACE_SPECULAR));
|
|
195
|
+
let split_sum = brdf_lut_baked(NV, FACE_ROUGHNESS);
|
|
196
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, split_sum);
|
|
197
|
+
|
|
198
|
+
let spec_direct = bsdf_ggx(bumped_n, l, v, FACE_ROUGHNESS) * sun * shadow * ltc_brdf_scale(NV, FACE_ROUGHNESS);
|
|
199
|
+
let spec_indirect = light.ambientColor.xyz;
|
|
200
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
201
|
+
|
|
202
|
+
// Indirect diffuse = base_color × L_w per Blender closure_eval_surface_lib.glsl line 302;
|
|
203
|
+
// probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).
|
|
204
|
+
let diffuse_radiance = principled_base * (sun * NL * shadow / PI_F + light.ambientColor.xyz);
|
|
205
|
+
let principled = diffuse_radiance + spec_radiance + p_emission + vec3f(sss);
|
|
206
|
+
|
|
207
|
+
// 混合着色器.001: Shader=相加着色器.001, Shader_001=原理化BSDF — Fac blends toward second
|
|
208
|
+
let final_color = mix(npr_stack, principled, FACE_MIX_NPR);
|
|
209
|
+
|
|
210
|
+
return vec4f(final_color, alpha);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
`
|