yum-archive/Tooner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum-archive/Tooner

yumAdd 4x4 textures to ssfd9b52e08

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