initial commit!
This commit is contained in:
230
graphics/prepare_rocket_projectile.py
Normal file
230
graphics/prepare_rocket_projectile.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
prepare_rocket_projectile.py
|
||||
|
||||
Convert rocket projectile directional sprite strip to VB tile format.
|
||||
Source: new_weapons/rocket[towards_us,away_from_us,...].png (5 frames, 29x20 each)
|
||||
|
||||
Produces 5 frame tile arrays. At runtime, H-flip provides the other 3 directions.
|
||||
Direction mapping:
|
||||
0: front (towards_us) = frame 0
|
||||
1: front-right = frame 4 (towards_us_but_alittle_right)
|
||||
2: right = frame 3 (right)
|
||||
3: back-right = frame 2 (away_from_us_but_alittle_to_right)
|
||||
4: back (away_from_us) = frame 1
|
||||
5: back-left = frame 2 H-flipped
|
||||
6: left = frame 3 H-flipped
|
||||
7: front-left = frame 4 H-flipped
|
||||
|
||||
Output: rocket_projectile_sprites.c/.h in src/vbdoom/assets/images/
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import shutil
|
||||
from PIL import Image
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
STRIP_PATH = os.path.join(SCRIPT_DIR, "new_weapons",
|
||||
"rocket[towards_us,away_from_us,away_from_us_but_alittle_to_right,right,towards_us_but_alittle_right].png")
|
||||
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "src", "vbdoom", "assets", "images")
|
||||
GRIT_EXE = os.path.join(SCRIPT_DIR, "grit_conversions", "grit.exe")
|
||||
GRIT_DIR = os.path.join(SCRIPT_DIR, "grit_conversions")
|
||||
|
||||
TILE_SIZE = 8
|
||||
GRIT_FLAGS = ['-fh!', '-ftc', '-gB2', '-p!', '-m!']
|
||||
NUM_FRAMES = 5
|
||||
FRAME_NAMES = ["towards_us", "away_from_us", "away_alittle_right", "right", "towards_alittle_right"]
|
||||
|
||||
MAGENTA_THRESHOLD = 30
|
||||
BAYER_2x2 = [[0.0, 0.5], [0.75, 0.25]]
|
||||
DITHER_STRENGTH = 22
|
||||
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, bg_color=(128, 0, 128, 255)):
|
||||
"""Check if pixel is background (transparent). Uses sampled bg_color with tolerance."""
|
||||
if a < 128:
|
||||
return True
|
||||
br, bg_g, bb = bg_color[0], bg_color[1], bg_color[2]
|
||||
if abs(r - br) < 15 and abs(g - bg_g) < 15 and abs(b - bb) < 15:
|
||||
return True
|
||||
if r < 10 and g < 10 and b < 10:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def crop_to_content(img, target_w, target_h, bg_color):
|
||||
"""Crop to bounding box of non-background content, pad to target size (centered)."""
|
||||
pix = img.load()
|
||||
w, h = img.size
|
||||
min_x, min_y, max_x, max_y = w, h, 0, 0
|
||||
has_content = False
|
||||
|
||||
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, bg_color):
|
||||
min_x = min(min_x, x)
|
||||
min_y = min(min_y, y)
|
||||
max_x = max(max_x, x)
|
||||
max_y = max(max_y, y)
|
||||
has_content = True
|
||||
|
||||
if not has_content:
|
||||
return Image.new('RGBA', (target_w, target_h), (0, 0, 0, 0))
|
||||
|
||||
cw = max_x - min_x + 1
|
||||
ch = max_y - min_y + 1
|
||||
cropped = img.crop((min_x, min_y, max_x + 1, max_y + 1))
|
||||
result = Image.new('RGBA', (target_w, target_h), (0, 0, 0, 0))
|
||||
ox = (target_w - cw) // 2
|
||||
oy = (target_h - ch) // 2
|
||||
result.paste(cropped, (ox, oy))
|
||||
return result
|
||||
|
||||
|
||||
def quantize_vb(img, bg_color=(128, 0, 128, 255)):
|
||||
pix = img.load()
|
||||
w, h = img.size
|
||||
out = Image.new('RGBA', (w, h), (0,0,0,0))
|
||||
opix = out.load()
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pix[x, y]
|
||||
if is_bg(r, g, b, a, bg_color):
|
||||
opix[x, y] = (0,0,0,0)
|
||||
continue
|
||||
lum = luminance(r, g, b)
|
||||
bayer = BAYER_2x2[y%2][x%2]
|
||||
lum_d = max(0, min(255, lum + (bayer - 0.5) * DITHER_STRENGTH))
|
||||
if lum_d < 42: vb = 1
|
||||
elif lum_d < 128: vb = 2
|
||||
else: vb = 3
|
||||
opix[x, y] = VB_PAL[vb]
|
||||
return out
|
||||
|
||||
|
||||
def run_grit(png_path):
|
||||
cmd = [GRIT_EXE, png_path] + GRIT_FLAGS
|
||||
result = subprocess.run(cmd, cwd=GRIT_DIR, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" GRIT ERROR: {result.stderr}")
|
||||
return []
|
||||
basename = os.path.splitext(os.path.basename(png_path))[0]
|
||||
c_file = os.path.join(GRIT_DIR, f'{basename}.c')
|
||||
if not os.path.exists(c_file):
|
||||
return []
|
||||
with open(c_file) as f:
|
||||
text = f.read()
|
||||
os.remove(c_file)
|
||||
m = re.search(r'const unsigned int \w+\[\d+\][^=]*=\s*\{([^}]+)\}', text, re.DOTALL)
|
||||
if not m:
|
||||
return []
|
||||
raw = m.group(1).replace('\n', ' ').replace('\t', ' ')
|
||||
return [int(x.strip(), 0) for x in raw.split(',') if x.strip()]
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Rocket Projectile Sprite Converter ===\n")
|
||||
|
||||
if not os.path.exists(STRIP_PATH):
|
||||
print(f"ERROR: Strip not found: {STRIP_PATH}")
|
||||
return
|
||||
|
||||
img = Image.open(STRIP_PATH).convert('RGBA')
|
||||
print(f"Strip: {img.size[0]}x{img.size[1]}")
|
||||
|
||||
# Sample background from top-left corner
|
||||
bg_color = img.getpixel((0, 0))
|
||||
print(f"Background color (top-left): RGBA{bg_color}")
|
||||
|
||||
fw = img.size[0] // NUM_FRAMES
|
||||
fh = img.size[1]
|
||||
# Pad each frame to 16x16 (2x2 tiles) - fits 15x14 first frame
|
||||
target_w = 16
|
||||
target_h = 16
|
||||
|
||||
temp_dir = os.path.join(GRIT_DIR, "_rocket_proj_temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
all_data = []
|
||||
for i in range(NUM_FRAMES):
|
||||
frame = img.crop((i * fw, 0, (i + 1) * fw, fh))
|
||||
# Crop to content, remove bg, pad to 16x16 centered
|
||||
padded = crop_to_content(frame, target_w, target_h, bg_color)
|
||||
quantized = quantize_vb(padded, bg_color)
|
||||
|
||||
path = os.path.join(temp_dir, f"rocket_proj_{i}.png")
|
||||
quantized.save(path)
|
||||
data = run_grit(path)
|
||||
all_data.append(data)
|
||||
tiles = len(data) // 4
|
||||
print(f"Frame {i} ({FRAME_NAMES[i]}): {tiles} tiles, {len(data)*4} bytes")
|
||||
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
tiles_per_frame = len(all_data[0]) // 4 # 4 (2x2 tiles)
|
||||
tw = target_w // TILE_SIZE # 2
|
||||
th = target_h // TILE_SIZE # 2
|
||||
|
||||
# Write C source
|
||||
c_path = os.path.join(OUTPUT_DIR, "rocket_projectile_sprites.c")
|
||||
h_path = os.path.join(OUTPUT_DIR, "rocket_projectile_sprites.h")
|
||||
|
||||
with open(c_path, 'w') as f:
|
||||
f.write("/* Rocket projectile directional sprites -- auto-generated */\n\n")
|
||||
for i, data in enumerate(all_data):
|
||||
f.write(f"const unsigned int rocketProjFrame{i}Tiles[{len(data)}] __attribute__((aligned(4))) = {{\n")
|
||||
for j in range(0, len(data), 4):
|
||||
chunk = data[j:j+4]
|
||||
vals = ', '.join(f'0x{v:08X}' for v in chunk)
|
||||
comma = ',' if j + 4 < len(data) else ''
|
||||
f.write(f' {vals}{comma}\n')
|
||||
f.write("};\n\n")
|
||||
|
||||
with open(h_path, 'w') as f:
|
||||
f.write("#ifndef __ROCKET_PROJECTILE_SPRITES_H__\n")
|
||||
f.write("#define __ROCKET_PROJECTILE_SPRITES_H__\n\n")
|
||||
f.write(f"#define ROCKET_PROJ_TILE_W {tw}\n")
|
||||
f.write(f"#define ROCKET_PROJ_TILE_H {th}\n")
|
||||
f.write(f"#define ROCKET_PROJ_TILES {tiles_per_frame}\n")
|
||||
f.write(f"#define ROCKET_PROJ_FRAME_BYTES {tiles_per_frame * 16}\n")
|
||||
f.write(f"#define ROCKET_PROJ_FRAME_COUNT {NUM_FRAMES}\n\n")
|
||||
|
||||
for i in range(NUM_FRAMES):
|
||||
f.write(f"extern const unsigned int rocketProjFrame{i}Tiles[{len(all_data[i])}];\n")
|
||||
|
||||
f.write(f"\n/* Frame pointer table (5 unique views) */\n")
|
||||
f.write(f"static const unsigned int* const ROCKET_PROJ_FRAMES[{NUM_FRAMES}] = {{\n")
|
||||
for i in range(NUM_FRAMES):
|
||||
comma = ',' if i < NUM_FRAMES - 1 else ''
|
||||
f.write(f" rocketProjFrame{i}Tiles{comma}\n")
|
||||
f.write("};\n\n")
|
||||
|
||||
f.write("/*\n")
|
||||
f.write(" * Direction mapping (8 directions, 0=front/towards player):\n")
|
||||
f.write(" * 0: front = frame 0\n")
|
||||
f.write(" * 1: front-right = frame 4\n")
|
||||
f.write(" * 2: right = frame 3\n")
|
||||
f.write(" * 3: back-right = frame 2\n")
|
||||
f.write(" * 4: back = frame 1\n")
|
||||
f.write(" * 5: back-left = frame 2 (H-flip)\n")
|
||||
f.write(" * 6: left = frame 3 (H-flip)\n")
|
||||
f.write(" * 7: front-left = frame 4 (H-flip)\n")
|
||||
f.write(" */\n")
|
||||
f.write("static const u8 ROCKET_PROJ_DIR_FRAME[8] = {0, 4, 3, 2, 1, 2, 3, 4};\n")
|
||||
f.write("static const u8 ROCKET_PROJ_DIR_HFLIP[8] = {0, 0, 0, 0, 0, 1, 1, 1};\n\n")
|
||||
|
||||
f.write("#endif\n")
|
||||
|
||||
print(f"\nWritten: {c_path}")
|
||||
print(f"Written: {h_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user