yum-archive/Tooner

A toon shader for Unity's BIRP.

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

yumAdd second mask for rim lighting & matcap2356cdf

master
11.6 KiB361 linesraw
1#!/usr/bin/env python3
2
3import argparse
4import subprocess
5import os
6import json
7from PIL import Image
8import random
9
10# Define the character ranges we want to include
11CHAR_RANGES = [
12    (32, 126),  # Printable ASCII
13    # Add more ranges here as needed, e.g.:
14    #(160, 255),  # Extended Latin
15]
16
17def calculate_grid_size(char_ranges):
18    """Calculate the smallest square grid that fits the highest character code"""
19    max_char = max(end for _, end in char_ranges)
20    grid_size = 1
21    while grid_size * grid_size < max_char:
22        grid_size += 1
23    return grid_size
24
25ATLAS_TYPES = [
26    "hardmask",  # binary image
27    "softmask",  # anti-aliased image
28    "sdf",       # signed distance field
29    "psdf",      # perpendicular distance field
30    "msdf",      # multi-channel signed distance field
31    "mtsdf"      # combined MSDF and true SDF
32]
33
34def calculate_font_size(resolution, base_size=None):
35    """Calculate the font size based on resolution, scaling from 512 resolution"""
36    if base_size is None:
37        base_size = 32  # Default size at 512 resolution
38    return (base_size * resolution) // 512
39
40def generate_atlas(font_path, resolution, draw_grid=False, type="msdf", font_size=None):
41    """Generates a font atlas using various distance field techniques.
42
43    This function creates a font atlas using msdf-atlas-gen, then rearranges the
44    characters to include gaps for non-printable characters. The output is saved
45    as 'atlas.png' in the current directory.
46
47    Args:
48        font_path (str): Path to the input font file (.ttf/.otf)
49        resolution (int): Width and height of the output atlas in pixels
50        draw_grid (bool, optional): If True, draws red grid lines on the output. 
51            Defaults to False.
52        type (str, optional): Atlas type to generate. See ATLAS_TYPES.
53        font_size (int, optional): Base font size in pixels at 512 resolution. 
54            Will be scaled for other resolutions.
55
56    Returns:
57        bool: True if atlas generation succeeded, False if an error occurred.
58
59    Raises:
60        subprocess.CalledProcessError: If msdf-atlas-gen fails to execute.
61    """
62    # Get font name from path
63    font_name = os.path.splitext(os.path.basename(font_path))[0]
64    
65    # Calculate grid size based on character ranges
66    grid_size = calculate_grid_size(CHAR_RANGES)
67    cell_size = resolution // grid_size
68    
69    # Calculate font size if not specified, scaling from 512 resolution
70    if font_size is None:
71        font_size = calculate_font_size(resolution)
72    else:
73        font_size = calculate_font_size(resolution, font_size)
74    
75    # Convert character ranges to command-line format
76    chars_str = ", ".join(f"[{start}, {end}]" for start, end in CHAR_RANGES)
77    
78    # Update the output filename to include resolution
79    output_filename = f"atlas_{font_name}_{resolution}_{type}"
80    
81    cmd = [
82        "msdf-atlas-gen/build/bin/Debug/msdf-atlas-gen.exe",
83        "-font", font_path,
84        "-type", type,
85        "-format", "png",
86        "-imageout", f"{output_filename}.png",
87        "-size", str(font_size),
88        "-pxrange", "4",
89        "-dimensions", str(resolution), str(resolution),
90        "-chars", chars_str,
91        "-uniformgrid",
92        "-uniformcols", str(grid_size),
93        "-uniformcell", str(cell_size), str(cell_size),
94        "-errorcorrection", "auto-full",
95        "-scanline",
96        #"-angle", "15d",
97        "-edgecoloring", "distance"
98    ]
99
100    try:
101        print("Running msdf-atlas-gen...")
102        print("Command:", end=" ")
103        for arg in cmd:
104            if arg.startswith('-'):
105                print(f"\n    {arg}", end=" ")
106            else:
107                print(arg, end=" ")
108        print()
109        result = subprocess.run(cmd, check=True, capture_output=True, text=True)
110        
111        # Print the output
112        if result.stdout:
113            print("msdf-atlas-gen output:")
114            print(result.stdout)
115        
116        # Rearrange the atlas to include gaps for non-printable characters
117        print("Rearranging atlas...")
118        rearrange_atlas(resolution, cell_size, cell_size, draw_grid, type, font_name)
119        
120        # Generate or update Unity meta file
121        generate_unity_meta(output_filename, resolution)
122        return True
123    except subprocess.CalledProcessError as e:
124        print(f"Error generating atlas: {e}")
125        print(f"Error output: {e.stderr}")
126        return False
127
128def draw_grid_lines(image, resolution):
129    """Draw red grid lines on the image"""
130    draw = ImageDraw.Draw(image)
131    grid_size = calculate_grid_size(CHAR_RANGES)
132    
133    # Draw vertical lines
134    for x in range(grid_size):
135        line_x = x * resolution // grid_size
136        draw.line([(line_x, 0), (line_x, resolution-1)], fill=(255, 0, 0), width=1)
137    
138    # Draw horizontal lines
139    for y in range(grid_size):
140        line_y = y * resolution // grid_size
141        draw.line([(0, line_y), (resolution-1, line_y)], fill=(255, 0, 0), width=1)
142    
143    # Draw the final borders
144    draw.line([(resolution-1, 0), (resolution-1, resolution-1)], fill=(255, 0, 0), width=1)
145    draw.line([(0, resolution-1), (resolution-1, resolution-1)], fill=(255, 0, 0), width=1)
146
147def rearrange_atlas(resolution, cell_width, cell_height, draw_grid=False, type="msdf", font_name=""):
148    """Rearrange the atlas to include gaps for non-printable characters"""
149    # Update input and output filenames to match generate_atlas format
150    input_filename = f"atlas_{font_name}_{resolution}_{type}.png"
151    original = Image.open(input_filename)
152    new_atlas = Image.new('RGBA', (resolution, resolution), (0, 0, 0, 255))
153    
154    grid_size = calculate_grid_size(CHAR_RANGES)
155    cell_size = resolution // grid_size
156    
157    # Track current position in the source atlas 
158    source_index = 0
159    
160    # Process each character range
161    for start, end in CHAR_RANGES:
162        for ascii_code in range(start, end + 1):
163            # Calculate source position (original atlas)
164            source_x = (source_index % grid_size) * cell_size
165            source_y = (source_index // grid_size) * cell_size
166            
167            # Calculate target position (new atlas)
168            target_x = ((ascii_code + 1) % grid_size) * cell_size
169            target_y = ((ascii_code + 1) // grid_size) * cell_size
170            
171            # Extract and paste the glyph
172            glyph = original.crop((
173                source_x,
174                source_y,
175                source_x + cell_size,
176                source_y + cell_size
177            ))
178            new_atlas.paste(glyph, (target_x, target_y))
179            source_index += 1
180    
181    # Draw the grid lines only if requested
182    if draw_grid:
183        draw_grid_lines(new_atlas, resolution)
184        
185    # Calculate actual used dimensions
186    used_resolution = cell_size * grid_size
187    # Crop to used dimensions and resize back to requested resolution
188    used_atlas = new_atlas.crop((0, 0, used_resolution, used_resolution))
189    final_atlas = used_atlas.resize((resolution, resolution), Image.LANCZOS)
190    
191    # Save with the same filename format (no change needed since input/output are the same)
192    final_atlas.save(input_filename)
193    print("Atlas rearranged successfully!")
194
195def generate_unity_meta(basename, resolution):
196    """Generate or update Unity meta file for the atlas texture."""
197    meta_path = f"{basename}.png.meta"
198    existing_guid = None
199    
200    # Try to read existing GUID if meta file exists
201    if os.path.exists(meta_path):
202        with open(meta_path, 'r') as f:
203            for line in f:
204                if 'guid: ' in line:
205                    existing_guid = line.split('guid: ')[1].strip()
206                    break
207    
208    # Generate new GUID if none exists
209    guid = existing_guid or ''.join('%x' % random.randrange(16) for _ in range(32))
210    
211    meta_template = f'''fileFormatVersion: 2
212guid: {guid}
213TextureImporter:
214  internalIDToNameTable: []
215  externalObjects: {{}}
216  serializedVersion: 12
217  mipmaps:
218    mipMapMode: 0
219    enableMipMap: 1
220    sRGBTexture: 1
221    linearTexture: 0
222    fadeOut: 0
223    borderMipMap: 0
224    mipMapsPreserveCoverage: 0
225    alphaTestReferenceValue: 0.5
226    mipMapFadeDistanceStart: 1
227    mipMapFadeDistanceEnd: 3
228  bumpmap:
229    convertToNormalMap: 0
230    externalNormalMap: 0
231    heightScale: 0.25
232    normalMapFilter: 0
233    flipGreenChannel: 0
234  isReadable: 0
235  streamingMipmaps: 0
236  streamingMipmapsPriority: 0
237  vTOnly: 0
238  ignoreMipmapLimit: 0
239  grayScaleToAlpha: 0
240  generateCubemap: 6
241  cubemapConvolution: 0
242  seamlessCubemap: 0
243  textureFormat: 1
244  maxTextureSize: {resolution}
245  textureSettings:
246    serializedVersion: 2
247    filterMode: 1
248    aniso: 1
249    mipBias: 0
250    wrapU: 0
251    wrapV: 0
252    wrapW: 0
253  nPOTScale: 1
254  lightmap: 0
255  compressionQuality: 50
256  spriteMode: 0
257  spriteExtrude: 1
258  spriteMeshType: 1
259  alignment: 0
260  spritePivot: {{x: 0.5, y: 0.5}}
261  spritePixelsToUnits: 100
262  spriteBorder: {{x: 0, y: 0, z: 0, w: 0}}
263  spriteGenerateFallbackPhysicsShape: 1
264  alphaUsage: 1
265  alphaIsTransparency: 0
266  spriteTessellationDetail: -1
267  textureType: 0
268  textureShape: 1
269  singleChannelComponent: 0
270  flipbookRows: 1
271  flipbookColumns: 1
272  maxTextureSizeSet: 0
273  compressionQualitySet: 0
274  textureFormatSet: 0
275  ignorePngGamma: 0
276  applyGammaDecoding: 0
277  swizzle: 50462976
278  cookieLightType: 0
279  platformSettings:
280  - serializedVersion: 3
281    buildTarget: DefaultTexturePlatform
282    maxTextureSize: {resolution}
283    resizeAlgorithm: 0
284    textureFormat: -1
285    textureCompression: 2
286    compressionQuality: 50
287    crunchedCompression: 0
288    allowsAlphaSplitting: 0
289    overridden: 0
290    ignorePlatformSupport: 0
291    androidETC2FallbackOverride: 0
292    forceMaximumCompressionQuality_BC6H_BC7: 0
293  - serializedVersion: 3
294    buildTarget: Standalone
295    maxTextureSize: {resolution}
296    resizeAlgorithm: 0
297    textureFormat: 3
298    textureCompression: 1
299    compressionQuality: 50
300    crunchedCompression: 0
301    allowsAlphaSplitting: 0
302    overridden: 1
303    ignorePlatformSupport: 0
304    androidETC2FallbackOverride: 0
305    forceMaximumCompressionQuality_BC6H_BC7: 0
306  - serializedVersion: 3
307    buildTarget: Android
308    maxTextureSize: {resolution}
309    resizeAlgorithm: 0
310    textureFormat: -1
311    textureCompression: 1
312    compressionQuality: 50
313    crunchedCompression: 0
314    allowsAlphaSplitting: 0
315    overridden: 0
316    ignorePlatformSupport: 0
317    androidETC2FallbackOverride: 0
318    forceMaximumCompressionQuality_BC6H_BC7: 0
319  spriteSheet:
320    serializedVersion: 2
321    sprites: []
322    outline: []
323    physicsShape: []
324    bones: []
325    spriteID: 
326    internalID: 0
327    vertices: []
328    indices: 
329    edges: []
330    weights: []
331    secondaryTextures: []
332    nameFileIdTable: {{}}
333  mipmapLimitGroupName: 
334  pSDRemoveMatte: 0
335  userData: 
336  assetBundleName: 
337  assetBundleVariant: 
338'''
339    
340    with open(meta_path, 'w') as f:
341        f.write(meta_template)
342
343def main():
344    parser = argparse.ArgumentParser(description='Generate a font atlas using msdf-atlas-gen')
345    parser.add_argument('font_path', help='Path to the font file (.ttf/.otf)')
346    parser.add_argument('resolution', type=int, help='Total atlas resolution (width=height)')
347    parser.add_argument('--grid', type=bool, default=False, help='Draw grid lines on the output atlas')
348    parser.add_argument('--type', type=str, default="msdf", choices=ATLAS_TYPES, 
349                       help='Type of atlas to generate')
350    parser.add_argument('--font-size', type=int, help='Base font size in pixels at 512 resolution. Will be scaled for other resolutions.')
351    args = parser.parse_args()
352
353    # Verify font file exists
354    if not os.path.isfile(args.font_path):
355        print(f"Error: Font file not found at {args.font_path}")
356        return
357
358    generate_atlas(args.font_path, args.resolution, draw_grid=args.grid, type=args.type, font_size=args.font_size)
359
360if __name__ == "__main__":
361    main()