voxel_build_verify.py
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')