-
Notifications
You must be signed in to change notification settings - Fork 13
/
sgb_border.py
310 lines (238 loc) · 9.85 KB
/
sgb_border.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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# This Python script requires Pillow to be installed.
# This can be done by writing the following in the command line (Windows) or terminal (Linux/Mac)
# assuming that Python is correctly installed: "python -m pip install Pillow" (without quotes)
# or "python3 -m pip install Pillow" or "pip install Pillow" or "pip3 install Pillow", depending on your setup.
from PIL import Image
from random import shuffle # everyday I'm shufflin'
def convert(im_path, outpath):
im_path = 'examples/images/sgb_border.png'
im = Image.open(im_path)
im = im.convert('RGB')
outpath = 'examples/images/sgb_border.inc'
# Inserts element e into list l assuming list never goes beyond a length if m, and if so, returns False.
def max_insert(l, e, m):
if len(l) < m:
l.append(e)
return True
else:
return False
colors = [] # all the colors in the image. Can be at most 64
tile_data = [] # max 256 elements
tile_map = [] # max 1024 elements
palette_deps = [] # one per tile, which colors need to exist together in one palette
palette_map = [] # one per tile, which palette each tile uses
palettes = [] # Max 4 palettes, each containing 16 colors
for i in range(4):
palettes.append([0]) # All palettes should contain transparancy
colors.append( (0,0,0) ) # This color should be first. It's used for transparency.
tile_data.append( [0]*64 ) # This tile should be first. It's used for transparency.
# Check dimensions of image
assert(im.size == (256,256))
# Loop through image to collect unique colors and palette dependencies
for iy in range(0, 256, 8):
for ix in range(0, 256, 8):
palette_dep = set()
for block_y in range(iy, iy+8):
for block_x in range(ix, ix+8):
col = im.getpixel((block_x, block_y))
if not col in colors:
if max_insert(colors, col, 64):
pass
else:
raise ValueError('Too many unique colors in the image')
col_index = colors.index(col)
palette_dep.add(col_index)
palette_deps.append(palette_dep)
# Make this non-deterministic so that, if it fails to build good palettes when it should be possible,
# you can just try again
shuffle(palette_deps)
for pd in palette_deps:
# First check if any existing palette contains all or some of the colors of this tile
best = -1
best_i = -1
for i,p in enumerate(palettes):
this_one = 0
remaining = 0
for c in pd:
if c in p:
this_one += 1
else:
remaining += 1
if this_one > best:
# Check if this one has room for all the remaining colors
if remaining + len(p) > 16:
# Yeah nah
continue
best = this_one
best_i = i
if best > -1:
palette_to_use = best_i
else:
# We need to create a new palette
palette_to_use = len(palettes)
new_palette = []
if not max_insert(palettes, new_palette, 4):
raise RuntimeError("Algorithm failed to fit all the colors into 4 palettes. Try running again or using fewer colors.")
# Add any colors to the palette that aren't already in there
for c in pd:
if not c in palettes[palette_to_use]:
if not max_insert(palettes[palette_to_use], c, 16):
raise RuntimeError("What is this I don't even")
# Loop through image to collect blocks
for iy in range(0, 256, 8):
for ix in range(0, 256, 8):
# Loop through 8x8 block
tile = []
# First find all the colors in the block and see which palette to use
block_cols = []
for block_y in range(iy, iy+8):
for block_x in range(ix, ix+8):
col = im.getpixel((block_x, block_y))
col_index = colors.index(col)
if not col_index in block_cols:
block_cols.append(col_index)
palette_index = -1
for ip,p in enumerate(palettes):
works = True
for c in block_cols:
if not c in p:
works = False
if works:
palette_index = ip
assert(palette_index > -1)
for block_y in range(iy, iy+8):
for block_x in range(ix, ix+8):
col = im.getpixel((block_x, block_y))
col_index = colors.index(col)
col_index_2 = palettes[palette_index].index(col_index)
tile.append(col_index_2)
if not tile in tile_data:
if max_insert(tile_data, tile, 256):
pass
else:
raise ValueError('Too many unique tiles in the image')
tile_index = tile_data.index(tile)
tile_map.append(tile_index)
palette_map.append(palette_index)
print("Unique colors used: {} / 64".format(len(colors)))
print("Unique tiles used: {} / 256".format(len(tile_data)))
assert(len(tile_map) == 32*32) # Otherwise the image is of incorrect size or something went wrong
# Pad tile data with zeros
while len(tile_data) < 256:
tile_data.append( [0]*64 )
# Pad colors with zeros
for p in palettes:
while len(p) < 16:
p.append(0)
s = ';This Super Game Boy border file is generated by sgb_border.py at https://github.com/ahrnbom/gingerbread'
def newline(s):
return s + '\n'
s = newline(s)
s = newline(s)
s = newline(s + 'SECTION "SGB Border",ROMX')
def strformat(p):
s = "DB"
for i in range(0,256,8):
pp = p[i:i+8]
if pp:
s += " " + str(int("".join(pp), 2)) + ','
s = s[:-1]
return s
# Converts a tile to the really strange SNES bitplane format
def convert_to_bitplanes(tile):
p01 = []
p23 = []
for ir in range(8):
start = 8*ir
row = [tile[x] for x in range(start, start+8)]
bincols = []
for col in row:
bincol = "{0:b}".format(col)
while len(bincol) < 4:
bincol = '0' + bincol
if len(bincol) >= 5:
print(col, bincol)
bincols.append(bincol)
p0 = [x[3] for x in bincols]
p1 = [x[2] for x in bincols]
p2 = [x[1] for x in bincols]
p3 = [x[0] for x in bincols]
p01.extend(p0)
p01.extend(p1)
p23.extend(p2)
p23.extend(p3)
return p01, p23
s = newline(s)
s = newline(s+'SGB_VRAM_TILEDATA1:')
for i_tile in range(128):
tile = tile_data[i_tile]
p01, p23 = convert_to_bitplanes(tile)
s = newline(s+strformat(p01))
s = newline(s+strformat(p23))
s = newline(s)
s = newline(s+'SGB_VRAM_TILEDATA2:')
for i_tile in range(128,256):
tile = tile_data[i_tile]
p01, p23 = convert_to_bitplanes(tile)
s = newline(s+strformat(p01))
s = newline(s+strformat(p23))
s = newline(s)
s = newline(s+'SGB_VRAM_TILEMAP:')
for t,p in zip(tile_map, palette_map):
bint = "{0:b}".format(t)
while len(bint) < 8:
bint = '0' + bint
if p == 0:
s2 = "%00010000"
elif p == 1:
s2 = "%00010100"
elif p == 2:
s2 = "%00011000"
elif p == 3:
s2 = "%00011100"
s = newline(s+'DB %' + bint + ', ' + s2)
s = newline(s)
s = newline(s)
def col2bits(x):
x = float(x)/256
x = int(round(31*x))
x = "{0:b}".format(x)
while len(x) < 5:
x = "0" + x
return x
for ip, palette in enumerate(palettes):
s = newline(s+'; Palette '+str(4+ip))
palette_colors = [colors[i] for i in palette]
for c in palette_colors:
r,g,b = c
r = col2bits(r)
g = col2bits(g)
b = col2bits(b)
s = newline(s + "DB %" + g[2:] + r + ', %0' + b + g[:2])
s = newline(s)
s = newline(s)
s = newline(s + 'SGB_VRAMTRANS_GBTILEMAP:')
for iy in range(32):
line = 'DB '
start = iy*20
for ix in range(20):
x = str(min(start+ix, 255))
while len(x) < 3:
x = ' '+x
line += x + ','
for ix in range(12):
line += ' 0,'
line = line[:-1]
s = newline(s+line)
with open(outpath, 'w') as f:
f.write(s)
if __name__ == "__main__":
import sys
if len(sys.argv) < 3:
print("Usage: python sgb_border.py INFILE OUTFILE")
print("INFILE should be a path to a .png image which fulfills the requirements for an SGB border")
print("OUTFILE should be a path where a .inc file should be created, which can be included in a Game Boy game written with RGBDS and GingerBread")
else:
im_path = sys.argv[1]
outpath = sys.argv[2]
convert(im_path, outpath)