sprites: initial implementation

This commit is contained in:
Zack Buhman 2023-07-25 20:52:44 +00:00
parent 3b9cbf82aa
commit 1b71c6cfb6
17 changed files with 414 additions and 148 deletions

View File

@ -6,6 +6,7 @@ LIB = ./saturn
SRC = main.o input.o
SRC += gen/maps.o
SRC += gen/map_objects.o
SRC += gen/sprites.o
DEP = $(patsubst %.o,%.d,$(SRC))
res = $(subst pokered/,res/,$(patsubst %.$(1),%.$(1).o,$(wildcard $(2)*.$(1))))
@ -13,9 +14,10 @@ res_png = $(subst pokered/,res/,$(patsubst %.png,%.$(1).o,$(wildcard $(2)*.png))
GFX_TILESETS = $(call res_png,2bpp,pokered/gfx/tilesets/)
GFX_BLOCKSETS = $(call res,bst,pokered/gfx/blocksets/)
GFX_SPRITES = $(call res_png,2bpp,pokered/gfx/sprites/)
MAPS_BLOCKS = $(call res,blk,pokered/maps/)
GENERATED = $(GFX_TILESETS) $(GFX_BLOCKSETS) $(MAPS_BLOCKS)
GENERATED = $(GFX_TILESETS) $(GFX_BLOCKSETS) $(GFX_SPRITES) $(MAPS_BLOCKS)
OBJ = $(SRC) $(GENERATED)
all: main.cue
@ -47,6 +49,10 @@ res/%.2bpp: pokered/%.png
@mkdir -p $(dir $@)
python tools/png_to_nbpp.py $< 2 $@
res/gfx/sprites/%.2bpp: pokered/gfx/sprites/%.png
@mkdir -p $(dir $@)
python tools/png_to_nbpp_sprite.py $< 2 $@
%.2bpp.h:
$(BUILD_BINARY_H)

113
main.cpp
View File

@ -1,6 +1,7 @@
#include <cstdint>
#include "vdp2.h"
#include "vdp1.h"
#include "scu.h"
#include "smpc.h"
#include "sh2.h"
@ -12,6 +13,7 @@
#include "input.hpp"
#include "gen/maps.hpp"
#include "gen/sprites.hpp"
#include "map_objects.hpp"
constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b)
@ -21,19 +23,14 @@ constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b)
void palette_data()
{
vdp2.cram.u16[0] = rgb15( 0, 0, 0);
vdp2.cram.u16[1] = rgb15(10, 10, 10);
vdp2.cram.u16[2] = rgb15(21, 21, 21);
vdp2.cram.u16[3] = rgb15(31, 31, 31);
vdp2.cram.u16[3] = rgb15( 0, 0, 0);
vdp2.cram.u16[2] = rgb15(10, 10, 10);
vdp2.cram.u16[1] = rgb15(21, 21, 21);
vdp2.cram.u16[0] = rgb15(31, 31, 31);
}
uint32_t cell_data(const start_size_t& buf, const uint32_t top)
static inline void _2bpp_4bpp_vram_copy(uint32_t * vram, const start_size_t& buf)
{
// round to nearest multiple of 32
const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20);
const uint32_t base_address = top - table_size; // in bytes
uint32_t * vram = &vdp2.vram.u32[(base_address / 4)];
for (uint32_t ix = 0; ix < buf.size / 4; ix += 1) {
const uint32_t pixels = reinterpret_cast<uint32_t const * const>(buf.start)[ix];
const uint32_t px0 = pixels >> 16 & 0xffff;
@ -52,6 +49,28 @@ uint32_t cell_data(const start_size_t& buf, const uint32_t top)
#undef lshift
#undef rshift
}
}
uint32_t character_pattern_table(const start_size_t& buf, const uint32_t top)
{
// round to nearest multiple of 32
const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20);
const uint32_t base_address = top - table_size;
uint32_t * vram = &vdp1.vram.u32[(base_address / 4)];
_2bpp_4bpp_vram_copy(vram, buf);
return base_address;
}
uint32_t cell_data(const start_size_t& buf, const uint32_t top)
{
// round to nearest multiple of 32
const uint32_t table_size = ((buf.size * 2) + 0x20 - 1) & (-0x20);
const uint32_t base_address = top - table_size; // in bytes
uint32_t * vram = &vdp2.vram.u32[(base_address / 4)];
_2bpp_4bpp_vram_copy(vram, buf);
return base_address;
}
@ -187,10 +206,74 @@ void smpc_int(void)
intback::fsm(digital_callback, nullptr);
}
void sprite()
{
uint32_t top = (sizeof (union vdp1_vram));
const spritesheet_t& spritesheet = spritesheets[spritesheet_t::oak];
uint32_t character_address = top = character_pattern_table(spritesheet.spritesheet, top);
uint32_t color_address = 0;
/* TVM settings must be performed from the second H-blank IN interrupt after the
V-blank IN interrupt to the H-blank IN interrupt immediately after the V-blank
OUT interrupt. */
// "normal" display resolution, 16 bits per pixel, 512x256 framebuffer
vdp1.reg.TVMR = TVMR__TVM__NORMAL;
// swap framebuffers every 1 cycle; non-interlace
vdp1.reg.FBCR = 0;
// during a framebuffer erase cycle, write the color "black" to each pixel
constexpr uint16_t black = 0x0000;
vdp1.reg.EWDR = black;
// the EWLR/EWRR macros use somewhat nontrivial math for the X coordinates
// erase upper-left coordinate
vdp1.reg.EWLR = EWLR__16BPP_X1(0) | EWLR__Y1(0);
// erase lower-right coordinate
vdp1.reg.EWRR = EWRR__16BPP_X3(319) | EWRR__Y3(239);
vdp1.vram.cmd[0].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__SYSTEM_CLIP_COORDINATES;
vdp1.vram.cmd[0].LINK = 0;
vdp1.vram.cmd[0].XC = 319;
vdp1.vram.cmd[0].YC = 239;
vdp1.vram.cmd[1].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__LOCAL_COORDINATE;
vdp1.vram.cmd[1].LINK = 0;
vdp1.vram.cmd[1].XA = 0;
vdp1.vram.cmd[1].YA = 0;
vdp1.vram.cmd[2].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__NORMAL_SPRITE;
vdp1.vram.cmd[2].LINK = 0;
// The "end code" is 0xf, which is being used in the mai sprite palette. If
// both transparency and end codes are enabled, it seems there are only 14
// usable colors in the 4-bit color mode.
vdp1.vram.cmd[2].PMOD = PMOD__ECD | PMOD__COLOR_MODE__COLOR_BANK_16;
// It appears Kronos does not correctly calculate the color address in the
// VDP1 debugger. Kronos will report FFFC when the actual color table address
// in this example is 7FFE0.
vdp1.vram.cmd[2].COLR = color_address; // non-palettized (rgb15) color data
vdp1.vram.cmd[2].SRCA = character_address >> 3;
vdp1.vram.cmd[2].SIZE = SIZE__X(16) | SIZE__Y(16);
vdp1.vram.cmd[2].XA = 5 * 16;
vdp1.vram.cmd[2].YA = 5 * 16;
vdp1.vram.cmd[3].CTRL = CTRL__END;
// start drawing (execute the command list) on every frame
vdp1.reg.PTMR = PTMR__PTM__FRAME_CHANGE;
}
void main()
{
state.map_ix = map_t::celadon_city;
v_blank_in();
sprite();
vdp2.reg.PRISA = PRISA__S0PRIN(7); // Sprite register 0 PRIority Number
// DISP: Please make sure to change this bit from 0 to 1 during V blank.
vdp2.reg.TVMD = ( TVMD__DISP | TVMD__LSMD__NON_INTERLACE
| TVMD__VRESO__240 | TVMD__HRESO__NORMAL_320);
@ -199,7 +282,7 @@ void main()
vdp2.reg.RAMCTL = RAMCTL__CRKTE | RAMCTL__CRMD__RGB_5BIT_1024 | RAMCTL__VRAMD | RAMCTL__VRBMD;
/* enable display of NBG0 */
vdp2.reg.BGON = BGON__N0ON;
vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON;
/* set character format for NBG0 to palettized 16 color
set enable "cell format" for NBG0
@ -216,10 +299,10 @@ void main()
2-word: value of bit 5-0 * 0x4000
*/
constexpr int plane_a = 0;
constexpr int plane_a_offset = plane_a * 0x2000;
//constexpr int plane_a_offset = plane_a * 0x2000;
constexpr int page_size = 64 * 64 * 2; // N0PNB__1WORD (16-bit)
constexpr int plane_size = page_size * 1;
//constexpr int page_size = 64 * 64 * 2; // N0PNB__1WORD (16-bit)
//constexpr int plane_size = page_size * 1;
vdp2.reg.MPOFN = MPOFN__N0MP(0); // bits 8~6
vdp2.reg.MPABN0 = MPABN0__N0MPB(0) | MPABN0__N0MPA(plane_a); // bits 5~0
@ -247,6 +330,4 @@ void main()
scu.reg.IST = 0;
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
state.map_ix = map_t::celadon_city;
}

6
start_size.hpp Normal file
View File

@ -0,0 +1,6 @@
#pragma once
struct start_size_t {
uint8_t const * const start;
uint32_t size;
};

View File

@ -1,67 +0,0 @@
#pragma once
#include "res/gfx/tilesets/cavern.4bpp.h"
#include "res/gfx/tilesets/cemetery.4bpp.h"
#include "res/gfx/tilesets/club.4bpp.h"
#include "res/gfx/tilesets/facility.4bpp.h"
#include "res/gfx/tilesets/forest.4bpp.h"
#include "res/gfx/tilesets/gate.4bpp.h"
#include "res/gfx/tilesets/gym.4bpp.h"
#include "res/gfx/tilesets/house.4bpp.h"
#include "res/gfx/tilesets/interior.4bpp.h"
#include "res/gfx/tilesets/lab.4bpp.h"
#include "res/gfx/tilesets/lobby.4bpp.h"
#include "res/gfx/tilesets/mansion.4bpp.h"
#include "res/gfx/tilesets/overworld.4bpp.h"
#include "res/gfx/tilesets/plateau.4bpp.h"
#include "res/gfx/tilesets/pokecenter.4bpp.h"
#include "res/gfx/tilesets/reds_house.4bpp.h"
#include "res/gfx/tilesets/ship.4bpp.h"
#include "res/gfx/tilesets/ship_port.4bpp.h"
#include "res/gfx/tilesets/underground.4bpp.h"
struct tileset {
enum {
cavern,
cemetery,
club,
facility,
forest,
gate,
gym,
house,
interior,
lab,
lobby,
mansion,
overworld,
plateau,
pokecenter,
reds_house,
ship,
ship_port,
underground,
};
static uint32_t pattern_name(uint32_t graphics_ix);
static uint32_t load(uint32_t top, uint32_t i);
};
struct buf_t {
uint32_t * buf;
uint32_t size;
uint32_t base_address;
};
extern uint32_t cell_data(const buf_t& buf, const uint32_t top);
uint32_t tileset::pattern_name(uint32_t graphics_ix)
{
return tilesets[graphics_ix].base_address / 32;
}
uint32_t tileset::load(uint32_t top, uint32_t i)
{
tilesets[i].base_address = top = cell_data(tilesets[i], top);
return top;
}

View File

@ -4,14 +4,17 @@ import sys
from generate import maps
from generate import map_objects
from generate import sprites
def generate(base_path):
files = [
files = [
(maps.generate_maps_header, "maps.hpp"),
(maps.generate_maps_source, "maps.cpp"),
(map_objects.generate_map_objects_source, "map_objects.cpp")
]
(map_objects.generate_map_objects_source, "map_objects.cpp"),
(sprites.generate_sprites_header, "sprites.hpp"),
(sprites.generate_sprites_source, "sprites.cpp"),
]
def generate(base_path):
for func, filename in files:
path = base_path / filename
with open(path, 'w') as f:

18
tools/generate/binary.py Normal file
View File

@ -0,0 +1,18 @@
def binary_res(path, suffix):
# _binary_res_gfx_blocksets_overworld_bst_start
name = path.replace('/', '_').replace('.', '_')
return f"&_binary_res_{name}_{suffix}"
def start_size_value(path):
if path is None:
return [
"0,",
"0,",
]
else:
start = binary_res(path, "start")
size = binary_res(path, "size")
return [
f"reinterpret_cast<uint8_t*>({start}),",
f"reinterpret_cast<uint32_t>({size}),",
]

View File

@ -1,19 +1,51 @@
"""
map_headers[0].tileset == 'OVERWORLD'
map_headers[0].name1 = 'PalletTown'
map_headers[0].name2 = 'PALLET_TOWN'
map_headers[0].blocks() == 'PalletTown_Blocks'
map_constants_list['PALLET_TOWN'] == MapConstant(10, 19)
maps_blocks_list['PalletTown_Blocks'] == 'maps/PalletTown.blk'
tileset_constants_list['OVERWORLD'] == 0
tileset_headers_list[0].name == 'Overworld'
tileset_headers_list[0].blockset() == 'Overworld_Block'
tileset_headers_list[0].gfx() == 'Overworld_GFX'
gfx_tilesets_list['Overworld_Block'] == 'gfx/blocksets/overworld.bst'
gfx_tilesets_list['Overworld_GFX'] == 'gfx/tilesets/overworld.2bpp'
"""
from parse import parse
from generate.sort import default_sort
from generate.binary import binary_res, start_size_value
from generate.generate import renderer
def sorted_map_constants_list():
return sorted(parse.map_constants_list.items(), key=default_sort)
def sorted_map_headers():
map_constants_list = sorted_map_constants_list()
map_headers_dict = dict((map_header.name2, map_header) for map_header in parse.map_headers)
# hack to remove unused/duplicate underground_path_route_7
map_headers = sorted(parse.map_headers, key=lambda m: m.name2)
return filter(lambda m: m.name1 != "UndergroundPathRoute7Copy", map_headers)
#map_headers = sorted(parse.map_headers, key=lambda m: m.name2)
#return filter(lambda m: m.name1 != "UndergroundPathRoute7Copy", map_headers)
return (
map_headers_dict[map_name2] for map_name2, _ in map_constants_list
if map_name2 in map_headers_dict
# e.g CERULEAN_TRASHED_HOUSE_COPY has no map header
)
def sorted_tilesets_constants_list():
return sorted(parse.tileset_constants_list.items(), key=default_sort)
def includes_header():
yield "#pragma once"
yield ""
yield '#include "../start_size.hpp"'
yield ""
for map_header in sorted_map_headers():
block_path = parse.maps_blocks_list[map_header.blocks()]
yield f'#include "../res/{block_path}.h"'
for tileset_name in sorted(parse.tileset_constants_list):
for tileset_name, _ in sorted_tilesets_constants_list():
tileset_index = parse.tileset_constants_list[tileset_name]
tileset_header = parse.tileset_headers_list[tileset_index]
@ -24,18 +56,10 @@ def includes_header():
yield f'#include "../res/{gfx_path}.h"'
yield ""
def struct_start_size_t():
return [
"struct start_size_t {",
"uint8_t const * const start;",
"uint32_t size;",
"};",
]
def struct_tileset_t():
tileset_names = (
f"{name.lower()},"
for name in sorted(parse.tileset_constants_list)
for name, _ in sorted_tilesets_constants_list()
)
return [
"struct tileset_t {",
@ -68,20 +92,6 @@ def struct_map_t():
"};",
]
def binary_res(path, suffix):
# _binary_res_gfx_blocksets_overworld_bst_start
name = path.replace('/', '_').replace('.', '_')
return f"_binary_res_{name}_{suffix}"
def start_size_value(path):
start = binary_res(path, "start")
size = binary_res(path, "size")
return [
f"reinterpret_cast<uint8_t*>(&{start}),",
f"reinterpret_cast<uint32_t>(&{size}),",
]
def blockset_tileset(name: str):
tileset_index = parse.tileset_constants_list[name]
tileset_header = parse.tileset_headers_list[tileset_index]
@ -105,8 +115,8 @@ def tilesets_header():
def tilesets():
yield "const tileset_t tilesets[] = {"
for tileset in sorted(parse.tileset_constants_list):
yield from blockset_tileset(tileset)
for name, _ in sorted_tilesets_constants_list():
yield from blockset_tileset(name)
yield "};"
def map(map_header):
@ -135,7 +145,6 @@ def maps():
def generate_maps_header():
render, out = renderer()
render(includes_header())
render(struct_start_size_t())
render(struct_tileset_t())
render(struct_map_t())
render(tilesets_header());
@ -154,19 +163,3 @@ def generate_maps_source():
render(tilesets())
render(maps())
return out
"""
map_headers[0].tileset == 'OVERWORLD'
map_headers[0].name1 = 'PalletTown'
map_headers[0].name2 = 'PALLET_TOWN'
map_headers[0].blocks() == 'PalletTown_Blocks'
map_constants_list['PALLET_TOWN'] == MapConstant(10, 19)
maps_blocks_list['PalletTown_Blocks'] == 'maps/PalletTown.blk'
tileset_constants_list['OVERWORLD'] == 0
tileset_headers_list[0].name == 'Overworld'
tileset_headers_list[0].blockset() == 'Overworld_Block'
tileset_headers_list[0].gfx() == 'Overworld_GFX'
gfx_tilesets_list['Overworld_Block'] == 'gfx/blocksets/overworld.bst'
gfx_tilesets_list['Overworld_GFX'] == 'gfx/tilesets/overworld.2bpp'
"""

5
tools/generate/sort.py Normal file
View File

@ -0,0 +1,5 @@
from operator import itemgetter
by_name = itemgetter(0)
by_index = itemgetter(1)
default_sort = by_name

98
tools/generate/sprites.py Normal file
View File

@ -0,0 +1,98 @@
"""
spritesheets_list[0] == Spritesheet('RedSprite', 3)
sprite_constants_list[0] == 'SPRITE_NONE'
gfx_sprites_list['RedSprite'] == 'gfx/sprites/red.2bpp'
"""
# insert a empty sprite for SPRITE_NONE
# remove SPRITE_ prefix
from parse import parse
from generate.sort import default_sort
from generate.binary import binary_res, start_size_value
from generate.generate import renderer
def sorted_sprite_constants_list():
return sorted(parse.sprite_constants_list.items(), key=default_sort)
def includes_header():
yield '#pragma once'
yield ''
yield '#include "../start_size.hpp"'
yield ''
for name, index in sorted_sprite_constants_list():
if name == 'SPRITE_NONE':
continue
assert index != 0, index
spritesheet = parse.spritesheets_list[index - 1]
sprite_path = parse.gfx_sprites_list[spritesheet.name]
yield f'#include "../res/{sprite_path}.h"'
def includes_source():
yield '#include <cstdint>'
yield ''
yield '#include "sprites.hpp"'
yield ''
def sprite_name(name):
assert name.startswith('SPRITE_'), name
return name.removeprefix('SPRITE_').lower()
def struct_spritesheet_t():
sprite_names = (
f"{sprite_name(name)},"
for name, _ in sorted_sprite_constants_list()
)
return [
"struct spritesheet_t {",
"start_size_t spritesheet;",
"uint8_t sprite_count;",
"",
"enum sprite {",
*sprite_names,
"};",
"};",
]
def sprites_header():
yield "extern const spritesheet_t spritesheets[];"
def generate_sprites_header():
render, out = renderer()
render(includes_header())
render(struct_spritesheet_t())
render(sprites_header())
return out
def sprite(name, index):
if name == 'SPRITE_NONE':
# special null sprite
sprite_path = None
sprite_count = 0
else:
# spritesheets_list does not include SPRITE_NULL at index 0
assert index != 0, index
spritesheet = parse.spritesheets_list[index - 1]
sprite_path = parse.gfx_sprites_list[spritesheet.name]
sprite_count = spritesheet.sprite_count
return [
f"[spritesheet_t::{sprite_name(name)}] = {{",
".spritesheet = {",
*start_size_value(sprite_path),
"},",
f".sprite_count = {sprite_count}",
"},",
]
def sprites():
yield "const spritesheet_t spritesheets[] = {"
for name, index in sorted_sprite_constants_list():
yield from sprite(name, index)
yield "};"
def generate_sprites_source():
render, out = renderer()
render(includes_source());
render(sprites())
return out

4
tools/palette.py Normal file
View File

@ -0,0 +1,4 @@
def intensity_to_index(px):
indices = {0x00: 3, 0x55: 2, 0xaa: 1, 0xff: 0}
assert px in indices, px
return indices[px]

View File

@ -1,5 +1,5 @@
from pprint import pprint
from parse import parse
for i in parse.map_objects_list.items():
for i in parse.sprite_constants_list:
pprint(i)

View File

@ -2,15 +2,16 @@ from parse.maps_blocks import tokenize_block, flatten
def tokenize_lines(lines):
for line in lines:
if '_GFX:' in line or '_Block:' in line:
if '::' in line:
yield tokenize_block(line, delim='::')
def parse(prefix):
path = prefix / 'gfx/tilesets.asm'
path = prefix / 'gfx/sprites.asm'
with open(path) as f:
tokens = tokenize_lines(f.read().split('\n'))
return list(
flatten(tokens,
endings=['_GFX', '_Block'],
base_path='gfx/')
)
l = list(flatten(tokens,
endings=['Sprite'],
base_path='gfx/'))
d = dict(l)
assert len(l) == len(d)
return d

View File

@ -12,6 +12,10 @@ from parse import map_objects
from parse import hidden_objects
from parse import map_constants
from parse import gfx_sprites
from parse import spritesheets
from parse import sprite_constants
prefix = Path(sys.argv[1])
map_headers = map_header.parse_all(prefix)
@ -30,5 +34,9 @@ map_constants_list = map_constants.parse(prefix)
#ledge_tiles.asm
#cut_tree_blocks.asm
# home/vcopy: animations
# sprites
gfx_sprites_list = gfx_sprites.parse(prefix)
spritesheets_list = spritesheets.parse(prefix)
sprite_constants_list = sprite_constants.parse(prefix)

View File

@ -0,0 +1,6 @@
from parse.tileset_constants import flatten, tokenize_lines
def parse(prefix):
path = prefix / 'constants/sprite_constants.asm'
with open(path) as f:
return dict(flatten(tokenize_lines(f.read().split('\n'))))

View File

@ -0,0 +1,39 @@
from dataclasses import dataclass
from parse.map_header import tokenize_line
from parse import number
def tokenize_lines(lines):
for line in lines:
if 'overworld_sprite ' in line:
yield tokenize_line(line)
@dataclass
class Spritesheet:
name: str
sprite_count: int
def translate_tile_count(tile_count):
# this is the number of non-animation sprites in the spritesheet
# the animation sprite is always at tile_index * 2
# all animated sprites have 2 animation frames
return int(tile_count) // 4
def flatten(tokens):
for ts in tokens:
if ts[0] != 'overworld_sprite':
continue
_, (name, tile_count) = ts
sprite_count = translate_tile_count(tile_count)
yield Spritesheet(
name=name,
sprite_count=sprite_count,
)
def parse(prefix):
path = prefix / 'data/sprites/sprites.asm'
with open(path) as f:
tokens = tokenize_lines(f.read().split('\n'))
return list(flatten(tokens))

View File

@ -3,10 +3,7 @@ import sys
from PIL import Image
def intensity_to_index(px):
indices = {0x00: 0, 0x55: 1, 0xaa: 2, 0xff: 3}
assert px in indices, px
return indices[px]
from palette import intensity_to_index
def convert(image, bpp):
assert bpp in {8, 4, 2}, bpp

View File

@ -0,0 +1,68 @@
import os
import sys
from PIL import Image
from palette import intensity_to_index
cell_width = 16
cell_height = 16
def convert(image, bpp):
assert bpp in {8, 4, 2}, bpp
bits_per_byte = 8
px_per_byte = bits_per_byte // bpp
px_per_row = cell_width
bytes_per_row = (px_per_row // px_per_byte)
assert image.mode == 'L', image.mode
width, height = image.size
buf = bytearray(width * height // px_per_byte)
for cell_y in range(height//cell_height):
cell_y_start = cell_y * bytes_per_row * cell_height
for cell_x in range(width//cell_width):
cell_x_start = cell_y_start + bytes_per_row * cell_x
for y in range(cell_height):
for x in range(cell_width):
px = im.getpixel((cell_x * cell_width + x, cell_y * cell_height + y))
index = intensity_to_index(px)
buf_ix = cell_x_start + x//px_per_byte + y * bytes_per_row
buf[buf_ix] |= (index << bpp * ((px_per_byte - 1) - (x % px_per_byte)))
return buf
# (pixels/row)
# ------------ *
# (pixels/byte)
def debug(buf, bpp):
assert bpp in {8, 4, 2}, bpp
bits_per_byte = 8
px_per_byte = bits_per_byte // bpp
px_per_row = cell_width
bytes_per_row = (px_per_row // px_per_byte)
bit_mask = (2 ** bpp) - 1
for row in range(len(buf) // bytes_per_row):
for x in range(bytes_per_row):
px = buf[row * bytes_per_row + x]
for shift in reversed(range(px_per_byte)):
print((px >> (shift * bpp)) & bit_mask, end='')
print()
if (row % cell_height == (cell_height - 1)):
print()
in_path = sys.argv[1]
bpp = int(sys.argv[2])
out_path = sys.argv[3]
im = Image.open(in_path)
buf = convert(im, bpp)
if 'NBPP_DEBUG' in os.environ:
debug(buf, bpp)
with open(out_path, 'wb') as f:
f.write(buf)