DramaticShapeVoxelMod archival mirror · read-only
assets/docs/buidling_to_voxel/voxel_build_verify.py

voxel_build_verify.py

assets/docs/buidling_to_voxel/voxel_build_verify.py6.2 KB100644c970d7420a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
"""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')