""" Generate VB-optimized wall texture tiles for the raycaster. 3 algorithmic patterns (brick, stone, tech) + 2 image-based (door, switch). 5 wall types, 8 columns x 8 rows x 2 lighting = 640 wall tiles + 70 transition. Switch "on" state tiles stored separately for runtime VRAM swap. Output: wall_textures.c and wall_textures.h """ import os import random from PIL import Image SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images") PREVIEW_DIR = os.path.join(SCRIPT_DIR, "wall_texture_previews") WALL_TEX_CHAR_START = 1211 # after pickups: 1175 + 36 = 1211 TILE_SIZE = 8 COLUMNS_PER_TEX = 8 ROWS_PER_TEX = 8 TILES_PER_LIGHTING = COLUMNS_PER_TEX * ROWS_PER_TEX # 64 TILES_PER_TYPE = TILES_PER_LIGHTING * 2 # 128 TEXTURES = [ "wall_startan", # Brick "wall_stone", # Stone blocks "wall_tech", # Tech panels "wall_door", # Door (from door_texture.png) "wall_switch", # Switch off state (from gaint-switch1-doom-palette.png) ] VB_PAL = {0: (0, 0, 0), 1: (80, 0, 0), 2: (170, 0, 0), 3: (255, 0, 0)} def luminance(r, g, b): return int(0.299 * r + 0.587 * g + 0.114 * b) # --------------------------------------------------------------------------- # Pattern generators -- each returns a 64x64 canvas of VB indices (1-3) # --------------------------------------------------------------------------- def gen_brick(): """Brick wall: horizontal mortar every 8 rows, staggered vertical joints.""" W, H = 64, 64 c = [[2] * W for _ in range(H)] for y in range(H): for x in range(W): row_in_brick = y % 16 is_h_mortar = row_in_brick == 0 or row_in_brick == 8 stagger = 0 if (y // 16) % 2 == 0 else 16 is_v_mortar = ((x + stagger) % 32) == 0 if is_h_mortar or is_v_mortar: c[y][x] = 1 else: if row_in_brick == 1 or row_in_brick == 9: c[y][x] = 3 elif row_in_brick == 7 or row_in_brick == 15: c[y][x] = 1 else: col_in_brick = (x + stagger) % 32 if col_in_brick == 1: c[y][x] = 3 elif col_in_brick == 31: c[y][x] = 1 else: if (x + y * 3) % 7 == 0: c[y][x] = 3 elif (x * 5 + y * 2) % 11 == 0: c[y][x] = 1 else: c[y][x] = 2 return c def gen_stone(): """Stone blocks: irregular block outlines with varied fill.""" W, H = 64, 64 c = [[2] * W for _ in range(H)] random.seed(42) blocks = [ (0, 0, 20, 12), (20, 0, 44, 10), (44, 0, 64, 14), (0, 12, 18, 28), (18, 10, 46, 26), (46, 14, 64, 30), (0, 28, 24, 44), (24, 26, 48, 42), (48, 30, 64, 46), (0, 44, 22, 58), (22, 42, 50, 56), (50, 46, 64, 60), (0, 58, 28, 64), (28, 56, 52, 64), (52, 60, 64, 64), ] for bx1, by1, bx2, by2 in blocks: fill = random.choice([2, 2, 2, 3]) for y in range(by1, min(by2, H)): for x in range(bx1, min(bx2, W)): if y == by1 or y == by2 - 1 or x == bx1 or x == bx2 - 1: c[y][x] = 1 elif y == by1 + 1 and x > bx1 and x < bx2 - 1: c[y][x] = 3 elif x == bx1 + 1 and y > by1 and y < by2 - 1: c[y][x] = 3 elif y == by2 - 2 and x > bx1 and x < bx2 - 1: c[y][x] = 1 elif x == bx2 - 2 and y > by1 and y < by2 - 1: c[y][x] = 1 else: if (x * 3 + y * 7) % 13 == 0: c[y][x] = 1 if fill == 2 else 2 else: c[y][x] = fill return c def gen_tech(): """Tech panels: clean grid with bright edges and dark recesses.""" W, H = 64, 64 c = [[2] * W for _ in range(H)] for y in range(H): for x in range(W): px = x % 16 py = y % 16 if px == 0 or py == 0: c[y][x] = 1 elif px == 1 or py == 1: c[y][x] = 3 elif px == 15 or py == 15: c[y][x] = 1 elif px == 14 or py == 14: c[y][x] = 2 else: panel_id = (x // 16) + (y // 16) * 4 if panel_id % 3 == 0: if py >= 6 and py <= 9: c[y][x] = 3 if (px + py) % 2 == 0 else 1 else: c[y][x] = 2 elif panel_id % 3 == 1: cx, cy = 8, 8 dist = abs(px - cx) + abs(py - cy) if dist <= 2: c[y][x] = 3 elif dist <= 4: c[y][x] = 1 else: c[y][x] = 2 else: if px == 8 or py == 8: c[y][x] = 1 else: c[y][x] = 2 return c def gen_from_image(image_path): """Load PNG, scale to 64x64, convert to VB palette (1-3 for walls).""" img = Image.open(image_path).convert('RGBA') resized = img.resize((64, 64), Image.LANCZOS) canvas = [[2] * 64 for _ in range(64)] pix = resized.load() for y in range(64): for x in range(64): r, g, b, a = pix[x, y] if a < 128: canvas[y][x] = 1 # transparent -> dark for walls else: gray = luminance(r, g, b) if gray < 85: canvas[y][x] = 1 elif gray < 170: canvas[y][x] = 2 else: canvas[y][x] = 3 return canvas def gen_door(): """Door texture from door_texture.png.""" path = os.path.join(SCRIPT_DIR, "door_texture.png") return gen_from_image(path) def gen_switch_off(): """Switch off state from gaint-switch1-doom-palette.png.""" path = os.path.join(SCRIPT_DIR, "gaint-switch1-doom-palette.png") return gen_from_image(path) def gen_switch_on(): """Switch on state from gaint-switch2-doom-palette.png.""" path = os.path.join(SCRIPT_DIR, "gaint-switch2-doom-palette.png") return gen_from_image(path) # --------------------------------------------------------------------------- # Tile operations # --------------------------------------------------------------------------- def slice_canvas_to_tiles(canvas): tiles = [[None] * COLUMNS_PER_TEX for _ in range(ROWS_PER_TEX)] for tr in range(ROWS_PER_TEX): for tc in range(COLUMNS_PER_TEX): tile = [] for y in range(TILE_SIZE): row = [] for x in range(TILE_SIZE): row.append(canvas[tr * TILE_SIZE + y][tc * TILE_SIZE + x]) tile.append(row) tiles[tr][tc] = tile return tiles def darken_tile(tile): return [[max(1, v - 1) for v in row] for row in tile] def tile_to_vb_2bpp(tile): data = [] for y in range(TILE_SIZE): u16_val = 0 for x in range(TILE_SIZE): u16_val |= (tile[y][x] << (x * 2)) data.append(u16_val & 0xFF) data.append((u16_val >> 8) & 0xFF) return data def bytes_to_u32(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 # --------------------------------------------------------------------------- # Transition tiles # --------------------------------------------------------------------------- def generate_transitions(rep_tiles): tiles = [] for ti in range(len(TEXTURES)): for tv in range(2): src = rep_tiles.get((ti, tv), [[1]*8]*8) for fill in range(1, 8): masked = [] blank_rows = 8 - fill for y in range(8): if y < blank_rows: masked.append([0] * 8) else: masked.append(list(src[y])) tiles.append(bytes_to_u32(tile_to_vb_2bpp(masked))) return tiles # --------------------------------------------------------------------------- # Preview # --------------------------------------------------------------------------- def save_preview(canvas, name, scale=4): os.makedirs(PREVIEW_DIR, exist_ok=True) pw, ph = 64 * scale, 64 * scale img = Image.new('RGB', (pw, ph)) for y in range(64): for x in range(64): color = VB_PAL[canvas[y][x]] for sy in range(scale): for sx in range(scale): img.putpixel((x * scale + sx, y * scale + sy), color) img.save(os.path.join(PREVIEW_DIR, f"{name}.png")) def save_combined_preview(canvases, names, scale=3): os.makedirs(PREVIEW_DIR, exist_ok=True) gap = 4 n = len(canvases) w = (64 * scale + gap) * n + gap h = (64 * scale + gap) * 2 + gap img = Image.new('RGB', (w, h), (20, 20, 20)) for i, (canvas, name) in enumerate(zip(canvases, names)): ox = gap + i * (64 * scale + gap) for y in range(64): for x in range(64): color = VB_PAL[canvas[y][x]] for sy in range(scale): for sx in range(scale): img.putpixel((ox + x*scale+sx, gap + y*scale+sy), color) oy = gap + 64 * scale + gap for y in range(64): for x in range(64): v = max(1, canvas[y][x] - 1) color = VB_PAL[v] for sy in range(scale): for sx in range(scale): img.putpixel((ox + x*scale+sx, oy + y*scale+sy), color) img.save(os.path.join(PREVIEW_DIR, "all_wall_textures.png")) # --------------------------------------------------------------------------- # Switch ON tiles (separate ROM array for runtime swap) # --------------------------------------------------------------------------- def generate_switch_on_tiles(): """Generate tile data for switch 'on' state, stored separately.""" canvas = gen_switch_on() tiles = slice_canvas_to_tiles(canvas) all_u32 = [] # Lit tiles for r in range(ROWS_PER_TEX): for c in range(COLUMNS_PER_TEX): all_u32.extend(bytes_to_u32(tile_to_vb_2bpp(tiles[r][c]))) # Dark tiles for r in range(ROWS_PER_TEX): for c in range(COLUMNS_PER_TEX): all_u32.extend(bytes_to_u32(tile_to_vb_2bpp(darken_tile(tiles[r][c])))) return all_u32 # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): os.makedirs(OUTPUT_DIR, exist_ok=True) generators = [gen_brick, gen_stone, gen_tech, gen_door, gen_switch_off] all_canvases = [] all_tiles = [] rep_tiles = {} for ti, (gen, name) in enumerate(zip(generators, TEXTURES)): canvas = gen() all_canvases.append(canvas) save_preview(canvas, name) print(f"Generated {name}: 64x64 canvas") tiles = slice_canvas_to_tiles(canvas) # Lit tiles for r in range(ROWS_PER_TEX): for c in range(COLUMNS_PER_TEX): t = tiles[r][c] if r == 0 and c == 0: rep_tiles[(ti, 0)] = t all_tiles.append(bytes_to_u32(tile_to_vb_2bpp(t))) # Dark tiles for r in range(ROWS_PER_TEX): for c in range(COLUMNS_PER_TEX): t = darken_tile(tiles[r][c]) if r == 0 and c == 0: rep_tiles[(ti, 1)] = t all_tiles.append(bytes_to_u32(tile_to_vb_2bpp(t))) save_combined_preview(all_canvases, TEXTURES) print(f"\nSaved previews to {PREVIEW_DIR}/") # Transitions trans = generate_transitions(rep_tiles) print(f"Generated {len(trans)} transition tiles") # Switch ON tiles (separate) switch_on_u32 = generate_switch_on_tiles() print(f"Generated switch ON tiles: {len(switch_on_u32)} u32 words") combined = all_tiles + trans total_words = len(combined) * 4 # --- Write C --- c_path = os.path.join(OUTPUT_DIR, "wall_textures.c") with open(c_path, 'w') as f: f.write("/*\n") f.write(" * Wall texture tiles for VB Doom raycaster.\n") f.write(" * Auto-generated by prepare_wall_textures.py\n") f.write(f" * {len(all_tiles)} wall tiles + {len(trans)} transition = {len(combined)} total\n") f.write(" * Patterns: brick, stone, tech, door, switch\n") f.write(" */\n\n") f.write('#include "wall_textures.h"\n\n') f.write(f"const unsigned int wallTextureTiles[{total_words}]" f" __attribute__((aligned(4))) =\n{{\n") for i, words in enumerate(combined): s = ", ".join(f"0x{w:08X}" for w in words) comma = "," if i < len(combined) - 1 else "" f.write(f"\t{s}{comma}\n") f.write("};\n\n") # Switch ON data (ROM, for runtime swap into VRAM) f.write("/* Switch ON state tiles -- copymem over switch VRAM chars on activation */\n") f.write(f"const unsigned int switchOnTiles[{len(switch_on_u32)}]" f" __attribute__((aligned(4))) =\n{{\n") for i in range(0, len(switch_on_u32), 8): chunk = switch_on_u32[i:i+8] s = ", ".join(f"0x{w:08X}" for w in chunk) comma = "," if i + 8 < len(switch_on_u32) else "" f.write(f"\t{s}{comma}\n") f.write("};\n") print(f"\nWrote {c_path}") # --- Write H --- trans_start = WALL_TEX_CHAR_START + len(all_tiles) tc_shift = 5 # Switch type is the last one (index 4, wall type 5) switch_type_idx = len(TEXTURES) - 1 switch_vram_offset = switch_type_idx * TILES_PER_TYPE h_path = os.path.join(OUTPUT_DIR, "wall_textures.h") with open(h_path, 'w') as f: f.write("#ifndef __WALL_TEXTURES_H__\n#define __WALL_TEXTURES_H__\n\n") f.write("/*\n * Wall texture tiles for VB Doom raycaster.\n") f.write(" * Auto-generated by prepare_wall_textures.py\n") f.write(f" * {len(all_tiles)} wall + {len(trans)} transition = {len(combined)} total\n *\n") f.write(f" * Layout per texture ({TILES_PER_TYPE} tiles):\n") f.write(f" * 0-{TILES_PER_LIGHTING-1}: lit ({ROWS_PER_TEX} rows x {COLUMNS_PER_TEX} cols)\n") f.write(f" * {TILES_PER_LIGHTING}-{TILES_PER_TYPE-1}: dark ({ROWS_PER_TEX} rows x {COLUMNS_PER_TEX} cols)\n *\n") f.write(f" * Tile index formula:\n") f.write(f" * WALL_TEX_CHAR_START + (wallType-1)*{TILES_PER_TYPE}" f" + tv*{TILES_PER_LIGHTING} + row*{COLUMNS_PER_TEX} + (tc >> {tc_shift})\n") f.write(f" * tv=0 lit, tv=1 dark; row = (texY >> 13) & 7\n */\n\n") f.write(f"#define WALL_TEX_COUNT {len(TEXTURES)}\n") f.write(f"#define WALL_TEX_COLS {COLUMNS_PER_TEX}\n") f.write(f"#define WALL_TEX_ROWS {ROWS_PER_TEX}\n") f.write(f"#define WALL_TEX_PER_TYPE {TILES_PER_TYPE}\n") f.write(f"#define WALL_TEX_LIT_BLOCK {TILES_PER_LIGHTING}\n") f.write(f"#define WALL_TEX_TOTAL {len(all_tiles)}\n") f.write(f"#define WALL_TEX_BYTES {len(combined) * 16}\n\n") f.write(f"#define WALL_TEX_CHAR_START {WALL_TEX_CHAR_START}\n\n") f.write(f"#define TRANS_TEX_CHAR_START {trans_start}\n") f.write(f"#define TRANS_TEX_COUNT {len(trans)}\n\n") f.write(f"extern const unsigned int wallTextureTiles[{total_words}];\n\n") # Switch ON data extern f.write(f"/* Switch ON tiles: {TILES_PER_TYPE} tiles in ROM for runtime swap */\n") f.write(f"extern const unsigned int switchOnTiles[{len(switch_on_u32)}];\n") f.write(f"#define SWITCH_ON_TILES_BYTES {len(switch_on_u32) * 4}\n") f.write(f"#define SWITCH_VRAM_OFFSET {switch_vram_offset}\n\n") for i, name in enumerate(TEXTURES): short = name.upper().replace('WALL_', '') f.write(f"#define WALL_TYPE_{short} {i + 1}\n") f.write("\n#endif\n") print(f"Wrote {h_path}") print(f"\nTotal: {len(combined)} tiles ({len(all_tiles)} wall + {len(trans)} transition)") print(f"Total bytes: {len(combined) * 16}") print(f"WALL_TEX_CHAR_START: {WALL_TEX_CHAR_START}") print(f"TRANS_TEX_CHAR_START: {trans_start}") after = trans_start + len(trans) print(f"After all: char {after} (free: {2048 - after})") if __name__ == '__main__': main()