163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""Stages 3+5: reference voxel builder (mirrors the JS in the HTML),
|
|
hidden-face culling, and isometric painter previews for verification.
|
|
Run after sprite_extract.py in the same directory."""
|
|
import json
|
|
from PIL import Image, ImageDraw
|
|
|
|
data = json.load(open('sprite_data.json'))
|
|
rows = data['rows']
|
|
WHITE, GREY, DARK, BLACK = 0, 1, 2, 3
|
|
PAL = [(0xff, 0xff, 0xff), (0xaa, 0xaa, 0xaa), (0x55, 0x55, 0x55), (0x00, 0x00, 0x00),
|
|
(0xb7, 0xc7, 0x8d), (0xb1, 0xc1, 0x87), (0xd9, 0xd3, 0xc2)]
|
|
GROUND1, GROUND2, PATH = 4, 5, 6
|
|
|
|
inside = lambda x, y: 0 <= x < 64 and 0 <= y < 48 and rows[y][x] != '.'
|
|
col = lambda x, y: int(rows[y][x])
|
|
|
|
D = 26 # wall depth in voxels
|
|
vox = {}
|
|
|
|
def interior_color(sx, sy):
|
|
"""side faces shouldn't be solid outline-black: sample inward past the outline"""
|
|
c = col(sx, sy)
|
|
if c != BLACK:
|
|
return c
|
|
step = 1 if sx < 32 else -1
|
|
for d in range(1, 4):
|
|
nx = sx + step * d
|
|
if inside(nx, sy) and col(nx, sy) != BLACK:
|
|
return col(nx, sy)
|
|
return c
|
|
|
|
# ---- walls: sprite rows 16..47 extruded straight back --------------------
|
|
for sy in range(16, 48):
|
|
y = 47 - sy
|
|
for sx in range(64):
|
|
if not inside(sx, sy):
|
|
continue
|
|
front, mid = col(sx, sy), interior_color(sx, sy)
|
|
for z in range(D):
|
|
vox[(sx, y, z)] = front if (z == 0 or z == D - 1) else mid
|
|
|
|
# ---- awning ledge: rows 24..31 protrude 2 front and back -----------------
|
|
for sy in range(24, 32):
|
|
y = 47 - sy
|
|
for sx in range(64):
|
|
if inside(sx, sy):
|
|
c = col(sx, sy)
|
|
for z in (-2, -1, D, D + 1):
|
|
vox[(sx, y, z)] = c
|
|
|
|
# ---- recess panes: delete the front voxel of every non-black pixel -------
|
|
RECESS = [(16, 23, 17, 23), (32, 39, 17, 23), (40, 47, 17, 23), # gable windows
|
|
(32, 39, 33, 39), (40, 47, 33, 39), # ground windows
|
|
(19, 28, 34, 45)] # door interior
|
|
for x0, x1, y0, y1 in RECESS:
|
|
for sy in range(y0, y1 + 1):
|
|
for sx in range(x0, x1 + 1):
|
|
if inside(sx, sy) and col(sx, sy) != BLACK:
|
|
vox.pop((sx, 47 - sy, 0), None)
|
|
|
|
# ---- roof: flat top over x16..47, DIAGONAL sloped ends (1 down per 2 out)
|
|
ROOF_Z0, ROOF_Z1 = -4, 29
|
|
ROOF_X0, ROOF_X1 = -4, 67
|
|
YTOP = 35
|
|
|
|
def T(x): # elevation profile = the sprite's / \ taper
|
|
if x < 16: return YTOP - ((16 - x + 1) // 2)
|
|
if x > 47: return YTOP - ((x - 47 + 1) // 2)
|
|
return YTOP # [2,1]/[3,1]: horizontal, flat
|
|
|
|
def roof_sy(z):
|
|
df, db = z - ROOF_Z0, ROOF_Z1 - z
|
|
if db <= 6: return db # back rows 0..6 (rim + courses)
|
|
if df <= 8: return 15 - df # front rows 15..7 (fascia + rim)
|
|
return 5 + ((df - 9) % 4) # middle: cycle rows 5..8 (course rhythm)
|
|
|
|
def roof_col(x, z):
|
|
sy = roof_sy(z)
|
|
if 0 <= x < 64 and inside(x, sy):
|
|
return col(x, sy) # real sprite pixels: slats on ends, courses on top
|
|
if x < 16: return WHITE if x % 3 == 1 else DARK
|
|
if x > 47: return WHITE if x % 3 == 2 else DARK
|
|
return GREY
|
|
|
|
for x in range(ROOF_X0, ROOF_X1 + 1):
|
|
t = T(x)
|
|
for z in range(ROOF_Z0, ROOF_Z1 + 1):
|
|
outer = x in (ROOF_X0, ROOF_X1) or z in (ROOF_Z0, ROOF_Z1)
|
|
for y in range(t - 3, t + 1):
|
|
if y == t:
|
|
c = BLACK if outer else roof_col(x, z)
|
|
elif outer:
|
|
c = GREY if y == t - 1 else BLACK
|
|
else:
|
|
c = DARK
|
|
vox[(x, y, z)] = c
|
|
|
|
# trim wall corners that would poke above the sloped ends
|
|
for x in range(0, 64):
|
|
t = T(x)
|
|
if t < 31:
|
|
for y in range(t + 1, 32):
|
|
for z in range(0, D):
|
|
vox.pop((x, y, z), None)
|
|
|
|
# ---- ground plate + path -------------------------------------------------
|
|
for x in range(-10, 75):
|
|
for z in range(-12, 39):
|
|
vox[(x, -1, z)] = GROUND1 if (x + z) & 1 else GROUND2
|
|
for x in range(17, 31):
|
|
for z in range(-12, 0):
|
|
vox[(x, -1, z)] = PATH
|
|
|
|
# ---- cull to shell -------------------------------------------------------
|
|
shell = []
|
|
for (x, y, z), c in vox.items():
|
|
if all((x + dx, y + dy, z + dz) in vox for dx, dy, dz in
|
|
((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))):
|
|
continue
|
|
shell.append((x, y, z, c))
|
|
print('voxels:', len(vox), 'shell:', len(shell))
|
|
|
|
# ---- isometric preview (front = -z, so mirror z for the render) ----------
|
|
S = 4
|
|
ZM = 40
|
|
pts = []
|
|
for x, y, z, c in shell:
|
|
pts.append((x, y, ZM - z, c))
|
|
pts.sort(key=lambda p: (p[0] + p[2], p[1]))
|
|
|
|
def P(x, y, z):
|
|
return (2 * (x - z) * S + 1250, ((x + z) - 2 * y) * S + 300)
|
|
|
|
img = Image.new('RGB', (1800, 900), (0xca, 0xdc, 0x9f))
|
|
dr = ImageDraw.Draw(img)
|
|
has = {(x, y, z) for x, y, z, c in pts}
|
|
def shade(c, f):
|
|
r, g, b = PAL[c]
|
|
return (int(r * f), int(g * f), int(b * f))
|
|
for x, y, z, c in pts:
|
|
if (x, y + 1, z) not in has: # top
|
|
dr.polygon([P(x, y + 1, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 1.0))
|
|
if (x + 1, y, z) not in has: # right (+x)
|
|
dr.polygon([P(x + 1, y, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x + 1, y, z + 1)], fill=shade(c, 0.62))
|
|
if (x, y, z + 1) not in has: # toward viewer (original -z front)
|
|
dr.polygon([P(x, y, z + 1), P(x + 1, y, z + 1), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 0.82))
|
|
img.save('preview_front.png')
|
|
|
|
# second angle: from the back-right, no mirror
|
|
pts2 = sorted(((x, y, z, c) for x, y, z, c in shell), key=lambda p: (p[0] + p[2], p[1]))
|
|
img2 = Image.new('RGB', (1800, 900), (0xca, 0xdc, 0x9f))
|
|
dr2 = ImageDraw.Draw(img2)
|
|
has2 = {(x, y, z) for x, y, z, c in pts2}
|
|
for x, y, z, c in pts2:
|
|
if (x, y + 1, z) not in has2:
|
|
dr2.polygon([P(x, y + 1, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 1.0))
|
|
if (x + 1, y, z) not in has2:
|
|
dr2.polygon([P(x + 1, y, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x + 1, y, z + 1)], fill=shade(c, 0.62))
|
|
if (x, y, z + 1) not in has2:
|
|
dr2.polygon([P(x, y, z + 1), P(x + 1, y, z + 1), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 0.82))
|
|
img2.save('preview_back.png')
|
|
print('previews written')
|