""" extract_doom_weapons.py Extract all weapon sprites from doom_weapons.png: 1. Detect individual sprites on magenta background 2. Save each as a separate PNG file 3. Convert each to VB 4-color (2bpp) tile data and save as C arrays Output: doom_graphics_unvirtualboyed/weapon_sprites/ sprite_00.png .. sprite_NN.png (original color PNGs) sprite_00_vb.png .. sprite_NN_vb.png (VB palette preview PNGs) sprite_manifest.txt (list of all sprites with dimensions) src/vbdoom/assets/images/weapons/ weapon_sprite_00.c/.h (VB-ready C arrays per sprite) ... The sprites are NOT grouped by weapon automatically since the layout varies between source images. The manifest file helps the user identify and organize them. """ import os import sys from PIL import Image SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) INPUT_IMG = os.path.join(SCRIPT_DIR, "more_doom_gfx", "doom_weapons.png") PNG_OUT_DIR = os.path.join(SCRIPT_DIR, "doom_graphics_unvirtualboyed", "weapon_sprites") C_OUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images", "weapons") TILE_SIZE = 8 MAGENTA_THRESHOLD = 30 VB_PALETTE = [ (0, 0, 0), # 0: Black (transparent) (85, 0, 0), # 1: Dark red (164, 0, 0), # 2: Medium red (239, 0, 0), # 3: Bright red ] def is_magenta(r, g, b): return r > 220 and g < MAGENTA_THRESHOLD and b > 220 def luminance(r, g, b): return int(0.299 * r + 0.587 * g + 0.114 * b) def map_to_vb_index(gray): if gray < 85: return 1 elif gray < 170: return 2 else: return 3 # --------------------------------------------------------------------------- # Sprite detection # --------------------------------------------------------------------------- def find_sprites(img): """Find all sprite bounding boxes in the image.""" w, h = img.size pixels = img.load() mask = [[False] * w for _ in range(h)] for y in range(h): for x in range(w): pix = pixels[x, y] r, g, b = pix[0], pix[1], pix[2] a = pix[3] if len(pix) > 3 else 255 if not is_magenta(r, g, b) and a > 128: mask[y][x] = True row_has = [any(mask[y]) for y in range(h)] bands = [] in_band = False for y in range(h): if row_has[y] and not in_band: start = y in_band = True elif not row_has[y] and in_band: bands.append((start, y - 1)) in_band = False if in_band: bands.append((start, h - 1)) sprites = [] for band_top, band_bottom in bands: col_has = [False] * w for x in range(w): for y in range(band_top, band_bottom + 1): if mask[y][x]: col_has[x] = True break in_s = False for x in range(w): if col_has[x] and not in_s: sl = x in_s = True elif not col_has[x] and in_s: sprites.append((sl, band_top, x - 1, band_bottom)) in_s = False if in_s: sprites.append((sl, band_top, w - 1, band_bottom)) # Tighten bounding boxes vertically tightened = [] for (l, t, r, b) in sprites: top_tight = b bot_tight = t for y in range(t, b + 1): for x in range(l, r + 1): if mask[y][x]: if y < top_tight: top_tight = y if y > bot_tight: bot_tight = y break tightened.append((l, top_tight, r, bot_tight)) return tightened # --------------------------------------------------------------------------- # VB tile encoding (same as prepare_wall_textures.py) # --------------------------------------------------------------------------- def tile_to_vb_2bpp(tile): data = [] for y in range(TILE_SIZE): lo_byte = 0 hi_byte = 0 for x in range(TILE_SIZE): idx = tile[y][x] bit0 = (idx >> 0) & 1 bit1 = (idx >> 1) & 1 lo_byte |= (bit0 << (7 - x)) hi_byte |= (bit1 << (7 - x)) data.append(lo_byte) data.append(hi_byte) return data def bytes_to_u32_array(data): words = [] for i in range(0, len(data), 4): w = data[i] | (data[i+1] << 8) | (data[i+2] << 16) | (data[i+3] << 24) words.append(w) return words # --------------------------------------------------------------------------- # Conversion and output # --------------------------------------------------------------------------- def extract_sprite_canvas(img, bbox): """Extract sprite pixels as VB index array, pad to tile-aligned size.""" l, t, r, b = bbox sw = r - l + 1 sh = b - t + 1 # Pad to tile-aligned dimensions tw = ((sw + TILE_SIZE - 1) // TILE_SIZE) * TILE_SIZE th = ((sh + TILE_SIZE - 1) // TILE_SIZE) * TILE_SIZE canvas = [[0] * tw for _ in range(th)] pixels = img.load() for sy in range(sh): for sx in range(sw): pix = pixels[l + sx, t + sy] r2, g2, b2 = pix[0], pix[1], pix[2] a = pix[3] if len(pix) > 3 else 255 if is_magenta(r2, g2, b2) or a < 128: canvas[sy][sx] = 0 else: gray = luminance(r2, g2, b2) canvas[sy][sx] = map_to_vb_index(gray) return canvas, tw, th def canvas_to_tiles(canvas, tw, th): """Slice canvas into 8x8 tiles, return tiles and map with deduplication.""" cols = tw // TILE_SIZE rows = th // TILE_SIZE unique_tiles = {} unique_tile_data = [] tile_map = [] for ty in range(rows): for tx in range(cols): tile = [] for y in range(TILE_SIZE): row = [] for x in range(TILE_SIZE): row.append(canvas[ty * TILE_SIZE + y][tx * TILE_SIZE + x]) tile.append(row) key = tuple(tuple(r) for r in tile) if key in unique_tiles: tile_idx = unique_tiles[key] else: tile_idx = len(unique_tile_data) unique_tiles[key] = tile_idx vb_bytes = tile_to_vb_2bpp(tile) unique_tile_data.append(bytes_to_u32_array(vb_bytes)) tile_map.append(tile_idx) return unique_tile_data, tile_map, cols, rows def save_vb_preview(canvas, tw, th, path): """Save a VB-palette preview PNG.""" preview = Image.new('RGB', (tw, th)) for y in range(th): for x in range(tw): preview.putpixel((x, y), VB_PALETTE[canvas[y][x]]) preview.save(path) def write_sprite_c(name, tile_data, tile_map, map_cols, map_rows, c_path, h_path): """Write VB-ready C arrays for one sprite.""" num_tiles = len(tile_data) all_u32 = [] for td in tile_data: all_u32.extend(td) with open(c_path, 'w') as f: f.write(f"/* {name} -- auto-generated by extract_doom_weapons.py */\n\n") f.write(f"const unsigned int {name}Tiles[{len(all_u32)}]" f" __attribute__((aligned(4))) =\n") f.write("{\n") for i in range(0, len(all_u32), 8): chunk = all_u32[i:i + 8] s = ",".join(f"0x{v:08X}" for v in chunk) comma = "," if i + 8 < len(all_u32) else "" f.write(f"\t{s}{comma}\n") f.write("};\n\n") f.write(f"const unsigned short {name}Map[{len(tile_map)}]" f" __attribute__((aligned(4))) =\n") f.write("{\n") for i in range(0, len(tile_map), map_cols): chunk = tile_map[i:i + map_cols] s = ",".join(f"0x{v:04X}" for v in chunk) comma = "," if i + map_cols < len(tile_map) else "" f.write(f"\t{s}{comma}\n") f.write("};\n") guard = name.upper() with open(h_path, 'w') as f: f.write(f"#ifndef __{guard}_H__\n") f.write(f"#define __{guard}_H__\n\n") f.write(f"#define {guard}_TILE_COUNT {num_tiles}\n") f.write(f"#define {guard}_TILE_BYTES {num_tiles * 16}\n") f.write(f"#define {guard}_MAP_COLS {map_cols}\n") f.write(f"#define {guard}_MAP_ROWS {map_rows}\n\n") f.write(f"extern const unsigned int {name}Tiles[{len(all_u32)}];\n") f.write(f"extern const unsigned short {name}Map[{len(tile_map)}];\n\n") f.write(f"#endif\n") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): img = Image.open(INPUT_IMG).convert('RGBA') w, h = img.size print(f"Loaded {INPUT_IMG}: {w}x{h}") os.makedirs(PNG_OUT_DIR, exist_ok=True) os.makedirs(C_OUT_DIR, exist_ok=True) bboxes = find_sprites(img) print(f"Found {len(bboxes)} sprites") manifest_lines = [] manifest_lines.append(f"# Doom weapon sprite manifest ({len(bboxes)} sprites)") manifest_lines.append(f"# Generated from doom_weapons.png ({w}x{h})") manifest_lines.append("#") manifest_lines.append("# Format: index bbox(l,t,r,b) size tiles_w x tiles_h unique_tiles") manifest_lines.append("#") for si, bbox in enumerate(bboxes): l, t, r, b = bbox sw = r - l + 1 sh = b - t + 1 name = f"weaponSprite{si:02d}" # Crop the sprite from the original image crop = img.crop((l, t, r + 1, b + 1)) png_path = os.path.join(PNG_OUT_DIR, f"sprite_{si:02d}.png") crop.save(png_path) # Convert to VB and save preview canvas, tw, th = extract_sprite_canvas(img, bbox) vb_path = os.path.join(PNG_OUT_DIR, f"sprite_{si:02d}_vb.png") save_vb_preview(canvas, tw, th, vb_path) # Generate tiles and C arrays tile_data, tile_map, map_cols, map_rows = canvas_to_tiles(canvas, tw, th) c_path = os.path.join(C_OUT_DIR, f"weapon_sprite_{si:02d}.c") h_path = os.path.join(C_OUT_DIR, f"weapon_sprite_{si:02d}.h") write_sprite_c(name, tile_data, tile_map, map_cols, map_rows, c_path, h_path) manifest_lines.append( f" {si:2d} ({l:4d},{t:3d})-({r:4d},{b:3d}) {sw:3d}x{sh:3d} " f"{map_cols:2d}x{map_rows:2d} tiles {len(tile_data):3d} unique" ) print(f" [{si:2d}] {sw:3d}x{sh:3d} -> {map_cols}x{map_rows} tiles, " f"{len(tile_data)} unique ({name})") # Write manifest manifest_path = os.path.join(PNG_OUT_DIR, "sprite_manifest.txt") with open(manifest_path, 'w') as f: f.write("\n".join(manifest_lines)) f.write("\n") print(f"\nWrote {len(bboxes)} sprite PNGs to {PNG_OUT_DIR}") print(f"Wrote {len(bboxes)} VB C arrays to {C_OUT_DIR}") print(f"Manifest: {manifest_path}") if __name__ == "__main__": main()