yum/3ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum/3ner
4a3c970
master
1#ifndef __APERIODIC_TILING_INC 2#define __APERIODIC_TILING_INC 3 4// 5D cut-and-project for Penrose tiling. 5// References: 6// https://www.shadertoy.com/view/XccXW8 (public domain) 7// https://gglouser.github.io/cut-and-project-tiling/docs/intro.html#how-it-works 8// https://www.youtube.com/watch?v=hwMAOFb6yvA 9 10#include "globals.cginc" 11#include "math.cginc" 12 13#if defined(_APERIODIC_TILING) 14 15static const float M5 = sqrt(2.0 / 5.0); 16static const float APERIODIC_FILTER_THRESHOLD = 0.5; 17static const float APERIODIC_FAR_THRESHOLD = 1.0; 18static const float APERIODIC_FILTER_BLEND_WIDTH = 0.5; 19static const float APERIODIC_SMOOTHSTEP_SQ_MEAN = 13.0 / 35.0; 20 21static const float4 basis_u5_03 = M5 * float4( 22 cos(0 * TAU / 10), 23 cos(1 * TAU / 10), 24 cos(2 * TAU / 10), 25 cos(3 * TAU / 10)); 26static const float basis_u5_44 = M5 * cos(4 * TAU / 10); 27 28static const float4 basis_v5_03 = M5 * float4( 29 sin(0 * TAU / 10), 30 sin(1 * TAU / 10), 31 sin(2 * TAU / 10), 32 sin(3 * TAU / 10)); 33static const float basis_v5_44 = M5 * sin(4 * TAU / 10); 34 35static const float2 aperiodic_tile_offsets[4] = { 36 float2(0.5, 0.5), 37 float2(0.5, -0.5), 38 float2(-0.5, 0.5), 39 float2(-0.5, -0.5) 40}; 41 42static const float4 aperiodic_face_a03[10] = { 43 float4(1, 0, 0, 0), 44 float4(1, 0, 0, 0), 45 float4(1, 0, 0, 0), 46 float4(1, 0, 0, 0), 47 float4(0, 1, 0, 0), 48 float4(0, 1, 0, 0), 49 float4(0, 1, 0, 0), 50 float4(0, 0, 1, 0), 51 float4(0, 0, 1, 0), 52 float4(0, 0, 0, 1) 53}; 54 55static const float aperiodic_face_a44[10] = { 56 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 57}; 58 59static const float4 aperiodic_face_b03[10] = { 60 float4(0, 1, 0, 0), 61 float4(0, 0, 1, 0), 62 float4(0, 0, 0, 1), 63 float4(0, 0, 0, 0), 64 float4(0, 0, 1, 0), 65 float4(0, 0, 0, 1), 66 float4(0, 0, 0, 0), 67 float4(0, 0, 0, 1), 68 float4(0, 0, 0, 0), 69 float4(0, 0, 0, 0) 70}; 71 72static const float aperiodic_face_b44[10] = { 73 0, 0, 0, 1, 0, 0, 1, 0, 1, 1 74}; 75 76static const float4 aperiodic_face_c03[10] = { 77 float4(0, 0, 1, 1), 78 float4(0, 1, 0, 1), 79 float4(0, 1, 1, 0), 80 float4(0, 1, 1, 1), 81 float4(1, 0, 0, 1), 82 float4(1, 0, 1, 0), 83 float4(1, 0, 1, 1), 84 float4(1, 1, 0, 0), 85 float4(1, 1, 0, 1), 86 float4(1, 1, 1, 0) 87}; 88 89static const float aperiodic_face_c44[10] = { 90 1, 1, 1, 0, 1, 1, 0, 1, 0, 0 91}; 92 93// Area weights for the 10 face orientations. Adjacent axis pairs are the thin 94// rhombs, separated pairs are the thick rhombs. These weights sum to 1. 95static const float aperiodic_face_weights[10] = { 96 0.0552786404500042, 97 0.1447213595499958, 98 0.1447213595499958, 99 0.0552786404500042, 100 0.0552786404500042, 101 0.1447213595499958, 102 0.1447213595499958, 103 0.0552786404500042, 104 0.1447213595499958, 105 0.0552786404500042 106}; 107 108// Precomputed per-face barycentric transforms. For each fixed face 109// orientation, this is inverse(float2x2(proj5(a), proj5(b))). 110static const float2x2 aperiodic_face_matrices[10] = { 111 float2x2(1.5811388300841898, -2.1762508994828216, -0.0, 2.6899940478558295), 112 float2x2(1.5811388300841895, -0.5137431483730078, -0.0, 1.6625077511098139), 113 float2x2(1.5811388300841895, 0.5137431483730076, -0.0, 1.6625077511098136), 114 float2x2(1.5811388300841895, 2.176250899482821, -0.0, 2.6899940478558286), 115 float2x2(2.558336368008464, -0.8312538755549072, -1.58113883008419, 2.176250899482822), 116 float2x2(1.5811388300841895, 0.5137431483730076, -0.9771975379242739, 1.3449970239279145), 117 float2x2(0.977197537924274, 1.3449970239279145, -0.9771975379242739, 1.3449970239279145), 118 float2x2(2.558336368008464, 0.8312538755549067, -2.5583363680084634, 0.8312538755549069), 119 float2x2(0.9771975379242742, 1.3449970239279148, -1.5811388300841895, 0.5137431483730078), 120 float2x2(1.5811388300841898, 2.176250899482821, -2.5583363680084634, -0.8312538755549066) 121}; 122 123float dot5(float4 a03, float a44, float4 b03, float b44) { 124 return dot(a03, b03) + a44 * b44; 125} 126 127float2 proj5(float4 p03, float p44) { 128 return float2( 129 dot5(p03, p44, basis_u5_03, basis_u5_44), 130 dot5(p03, p44, basis_v5_03, basis_v5_44)); 131} 132 133float3 aperiodic_face_color(int face_id) { 134 if (face_id == 0) return _Aperiodic_Tiling_Color_0.rgb; 135 if (face_id == 1) return _Aperiodic_Tiling_Color_1.rgb; 136 if (face_id == 2) return _Aperiodic_Tiling_Color_2.rgb; 137 if (face_id == 3) return _Aperiodic_Tiling_Color_3.rgb; 138 if (face_id == 4) return _Aperiodic_Tiling_Color_4.rgb; 139 if (face_id == 5) return _Aperiodic_Tiling_Color_5.rgb; 140 if (face_id == 6) return _Aperiodic_Tiling_Color_6.rgb; 141 if (face_id == 7) return _Aperiodic_Tiling_Color_7.rgb; 142 if (face_id == 8) return _Aperiodic_Tiling_Color_8.rgb; 143 return _Aperiodic_Tiling_Color_9.rgb; 144} 145 146float2 aperiodic_tile_barycentric(float2 uv_m, float2x2 m, float2 s, float2 q, 147 float4 c03, float c44) { 148 float4 shift03 = c03 * round(q.x * basis_u5_03 + q.y * basis_v5_03); 149 float shift44 = c44 * round(q.x * basis_u5_44 + q.y * basis_v5_44); 150 return uv_m - mul(m, proj5(shift03, shift44)) - s; 151} 152 153float interval_box_coverage(float center, float half_extent, float radius) { 154 if (half_extent <= 1e-5) { 155 return abs(center) <= radius ? 1.0 : 0.0; 156 } 157 158 float lo = center - half_extent; 159 float hi = center + half_extent; 160 float overlap = max(min(hi, radius) - max(lo, -radius), 0.0); 161 return overlap / (2.0 * half_extent); 162} 163 164float square_box_coverage(float2 barycentric, float2 half_extents, float radius) { 165 return interval_box_coverage(barycentric.x, half_extents.x, radius) * 166 interval_box_coverage(barycentric.y, half_extents.y, radius); 167} 168 169#if defined(_APERIODIC_TILING_NORMALS) 170float3 aperiodic_tiling_normal(float2 barycentric) { 171 float bevel_width = min(_Aperiodic_Tiling_Normal_Thickness, 0.5); 172 if (bevel_width <= 1e-5) { 173 return float3(0.0, 0.0, 1.0); 174 } 175 176 float flat_limit = 0.5 - bevel_width; 177 float2 edge_factor = smoothstep(flat_limit, 0.5, abs(barycentric)); 178 float2 xy = sign(barycentric) * edge_factor * _Aperiodic_Tiling_Normal_Strength; 179 return normalize(float3(xy, 1.0)); 180} 181#endif // _APERIODIC_TILING_NORMALS 182 183struct AperiodicPointSample { 184 float distance_to_edge; 185 float2 barycentric; 186 int face_id; 187}; 188 189struct AperiodicFootprintRange { 190 float min_extent; 191 float max_extent; 192}; 193 194void aperiodic_accumulate_point_orientation(float2 uv, float4 p03, float p44, int face_id, 195 inout AperiodicPointSample best) { 196 float2x2 m = aperiodic_face_matrices[face_id]; 197 float4 a03 = aperiodic_face_a03[face_id]; 198 float a44 = aperiodic_face_a44[face_id]; 199 float4 b03 = aperiodic_face_b03[face_id]; 200 float b44 = aperiodic_face_b44[face_id]; 201 float4 c03 = aperiodic_face_c03[face_id]; 202 float c44 = aperiodic_face_c44[face_id]; 203 204 float2 r = round(float2(dot5(p03, p44, a03, a44), dot5(p03, p44, b03, b44))); 205 float2 uv_m = mul(m, uv); 206 207 [unroll] 208 for (int candidate_id = 0; candidate_id < 4; ++candidate_id) { 209 float2 s = r + aperiodic_tile_offsets[candidate_id]; 210 float2 q = mul(s, m); 211 float2 barycentric = aperiodic_tile_barycentric(uv_m, m, s, q, c03, c44); 212 float distance_to_edge = 0.5 - max(abs(barycentric.x), abs(barycentric.y)); 213 bool better = distance_to_edge > best.distance_to_edge; 214 best.distance_to_edge = better ? distance_to_edge : best.distance_to_edge; 215 best.barycentric = better ? barycentric : best.barycentric; 216 best.face_id = better ? face_id : best.face_id; 217 } 218} 219 220void sample_aperiodic_tiling_point(float2 uv, float4 p03, float p44, out float3 albedo, 221 out float3 tiling_normal_tangent) { 222 AperiodicPointSample best; 223 best.distance_to_edge = -1e10; 224 best.barycentric = 0.0; 225 best.face_id = 0; 226 227 [unroll] 228 for (int face_id = 0; face_id < 10; ++face_id) { 229 aperiodic_accumulate_point_orientation(uv, p03, p44, face_id, best); 230 } 231 232 float edge_width = min(_Aperiodic_Tiling_Edge_Thickness, 0.5); 233 float edge_sd = best.distance_to_edge - edge_width; 234 float edge_sd_aa = max(abs(fwidth(edge_sd)), 1e-4); 235 float edge_mask = smoothstep(-edge_sd_aa * 0.5, edge_sd_aa * 0.5, edge_sd); 236 albedo = lerp(_Aperiodic_Tiling_Edge_Color.rgb, aperiodic_face_color(best.face_id), edge_mask); 237 238#if defined(_APERIODIC_TILING_NORMALS) 239 float3 tile_normal = aperiodic_tiling_normal(best.barycentric); 240 tiling_normal_tangent = normalize(float3(tile_normal.xy * edge_mask, 1.0)); 241#else 242 tiling_normal_tangent = 0.0; 243#endif 244} 245 246AperiodicFootprintRange aperiodic_footprint_range(float2 uv_ddx, float2 uv_ddy) { 247 AperiodicFootprintRange range; 248 range.min_extent = 1e10; 249 range.max_extent = 0.0; 250 251 [unroll] 252 for (int face_id = 0; face_id < 10; ++face_id) { 253 float2x2 m = aperiodic_face_matrices[face_id]; 254 float2 dbdx = mul(m, uv_ddx); 255 float2 dbdy = mul(m, uv_ddy); 256 float face_extent = max(abs(dbdx.x) + abs(dbdy.x), abs(dbdx.y) + abs(dbdy.y)) * 0.5; 257 range.min_extent = min(range.min_extent, face_extent); 258 range.max_extent = max(range.max_extent, face_extent); 259 } 260 261 return range; 262} 263 264void aperiodic_accumulate_filtered_orientation(float2 uv, float4 p03, float p44, 265 float2 uv_ddx, float2 uv_ddy, int face_id, 266 float inner_radius, inout float3 face_color_sum, 267 inout float face_sum) { 268 float2x2 m = aperiodic_face_matrices[face_id]; 269 float4 a03 = aperiodic_face_a03[face_id]; 270 float a44 = aperiodic_face_a44[face_id]; 271 float4 b03 = aperiodic_face_b03[face_id]; 272 float b44 = aperiodic_face_b44[face_id]; 273 float4 c03 = aperiodic_face_c03[face_id]; 274 float c44 = aperiodic_face_c44[face_id]; 275 276 float2 r = round(float2(dot5(p03, p44, a03, a44), dot5(p03, p44, b03, b44))); 277 float2 uv_m = mul(m, uv); 278 float2 dbdx = mul(m, uv_ddx); 279 float2 dbdy = mul(m, uv_ddy); 280 float2 half_extents = 0.5 * (abs(dbdx) + abs(dbdy)); 281 float2 inner_limit = half_extents + inner_radius; 282 float3 face_color = aperiodic_face_color(face_id); 283 284 [unroll] 285 for (int candidate_id = 0; candidate_id < 4; ++candidate_id) { 286 float2 s = r + aperiodic_tile_offsets[candidate_id]; 287 float2 q = mul(s, m); 288 float2 barycentric = aperiodic_tile_barycentric(uv_m, m, s, q, c03, c44); 289 290 if (abs(barycentric.x) >= inner_limit.x || abs(barycentric.y) >= inner_limit.y) { 291 continue; 292 } 293 294 float inner = square_box_coverage(barycentric, half_extents, inner_radius); 295 face_color_sum += face_color * inner; 296 face_sum += inner; 297 } 298} 299 300void sample_aperiodic_tiling_filtered(float2 uv, float4 p03, float p44, float2 uv_ddx, 301 float2 uv_ddy, out float3 albedo, 302 out float3 tiling_normal_tangent) { 303 float inner_radius = max(0.5 - min(_Aperiodic_Tiling_Edge_Thickness, 0.5), 0.0); 304 float3 face_color_sum = 0.0; 305 float face_sum = 0.0; 306 307 [unroll] 308 for (int face_id = 0; face_id < 10; ++face_id) { 309 aperiodic_accumulate_filtered_orientation( 310 uv, p03, p44, uv_ddx, uv_ddy, face_id, inner_radius, face_color_sum, face_sum); 311 } 312 313 float edge_weight = saturate(1.0 - face_sum); 314 albedo = face_color_sum + _Aperiodic_Tiling_Edge_Color.rgb * edge_weight; 315 316#if defined(_APERIODIC_TILING_NORMALS) 317 // In the filtered regime the bevel is subpixel detail, so keep the normal flat. 318 tiling_normal_tangent = float3(0.0, 0.0, 1.0); 319#else 320 tiling_normal_tangent = 0.0; 321#endif 322} 323 324float3 sample_aperiodic_tiling_far_albedo() { 325 float3 face_mean = 0.0; 326 327 [unroll] 328 for (int face_id = 0; face_id < 10; ++face_id) { 329 face_mean += aperiodic_face_color(face_id) * aperiodic_face_weights[face_id]; 330 } 331 332 float inner_width = saturate(1.0 - 2.0 * min(_Aperiodic_Tiling_Edge_Thickness, 0.5)); 333 float inner_fraction = inner_width * inner_width; 334 return lerp(_Aperiodic_Tiling_Edge_Color.rgb, face_mean, inner_fraction); 335} 336 337float aperiodic_minified_normal_kernel_roughness(float minified_weight) { 338#if defined(_APERIODIC_TILING_NORMALS) 339 float bevel_width = min(_Aperiodic_Tiling_Normal_Thickness, 0.5); 340 if (bevel_width <= 1e-5 || minified_weight <= 1e-5) { 341 return 0.0; 342 } 343 344 float flat_width = saturate(1.0 - 2.0 * bevel_width); 345 float bevel_fraction = 1.0 - flat_width * flat_width; 346 float slope_variance = 347 _Aperiodic_Tiling_Normal_Strength * _Aperiodic_Tiling_Normal_Strength * 348 bevel_fraction * APERIODIC_SMOOTHSTEP_SQ_MEAN; 349 return minified_weight * min(2.0 * slope_variance, _Specular_AA_Threshold); 350#else 351 return 0.0; 352#endif 353} 354 355void apply_aperiodic_smoothness(inout float smoothness, float minified_weight) { 356 float kernel_roughness = aperiodic_minified_normal_kernel_roughness(minified_weight); 357 if (kernel_roughness <= 1e-5) { 358 return; 359 } 360 361 float perceptual_roughness = 1.0 - smoothness; 362 float roughness = perceptual_roughness * perceptual_roughness; 363 float square_roughness = saturate(roughness * roughness + kernel_roughness); 364 smoothness = 1.0 - saturate(sqrt(sqrt(square_roughness))); 365} 366 367void sample_aperiodic_tiling_minified(float2 uv, float4 p03, float p44, float2 uv_ddx, 368 float2 uv_ddy, float far_weight, out float3 albedo, 369 out float3 tiling_normal_tangent) { 370 float3 filtered_albedo; 371 float3 filtered_normal_tangent; 372 float3 far_albedo = sample_aperiodic_tiling_far_albedo(); 373 374 sample_aperiodic_tiling_filtered( 375 uv, p03, p44, uv_ddx, uv_ddy, filtered_albedo, filtered_normal_tangent); 376 albedo = lerp(filtered_albedo, far_albedo, far_weight); 377 378#if defined(_APERIODIC_TILING_NORMALS) 379 tiling_normal_tangent = normalize(lerp( 380 filtered_normal_tangent, float3(0.0, 0.0, 1.0), far_weight)); 381#else 382 tiling_normal_tangent = 0.0; 383#endif 384} 385 386void sample_aperiodic_tiling(float2 uv, out float3 albedo, out float3 tiling_normal_tangent, 387 out float minified_weight) { 388 float4 p03 = uv.x * basis_u5_03 + uv.y * basis_v5_03; 389 float p44 = uv.x * basis_u5_44 + uv.y * basis_v5_44; 390 float2 uv_ddx = ddx(uv); 391 float2 uv_ddy = ddy(uv); 392 AperiodicFootprintRange footprint = aperiodic_footprint_range(uv_ddx, uv_ddy); 393 float filter_end = APERIODIC_FILTER_THRESHOLD + APERIODIC_FILTER_BLEND_WIDTH; 394 float far_weight = smoothstep( 395 APERIODIC_FILTER_THRESHOLD, APERIODIC_FAR_THRESHOLD, footprint.min_extent); 396 397 [branch] 398 if (footprint.max_extent >= filter_end) { 399 sample_aperiodic_tiling_minified( 400 uv, p03, p44, uv_ddx, uv_ddy, far_weight, albedo, tiling_normal_tangent); 401 minified_weight = 1.0; 402 } else { 403 float3 point_albedo; 404 float3 point_normal_tangent; 405 float3 minified_albedo; 406 float3 minified_normal_tangent; 407 float filter_weight = smoothstep(0.0, filter_end, footprint.max_extent); 408 409 sample_aperiodic_tiling_point(uv, p03, p44, point_albedo, point_normal_tangent); 410 sample_aperiodic_tiling_minified( 411 uv, p03, p44, uv_ddx, uv_ddy, far_weight, minified_albedo, minified_normal_tangent); 412 albedo = lerp(point_albedo, minified_albedo, filter_weight); 413 tiling_normal_tangent = normalize(lerp( 414 point_normal_tangent, minified_normal_tangent, filter_weight)); 415 minified_weight = filter_weight; 416 } 417} 418#endif // defined(_APERIODIC_TILING) 419 420void apply_aperiodic_tiling(float2 uv, inout float3 albedo, inout float smoothness, 421 inout float3 normal_tangent) { 422#if defined(_APERIODIC_TILING) 423 uv *= _Aperiodic_Tiling_Scale; 424 float3 tiling_normal_tangent; 425 float minified_weight; 426 sample_aperiodic_tiling(uv, albedo, tiling_normal_tangent, minified_weight); 427#if defined(_APERIODIC_TILING_NORMALS) 428 normal_tangent = tiling_normal_tangent; 429 apply_aperiodic_smoothness(smoothness, minified_weight); 430#endif 431#endif // _APERIODIC_TILING 432} 433 434#endif // __APERIODIC_TILING_INC