yum/3ner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum/3ner

yumMore clanker adjustments to glitterb7d6ad4

master
12.9 KiB382 linesraw
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