fix(jukebox): make cabinet solid rectangular with zero gaps, relocate to East wall beside billiards
BIN
bin/poker.exe
|
|
@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
|
||||||
{
|
{
|
||||||
static class Program
|
static class Program
|
||||||
{
|
{
|
||||||
const string ExpectedPokerSha256 = "7488391B71FCEB1C2A2F68C06445CC1421A650793205498BA19E787C9E184297";
|
const string ExpectedPokerSha256 = "07D474A0223390E7AC4F4F779818D77CC2DAE5123F4BDB0F72DD975473267310";
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main()
|
static void Main()
|
||||||
|
|
|
||||||
BIN
dist/Assembler Casino.exe
vendored
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 187 KiB |
BIN
poker.exe
|
|
@ -52,60 +52,56 @@ jackbox_surface_missing:
|
||||||
ret
|
ret
|
||||||
ResolveJackboxSurface3D ENDP
|
ResolveJackboxSurface3D ENDP
|
||||||
|
|
||||||
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
|
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
|
||||||
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
|
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
|
||||||
; Footprint: width 0.75m, depth 0.55m, height 1.85m
|
; Solid rectangular cabinet against East wall beside billiards:
|
||||||
mov eax, wx
|
; Depth along X: 0.55m (x0 = 30.40m, x1 = 30.95m against wall at 31.0m)
|
||||||
sub eax, FP_1*15/40
|
; Width along Y: 0.85m (y0 = 24.575m, y1 = 25.425m, centered at Y = 25.0m)
|
||||||
mov x0, eax
|
; Height Z: 1.85m
|
||||||
add eax, FP_1*15/20
|
mov x0, FP_1*304/10
|
||||||
mov x1, eax
|
mov x1, FP_1*3095/100
|
||||||
mov eax, wy
|
mov y0, 25*FP_1 - FP_1*425/1000
|
||||||
sub eax, FP_1*11/40
|
mov y1, 25*FP_1 + FP_1*425/1000
|
||||||
mov y0, eax
|
|
||||||
add eax, FP_1*11/20
|
; 1. Back Face (against East wall: X = x1)
|
||||||
mov y1, eax
|
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
|
||||||
|
; 2. North Side (Y = y0)
|
||||||
; Rear: North face (Y = y0)
|
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
|
||||||
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
|
; 3. South Side (Y = y1)
|
||||||
; Sides: West face (X = x0) and East face (X = x1)
|
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
|
||||||
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
|
; 4. Front Face (facing West towards billiards: X = x0)
|
||||||
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
|
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
|
||||||
; Front: South face (Y = y1)
|
; 5. Top Lid (horizontal cap at z = 1.85m)
|
||||||
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
|
invoke DrawCityHorizontal3D, x0, x1, y0, y1, FP_1*37/20, 110
|
||||||
ret
|
ret
|
||||||
DrawJackboxCabinet3D ENDP
|
DrawJackboxCabinet3D ENDP
|
||||||
|
|
||||||
RenderJackboxCabinet3D PROC
|
RenderJackboxCabinet3D PROC
|
||||||
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
|
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
|
||||||
ret
|
ret
|
||||||
RenderJackboxCabinet3D ENDP
|
RenderJackboxCabinet3D ENDP
|
||||||
|
|
||||||
JackboxCanWalk3D PROC px:DWORD, py:DWORD
|
JackboxCanWalk3D PROC px:DWORD, py:DWORD
|
||||||
mov eax, py
|
mov eax, px
|
||||||
sub eax, JACKBOX_Y
|
cmp eax, FP_1*303/10
|
||||||
test eax, eax
|
jb jackbox_walk_clear
|
||||||
jns @F
|
cmp eax, 31*FP_1+9830
|
||||||
neg eax
|
ja jackbox_walk_clear
|
||||||
@@:
|
mov eax, py
|
||||||
cmp eax, FP_1*11/20
|
sub eax, 25*FP_1
|
||||||
ja jackbox_walk_clear
|
test eax, eax
|
||||||
mov eax, px
|
jns @F
|
||||||
sub eax, JACKBOX_X
|
neg eax
|
||||||
test eax, eax
|
@@:
|
||||||
jns @F
|
cmp eax, FP_1*55/100
|
||||||
neg eax
|
ja jackbox_walk_clear
|
||||||
@@:
|
xor eax, eax
|
||||||
cmp eax, FP_1*15/20
|
ret
|
||||||
jbe jackbox_walk_blocked
|
jackbox_walk_clear:
|
||||||
jackbox_walk_clear:
|
mov eax, 1
|
||||||
mov eax, 1
|
ret
|
||||||
ret
|
JackboxCanWalk3D ENDP
|
||||||
jackbox_walk_blocked:
|
|
||||||
xor eax, eax
|
|
||||||
ret
|
|
||||||
JackboxCanWalk3D ENDP
|
|
||||||
|
|
||||||
PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD
|
PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD
|
||||||
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
|
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
|
||||||
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0
|
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
MODE_JACKBOX equ 28
|
MODE_JACKBOX equ 28
|
||||||
INTERACT_JACKBOX equ 48
|
INTERACT_JACKBOX equ 48
|
||||||
|
|
||||||
JACKBOX_X equ 6*FP_1
|
JACKBOX_X equ FP_1*306/10
|
||||||
JACKBOX_Y equ 20*FP_1
|
JACKBOX_Y equ 25*FP_1
|
||||||
JACKBOX_TRACK_COUNT equ 10
|
JACKBOX_TRACK_COUNT equ 10
|
||||||
|
|
||||||
MATERIAL_JACKBOX_FRONT equ 161
|
MATERIAL_JACKBOX_FRONT equ 161
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,124 @@
|
||||||
"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.
|
"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.
|
||||||
|
|
||||||
Processes high-resolution source images, removes studio grey background with clean
|
Processes high-resolution source images into a solid rectangular cabinet ('прямоугольный шкаф'):
|
||||||
alpha masking and anti-aliasing, aligns 3D cabinet surfaces (Front, Side, Back),
|
- Front: Arched jukebox art framed in rich dark mahogany wood casing (solid 256x512 BGRA).
|
||||||
and outputs binary packs (ANM1).
|
- 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
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import struct
|
import struct
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
SOURCE = ROOT / "assets/sprites/jackbox"
|
SOURCE = ROOT / "assets/sprites/jackbox"
|
||||||
OUT = ROOT / "assets/animations/hall"
|
OUT = ROOT / "assets/animations/hall"
|
||||||
|
|
||||||
|
# Deep rich dark walnut / mahogany wood color (BGR)
|
||||||
|
WOOD_COLOR = np.array([16, 26, 48], dtype=np.uint8)
|
||||||
|
|
||||||
def clean_cutout(img_bgr: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
||||||
"""Segment studio grey background using connected components from image borders."""
|
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]
|
h, w = img_bgr.shape[:2]
|
||||||
arr = img_bgr.astype(np.float32)
|
|
||||||
|
|
||||||
# Detect neutral studio grey pixels: low saturation and mid-low brightness
|
# Detect studio grey background
|
||||||
color_diff = np.maximum(
|
diff = np.maximum(
|
||||||
np.abs(arr[:, :, 0] - arr[:, :, 1]),
|
np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 1].astype(int)),
|
||||||
np.maximum(np.abs(arr[:, :, 1] - arr[:, :, 2]), np.abs(arr[:, :, 0] - arr[:, :, 2]))
|
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)))
|
||||||
)
|
)
|
||||||
brightness = np.mean(arr, axis=2)
|
bright = np.mean(img_bgr, axis=2)
|
||||||
is_bg_cand = (color_diff < 10) & (brightness >= 18) & (brightness <= 65)
|
is_bg = (diff < 10) & (bright >= 18) & (bright <= 65)
|
||||||
|
|
||||||
# Seed from all 4 borders
|
# Seed from borders
|
||||||
seed_mask = np.zeros((h, w), dtype=np.uint8)
|
seed_mask = np.zeros((h, w), dtype=np.uint8)
|
||||||
seed_mask[0, :] = is_bg_cand[0, :]
|
seed_mask[0, :] = is_bg[0, :]
|
||||||
seed_mask[-1, :] = is_bg_cand[-1, :]
|
seed_mask[-1, :] = is_bg[-1, :]
|
||||||
seed_mask[:, 0] = is_bg_cand[:, 0]
|
seed_mask[:, 0] = is_bg[:, 0]
|
||||||
seed_mask[:, -1] = is_bg_cand[:, -1]
|
seed_mask[:, -1] = is_bg[:, -1]
|
||||||
|
|
||||||
num_labels, labels = cv2.connectedComponents(is_bg_cand.astype(np.uint8))
|
num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8))
|
||||||
border_labels = np.unique(labels[seed_mask == 1])
|
border_labels = np.unique(labels[seed_mask == 1])
|
||||||
border_labels = border_labels[border_labels != 0]
|
border_labels = border_labels[border_labels != 0]
|
||||||
|
is_real_bg = np.isin(labels, border_labels)
|
||||||
|
|
||||||
is_bg = np.isin(labels, border_labels)
|
# Replace studio grey background with dark mahogany wood casing
|
||||||
clean_fg = (~is_bg).astype(np.uint8)
|
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)
|
||||||
|
|
||||||
# Fill internal holes in foreground if any
|
# Resize to 256x512 BGRA, 100% opaque (no transparency gaps!)
|
||||||
num_fg, fg_labels = cv2.connectedComponents(clean_fg)
|
resized = cv2.resize(front_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4)
|
||||||
fg_sizes = np.bincount(fg_labels.flatten())
|
bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA)
|
||||||
fg_sizes[0] = 0
|
bgra[:, :, 3] = 255
|
||||||
main_fg_label = np.argmax(fg_sizes)
|
return bgra
|
||||||
clean_fg = (fg_labels == main_fg_label).astype(np.uint8)
|
|
||||||
|
|
||||||
# Anti-aliased alpha falloff via distance transform
|
|
||||||
dist_in = cv2.distanceTransform(clean_fg, cv2.DIST_L2, 3)
|
|
||||||
dist_out = cv2.distanceTransform(1 - clean_fg, cv2.DIST_L2, 3)
|
|
||||||
alpha = np.clip((dist_in - dist_out + 0.5) * 255.0, 0, 255).astype(np.uint8)
|
|
||||||
|
|
||||||
bgra = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2BGRA)
|
def make_solid_side(img_bgr: np.ndarray) -> np.ndarray:
|
||||||
bgra[:, :, 3] = alpha
|
"""Crop side image to a solid rectangular cabinet side panel."""
|
||||||
return bgra, clean_fg
|
# 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):
|
def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int):
|
||||||
|
|
@ -78,31 +139,18 @@ def main():
|
||||||
assert side_path.exists(), f"Missing {side_path}"
|
assert side_path.exists(), f"Missing {side_path}"
|
||||||
assert ui_path.exists(), f"Missing {ui_path}"
|
assert ui_path.exists(), f"Missing {ui_path}"
|
||||||
|
|
||||||
print("Extracting and segmenting Jukebox textures...")
|
print("Building solid rectangular Jukebox cabinet textures...")
|
||||||
|
|
||||||
# 1. Front Texture
|
# 1. Front Texture: solid rectangular face
|
||||||
front_bgr = cv2.imread(str(front_path))
|
front_bgr = cv2.imread(str(front_path))
|
||||||
front_bgra, front_fg = clean_cutout(front_bgr)
|
front_tex = make_solid_front(front_bgr)
|
||||||
front_tex = cv2.resize(front_bgra, (256, 512), interpolation=cv2.INTER_LANCZOS4)
|
|
||||||
|
|
||||||
# 2. Side Texture: clean, crop margins, flip horizontally so back is x=0 and front is x=255
|
# 2. Side Texture: solid rectangular side panel
|
||||||
side_bgr = cv2.imread(str(side_path))
|
side_bgr = cv2.imread(str(side_path))
|
||||||
side_bgra, side_fg = clean_cutout(side_bgr)
|
side_tex = make_solid_side(side_bgr)
|
||||||
# Crop to actual jukebox silhouette (remove studio floor and top empty margins)
|
|
||||||
side_cropped = side_bgra[80:2912, 0:1350]
|
|
||||||
side_flipped = cv2.flip(side_cropped, 1)
|
|
||||||
side_tex = cv2.resize(side_flipped, (256, 512), interpolation=cv2.INTER_LANCZOS4)
|
|
||||||
|
|
||||||
# 3. Rear Texture: authentic dark walnut cabinet panel with ventilation slots and arch top
|
# 3. Rear Texture: solid dark walnut panel
|
||||||
rear_tex = np.zeros((512, 256, 4), dtype=np.uint8)
|
rear_tex = make_solid_back()
|
||||||
for y in range(512):
|
|
||||||
grain = int(math.sin(y * 0.2) * 5 + math.sin(y * 0.05) * 8)
|
|
||||||
rear_tex[y, :, 0] = np.clip(20 + grain, 0, 255) # B
|
|
||||||
rear_tex[y, :, 1] = np.clip(28 + grain + 2, 0, 255) # G
|
|
||||||
rear_tex[y, :, 2] = np.clip(45 + grain + 5, 0, 255) # R
|
|
||||||
for y in range(120, 220, 10):
|
|
||||||
rear_tex[y:y+3, 40:216, :3] = 12 # Dark ventilation slots
|
|
||||||
rear_tex[:, :, 3] = cv2.resize(front_bgra[:, :, 3], (256, 512), interpolation=cv2.INTER_LANCZOS4)
|
|
||||||
|
|
||||||
# Pack 3D world frames: Frame 0 = Front, Frame 1 = Side, Frame 2 = Rear
|
# 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)
|
pack_bgra(OUT / "jackbox-v1.bin", [front_tex, side_tex, rear_tex], 256, 512)
|
||||||
|
|
@ -113,7 +161,7 @@ def main():
|
||||||
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
|
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
|
||||||
pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480)
|
pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480)
|
||||||
|
|
||||||
print("Jukebox asset packaging complete.")
|
print("Solid Jukebox asset packaging complete.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ def test_code_structure():
|
||||||
# Check constants
|
# Check constants
|
||||||
assert "MODE_JACKBOX equ 28" in data_inc
|
assert "MODE_JACKBOX equ 28" in data_inc
|
||||||
assert "INTERACT_JACKBOX equ 48" in data_inc
|
assert "INTERACT_JACKBOX equ 48" in data_inc
|
||||||
assert "JACKBOX_X equ 6*FP_1" in data_inc
|
assert "JACKBOX_X equ FP_1*306/10" in data_inc
|
||||||
assert "JACKBOX_Y equ 20*FP_1" in data_inc
|
assert "JACKBOX_Y equ 25*FP_1" in data_inc
|
||||||
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
|
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
|
||||||
|
|
||||||
# Check procs
|
# Check procs
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Visual simulation of the 3D Jukebox cabinet in the Casino Hall.
|
Visual simulation of the solid rectangular 3D Jukebox cabinet beside billiards.
|
||||||
Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back).
|
Loads assets/animations/hall/jackbox-v1.bin directly (3 frames: Front, Side, Back).
|
||||||
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D with alpha transparency.
|
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D and DrawCityHorizontal3D.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
|
|
@ -56,7 +56,7 @@ def get_tex_pixel(slot, tx, ty):
|
||||||
# 3. Load Jackbox Art pack (ANM1)
|
# 3. Load Jackbox Art pack (ANM1)
|
||||||
jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read()
|
jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read()
|
||||||
magic, jw, jh, jframes, jlen = struct.unpack('<4s4I', jackbox_raw[:20])
|
magic, jw, jh, jframes, jlen = struct.unpack('<4s4I', jackbox_raw[:20])
|
||||||
assert magic == b'ANM1' and (jw, jh, jframes) == (256, 512, 3), f'Invalid pack: {magic}, {jw}x{jh}, {jframes}'
|
assert magic == b'ANM1' and (jw, jh, jframes) == (256, 512, 3)
|
||||||
jpixels = jackbox_raw[20:]
|
jpixels = jackbox_raw[20:]
|
||||||
|
|
||||||
def get_jackbox_pixel(frame, tx, ty):
|
def get_jackbox_pixel(frame, tx, ty):
|
||||||
|
|
@ -67,7 +67,7 @@ def get_jackbox_pixel(frame, tx, ty):
|
||||||
tableCos = [round(math.cos(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
|
tableCos = [round(math.cos(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
|
||||||
tableSin = [round(math.sin(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
|
tableSin = [round(math.sin(i * 2 * math.pi / 512) * FP_1) for i in range(512)]
|
||||||
|
|
||||||
def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
|
def render_scene(cam_x, cam_y, cam_angle, out_path):
|
||||||
playerX = int(cam_x * FP_1)
|
playerX = int(cam_x * FP_1)
|
||||||
playerY = int(cam_y * FP_1)
|
playerY = int(cam_y * FP_1)
|
||||||
playerAngle = cam_angle % 512
|
playerAngle = cam_angle % 512
|
||||||
|
|
@ -181,7 +181,7 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
|
||||||
screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
|
screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
|
||||||
depthBuffer[y * SCREEN_W + col] = perpWallDist
|
depthBuffer[y * SCREEN_W + col] = perpWallDist
|
||||||
|
|
||||||
def draw_vertical(axis, coord, loBound, hiBound, zLow, zHigh, frame_idx):
|
def draw_vert(axis, coord, loBound, hiBound, zLow, zHigh, frame_idx):
|
||||||
for col in range(SCREEN_W):
|
for col in range(SCREEN_W):
|
||||||
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
|
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
|
||||||
if abs(ray_dir) < 256:
|
if abs(ray_dir) < 256:
|
||||||
|
|
@ -218,30 +218,57 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
|
||||||
if srcY < 0 or srcY >= jh:
|
if srcY < 0 or srcY >= jh:
|
||||||
continue
|
continue
|
||||||
r, g, b, a = get_jackbox_pixel(frame_idx, srcX, srcY)
|
r, g, b, a = get_jackbox_pixel(frame_idx, srcX, srcY)
|
||||||
# Alpha testing for materials >= 104 (cmp eax, 80000000h; jb city_vertical_next_pixel)
|
|
||||||
if a < 128:
|
|
||||||
continue
|
|
||||||
if depth < depthBuffer[row * SCREEN_W + col]:
|
if depth < depthBuffer[row * SCREEN_W + col]:
|
||||||
depthBuffer[row * SCREEN_W + col] = depth
|
depthBuffer[row * SCREEN_W + col] = depth
|
||||||
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
|
screen_pixels[row * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
|
||||||
|
|
||||||
# Footprint: width 0.75m, depth 0.55m, height 1.85m
|
# 1. Pool table geometry: X in [26.0, 28.0], Y in [24.0, 26.0], Z in [0, 0.85m]
|
||||||
wx = int(jb_x * FP_1)
|
def draw_pool_side(axis, coord, lo, hi, z0, z1):
|
||||||
wy = int(jb_y * FP_1)
|
for col in range(SCREEN_W):
|
||||||
x0 = wx - FP_1 * 15 // 40
|
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
|
||||||
x1 = wx + FP_1 * 15 // 40
|
if abs(ray_dir) < 256:
|
||||||
y0 = wy - FP_1 * 11 // 40
|
continue
|
||||||
y1 = wy + FP_1 * 11 // 40
|
orig = playerX if axis == 0 else playerY
|
||||||
zHigh = FP_1 * 37 // 20
|
num = (coord - orig)
|
||||||
|
depth = (num << 16) // ray_dir
|
||||||
|
if depth <= 4096 or depth >= zBuffer[col]:
|
||||||
|
continue
|
||||||
|
along = playerY + ((depth * cityRayY[col]) >> 16) if axis == 0 else playerX + ((depth * cityRayX[col]) >> 16)
|
||||||
|
if along < lo or along >= hi:
|
||||||
|
continue
|
||||||
|
top = (HALL_STANDING_EYE - z1) * SCREEN_H // depth + HALF_H
|
||||||
|
bottom = (HALL_STANDING_EYE - z0) * SCREEN_H // depth + HALF_H
|
||||||
|
r_start = max(0, top)
|
||||||
|
r_end = min(SCREEN_H, bottom)
|
||||||
|
shade = max(40, min(255, 255 - (depth >> 12)))
|
||||||
|
for row in range(r_start, r_end):
|
||||||
|
if depth < depthBuffer[row * SCREEN_W + col]:
|
||||||
|
depthBuffer[row * SCREEN_W + col] = depth
|
||||||
|
screen_pixels[row * SCREEN_W + col] = ((50 * shade) >> 8, (25 * shade) >> 8, (15 * shade) >> 8)
|
||||||
|
|
||||||
# 1. Rear Face (Y = y0) - Frame 2
|
draw_pool_side(1, int(24.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
|
||||||
draw_vertical(1, y0, x0, x1, 0, zHigh, 2)
|
draw_pool_side(1, int(26.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_1))
|
||||||
# 2. Left Side (X = x0) - Frame 1
|
draw_pool_side(0, int(26.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
|
||||||
draw_vertical(0, x0, y0, y1, 0, zHigh, 1)
|
draw_pool_side(0, int(28.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
|
||||||
# 3. Right Side (X = x1) - Frame 1
|
|
||||||
draw_vertical(0, x1, y0, y1, 0, zHigh, 1)
|
# 2. Solid Rectangular Jukebox Cabinet (at East wall, next to billiards table!)
|
||||||
# 4. Front Face (Y = y1) - Frame 0
|
# Depth: X in [30.40m .. 30.95m] (back against East wall at 31.0m)
|
||||||
draw_vertical(1, y1, x0, x1, 0, zHigh, 0)
|
# Width: Y in [24.575m .. 25.425m] (centered at Y = 25.0m)
|
||||||
|
# Height: Z in [0 .. 1.85m]
|
||||||
|
x0 = int(30.40 * FP_1)
|
||||||
|
x1 = int(30.95 * FP_1)
|
||||||
|
y0 = int(24.575 * FP_1)
|
||||||
|
y1 = int(25.425 * FP_1)
|
||||||
|
zHigh = int(1.85 * FP_1)
|
||||||
|
|
||||||
|
# Back Face (against wall: X = x1) - Frame 2
|
||||||
|
draw_vert(0, x1, y0, y1, 0, zHigh, 2)
|
||||||
|
# North Side (Y = y0) - Frame 1
|
||||||
|
draw_vert(1, y0, x0, x1, 0, zHigh, 1)
|
||||||
|
# South Side (Y = y1) - Frame 1
|
||||||
|
draw_vert(1, y1, x0, x1, 0, zHigh, 1)
|
||||||
|
# Front Face (facing West towards billiards table: X = x0) - Frame 0
|
||||||
|
draw_vert(0, x0, y0, y1, 0, zHigh, 0)
|
||||||
|
|
||||||
img = Image.new('RGB', (SCREEN_W, SCREEN_H))
|
img = Image.new('RGB', (SCREEN_W, SCREEN_H))
|
||||||
img.putdata(screen_pixels)
|
img.putdata(screen_pixels)
|
||||||
|
|
@ -249,13 +276,12 @@ def render_scene(cam_x, cam_y, cam_angle, jb_x, jb_y, out_path):
|
||||||
print(f'Rendered: {out_path.name}')
|
print(f'Rendered: {out_path.name}')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
jb_x, jb_y = 6.0, 20.0
|
# 1. Front View: Standing near billiards at (26.0, 25.0) looking East (angle=0) past table at Jukebox
|
||||||
# 1. Front View (from South looking North directly at Front)
|
render_scene(26.0, 25.0, 0, DOCS_DIR / 'jukebox_3d_front.png')
|
||||||
render_scene(6.0, 22.2, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_front.png')
|
# 2. Angle View: Standing at (27.5, 22.5) looking South-East (angle=480) at pool table and Jukebox
|
||||||
# 2. 45-degree angle View (from South-East looking North-West)
|
render_scene(27.5, 22.5, 480, DOCS_DIR / 'jukebox_3d_angle.png')
|
||||||
render_scene(7.2, 21.8, 416, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_angle.png')
|
# 3. Close-up View: Standing at (29.2, 25.0) looking East (angle=0) right at Jukebox
|
||||||
# 3. Side View (from East looking West)
|
render_scene(29.2, 25.0, 0, DOCS_DIR / 'jukebox_3d_close.png')
|
||||||
render_scene(8.0, 20.0, 128, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_side.png')
|
# 4. Side View: Standing at (30.0, 27.5) looking North (angle=384) at South side of Jukebox
|
||||||
# 4. Close-up Front View (standing right at the jukebox)
|
render_scene(30.0, 27.5, 384, DOCS_DIR / 'jukebox_3d_side.png')
|
||||||
render_scene(6.0, 21.1, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_close.png')
|
|
||||||
print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')
|
print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')
|
||||||
|
|
|
||||||