168 lines
6.6 KiB
Python
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()
|