yum-archive/2ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum-archive/2ner
2a5186a
master
1#!/usr/bin/env python3 2 3import argparse 4import subprocess 5import os 6import json 7from PIL import Image, ImageDraw 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()