KAZIK/tools/tests/test_jukebox_render_all_angles.py

367 lines
15 KiB
Python

# -*- coding: utf-8 -*-
"""
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 and DrawCityHorizontal3D.
"""
import math
import struct
import re
from pathlib import Path
import numpy as np
from PIL import Image
REPO_ROOT = Path(__file__).resolve().parents[2]
DOCS_DIR = REPO_ROOT / 'docs' / 'previews_minigames'
DOCS_DIR.mkdir(parents=True, exist_ok=True)
FP_1 = 65536
SCREEN_W = 640
SCREEN_H = 480
HALF_W = 320
HALF_H = 240
HALL_STANDING_EYE = FP_1 * 21 // 20
# 1. Load map
with open(REPO_ROOT / 'casino3d_data.inc', 'r', encoding='latin1') as f:
text = f.read()
lines = text.splitlines()
map_lines = []
capturing = False
for l in lines:
if 'casinoMap db' in l:
capturing = True
if capturing:
map_lines.append(l)
if len(map_lines) == 32:
break
casinoMap = []
for l in map_lines:
vals = [int(x.strip()) for x in re.findall(r'\d+', l)]
casinoMap.extend(vals)
# 2. Load HD textures
tex_raw = open(REPO_ROOT / 'assets/textures/casino3d-hd.bin', 'rb').read()
slot_size = 128 * 128 * 4
def get_tex_pixel(slot, tx, ty):
idx = slot * slot_size + (ty * 128 + tx) * 4
if idx + 4 > len(tex_raw):
return (50, 50, 50, 255)
b, g, r, a = tex_raw[idx:idx+4]
return (r, g, b, a)
# 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)
jpixels = jackbox_raw[20:]
# 4. Load Terminals Art pack (ANM1) for authentic wooden side panels (Frame 3)
term_raw = open(REPO_ROOT / 'assets/animations/hall/terminals-v2.bin', 'rb').read()
tmagic, tw, th, tframes, tlen = struct.unpack('<4s4I', term_raw[:20])
tpixels = term_raw[20:]
def get_terminal_pixel(frame, tx, ty):
idx = (frame * tw * th + ty * tw + tx) * 4
b, g, r, a = tpixels[idx:idx+4]
return (r, g, b, a)
def get_jackbox_pixel(frame, tx, ty):
idx = frame * (jw * jh * 4) + (ty * jw + tx) * 4
b, g, r, a = jpixels[idx:idx+4]
return (r, g, b, a)
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, out_path):
playerX = int(cam_x * FP_1)
playerY = int(cam_y * FP_1)
playerAngle = cam_angle % 512
playerDirX = tableCos[playerAngle]
playerDirY = tableSin[playerAngle]
playerPlaneX = -66 * playerDirY // 100
playerPlaneY = 66 * playerDirX // 100
screen_pixels = [(20, 20, 25)] * (SCREEN_W * SCREEN_H)
zBuffer = [0x7FFFFFFF] * SCREEN_W
depthBuffer = [0x7FFFFFFF] * (SCREEN_W * SCREEN_H)
# Floor & Ceiling
for y in range(HALF_H, SCREEN_H):
pDist = (HALF_H * FP_1) // (y - HALF_H + 1)
shade = max(35, min(255, 255 - (pDist >> 13)))
cShade = max(20, shade - 25)
fStepX = (playerPlaneX * 2 * pDist // FP_1) // SCREEN_W
fStepY = (playerPlaneY * 2 * pDist // FP_1) // SCREEN_W
fX = playerX + ((playerDirX - playerPlaneX) * pDist // FP_1)
fY = playerY + ((playerDirY - playerPlaneY) * pDist // FP_1)
for x in range(SCREEN_W):
tx = (fX >> 9) & 127
ty = (fY >> 9) & 127
r, g, b, _ = get_tex_pixel(0, tx, ty)
screen_pixels[y * SCREEN_W + x] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
cx = (fX >> 10) & 127
cy = (fY >> 10) & 127
cr, cg, cb, _ = get_tex_pixel(5, cx, cy)
screen_pixels[(SCREEN_H - 1 - y) * SCREEN_W + x] = ((cr * cShade) >> 8, (cg * cShade) >> 8, (cb * cShade) >> 8)
fX += fStepX
fY += fStepY
cityRayX = [0] * SCREEN_W
cityRayY = [0] * SCREEN_W
for col in range(SCREEN_W):
camX = (2 * col - SCREEN_W) * FP_1 // SCREEN_W
cityRayX[col] = playerDirX + (playerPlaneX * camX // FP_1)
cityRayY[col] = playerDirY + (playerPlaneY * camX // FP_1)
# Walls
for col in range(SCREEN_W):
rayDirX = cityRayX[col]
rayDirY = cityRayY[col]
mapX = playerX >> 16
mapY = playerY >> 16
deltaDistX = (1 << 32) // abs(rayDirX) if rayDirX != 0 else 0x7FFFFFFF
deltaDistY = (1 << 32) // abs(rayDirY) if rayDirY != 0 else 0x7FFFFFFF
if rayDirX < 0:
stepX = -1
sideDistX = ((playerX - (mapX << 16)) * deltaDistX) >> 16
else:
stepX = 1
sideDistX = (((mapX + 1 << 16) - playerX) * deltaDistX) >> 16
if rayDirY < 0:
stepY = -1
sideDistY = ((playerY - (mapY << 16)) * deltaDistY) >> 16
else:
stepY = 1
sideDistY = (((mapY + 1 << 16) - playerY) * deltaDistY) >> 16
side = 0
hit = 0
steps = 0
while hit == 0 and steps < 64:
steps += 1
if sideDistX < sideDistY:
sideDistX += deltaDistX
mapX += stepX
side = 0
else:
sideDistY += deltaDistY
mapY += stepY
side = 1
if 0 <= mapX < 32 and 0 <= mapY < 32:
tile = casinoMap[mapY * 32 + mapX]
if tile != 0:
hit = tile
else:
hit = 1
perpWallDist = sideDistX - deltaDistX if side == 0 else sideDistY - deltaDistY
perpWallDist = max(2048, perpWallDist)
zBuffer[col] = perpWallDist
lineH = (SCREEN_H * FP_1) // perpWallDist
drawStart = max(0, HALF_H - lineH // 2)
drawEnd = min(SCREEN_H - 1, HALF_H + lineH // 2)
wallHit = playerY + ((perpWallDist * rayDirY) >> 16) if side == 0 else playerX + ((perpWallDist * rayDirX) >> 16)
texX = ((wallHit & 0xFFFF) * 128) >> 16
if (side == 0 and rayDirX > 0) or (side == 1 and rayDirY < 0):
texX = 127 - texX
shade = max(40, min(255, 255 - (perpWallDist >> 12)))
if side == 1:
shade = (shade * 180) >> 8
texId = hit if hit != 0 else 1
for y in range(drawStart, drawEnd):
texY = (((y * 2 - SCREEN_H + lineH) * 128) // (lineH * 2)) & 127
r, g, b, _ = get_tex_pixel(texId, texX, texY)
screen_pixels[y * SCREEN_W + col] = ((r * shade) >> 8, (g * shade) >> 8, (b * shade) >> 8)
depthBuffer[y * SCREEN_W + col] = perpWallDist
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:
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 < loBound or along >= hiBound:
continue
srcX = (along - loBound) * jw // (hiBound - loBound)
if srcX < 0 or srcX >= jw:
continue
top = (HALL_STANDING_EYE - zHigh) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - zLow) * SCREEN_H // depth + HALF_H
span = bottom - top
if span <= 0:
continue
stepY = (jh << 16) // span
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
curY = (r_start - top) * stepY
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
srcY = (curY >> 16)
curY += stepY
if srcY < 0 or srcY >= jh:
continue
r, g, b, a = get_jackbox_pixel(frame_idx, srcX, srcY)
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)
# 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)
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. Wall Art / Painting 116 on East Wall: X = 30.98m, Y in [25.0m .. 26.5m], Z in [1.0m .. 2.0m]
def draw_painting(axis, coord, lo, hi, z0, z1, slot):
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
span = bottom - top
if span <= 0:
continue
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
span_along = hi - lo
tx = int((along - lo) * 128 / span_along) % 128
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
ty = int((row - top) * 128 / span) % 128
r, g, b, a = get_tex_pixel(slot, tx, ty)
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)
def draw_term_vert(axis, coord, lo, hi, 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:
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
span_along = hi - lo
srcX = int((along - lo) * tw / span_along)
if srcX < 0 or srcX >= tw:
continue
top = (HALL_STANDING_EYE - zHigh) * SCREEN_H // depth + HALF_H
bottom = (HALL_STANDING_EYE - zLow) * SCREEN_H // depth + HALF_H
span = bottom - top
if span <= 0:
continue
stepY = (th << 16) // span
r_start = max(0, top)
r_end = min(SCREEN_H, bottom)
curY = (r_start - top) * stepY
shade = max(40, min(255, 255 - (depth >> 12)))
for row in range(r_start, r_end):
srcY = (curY >> 16)
curY += stepY
if srcY < 0 or srcY >= th:
continue
r, g, b, a = get_terminal_pixel(frame_idx, srcX, srcY)
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)
draw_painting(0, int(30.98 * FP_1), int(25.0 * FP_1), int(26.5 * FP_1), FP_1, 2 * FP_1, 116)
# 3. Solid Rectangular Jukebox Cabinet (at East wall, Y = 23.5m, clearing Painting 116!)
# Depth: X in [30.40m .. 30.95m] (back against East wall at 31.0m)
# Width: Y in [23.075m .. 23.925m] (centered at Y = 23.5m)
# Height: Z in [0 .. 1.85m]
x0 = int(30.40 * FP_1)
x1 = int(30.95 * FP_1)
y0 = int(23.075 * FP_1)
y1 = int(23.925 * 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) - Authentic terminal wooden panel (Frame 3)
draw_term_vert(1, y0, x0, x1, 0, zHigh, 3)
# South Side (Y = y1) - Authentic terminal wooden panel (Frame 3)
draw_term_vert(1, y1, x0, x1, 0, zHigh, 3)
# 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)
img.save(str(out_path))
print(f'Rendered: {out_path.name}')
if __name__ == '__main__':
# 1. Front View: Standing near billiards at (26.0, 25.0) looking East (angle=0) past table at Jukebox
render_scene(26.0, 24.5, 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(28.8, 25.0, 960, 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, 23.5, 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.6, 25.5, 768, DOCS_DIR / 'jukebox_3d_side.png')
print('All Jukebox 3D preview frames rendered to docs/previews_minigames/!')