Files
WolfensteinVB/graphics/prepare_le_map.py
2026-02-19 23:28:57 +01:00

223 lines
7.4 KiB
Python

"""
prepare_le_map.py
Convert le_map.png to VB 2bpp tile + map C arrays for the intermission screen.
Handles palette remapping (sorts by brightness to match VB hardware palette),
tile deduplication with H/V/HV flip reduction, and outputs le_mapTiles[]
and le_mapMap[] arrays compatible with the intermission.c code.
Output: src/vbdoom/assets/images/le_map.c
"""
import os
from PIL import Image
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
INPUT_PNG = os.path.join(SCRIPT_DIR, "Mapperu.png")
OUTPUT_C = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images", "le_map.c")
TILE_W = 8
TILE_H = 8
MAP_W = 40 # tiles across (320 / 8) -- same as title screen
MAP_H = 26 # tiles down (208 / 8)
# BGMap entry flip bits (VB hardware)
HFLIP_BIT = 1 << 13
VFLIP_BIT = 1 << 12
def build_palette_remap(img):
"""Build a remap table: PNG palette index -> VB brightness level (0-3).
Sorts the palette entries by red channel brightness so index 0 = black,
1 = darkest, 2 = medium, 3 = brightest -- matching VB GPLT0 = 0xE4.
"""
pal = img.getpalette()
if pal is None:
raise ValueError("Image must be indexed (palette mode P)")
used_indices = sorted(set(img.getdata()))
idx_brightness = []
for idx in used_indices:
r = pal[idx * 3]
idx_brightness.append((idx, r))
# Sort by brightness (red channel), assign VB levels 0..N
idx_brightness.sort(key=lambda x: x[1])
remap = {}
for vb_level, (png_idx, _) in enumerate(idx_brightness):
remap[png_idx] = vb_level
print(f" Palette remap: { {k: v for k, v in sorted(remap.items())} }")
return remap
def extract_tile_pixels(img, tx, ty, remap):
"""Return 64-element tuple of remapped VB pixel values for tile (tx, ty)."""
pixels = []
x0, y0 = tx * TILE_W, ty * TILE_H
for py in range(TILE_H):
for px in range(TILE_W):
val = img.getpixel((x0 + px, y0 + py))
pixels.append(remap.get(val, 0))
return tuple(pixels)
def flip_h(pixels):
"""Horizontally flip an 8x8 tile (mirror left-right)."""
out = []
for row in range(TILE_H):
for col in range(TILE_W - 1, -1, -1):
out.append(pixels[row * TILE_W + col])
return tuple(out)
def flip_v(pixels):
"""Vertically flip an 8x8 tile (mirror top-bottom)."""
out = []
for row in range(TILE_H - 1, -1, -1):
for col in range(TILE_W):
out.append(pixels[row * TILE_W + col])
return tuple(out)
def encode_tile_words(pixels):
"""Convert 64-pixel tuple to 4 VB 2bpp u32 words.
VB sequential 2bpp format: each row is a u16 (little-endian) where
pixel 0 occupies bits [1:0], pixel 1 bits [3:2], ..., pixel 7 bits [15:14].
Each u32 word packs two consecutive rows:
word = u16_row_even | (u16_row_odd << 16)
"""
words = []
for pair in range(4):
row_a = pair * 2
row_b = pair * 2 + 1
u16_a = 0
u16_b = 0
for x in range(TILE_W):
u16_a |= (pixels[row_a * TILE_W + x] << (x * 2))
u16_b |= (pixels[row_b * TILE_W + x] << (x * 2))
word = u16_a | (u16_b << 16)
words.append(word)
return tuple(words)
def convert():
print(f"Reading {INPUT_PNG} ...")
img = Image.open(INPUT_PNG)
w, h = img.size
assert (w, h) == (MAP_W * TILE_W, MAP_H * TILE_H), \
f"Expected {MAP_W*TILE_W}x{MAP_H*TILE_H}, got {w}x{h}"
remap = build_palette_remap(img)
# ----- build tile bank with flip-reduction -----
tile_bank = [] # list of encoded word-tuples (4 u32 each)
tile_lookup = {} # pixel_tuple -> tile_index
map_entries = [] # one u16 per map cell
# Force tile 0 to be all-zeros (blank/transparent).
# BGMap entries cleared to 0x0000 reference tile 0, so it MUST be blank
# for the skull overlay worlds to not show garbage in unused cells.
blank_pixels = tuple([0] * 64)
tile_bank.append(encode_tile_words(blank_pixels))
tile_lookup[blank_pixels] = 0
for ty in range(MAP_H):
for tx in range(MAP_W):
pixels = extract_tile_pixels(img, tx, ty, remap)
found = False
# Try the tile as-is, then each flipped variant.
# If flip_h(pixels) is already in the bank, the hardware H-flip
# will reconstruct the original pixels on screen.
for variant, flip_bits in [
(pixels, 0),
(flip_h(pixels), HFLIP_BIT),
(flip_v(pixels), VFLIP_BIT),
(flip_h(flip_v(pixels)), HFLIP_BIT | VFLIP_BIT),
]:
if variant in tile_lookup:
idx = tile_lookup[variant]
map_entries.append(idx | flip_bits)
found = True
break
if not found:
idx = len(tile_bank)
tile_bank.append(encode_tile_words(pixels))
tile_lookup[pixels] = idx
map_entries.append(idx)
num_tiles = len(tile_bank)
num_words = num_tiles * 4
tiles_bytes = num_tiles * 16
map_count = len(map_entries)
map_bytes = map_count * 2
print(f" Unique tiles : {num_tiles} ({tiles_bytes} bytes)")
print(f" Map entries : {map_count} ({map_bytes} bytes)")
print(f" Total : {tiles_bytes + map_bytes} bytes")
# ----- write C source -----
with open(OUTPUT_C, "w") as f:
f.write(f"\n//{{{{BLOCK(le_map)\n\n")
f.write(f"//======================================================================\n")
f.write(f"//\n")
f.write(f"//\tle_map, {w}x{h}@2, \n")
f.write(f"//\t+ {num_tiles} tiles (t|f reduced) not compressed\n")
f.write(f"//\t+ regular map (flat), not compressed, {MAP_W}x{MAP_H} \n")
f.write(f"//\tTotal size: {tiles_bytes} + {map_bytes} = {tiles_bytes + map_bytes}\n")
f.write(f"//\n")
f.write(f"//\tGenerated by prepare_le_map.py from le_map.png\n")
f.write(f"//\n")
f.write(f"//======================================================================\n\n")
f.write(f"const unsigned int le_mapTiles[{num_words}] __attribute__((aligned(4)))=\n{{\n")
all_words = [w for tile in tile_bank for w in tile]
for i, word in enumerate(all_words):
if i % 8 == 0:
f.write("\t")
f.write(f"0x{word:08X}")
if i < num_words - 1:
f.write(",")
if i % 8 == 7:
f.write("\n")
if num_words % 8 != 0:
f.write("\n")
f.write(f"}};\n\n")
f.write(f"const unsigned short le_mapMap[{map_count}] __attribute__((aligned(4)))=\n{{\n")
for i, entry in enumerate(map_entries):
if i % 8 == 0:
f.write("\t")
f.write(f"0x{entry:04X}")
if i < map_count - 1:
f.write(",")
if i % 8 == 7:
f.write("\n")
if map_count % 8 != 0:
f.write("\n")
f.write(f"}};\n\n")
f.write(f"//}}}}BLOCK(le_map)\n")
print(f"\nWritten to {OUTPUT_C}")
return tiles_bytes, num_tiles
if __name__ == "__main__":
tiles_bytes, num_tiles = convert()
print(f"\n>>> le_mapTiles: {num_tiles} tiles, {tiles_bytes} bytes")
print(f">>> Use copymem size: {tiles_bytes}")
print(f">>> Generated from Mapperu.png ({MAP_W*TILE_W}x{MAP_H*TILE_H})")