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,386 @@
"""
prepare_particles.py
Convert PUFF A-D particle sprites to VB format and create shotgun group variants.
Single puffs: variable size per frame:
PuffA0: 8x8 (1x1 tile)
PuffB0: 8x8 (1x1 tile)
PuffC0: 16x16 (2x2 tiles)
PuffD0: 16x16 (2x2 tiles)
Shotgun groups: 32x32 pixels (4x4 tiles), 4 variants x 4 frames
All tile data is deduplicated.
Output: particle_sprites.c and particle_sprites.h
"""
import os
import random
from PIL import Image
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PARTICLE_DIR = os.path.join(SCRIPT_DIR, "particles")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images")
PREVIEW_DIR = os.path.join(SCRIPT_DIR, "particle_previews")
PARTICLE_CHAR_START = 764
TILE_SIZE = 8
# Per-frame puff sizes
PUFF_FRAMES = ["PUFFA0.png", "PUFFB0.png", "PUFFC0.png", "PUFFD0.png"]
PUFF_SIZES = [8, 8, 16, 16] # target pixel size per frame
GROUP_SIZE = 32
GROUP_TILES_W = 4
GROUP_TILES_H = 4
GROUP_TILE_COUNT = GROUP_TILES_W * GROUP_TILES_H # 16
NUM_GROUP_VARIANTS = 4
MAGENTA_THRESHOLD = 30
VB_PAL = {0: (0, 0, 0, 0), 1: (80, 0, 0, 255), 2: (170, 0, 0, 255), 3: (255, 0, 0, 255)}
def luminance(r, g, b):
return int(0.299 * r + 0.587 * g + 0.114 * b)
def is_bg(r, g, b, a):
if a < 128:
return True
if g > 200 and b > 200 and r < 40:
return True
if r > 220 and g < MAGENTA_THRESHOLD and b > 220:
return True
return False
def load_and_convert_puff(filepath, target_size):
"""Load a puff PNG, detect bbox, resize to target_size, convert to VB indices."""
img = Image.open(filepath).convert('RGBA')
w, h = img.size
pix = img.load()
min_x, min_y, max_x, max_y = w, h, 0, 0
for y in range(h):
for x in range(w):
r, g, b, a = pix[x, y]
if not is_bg(r, g, b, a):
min_x = min(min_x, x)
min_y = min(min_y, y)
max_x = max(max_x, x)
max_y = max(max_y, y)
if max_x < min_x:
return [[0] * target_size for _ in range(target_size)]
crop = img.crop((min_x, min_y, max_x + 1, max_y + 1))
cw, ch = crop.size
scale = min(target_size / cw, target_size / ch)
new_w = max(1, int(cw * scale))
new_h = max(1, int(ch * scale))
resized = crop.resize((new_w, new_h), Image.LANCZOS)
canvas = [[0] * target_size for _ in range(target_size)]
off_x = (target_size - new_w) // 2
off_y = (target_size - new_h) // 2
rpix = resized.load()
for y in range(new_h):
for x in range(new_w):
r, g, b, a = rpix[x, y]
dx = off_x + x
dy = off_y + y
if dx < target_size and dy < target_size:
if is_bg(r, g, b, a):
canvas[dy][dx] = 0
else:
gray = luminance(r, g, b)
if gray < 85:
canvas[dy][dx] = 1
elif gray < 170:
canvas[dy][dx] = 2
else:
canvas[dy][dx] = 3
return canvas
def composite_group(puff_canvases, variant_idx, frame_idx):
"""Create a 32x32 shotgun group by compositing multiple puff copies.
Uses the appropriately-sized puff for this frame."""
random.seed(42 + variant_idx * 17 + frame_idx)
group = [[0] * GROUP_SIZE for _ in range(GROUP_SIZE)]
puff = puff_canvases[frame_idx]
puff_h = len(puff)
puff_w = len(puff[0]) if puff_h > 0 else 0
num_puffs = 4 + (variant_idx % 2)
for _ in range(num_puffs):
ox = random.randint(-2, GROUP_SIZE - puff_w + 2)
oy = random.randint(-2, GROUP_SIZE - puff_h + 2)
for y in range(puff_h):
for x in range(puff_w):
dx = ox + x
dy = oy + y
if 0 <= dx < GROUP_SIZE and 0 <= dy < GROUP_SIZE:
val = puff[y][x]
if val > 0:
group[dy][dx] = max(group[dy][dx], val)
return group
def slice_tiles(canvas, tw, th):
tiles = []
for tr in range(th):
for tc in range(tw):
tile = []
for y in range(TILE_SIZE):
row = []
for x in range(TILE_SIZE):
py = tr * TILE_SIZE + y
px = tc * TILE_SIZE + x
if py < len(canvas) and px < len(canvas[0]):
row.append(canvas[py][px])
else:
row.append(0)
tile.append(row)
tiles.append(tile)
return tiles
def tile_to_tuple(tile):
return tuple(tuple(row) for row in tile)
def hflip_tile(tile):
return [row[::-1] for row in tile]
def tile_to_vb_2bpp(tile):
"""Convert 8x8 tile to VB 2bpp sequential format (16 bytes).
VB stores each row as a u16 (LE) with pixel 0 at bits[1:0] (LSB).
"""
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
def save_preview(canvas, path, scale=4):
os.makedirs(os.path.dirname(path), exist_ok=True)
h = len(canvas)
w = len(canvas[0])
img = Image.new('RGBA', (w * scale, h * scale), (0, 0, 0, 255))
pix = img.load()
for y in range(h):
for x in range(w):
color = VB_PAL[canvas[y][x]]
for sy in range(scale):
for sx in range(scale):
pix[x * scale + sx, y * scale + sy] = color
img.save(path)
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(PREVIEW_DIR, exist_ok=True)
# --- Load single puffs with per-frame sizes ---
puff_canvases = []
for fi, fname in enumerate(PUFF_FRAMES):
path = os.path.join(PARTICLE_DIR, fname)
target = PUFF_SIZES[fi]
canvas = load_and_convert_puff(path, target)
puff_canvases.append(canvas)
save_preview(canvas, os.path.join(PREVIEW_DIR, f"puff_{fname[:-4]}.png"))
print(f"Loaded {fname}: {target}x{target}")
# --- Create shotgun group variants ---
group_canvases = []
for vi in range(NUM_GROUP_VARIANTS):
frames = []
for fi in range(4):
group = composite_group(puff_canvases, vi, fi)
frames.append(group)
save_preview(group, os.path.join(PREVIEW_DIR,
f"group_{vi}_frame{fi}.png"))
group_canvases.append(frames)
print(f"Generated shotgun group variant {vi}: 4 frames")
# --- Deduplicate all tiles ---
unique_tiles = {}
unique_tile_data = []
def add_tile(tile):
key = tile_to_tuple(tile)
flags = 0
if key in unique_tiles:
return unique_tiles[key], flags
hkey = tile_to_tuple(hflip_tile(tile))
if hkey in unique_tiles:
return unique_tiles[hkey], 0x2000
idx = len(unique_tile_data)
unique_tiles[key] = idx
unique_tile_data.append(bytes_to_u32(tile_to_vb_2bpp(tile)))
return idx, flags
# Single puff maps: variable tiles per frame
# Frame 0,1: 1x1 = 1 tile; Frame 2,3: 2x2 = 4 tiles
puff_maps = []
puff_tile_counts = []
for fi, canvas in enumerate(puff_canvases):
sz = PUFF_SIZES[fi]
tw = sz // TILE_SIZE
th = sz // TILE_SIZE
tile_count = tw * th
puff_tile_counts.append(tile_count)
tiles = slice_tiles(canvas, tw, th)
frame_map = []
for t in tiles:
idx, flags = add_tile(t)
char_idx = PARTICLE_CHAR_START + idx
frame_map.append(char_idx | flags)
puff_maps.append(frame_map)
# Group maps: 4 variants x 4 frames x 16 tiles
group_maps = []
for vi in range(NUM_GROUP_VARIANTS):
variant_maps = []
for fi in range(4):
canvas = group_canvases[vi][fi]
tiles = slice_tiles(canvas, GROUP_TILES_W, GROUP_TILES_H)
frame_map = []
for t in tiles:
idx, flags = add_tile(t)
char_idx = PARTICLE_CHAR_START + idx
frame_map.append(char_idx | flags)
variant_maps.append(frame_map)
group_maps.append(variant_maps)
num_tiles = len(unique_tile_data)
print(f"\nTotal unique particle tiles: {num_tiles}")
print(f"Tile data: {num_tiles * 16} bytes")
# Compute puff map offsets for variable-size frames
# puffMap layout: [frame0 tiles...][frame1 tiles...][frame2 tiles...][frame3 tiles...]
puff_offsets = []
off = 0
for tc in puff_tile_counts:
puff_offsets.append(off)
off += tc
total_puff_entries = off
# --- Write C file ---
c_path = os.path.join(OUTPUT_DIR, "particle_sprites.c")
with open(c_path, 'w') as f:
f.write("/* Particle sprite tiles -- auto-generated by prepare_particles.py */\n\n")
# Tile data
all_u32 = []
for td in unique_tile_data:
all_u32.extend(td)
f.write(f"/* {num_tiles} unique particle tiles */\n")
f.write(f"const unsigned int particleTiles[{len(all_u32)}]"
f" __attribute__((aligned(4))) =\n{{\n")
for i in range(0, len(all_u32), 8):
chunk = all_u32[i:i+8]
s = ",".join(f"0x{w:08X}" for w in chunk)
comma = "," if i + 8 < len(all_u32) else ""
f.write(f"\t{s}{comma}\n")
f.write("};\n\n")
# Single puff maps (variable-size frames concatenated)
all_puff = []
for fm in puff_maps:
all_puff.extend(fm)
f.write(f"/* Single puff maps: {total_puff_entries} entries "
f"(frames: {','.join(str(tc) for tc in puff_tile_counts)} tiles) */\n")
f.write(f"const unsigned short puffMap[{len(all_puff)}]"
f" __attribute__((aligned(4))) =\n{{\n")
# Write frame by frame with comments
for fi in range(4):
start = puff_offsets[fi]
count = puff_tile_counts[fi]
chunk = all_puff[start:start + count]
s = ",".join(f"0x{v:04X}" for v in chunk)
comma = "," if fi < 3 else ""
f.write(f"\t{s}{comma} /* frame {fi}: {PUFF_SIZES[fi]}x{PUFF_SIZES[fi]} */\n")
f.write("};\n\n")
# Shotgun group maps (unchanged structure)
all_group = []
for vm in group_maps:
for fm in vm:
all_group.extend(fm)
f.write(f"/* Shotgun group maps: {NUM_GROUP_VARIANTS} variants x 4 frames x {GROUP_TILE_COUNT} tiles */\n")
f.write(f"const unsigned short shotgunGroupMap[{len(all_group)}]"
f" __attribute__((aligned(4))) =\n{{\n")
for i in range(0, len(all_group), GROUP_TILE_COUNT):
chunk = all_group[i:i+GROUP_TILE_COUNT]
s = ",".join(f"0x{v:04X}" for v in chunk)
comma = "," if i + GROUP_TILE_COUNT < len(all_group) else ""
f.write(f"\t{s}{comma}\n")
f.write("};\n")
print(f"\nWrote {c_path}")
# --- Write H file ---
h_path = os.path.join(OUTPUT_DIR, "particle_sprites.h")
with open(h_path, 'w') as f:
f.write("#ifndef __PARTICLE_SPRITES_H__\n#define __PARTICLE_SPRITES_H__\n\n")
f.write(f"#define PARTICLE_CHAR_START {PARTICLE_CHAR_START}\n")
f.write(f"#define PARTICLE_TILE_COUNT {num_tiles}\n")
f.write(f"#define PARTICLE_TILE_BYTES {num_tiles * 16}\n\n")
f.write("/* Per-frame puff sizes (pixels) and tile counts */\n")
for fi in range(4):
sz = PUFF_SIZES[fi]
tw = sz // TILE_SIZE
f.write(f"#define PUFF_FRAME{fi}_SIZE {sz}\n")
f.write(f"#define PUFF_FRAME{fi}_TILES_W {tw}\n")
f.write(f"#define PUFF_FRAME{fi}_TILES {tw * tw}\n")
f.write(f"\n/* Puff map offsets (index into puffMap[]) */\n")
for fi in range(4):
f.write(f"#define PUFF_OFFSET{fi} {puff_offsets[fi]}\n")
f.write(f"#define PUFF_FRAME_COUNT 4\n\n")
f.write(f"/* Shotgun group: {GROUP_SIZE}x{GROUP_SIZE} px = {GROUP_TILES_W}x{GROUP_TILES_H} tiles */\n")
f.write(f"#define GROUP_TILES_W {GROUP_TILES_W}\n")
f.write(f"#define GROUP_TILES_H {GROUP_TILES_H}\n")
f.write(f"#define GROUP_FRAME_TILES {GROUP_TILE_COUNT}\n")
f.write(f"#define GROUP_VARIANT_COUNT {NUM_GROUP_VARIANTS}\n")
f.write(f"#define GROUP_FRAME_COUNT 4\n\n")
f.write(f"extern const unsigned int particleTiles[{len(all_u32)}];\n")
f.write(f"extern const unsigned short puffMap[{total_puff_entries}];\n")
f.write(f"extern const unsigned short shotgunGroupMap[{len(all_group)}];\n\n")
f.write("#endif\n")
print(f"Wrote {h_path}")
print(f"\nVRAM: PARTICLE_CHAR_START={PARTICLE_CHAR_START}, tiles={num_tiles}, "
f"last char={PARTICLE_CHAR_START + num_tiles - 1}")
if __name__ == "__main__":
main()