yum/3ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum/3ner
b7d6ad4
master
1#ifndef __GLITTER_INC 2#define __GLITTER_INC 3 4/* 5 * This is an implementation of Kemppinen et. al.'s "Evaluating and Sampling 6 * Glinty NDFs in Constant Time". 7 * It is ported from: https://www.shadertoy.com/view/tcdGDl 8 * Since no license terms are listed in the shader body, it is protected by 9 * the default Shadertoy license (per https://www.shadertoy.com/terms), 10 * which is the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 11 * Unported License: https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en 12 * 13 * I have made changes to this code. They are: 14 * 1. Syntax changes required to translate GLSL to HLSL. 15 * 2. Stylistic preferences, like using "1" or "1.0" instead of "1.". 16 * 3. Replaced the original glitter RNG with a stronger integer-domain 17 * permuted congruential generator (PCG) hash. 18 * 4. The `GetGlitterLighting` function, which populates data required for 19 * indirect glitter. The original paper only discusses analytic lighting. 20 * 21 * @article{KPT:2025:Glinty, 22 * title = {Evaluating and Sampling Glinty NDFs in Constant Time}, 23 * author = {Kemppinen, Pauli and Paulin, LoÏs and Thonat, 24 * Théo and Thiery, Jean-Marc and Lehtinen, Jaakko and Boubekeur, 25 * Tamy}, 26 * year = {2025}, 27 * journal = {ACM Transactions on Graphics (Proc. SIGGRAPH Asia 2025)}, 28 * volume = {44}, 29 * number = {6}, 30 * articleno = {255}, 31 * } 32 */ 33 34#define PI 3.1415926535897932384626433832795028841971 35// Remaps [0, UINT_MAX] to [0, 1] 36#define UINT_TO_UNIT (1.0 / 4294967296.0) 37 38#define GLITTER_AMOUNT_MAX 64.0 39#define GLITTER_REFERENCE_AMOUNT 0.5 40#define GLITTER_REFERENCE_N 8.0e6 41#define GLITTER_POPULATION_SCALE \ 42 (GLITTER_AMOUNT_MAX / GLITTER_REFERENCE_AMOUNT) 43 44// Lambert azimuthal equal area projection 45float2 lambert(float3 v) { 46 return v.xy / sqrt(1 + v.z); 47} 48 49// v is a microfacet normal that has been squished according to alpha, a 50// roughness parameter. 51float3 ndf_to_disk_ggx(float3 v, float alpha) { 52 // Map `v` onto a hemisphere. 53 float3 hemi = float3(v.xy / alpha, v.z); 54 float denom = dot(hemi, hemi); 55 // Project onto circle with equal area projection, and remap from [-1, 1] 56 // to [0, 1]. 57 float2 v_disk = lambert(normalize(hemi)) * 0.5 + 0.5; 58 float jacobian_determinant = 1.0 / (alpha * alpha * denom * denom); 59 return float3(v_disk, jacobian_determinant); 60} 61 62// Computes (M^T M)^-1 63float2x2 inv_quadratic(float2x2 M) { 64 float D = determinant(M); 65 float2 c0 = transpose(M)[0] / D; 66 float2 c1 = transpose(M)[1] / D; 67 float A = dot(c0, c0); 68 float B = -dot(c0, c1); 69 float C = dot(c1, c1); 70 return transpose(float2x2(float2(C, B), float2(B, A))); 71} 72 73float2x2 uv_ellipsoid(float2x2 uv_J) { 74 float2x2 Q = inv_quadratic(transpose(uv_J)); 75 float2 q0 = transpose(Q)[0]; 76 float2 q1 = transpose(Q)[1]; 77 float tr = 0.5 * (q0.x + q1.y); 78 float D = sqrt(max(0.0, tr * tr - determinant(Q))); 79 float l1 = tr - D; 80 float l2 = tr + D; 81 float2 v1 = float2(l1 - q1.y, q0.y); 82 float2 v2 = float2(q1.x, l2 - q0.x); 83 float2 n = 1.0/sqrt(float2(l1, l2)); 84 return transpose(float2x2(normalize(v1) * n.x, normalize(v2) * n.y)); 85} 86 87float QueryLod(float2x2 uv_J, float filter_size) { 88 float s0 = length(transpose(uv_J)[0]); 89 float s1 = length(transpose(uv_J)[1]); 90 return log2(max(s0, s1) * filter_size) + pow(2.0, filter_size); 91} 92 93float2x2 inverse(float2x2 m) { 94 float det = (m[0][0] * m[1][1]) - (m[0][1] * m[1][0]); 95 96 return float2x2( 97 m[1][1], -m[0][1], 98 -m[1][0], m[0][0] 99 ) / det; 100} 101 102float normal(float2x2 cov, float2 x) { 103 return exp(-.5 * dot(x, mul(inverse(cov), x))) 104 / (sqrt(determinant(cov)) * 2.0 * PI); 105} 106 107uint2 pcg2d(uint2 v) { 108 v = v * 1664525u + 1013904223u; 109 v.x += v.y * 1664525u; 110 v.y += v.x * 1664525u; 111 112 v = v ^ (v>>16u); 113 114 v.x += v.y * 1664525u; 115 v.y += v.x * 1664525u; 116 v = v ^ (v>>16u); 117 return v; 118} 119 120uint4 pcg4d(uint4 v) { 121 v = v * 1664525u + 1013904223u; 122 v.x += v.y * v.w; 123 v.y += v.z * v.x; 124 v.z += v.x * v.y; 125 v.w += v.y * v.z; 126 127 v = v ^ (v >> 16u); 128 129 v.x += v.y * v.w; 130 v.y += v.z * v.x; 131 v.z += v.x * v.y; 132 v.w += v.y * v.z; 133 return v; 134} 135 136float2 rand(uint4 v) { 137 return float2(pcg4d(v).xy) * UINT_TO_UNIT; 138} 139 140float2 Rand2D(float2 x, float2 y, float l, uint i) { 141 // `x` and `y` are cell-center coordinates (n + 0.5). Hash the integer cell IDs 142 // directly so nearby cells do not alias through IEEE-754 bit patterns. 143 uint4 seed = uint4(uint2(x), uint2(y)); 144 uint2 salt = pcg2d(uint2(asuint(l), i)); 145 seed ^= uint4(salt.x, salt.y, salt.y ^ 0x9e3779b9u, salt.x ^ 0x85ebca6bu); 146 return rand(seed); 147} 148 149float Rand1D(float2 x, float2 y, float l, uint i) { 150 return Rand2D(x, y, l, i).x; 151} 152 153// Bürmann series, see https://en.wikipedia.org/wiki/Error_function 154float erf(float x) { 155 float e = exp(-x*x); 156 return sign(x) * 2.0 * sqrt((1.0 - e) / PI) * 157 (sqrt(PI) * 0.5 + 31.0/200.0 * e - 341.0/8000.0 * e * e); 158} 159 160float cdf(float x, float mu, float sigma) { 161 return 0.5 + 0.5 * erf((x-mu)/(sigma*sqrt(2.0))); 162} 163 164float integrate_interval(float x, float size, float mu, float stdev, 165 float lower_limit, float upper_limit) { 166 return cdf(min(x+size, upper_limit), mu, stdev) 167 - cdf(max(x-size, lower_limit), mu, stdev); 168} 169 170float integrate_box(float2 x, float2 size, float2 mu, float2x2 sigma, 171 float2 lower_limit, float2 upper_limit) { 172 return 173 integrate_interval(x.x, size.x, mu.x, 174 sqrt(sigma[0][0]), lower_limit.x, upper_limit.x) * 175 integrate_interval(x.y, size.y, mu.y, 176 sqrt(sigma[1][1]), lower_limit.y, upper_limit.y); 177} 178 179float compensation(float2 x, float2x2 sigma, float res, float2 base_i, 180 float2 neighbor_step, int cell_count) { 181 float containing = integrate_box(0.5, 0.5, x, sigma, 0.0, 1.0); 182 float explicitly_evaluated = 0.0; 183 184 [loop] 185 for (int cell_index = 0; cell_index < 4; ++cell_index) { 186 if (cell_index >= cell_count) { 187 break; 188 } 189 190 float2 cell_offset = 0.0; 191 if (cell_index == 1) { 192 cell_offset = float2(neighbor_step.x, 0.0); 193 } else if (cell_index == 2) { 194 cell_offset = float2(0.0, neighbor_step.y); 195 } else if (cell_index == 3) { 196 cell_offset = neighbor_step; 197 } 198 199 float2 sampled_cell_center = (base_i + cell_offset) / res; 200 explicitly_evaluated += integrate_box(sampled_cell_center, 0.5 / res, 201 x, sigma, 0.0, 1.0); 202 } 203 204 return containing - explicitly_evaluated; 205} 206 207float3 disk_to_ndf_ggx(float2 v_disk, float alpha) { 208 float2 p = v_disk * 2.0f - 1.0f; 209 float r2 = saturate(dot(p, p)); 210 float3 hemi = float3(p * sqrt(max(1e-6f, 2.0f - r2)), 1.0f - r2); 211 float alpha2 = alpha * alpha; 212 float denom = 213 sqrt(max(1e-6f, alpha2 * dot(hemi.xy, hemi.xy) + hemi.z * hemi.z)); 214 return float3(alpha * hemi.xy, hemi.z) / denom; 215} 216 217// Algorithm 1 from Kemppinen et. al. 218float D_Kemppinen(float3 h, float alpha, float glint_alpha, int angular_cells, 219 float2 uv, float2x2 uv_J, float N, float amount, float filter_size, 220 out float3 micro_normal) { 221 float res = sqrt(N); 222 float2 x_s = uv; 223 float3 x_a_and_d = ndf_to_disk_ggx(h, alpha); 224 float2 x_a = x_a_and_d.xy; 225 float d = x_a_and_d.z; 226 int angular_sample_count = clamp(angular_cells, 1, 4); 227 228 // The paper normalizes both Gaussian kernels and the point population, so 229 // narrower kernels and smaller populations have taller individual peaks. 230 // That is desirable for an energy-preserving NDF, but makes roughness and 231 // density alter the apparent size of a glint. 232 // Preserve the peaks at the original defaults instead: roughness controls 233 // angular width, filter_size controls spatial width, and amount controls only 234 // how many flakes are active. 235 float angular_peak_scale = pow(glint_alpha / 0.01, 2.0); 236 float spatial_peak_scale = pow(filter_size / 0.7, 2.0); 237 // Scaling the maximum population and each point's weight together keeps the 238 // reference amount's population, peak, and average energy unchanged. 239 float profile_scale = GLITTER_POPULATION_SCALE 240 * angular_peak_scale * spatial_peak_scale; 241 float amount_fraction = saturate(amount / GLITTER_AMOUNT_MAX); 242 243 // Both the spatial and angular neighborhoods require at least a 2x2 grid. 244 float max_lod = floor(log2(res)) - 1.0; 245 float lambda = clamp(QueryLod(res * uv_J, filter_size), 1.0, 246 max_lod); 247 248 float D_filter = 0; 249 float best_weight = 0; 250 float2 best_g_a = x_a; 251 252 [loop] 253 for (float m = 0; m < 2; m += 1) { 254 float l = floor(lambda) + m; 255 256 float w_lambda = 1.0 - abs(lambda - l); 257 float res_s = res * pow(2, -l); 258 float res_a = pow(2, l); 259 260 float2x2 uv_J2 = filter_size * uv_J; 261 float2x2 sigma_s = mul(uv_J2, transpose(uv_J2)); 262 263 float2x2 sigma_a = d * pow(glint_alpha, 2) * float2x2(1, 0, 0, 1); 264 265 float2 base_i_a = floor(x_a * res_a) + 0.5; 266 float2 i_a = clamp(base_i_a, 0.5, res_a - 0.5); 267 float2 angular_frac = frac(x_a * res_a) - 0.5; 268 float2 angular_step = lerp(float2(-1.0, -1.0), float2(1.0, 1.0), 269 step(0.0, angular_frac)); 270 // At the domain boundary, point inward so all four candidates remain 271 // distinct rather than accumulating the same random point repeatedly. 272 angular_step = lerp(angular_step, -angular_step, 273 step(i_a + angular_step, 0.0) + step(res_a, i_a + angular_step)); 274 275 float2 base_i_s = floor(x_s * res_s) + 0.5; 276 float2 i_s = clamp(base_i_s, 0.5, res_s - 0.5); 277 float2 spatial_frac = frac(x_s * res_s) - 0.5; 278 float2 spatial_step = lerp(float2(-1.0, -1.0), float2(1.0, 1.0), 279 step(0.0, spatial_frac)); 280 spatial_step = lerp(spatial_step, -spatial_step, 281 step(i_s + spatial_step, 0.0) + step(res_s, i_s + spatial_step)); 282 283 [loop] 284 for (int angular_index = 0; angular_index < 4; ++angular_index) { 285 if (angular_index >= angular_sample_count) { 286 break; 287 } 288 289 float2 angular_offset = 0.0; 290 if (angular_index == 1) { 291 angular_offset = float2(angular_step.x, 0.0); 292 } else if (angular_index == 2) { 293 angular_offset = float2(0.0, angular_step.y); 294 } else if (angular_index == 3) { 295 angular_offset = angular_step; 296 } 297 298 float2 i_a_neighbor = i_a + angular_offset; 299 300 [loop] 301 for (int spatial_index = 0; spatial_index < 4; ++spatial_index) { 302 float2 spatial_offset = 0.0; 303 if (spatial_index == 1) { 304 spatial_offset = float2(spatial_step.x, 0.0); 305 } else if (spatial_index == 2) { 306 spatial_offset = float2(0.0, spatial_step.y); 307 } else if (spatial_index == 3) { 308 spatial_offset = spatial_step; 309 } 310 311 float2 i_s_neighbor = i_s + spatial_offset; 312 float2 g_s = (i_s_neighbor + Rand2D(i_s_neighbor, i_a_neighbor, l, 1u) - .5) / res_s; 313 float2 g_a = (i_a_neighbor + Rand2D(i_s_neighbor, i_a_neighbor, l, 2u) - .5) / res_a; 314 315 float r = Rand1D(i_s_neighbor, i_a_neighbor, l, 4u); 316 float roulette = smoothstep(max(.0, r-.1), min(1.0, r+.1), w_lambda); 317 float amount_r = Rand1D(i_s_neighbor, i_a_neighbor, l, 8u); 318 float active = smoothstep(max(0.0, amount_r - 0.02), 319 min(1.0, amount_r + 0.02), amount_fraction); 320 321 float w = active * roulette * normal(sigma_a, x_a - g_a) 322 * normal(sigma_s, x_s - g_s) * profile_scale / N; 323 D_filter += w; 324 if (w > best_weight) { 325 best_weight = w; 326 best_g_a = g_a; 327 } 328 } 329 } 330 D_filter += amount_fraction * w_lambda * profile_scale 331 * compensation(x_a, sigma_a, res_a, i_a, angular_step, 332 angular_sample_count); 333 } 334 335 micro_normal = normalize(disk_to_ndf_ggx(best_g_a, alpha)); 336 return D_filter * d / PI; 337} 338 339#if defined(_GLITTER) 340struct LightGlitter { 341 float direct_D; 342 343 float indirect_D; 344 float indirect_NoL; 345 float indirect_LoH; 346}; 347 348// Glitter data getter to be run from lighting code. 349LightGlitter GetGlitterLighting( 350 float glitter_amount, float glitter_roughness, int glitter_angular_cells, 351 float glitter_filter_size, float2 uv, float3x3 tbn, float roughness, 352 float3 normal, float3 V, float3 direct_H, float3 indirect_dir) { 353 LightGlitter g; 354 float2x2 uv_J = uv_ellipsoid(transpose(float2x2(ddx(uv), ddy(uv)))); 355 // Keep the procedural population fixed. `glitter_amount` independently 356 // controls the fraction of that population which is active in D_Kemppinen. 357 float N = GLITTER_REFERENCE_N * GLITTER_POPULATION_SCALE; 358 359 // Direct 360 float3 direct_H_tangent = mul(direct_H, transpose(tbn)); 361 float3 direct_micro_normal; // unused 362 g.direct_D = D_Kemppinen(direct_H_tangent, roughness, glitter_roughness, 363 glitter_angular_cells, uv, uv_J, N, glitter_amount, 364 glitter_filter_size, 365 direct_micro_normal); 366 367 // Indirect 368 float3 indirect_H = normalize(V + indirect_dir); 369 float3 indirect_H_tangent = mul(indirect_H, transpose(tbn)); 370 float3 indirect_micro_normal; // unused, but required by D_Kemppinen 371 g.indirect_D = D_Kemppinen(indirect_H_tangent, roughness, glitter_roughness, 372 glitter_angular_cells, uv, uv_J, N, glitter_amount, 373 glitter_filter_size, 374 indirect_micro_normal); 375 g.indirect_NoL = max(1e-4, dot(normal, indirect_dir)); 376 g.indirect_LoH = max(1e-4, dot(indirect_dir, indirect_H)); 377 378 return g; 379} 380#endif // _GLITTER 381 382#endif // __GLITTER_INC