462 lines
17 KiB
Python
462 lines
17 KiB
Python
"""
|
|
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()
|