def main(args): try: src_dir = Path(os.environ['MESON_SOURCE_ROOT']) build_dir = Path(os.environ['MESON_BUILD_ROOT']) except KeyError: print("This script should not be ran directly", file=sys.stderr) exit(1) with ThreadPoolExecutor() as ex: futures = [] for target in args[1:]: futures.append(ex.submit(lambda: ninja('-vC', build_dir, target))) wait_for_futures(futures)
def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_formats[0], border=1, force_single=False, crop=True, leanify=True): overrides = Path(overrides).resolve() src = Path(src).resolve() dst = Path(dst).resolve() sprite_configs = {} def get_border(sprite, default_border=border): return max(default_border, int(sprite_configs[sprite].get('border', default_border))) try: texture_local_overrides = (src / 'atlas.tex').read_text() except FileNotFoundError: texture_local_overrides = None try: texture_global_overrides = (overrides / 'atlas.tex').read_text() except FileNotFoundError: texture_global_overrides = None total_images = 0 packed_images = 0 rects = [] def imgfilter(path): return (path.is_file() and path.suffix[1:].lower() in texture_formats and path.with_suffix('').suffix != '.alphamap') for path in sorted(filter(imgfilter, src.glob('**/*.*'))): img = Image.open(path) sprite_name = path.relative_to(src).with_suffix('').as_posix() sprite_config_path = overrides / (get_override_file_name(sprite_name) + '.conf') sprite_configs[sprite_name] = parse_sprite_conf(sprite_config_path) border = get_border(sprite_name) rects.append((img.size[0] + border * 2, img.size[1] + border * 2, (path, sprite_name))) img.close() pack_result = pack_rects_brute_force(rects=rects, bin_size=binsize, single_bin=force_single) if not pack_result.success: missing = len(rects) - pack_result.num_images_packed raise TaiseiError( f'{missing} sprite{"s were" if missing > 1 else " was"} not packed (bin size is too small?)' ) futures = [] with ExitStack() as stack: # Do everything in a temporary directory first temp_dst = Path( stack.enter_context( TemporaryDirectory(prefix=f'taisei-atlas-{atlasname}'))) # Run multiple leanify processes in parallel, in case we end up with multiple pages # Yeah I'm too lazy to use Popen properly executor = stack.enter_context(ThreadPoolExecutor()) for i, bin in enumerate(pack_result.packer): textureid = f'atlas_{atlasname}_{i}' # dstfile = temp_dst / f'{textureid}.{tex_format}' # NOTE: we always save PNG first and convert with an external tool later if needed. dstfile = temp_dst / f'{textureid}.png' dstfile_alphamap = temp_dst / f'{textureid}.alphamap.png' print(dstfile) dstfile_meta = temp_dst / f'{textureid}.tex' actual_size = [0, 0] if crop: for rect in bin: if rect.x + rect.width > actual_size[0]: actual_size[0] = rect.x + rect.width if rect.y + rect.height > actual_size[1]: actual_size[1] = rect.y + rect.height else: actual_size = (bin.width, bin.height) base_composite_cmd = [ 'convert', '-verbose', '-size', f'{actual_size[0]}x{actual_size[1]}', ] composite_cmd = base_composite_cmd.copy() + ['xc:none'] alphamap_composite_cmd = None for rect in bin: img_path, name = rect.rid border = get_border(name) alphamap_path = find_alphamap(img_path) composite_cmd += [ str(img_path), '-geometry', '{:+}{:+}'.format(rect.x + border, rect.y + border), '-composite' ] if alphamap_path: if alphamap_composite_cmd is None: alphamap_composite_cmd = base_composite_cmd.copy() + [ 'xc:white', '-colorspace', 'Gray', ] alphamap_composite_cmd += [ str(alphamap_path), '-geometry', '{:+}{:+}'.format(rect.x + border, rect.y + border), '-channel', 'R', '-separate', '-composite', ] override_path = overrides / get_override_file_name(name) if override_path.exists(): override_contents = override_path.read_text() else: override_contents = None write_override_template( override_path, (rect.width - border * 2, rect.height - border * 2)) write_sprite_def( temp_dst / f'{name}.spr', textureid, (rect.x + border, rect.y + border, rect.width - border * 2, rect.height - border * 2), actual_size, overrides=override_contents) composite_cmd += [str(dstfile)] write_texture_def(dstfile_meta, textureid, tex_format, texture_global_overrides, texture_local_overrides, alphamap_tex_fmt=None if alphamap_composite_cmd is None else 'png') def process(composite_cmd, dstfile, tex_format=tex_format): subprocess.check_call(composite_cmd) oldfmt = dstfile.suffix[1:].lower() if oldfmt != tex_format: new_dstfile = dstfile.with_suffix(f'.{tex_format}') if tex_format == 'webp': subprocess.check_call([ 'cwebp', '-progress', '-preset', 'icon', '-z', '9', '-lossless', '-q', '100', '-m', '6', str(dstfile), '-o', str(new_dstfile), ]) else: raise TaiseiError( f'Unhandled conversion {oldfmt} -> {tex_format}') dstfile.unlink() dstfile = new_dstfile if leanify: subprocess.check_call(['leanify', '-v', str(dstfile)]) futures.append( executor.submit(lambda: process(composite_cmd, dstfile))) if alphamap_composite_cmd is not None: alphamap_composite_cmd += [str(dstfile_alphamap)] futures.append( executor.submit(lambda: process(alphamap_composite_cmd, dstfile_alphamap, tex_format='png'))) # Wait for subprocesses to complete. wait_for_futures(futures) executor.shutdown(wait=True) # Only now, if everything is ok so far, copy everything to the destination, possibly overwriting previous results pattern = re.compile( rf'^atlas_{re.escape(atlasname)}_\d+(?:\.alphamap)?.({"|".join(texture_formats + ["tex"])})$' ) for path in dst.iterdir(): if pattern.match(path.name): path.unlink() targets = list(temp_dst.glob('**/*')) for dir in (p.relative_to(temp_dst) for p in targets if p.is_dir()): (dst / dir).mkdir(parents=True, exist_ok=True) for file in (p.relative_to(temp_dst) for p in targets if not p.is_dir()): shutil.copyfile(str(temp_dst / file), str(dst / file))
def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_formats[0], border=1, force_single=False, crop=True, leanify=True): overrides = Path(overrides).resolve() src = Path(src).resolve() dst = Path(dst).resolve() sprite_configs = {} def get_border(sprite, default_border=border): return max(default_border, int(sprite_configs[sprite].get('border', default_border))) try: texture_local_overrides = (src / 'atlas.tex').read_text() except FileNotFoundError: texture_local_overrides = None try: texture_global_overrides = (overrides / 'atlas.tex').read_text() except FileNotFoundError: texture_global_overrides = None total_images = 0 packed_images = 0 rects = [] for path in src.glob('**/*.*'): if path.is_file() and path.suffix[1:].lower() in texture_formats: img = Image.open(path) sprite_name = path.relative_to(src).with_suffix('').as_posix() sprite_config_path = overrides / ( get_override_file_name(sprite_name) + '.conf') sprite_configs[sprite_name] = parse_sprite_conf(sprite_config_path) border = get_border(sprite_name) rects.append((img.size[0] + border * 2, img.size[1] + border * 2, (path, sprite_name))) img.close() total_images = len(rects) make_packer = lambda: rectpack.newPacker( # No rotation support in Taisei yet rotation=False, # Fine-tuned for least area used after crop sort_algo=rectpack.SORT_SSIDE, bin_algo=rectpack.PackingBin.BFF, pack_algo=rectpack.MaxRectsBl, ) binsize = list(binsize) if force_single: while True: packer = make_packer() packer.add_bin(*binsize) for rect in rects: packer.add_rect(*rect) packer.pack() if sum(len(bin) for bin in packer) == total_images: break if binsize[1] < binsize[0]: binsize[1] *= 2 else: binsize[0] *= 2 else: packer = make_packer() for rect in rects: packer.add_rect(*rect) packer.add_bin(*binsize) packer.pack() packed_images = sum(len(bin) for bin in packer) if total_images != packed_images: missing = total_images - packed_images raise TaiseiError( f'{missing} sprite{"s were" if missing > 1 else " was"} not packed (bin size is too small?)' ) futures = [] with ExitStack() as stack: # Do everything in a temporary directory first temp_dst = Path( stack.enter_context( TemporaryDirectory(prefix=f'taisei-atlas-{atlasname}'))) # Run multiple leanify processes in parallel, in case we end up with multiple pages # Yeah I'm too lazy to use Popen properly executor = stack.enter_context(ThreadPoolExecutor()) for i, bin in enumerate(packer): textureid = f'atlas_{atlasname}_{i}' # dstfile = temp_dst / f'{textureid}.{tex_format}' # NOTE: we always save PNG first and convert with an external tool later if needed. dstfile = temp_dst / f'{textureid}.png' print(dstfile) dstfile_meta = temp_dst / f'{textureid}.tex' write_texture_def(dstfile_meta, textureid, tex_format, texture_global_overrides, texture_local_overrides) actual_size = [0, 0] if crop: for rect in bin: if rect.x + rect.width > actual_size[0]: actual_size[0] = rect.x + rect.width if rect.y + rect.height > actual_size[1]: actual_size[1] = rect.y + rect.height else: actual_size = (bin.width, bin.height) composite_cmd = [ 'convert', '-verbose', '-size', f'{actual_size[0]}x{actual_size[1]}', 'xc:none', ] for rect in bin: img_path, name = rect.rid border = get_border(name) composite_cmd += [ str(img_path), '-geometry', '{:+}{:+}'.format(rect.x + border, rect.y + border), '-composite' ] override_path = overrides / get_override_file_name(name) if override_path.exists(): override_contents = override_path.read_text() else: override_contents = None write_override_template(override_path, img.size) write_sprite_def( temp_dst / f'{name}.spr', textureid, (rect.x + border, rect.y + border, rect.width - border * 2, rect.height - border * 2), img.size, overrides=override_contents) composite_cmd += [str(dstfile)] @executor.submit def process(dstfile=dstfile): subprocess.check_call(composite_cmd) oldfmt = dstfile.suffix[1:].lower() if oldfmt != tex_format: new_dstfile = dstfile.with_suffix(f'.{tex_format}') if tex_format == 'webp': subprocess.check_call([ 'cwebp', '-progress', '-preset', 'drawing', '-z', '9', '-lossless', '-q', '100', str(dstfile), '-o', str(new_dstfile), ]) else: raise TaiseiError( f'Unhandled conversion {oldfmt} -> {tex_format}') dstfile.unlink() dstfile = new_dstfile if leanify: subprocess.check_call(['leanify', '-v', str(dstfile)]) futures.append(process) # Wait for subprocesses to complete. wait_for_futures(futures) executor.shutdown(wait=True) # Only now, if everything is ok so far, copy everything to the destination, possibly overwriting previous results pattern = re.compile( rf'^atlas_{re.escape(atlasname)}_\d+.({"|".join(texture_formats + ["tex"])})$' ) for path in dst.iterdir(): if pattern.match(path.name): path.unlink() targets = list(temp_dst.glob('**/*')) for dir in (p.relative_to(temp_dst) for p in targets if p.is_dir()): (dst / dir).mkdir(parents=True, exist_ok=True) for file in (p.relative_to(temp_dst) for p in targets if not p.is_dir()): shutil.copyfile(str(temp_dst / file), str(dst / file))