KAZIK/tools/tests/simulate_render.py

274 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
import math
import struct
from PIL import Image
SCREEN_W = 640
SCREEN_H = 480
HALF_W = 320
HALF_H = 240
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
FP_1 = 65536
FP_HALF = 32768
MAP_WIDTH = 32
MAP_HEIGHT = 32
# Load map
with open(REPO_ROOT / 'casino3d_data.inc', 'r', encoding='latin1') as f:
text = f.read()
import re
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)
# Load 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
b, g, r, a = tex_raw[idx:idx+4]
return (r, g, b, a)
# Angle table (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)]
# Player State
playerX = 16 * FP_1 + FP_HALF
playerY = 16 * FP_1 + FP_HALF
playerAngle = 384 # Facing North
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
# 1. Ceiling and Floor
for y in range(HALF_H, SCREEN_H):
floorRow = y
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):
# Floor
tx = (fX >> 9) & 127
ty = (fY >> 9) & 127
r, g, b, _ = get_tex_pixel(0, tx, ty)
r = (r * shade) >> 8
g = (g * shade) >> 8
b = (b * shade) >> 8
screen_pixels[floorRow * SCREEN_W + x] = (r, g, b)
# Ceiling
cx = (fX >> 10) & 127
cy = (fY >> 10) & 127
cr, cg, cb, _ = get_tex_pixel(5, cx, cy)
cr = (cr * cShade) >> 8
cg = (cg * cShade) >> 8
cb = (cb * cShade) >> 8
screen_pixels[(SCREEN_H - 1 - floorRow) * SCREEN_W + x] = (cr, cg, cb)
fX += fStepX
fY += fStepY
# 2. Walls
for col in range(SCREEN_W):
camX = (2 * col - SCREEN_W) * FP_1 // SCREEN_W
rayDirX = playerDirX + (playerPlaneX * camX // FP_1)
rayDirY = playerDirY + (playerPlaneY * camX // FP_1)
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 < MAP_WIDTH and 0 <= mapY < MAP_HEIGHT:
tile = casinoMap[mapY * 32 + mapX]
if tile != 0:
hit = tile
else:
hit = 1
if side == 0:
perpWallDist = sideDistX - deltaDistX
else:
perpWallDist = 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)
if side == 0:
wallHit = playerY + ((perpWallDist * rayDirY) >> 16)
else:
wallHit = 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)
r = (r * shade) >> 8
g = (g * shade) >> 8
b = (b * shade) >> 8
screen_pixels[y * SCREEN_W + col] = (r, g, b)
# 3. Sprites
sprites = [
{'name': 'VIP Dancer 1', 'x': 13 * FP_1 + FP_HALF, 'y': 4 * FP_1, 'tex': 10},
{'name': 'VIP Dancer 2', 'x': 16 * FP_1 + FP_HALF, 'y': 3 * FP_1 + FP_HALF, 'tex': 10},
{'name': 'VIP Dancer 3', 'x': 19 * FP_1 + FP_HALF, 'y': 4 * FP_1, 'tex': 10},
{'name': 'Blackjack', 'x': 12 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 31},
{'name': 'Poker', 'x': 19 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 35},
{'name': 'Casino Roulette', 'x': 16 * FP_1 + FP_HALF, 'y': 9 * FP_1, 'tex': 32},
{'name': 'Classic Roulette', 'x': 16 * FP_1 + FP_HALF, 'y': 12 * FP_1, 'tex': 32},
{'name': 'Slots 777', 'x': 5 * FP_1, 'y': 7 * FP_1, 'tex': 24},
{'name': 'Slots CloverPit', 'x': 5 * FP_1, 'y': 10 * FP_1, 'tex': 24},
{'name': 'Classic Slot', 'x': 5 * FP_1, 'y': 13 * FP_1, 'tex': 24},
{'name': 'Umamusume', 'x': 5 * FP_1, 'y': 16 * FP_1, 'tex': 36},
{'name': 'DBD Skillcheck', 'x': 5 * FP_1, 'y': 19 * FP_1, 'tex': 36},
{'name': 'Cashier / ATM', 'x': 27 * FP_1, 'y': 7 * FP_1, 'tex': 33},
{'name': 'VIP Boutique', 'x': 27 * FP_1, 'y': 10 * FP_1, 'tex': 30},
{'name': 'Online Casino', 'x': 27 * FP_1, 'y': 13 * FP_1, 'tex': 36},
{'name': 'Achievements', 'x': 27 * FP_1, 'y': 16 * FP_1, 'tex': 36},
{'name': 'Stats Board', 'x': 27 * FP_1, 'y': 19 * FP_1, 'tex': 36},
{'name': 'Durak', 'x': 12 * FP_1, 'y': 23 * FP_1, 'tex': 37},
{'name': 'Buckshot Roulette', 'x': 16 * FP_1, 'y': 23 * FP_1, 'tex': 34},
{'name': 'Russian Roulette', 'x': 20 * FP_1, 'y': 23 * FP_1, 'tex': 34},
{'name': 'Craps / Dice', 'x': 12 * FP_1, 'y': 26 * FP_1, 'tex': 38},
{'name': 'Skulls & Cups', 'x': 16 * FP_1, 'y': 26 * FP_1, 'tex': 38},
{'name': 'Coin Flip', 'x': 20 * FP_1, 'y': 26 * FP_1, 'tex': 38},
{'name': 'Training', 'x': 10 * FP_1, 'y': 14 * FP_1, 'tex': 37},
{'name': 'Observer AI', 'x': 22 * FP_1, 'y': 14 * FP_1, 'tex': 37},
{'name': 'Daily Tasks', 'x': 27 * FP_1, 'y': 22 * FP_1, 'tex': 36},
{'name': 'Settings', 'x': 27 * FP_1, 'y': 25 * FP_1, 'tex': 36},
{'name': 'Hell Tower Gate', 'x': 16 * FP_1, 'y': 29 * FP_1, 'tex': 39},
]
# Calculate distances
for s in sprites:
dx = (s['x'] - playerX) >> 8
dy = (s['y'] - playerY) >> 8
s['distSq'] = dx * dx + dy * dy
# Sort far to near
sprites.sort(key=lambda s: s['distSq'], reverse=True)
det = ((playerPlaneX * playerDirY) >> 16) - ((playerPlaneY * playerDirX) >> 16)
if det == 0:
det = 1
for s in sprites:
relX = s['x'] - playerX
relY = s['y'] - playerY
# Standard Lodev inverse camera transform:
# transX = (dirY * relX - dirX * relY) / det
# transY = (-planeY * relX + planeX * relY) / det
numX = ((playerDirY * relX) >> 16) - ((playerDirX * relY) >> 16)
transX = (numX << 16) // det
numY = ((-playerPlaneY * relX) >> 16) + ((playerPlaneX * relY) >> 16)
transY = (numY << 16) // det
# Discard sprites behind camera
if transY <= 2048:
continue
sprScreenX = HALF_W + (transX * HALF_W) // transY
sprH = (SCREEN_H * FP_1) // transY
sprW = sprH
drawStartY = max(0, HALF_H - sprH // 2)
drawEndY = min(SCREEN_H - 1, HALF_H + sprH // 2)
drawStartX = max(0, sprScreenX - sprW // 2)
drawEndX = min(SCREEN_W - 1, sprScreenX + sprW // 2)
shade = max(40, min(255, 255 - (transY >> 12)))
for stripe in range(drawStartX, drawEndX):
if 0 <= stripe < SCREEN_W and transY < zBuffer[stripe]:
texX = ((stripe - (sprScreenX - sprW // 2)) * 128 // sprW) & 127
for y in range(drawStartY, drawEndY):
texY = ((y - (HALF_H - sprH // 2)) * 128 // sprH) & 127
r, g, b, _ = get_tex_pixel(s['tex'], texX, texY)
# Chroma key check: Magenta (0xFF00FF) or pure black (0,0,0)
if (r > 240 and g < 20 and b > 240) or (r == 0 and g == 0 and b == 0):
continue
r = (r * shade) >> 8
g = (g * shade) >> 8
b = (b * shade) >> 8
screen_pixels[y * SCREEN_W + stripe] = (r, g, b)
# Save image
img = Image.new('RGB', (SCREEN_W, SCREEN_H))
img.putdata(screen_pixels)
out_path = REPO_ROOT / 'docs' / 'render_view.png'
img.save(out_path)
print(f'{out_path} saved successfully!')