initial commit!

This commit is contained in:
2026-02-19 23:28:57 +01:00
parent b0d594a9c0
commit 2a36117c25
1558 changed files with 74163 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
"""
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()