KAZIK/tools/generators/pack_jackbox.py

168 lines
6.6 KiB
Python

"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.
Processes high-resolution source images into a solid rectangular cabinet ('прямоугольный шкаф'):
- Front: Arched jukebox art framed in rich dark mahogany wood casing (solid 256x512 BGRA).
- Side: Cropped rectangular wooden cabinet side panel with chrome accents (solid 256x512 BGRA).
- Back: Matching dark walnut cabinet rear panel with ventilation slots (solid 256x512 BGRA).
- UI: 640x480 BGRA track selector screen.
Eliminates all grey studio backgrounds, transparent gaps, and seam holes.
"""
import math
from pathlib import Path
import struct
import cv2
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "assets/sprites/jackbox"
OUT = ROOT / "assets/animations/hall"
# Deep rich dark walnut / mahogany wood color (BGR)
WOOD_COLOR = np.array([16, 26, 48], dtype=np.uint8)
def make_solid_front(img_bgr: np.ndarray) -> np.ndarray:
"""Frame the arched jukebox front into a solid rectangular wooden cabinet face."""
h, w = img_bgr.shape[:2]
# Detect studio grey background
diff = np.maximum(
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 1].astype(int)),
np.maximum(np.abs(img_bgr[:, :, 1].astype(int) - img_bgr[:, :, 2].astype(int)),
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 2].astype(int)))
)
bright = np.mean(img_bgr, axis=2)
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
# Seed from borders
seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
# Replace studio grey background with dark mahogany wood casing
front_solid = img_bgr.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
front_solid[:, :, c] = np.clip(
front_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Resize to 256x512 BGRA, 100% opaque (no transparency gaps!)
resized = cv2.resize(front_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
bgra[:, :, 3] = 255
return bgra
def make_solid_side(img_bgr: np.ndarray) -> np.ndarray:
"""Crop side image to a solid rectangular cabinet side panel."""
# Crop to actual rectangular cabinet profile:
# y: 90..2910 (removes studio top margin and floor shadow)
# x: 120..1345 (from front edge to back vertical edge)
side_crop = img_bgr[90:2910, 120:1345]
h, w = side_crop.shape[:2]
# Detect studio grey in the top corner outside the curve
diff = np.maximum(
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 1].astype(int)),
np.maximum(np.abs(side_crop[:, :, 1].astype(int) - side_crop[:, :, 2].astype(int)),
np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 2].astype(int)))
)
bright = np.mean(side_crop, axis=2)
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
seed_mask = np.zeros((h, w), dtype=np.uint8)
seed_mask[0, :] = is_bg[0, :]
seed_mask[-1, :] = is_bg[-1, :]
seed_mask[:, 0] = is_bg[:, 0]
seed_mask[:, -1] = is_bg[:, -1]
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
border_labels = np.unique(labels[seed_mask == 1])
border_labels = border_labels[border_labels != 0]
is_real_bg = np.isin(labels, border_labels)
# Fill corner with dark mahogany cabinet wood
side_solid = side_crop.copy()
mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0)
for c in range(3):
side_solid[:, :, c] = np.clip(
side_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask,
0, 255
).astype(np.uint8)
# Left of texture (x=0) is FRONT edge; right of texture (x=max) is BACK edge.
# Matches along_x from x0 (front) to x1 (back against wall).
resized = cv2.resize(side_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
bgra[:, :, 3] = 255
return bgra
def make_solid_back() -> np.ndarray:
"""Generate a solid dark walnut cabinet rear panel with ventilation slots."""
rear = np.zeros((512, 256, 4), dtype=np.uint8)
for y in range(512):
grain = int(math.sin(y * 0.2) * 4 + math.sin(y * 0.05) * 6)
rear[y, :, 0] = np.clip(int(WOOD_COLOR[0]) + grain, 0, 255) # B
rear[y, :, 1] = np.clip(int(WOOD_COLOR[1]) + grain + 1, 0, 255) # G
rear[y, :, 2] = np.clip(int(WOOD_COLOR[2]) + grain + 3, 0, 255) # R
rear[y, :, 3] = 255 # Fully opaque
for y in range(120, 240, 12):
rear[y:y+3, 32:224, :3] = 10 # Dark ventilation slots
return rear
def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int):
"""Pack an array of (H, W, 4) BGRA numpy uint8 images into an ANM1 binary pack."""
raw = b"".join(f.tobytes() for f in frames)
path.parent.mkdir(parents=True, exist_ok=True)
header = struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw))
path.write_bytes(header + raw)
print(f"Packed {len(frames)} frame(s) ({w}x{h}) into {path.name}: {len(raw)} bytes")
def main():
front_path = SOURCE / "Jackbox_front.jpg"
side_path = SOURCE / "Jackbox_side.jpg"
ui_path = SOURCE / "Jackbox_UI.jpg"
assert front_path.exists(), f"Missing {front_path}"
assert side_path.exists(), f"Missing {side_path}"
assert ui_path.exists(), f"Missing {ui_path}"
print("Building solid rectangular Jukebox cabinet textures...")
# 1. Front Texture: solid rectangular face
front_bgr = cv2.imread(str(front_path))
front_tex = make_solid_front(front_bgr)
# 2. Side Texture: solid rectangular side panel
side_bgr = cv2.imread(str(side_path))
side_tex = make_solid_side(side_bgr)
# 3. Rear Texture: solid dark walnut panel
rear_tex = make_solid_back()
# Pack 3D world frames: Frame 0 = Front, Frame 1 = Side, Frame 2 = Rear
pack_bgra(OUT / "jackbox-v1.bin", [front_tex, side_tex, rear_tex], 256, 512)
# 4. Screen UI Pack: 640x480 BGRA
ui_bgr = cv2.imread(str(ui_path))
ui_bgra = cv2.cvtColor(ui_bgr, cv2.COLOR_BGR2BGRA)
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480)
print("Solid Jukebox asset packaging complete.")
if __name__ == "__main__":
main()