yum-archive/Tooner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum-archive/Tooner
9b52e08
master
1import numpy as np 2from PIL import Image ,ImageDraw 3import argparse 4import math 5 6def create_circle_image (res ,radius ,center = None ): 7"""Create a binary image with a white circle on black background.""" 8if center is None : 9center = (res / 2 ,res / 2 ) 10 11img = Image .new ('L' , (res ,res ),0 ) 12draw = ImageDraw .Draw (img ) 13 14# Convert radius and center to pixels 15r_px = int (radius * res ) 16cx ,cy = int (center [0 ]),int (center [1 ]) 17 18# Draw white circle (255) 19draw .ellipse ([cx - r_px ,cy - r_px ,cx + r_px ,cy + r_px ],fill = 255 ) 20 21# Handle edge case 1: circle is on left edge 22if cx == 0 : 23cx2 = res 24draw .ellipse ([cx2 - r_px ,cy - r_px ,cx2 + r_px ,cy + r_px ],fill = 255 ) 25# Handle edge case 2: circle is on top edge 26if cy == 0 : 27cy2 = res 28draw .ellipse ([cx - r_px ,cy2 - r_px ,cx + r_px ,cy2 + r_px ],fill = 255 ) 29# Handle edge case 3: circle is on top left corner 30if cx == 0 and cy == 0 : 31cx2 = res 32cy2 = res 33draw .ellipse ([cx2 - r_px ,cy2 - r_px ,cx2 + r_px ,cy2 + r_px ],fill = 255 ) 34return img 35 36def get_bayer_location (nth ,res ): 37""" 38Returns the normalized location (x, y) for the nth dot in a recursive Bayer dithering pattern. 3940 The pattern is constructed by writing nth in base-4 and mapping each digit as follows: 410 -> (0, 0) 421 -> (1, 1) 432 -> (1, 0) 443 -> (0, 1) 4546 Each digit is weighted by successive powers of 1/2 so that: 47x = sum_{i=0}^{k-1} (digit_x(i)) / 2^(i+1) 48y = sum_{i=0}^{k-1} (digit_y(i)) / 2^(i+1) 4950 The parameter `res` is expected to be a power-of-two (and in practice an even number); we let k = (res+1)//2. 51For example: 52- For res == 2, k = 1, and the function returns one of: (0,0), (0.5,0.5), (0.5,0), or (0,0.5). 53- For res == 4, k = 2, and the function returns positions on a 4×4 grid. 54- For res == 8, k = 4, and the function returns positions on a 16×16 grid. 5556 If nth is greater than or equal to 4^k then an error is raised. 57""" 58k = (res + 1 )// 2 # determine the number of base-4 digits to use 59if nth >= 4 ** k : 60raise ValueError (f"nth value { nth } too large for given res { res } (max is { 4 ** k - 1 } )." ) 61 62x ,y = 0.0 ,0.0 63# Process each base-4 digit (least significant first) 64for i in range (k ): 65digit = nth % 4 66nth //= 4 67weight = 1 / (2 ** (i + 1 )) 68if digit == 0 : 69dx ,dy = 0 ,0 70elif digit == 1 : 71dx ,dy = 1 ,1 72elif digit == 2 : 73dx ,dy = 1 ,0 74elif digit == 3 : 75dx ,dy = 0 ,1 76else : 77raise ValueError ("Unexpected digit encountered while converting number to base-4" ) 78x += dx * weight 79y += dy * weight 80return (x ,y ) 81 82 83def main (): 84parser = argparse .ArgumentParser (description = "Generate dot and circle images." ) 85parser .add_argument ("--res" ,type = int ,default = 128 ,help = "Resolution of the images." ) 86args = parser .parse_args () 87 88res = args .res 89initial_area = 0.1 90 91bayer_res = 16 92total_layers = bayer_res * bayer_res 93# Calculate number of digits needed for zero padding 94num_digits = len (str (total_layers )) 95 96for layer in range (1 ,total_layers + 1 ): 97# Calculate radius for this layer (split area among n circles) 98radius = math .sqrt (initial_area / layer )* res 99 100# Create the combined image for this layer 101layer_img = Image .new ('L' , (res ,res ),0 ) 102 103# Place n circles according to Bayer pattern 104for n in range (layer ): 105# Get normalized coordinates from Bayer pattern 106x ,y = get_bayer_location (n ,bayer_res ) 107# Convert to pixel coordinates 108center = (x * res ,y * res ) 109# Create and add circle 110circle = create_circle_image (res ,radius / res ,center = center ) 111 112layer_img = Image .fromarray (np .maximum (np .array (layer_img ),np .array (circle ))) 113 114# Save the layer with zero-padded number 115layer_img .save (f"dots_L { layer :0{ num_digits }d } .png" ) 116 117if __name__ == "__main__" : 118main ()