fix(jukebox): make cabinet solid rectangular with zero gaps, relocate to East wall beside billiards

This commit is contained in:
Игорь Федосеев 2026-10-25 16:43:43 +03:00
parent 0a85e271e9
commit 6bf2a2885c
15 changed files with 217 additions and 147 deletions

Binary file not shown.

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "7488391B71FCEB1C2A2F68C06445CC1421A650793205498BA19E787C9E184297";
const string ExpectedPokerSha256 = "07D474A0223390E7AC4F4F779818D77CC2DAE5123F4BDB0F72DD975473267310";
[STAThread]
static void Main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 187 KiB

BIN
poker.exe

Binary file not shown.

View file

@ -52,60 +52,56 @@ jackbox_surface_missing:
ret
ResolveJackboxSurface3D ENDP
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
; Footprint: width 0.75m, depth 0.55m, height 1.85m
mov eax, wx
sub eax, FP_1*15/40
mov x0, eax
add eax, FP_1*15/20
mov x1, eax
mov eax, wy
sub eax, FP_1*11/40
mov y0, eax
add eax, FP_1*11/20
mov y1, eax
; Rear: North face (Y = y0)
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
; Sides: West face (X = x0) and East face (X = x1)
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
; Front: South face (Y = y1)
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
ret
DrawJackboxCabinet3D ENDP
RenderJackboxCabinet3D PROC
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
ret
RenderJackboxCabinet3D ENDP
JackboxCanWalk3D PROC px:DWORD, py:DWORD
mov eax, py
sub eax, JACKBOX_Y
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_1*11/20
ja jackbox_walk_clear
mov eax, px
sub eax, JACKBOX_X
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_1*15/20
jbe jackbox_walk_blocked
jackbox_walk_clear:
mov eax, 1
ret
jackbox_walk_blocked:
xor eax, eax
ret
JackboxCanWalk3D ENDP
DrawJackboxCabinet3D PROC uses ebx esi edi wx:DWORD, wy:DWORD
local x0:DWORD, x1:DWORD, y0:DWORD, y1:DWORD
; Solid rectangular cabinet against East wall beside billiards:
; Depth along X: 0.55m (x0 = 30.40m, x1 = 30.95m against wall at 31.0m)
; Width along Y: 0.85m (y0 = 24.575m, y1 = 25.425m, centered at Y = 25.0m)
; Height Z: 1.85m
mov x0, FP_1*304/10
mov x1, FP_1*3095/100
mov y0, 25*FP_1 - FP_1*425/1000
mov y1, 25*FP_1 + FP_1*425/1000
; 1. Back Face (against East wall: X = x1)
invoke DrawCityVertical3D, 0, x1, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_BACK
; 2. North Side (Y = y0)
invoke DrawCityVertical3D, 1, y0, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
; 3. South Side (Y = y1)
invoke DrawCityVertical3D, 1, y1, x0, x1, 0, FP_1*37/20, MATERIAL_JACKBOX_SIDE
; 4. Front Face (facing West towards billiards: X = x0)
invoke DrawCityVertical3D, 0, x0, y0, y1, 0, FP_1*37/20, MATERIAL_JACKBOX_FRONT
; 5. Top Lid (horizontal cap at z = 1.85m)
invoke DrawCityHorizontal3D, x0, x1, y0, y1, FP_1*37/20, 110
ret
DrawJackboxCabinet3D ENDP
RenderJackboxCabinet3D PROC
invoke DrawJackboxCabinet3D, JACKBOX_X, JACKBOX_Y
ret
RenderJackboxCabinet3D ENDP
JackboxCanWalk3D PROC px:DWORD, py:DWORD
mov eax, px
cmp eax, FP_1*303/10
jb jackbox_walk_clear
cmp eax, 31*FP_1+9830
ja jackbox_walk_clear
mov eax, py
sub eax, 25*FP_1
test eax, eax
jns @F
neg eax
@@:
cmp eax, FP_1*55/100
ja jackbox_walk_clear
xor eax, eax
ret
jackbox_walk_clear:
mov eax, 1
ret
JackboxCanWalk3D ENDP
PlayJukeboxTrack3D PROC uses ebx trackIndex:DWORD
invoke mciSendStringA, OFFSET jboxStopCmd, 0, 0, 0
invoke mciSendStringA, OFFSET jboxCloseCmd, 0, 0, 0

View file

@ -5,8 +5,8 @@
MODE_JACKBOX equ 28
INTERACT_JACKBOX equ 48
JACKBOX_X equ 6*FP_1
JACKBOX_Y equ 20*FP_1
JACKBOX_X equ FP_1*306/10
JACKBOX_Y equ 25*FP_1
JACKBOX_TRACK_COUNT equ 10
MATERIAL_JACKBOX_FRONT equ 161

View file

@ -1,63 +1,124 @@
"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer.
Processes high-resolution source images, removes studio grey background with clean
alpha masking and anti-aliasing, aligns 3D cabinet surfaces (Front, Side, Back),
and outputs binary packs (ANM1).
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
from PIL import Image
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 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]
arr = img_bgr.astype(np.float32)
# Detect neutral studio grey pixels: low saturation and mid-low brightness
color_diff = np.maximum(
np.abs(arr[:, :, 0] - arr[:, :, 1]),
np.maximum(np.abs(arr[:, :, 1] - arr[:, :, 2]), np.abs(arr[:, :, 0] - arr[:, :, 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)))
)
brightness = np.mean(arr, axis=2)
is_bg_cand = (color_diff < 10) & (brightness >= 18) & (brightness <= 65)
bright = np.mean(img_bgr, axis=2)
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[0, :] = is_bg_cand[0, :]
seed_mask[-1, :] = is_bg_cand[-1, :]
seed_mask[:, 0] = is_bg_cand[:, 0]
seed_mask[:, -1] = is_bg_cand[:, -1]
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_cand.astype(np.uint8))
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)
is_bg = np.isin(labels, border_labels)
clean_fg = (~is_bg).astype(np.uint8)
# 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)
# Fill internal holes in foreground if any
num_fg, fg_labels = cv2.connectedComponents(clean_fg)
fg_sizes = np.bincount(fg_labels.flatten())
fg_sizes[0] = 0
main_fg_label = np.argmax(fg_sizes)
clean_fg = (fg_labels == main_fg_label).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
# 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)
bgra[:, :, 3] = alpha
return bgra, clean_fg
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):
@ -78,31 +139,18 @@ def main():
assert side_path.exists(), f"Missing {side_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_bgra, front_fg = clean_cutout(front_bgr)
front_tex = cv2.resize(front_bgra, (256, 512), interpolation=cv2.INTER_LANCZOS4)
front_tex = make_solid_front(front_bgr)
# 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_bgra, side_fg = clean_cutout(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)
side_tex = make_solid_side(side_bgr)
# 3. Rear Texture: authentic dark walnut cabinet panel with ventilation slots and arch top
rear_tex = np.zeros((512, 256, 4), dtype=np.uint8)
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)
# 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)
@ -113,7 +161,7 @@ def main():
ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4)
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__":

View file

@ -63,8 +63,8 @@ def test_code_structure():
# Check constants
assert "MODE_JACKBOX equ 28" in data_inc
assert "INTERACT_JACKBOX equ 48" in data_inc
assert "JACKBOX_X equ 6*FP_1" in data_inc
assert "JACKBOX_Y equ 20*FP_1" in data_inc
assert "JACKBOX_X equ FP_1*306/10" in data_inc
assert "JACKBOX_Y equ 25*FP_1" in data_inc
assert "MATERIAL_JACKBOX_BACK equ 163" in data_inc
# Check procs

View file

@ -1,8 +1,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).
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D with alpha transparency.
Renders pixel-perfect 640x480 views matching MASM32 DrawCityVertical3D and DrawCityHorizontal3D.
"""
import math
import struct
@ -56,7 +56,7 @@ def get_tex_pixel(slot, tx, ty):
# 3. Load Jackbox Art pack (ANM1)
jackbox_raw = open(REPO_ROOT / 'assets/animations/hall/jackbox-v1.bin', 'rb').read()
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:]
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)]
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)
playerY = int(cam_y * FP_1)
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)
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):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
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:
continue
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]:
depthBuffer[row * SCREEN_W + col] = depth
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
wx = int(jb_x * FP_1)
wy = int(jb_y * FP_1)
x0 = wx - FP_1 * 15 // 40
x1 = wx + FP_1 * 15 // 40
y0 = wy - FP_1 * 11 // 40
y1 = wy + FP_1 * 11 // 40
zHigh = FP_1 * 37 // 20
# 1. Pool table geometry: X in [26.0, 28.0], Y in [24.0, 26.0], Z in [0, 0.85m]
def draw_pool_side(axis, coord, lo, hi, z0, z1):
for col in range(SCREEN_W):
ray_dir = cityRayX[col] if axis == 0 else cityRayY[col]
if abs(ray_dir) < 256:
continue
orig = playerX if axis == 0 else playerY
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_vertical(1, y0, x0, x1, 0, zHigh, 2)
# 2. Left Side (X = x0) - Frame 1
draw_vertical(0, x0, y0, y1, 0, zHigh, 1)
# 3. Right Side (X = x1) - Frame 1
draw_vertical(0, x1, y0, y1, 0, zHigh, 1)
# 4. Front Face (Y = y1) - Frame 0
draw_vertical(1, y1, x0, x1, 0, zHigh, 0)
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_pool_side(1, int(26.0*FP_1), int(26.0*FP_1), int(28.0*FP_1), 0, int(0.85*FP_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_pool_side(0, int(28.0*FP_1), int(24.0*FP_1), int(26.0*FP_1), 0, int(0.85*FP_1))
# 2. Solid Rectangular Jukebox Cabinet (at East wall, next to billiards table!)
# Depth: X in [30.40m .. 30.95m] (back against East wall at 31.0m)
# 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.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}')
if __name__ == '__main__':
jb_x, jb_y = 6.0, 20.0
# 1. Front View (from South looking North directly at Front)
render_scene(6.0, 22.2, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_front.png')
# 2. 45-degree angle View (from South-East looking North-West)
render_scene(7.2, 21.8, 416, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_angle.png')
# 3. Side View (from East looking West)
render_scene(8.0, 20.0, 128, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_side.png')
# 4. Close-up Front View (standing right at the jukebox)
render_scene(6.0, 21.1, 384, jb_x, jb_y, DOCS_DIR / 'jukebox_3d_close.png')
# 1. Front View: Standing near billiards at (26.0, 25.0) looking East (angle=0) past table at Jukebox
render_scene(26.0, 25.0, 0, 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
render_scene(27.5, 22.5, 480, DOCS_DIR / 'jukebox_3d_angle.png')
# 3. Close-up View: Standing at (29.2, 25.0) looking East (angle=0) right at Jukebox
render_scene(29.2, 25.0, 0, DOCS_DIR / 'jukebox_3d_close.png')
# 4. Side View: Standing at (30.0, 27.5) looking North (angle=384) at South side of Jukebox
render_scene(30.0, 27.5, 384, DOCS_DIR / 'jukebox_3d_side.png')
print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')