VASTLY improved how I generate tiles and tiledata

This commit is contained in:
2022-05-07 21:57:15 -07:00
parent bee024c3e1
commit 4ff43893d0
66 changed files with 3472 additions and 3186 deletions

View File

@@ -1,5 +1,5 @@
const process = require('process');
const { spawnSync, execSync } = require('child_process');
execSync(`scp ./build/Penny.gb root@ywbud3:/storage/roms/gb/Penny.gb`);
const process = require('process');
const { spawnSync, execSync } = require('child_process');
execSync(`scp ./build/Penny.gb root@ywbud3:/storage/roms/gb/Penny.gb`);
execSync(`echo "systemctl stop emustation.service; killall emulationstation; retroarch -L /lib/libretro/gambatte_libretro.so '/storage/roms/gb/Penny.gb';" | ssh root@ywbud3 /bin/bash`);

View File

@@ -1,124 +1,124 @@
const fs = require('fs');
const path = require('path');
const process = require('process');
const { spawnSync, execSync } = require('child_process');
const { png2gb } = require('./png2gb');
const { string2gb } = require('./string2gb');
const DIR_BUILD = path.resolve('build');
const DIR_GENERATED = path.resolve(DIR_BUILD, 'generated');
const DIR_OBJ = path.resolve(DIR_BUILD, 'obj');
const DIR_SRC = path.resolve('src');
const DIR_GBDK = path.resolve(process.env['GBDKDIR']);
const DIR_ASSETS = path.resolve('assets');
const DIR_IMAGES = path.resolve(DIR_ASSETS, 'images');
const FILE_OUT = path.resolve(DIR_BUILD, 'Penny.gb');
const FILE_LINKFILE = path.join(DIR_BUILD, `linkflile.lk`);
const LCC = path.join(DIR_GBDK, 'bin', 'lcc');
const LCCFLAGS = `-I${DIR_GENERATED} -I${DIR_SRC}`;
const compiledSources = [];
// Create build dirs
[
DIR_BUILD, DIR_GENERATED, DIR_OBJ
].forEach(d => {
if(fs.existsSync(d)) return;
fs.mkdirSync(d);
});
// Scandir
const buildSourceFiles = directory => {
const sources = [];
fs.readdirSync(directory).forEach(file => {
const fullPath = path.join(directory, file);
const stats = fs.statSync(fullPath);
if(stats.isDirectory()) {
sources.push(...buildSourceFiles(fullPath));
return;
}
if(file.endsWith('.c')) sources.push(fullPath);
});
return sources;
}
const logOut = (out, buffer) => {
const str = [
out.stderr, out.stdout
].filter(n => n)
.map(n => n.toString())
.filter(n => n)
.join('')
;
if(!str || !str.length) return;
buffer(str);
}
const compileC = (cFile) => {
const fileNameOut = path.basename(cFile, '.c') + '.o';
const fileOut = path.join(DIR_OBJ, fileNameOut);
compiledSources.push(path.join(fileNameOut));
if(fs.existsSync(fileOut)) return;
let result;
try {
result = execSync(`${LCC} ${LCCFLAGS} -c -o ${fileOut} ${cFile}`);
logOut(result, console.log);
} catch(e) {
logOut(e, e => {
console.error(e);
process.exit(1);
});
throw e;
}
}
// Generate strings
// let dataStringH = '#pragma once\n#include "libs.h"\n';
// let dataStringC = '#include "STRINGS.h"\n';
// Object.entries(GAME_STRINGS).forEach(entry => {
// const [ name, str ] = entry;
// const { dataH, dataC } = string2gb(str, name);
// dataStringH += dataH+'\n', dataStringC += dataC+'\n';
// });
// fs.writeFileSync(path.join(DIR_GENERATED, 'STRINGS.h'), dataStringH);
// fs.writeFileSync(path.join(DIR_GENERATED, 'STRINGS.c'), dataStringC);
// compileC(path.join(DIR_GENERATED, 'STRINGS.c'));
// Gen imagery
fs.readdirSync(DIR_IMAGES).forEach(img => {
if(!img.endsWith(".png")) return;
const { fileC, fileH } = png2gb(
path.join(DIR_IMAGES, img), DIR_GENERATED,
path.basename(img, '.png').toUpperCase()
);
compileC(fileC);
})
// Get a list of sources and build each of them prior to linking.
const allSources = buildSourceFiles(DIR_SRC);
for(let i = 0; i < allSources.length; i++) {
compileC(allSources[i]);
}
// Generate a linkfile.
fs.writeFileSync(FILE_LINKFILE, compiledSources.map(cs=>{
return path.join(DIR_OBJ, cs);
}).join('\n'));
// Compile BIN
let result;
try {
result = execSync(`${LCC} ${LCCFLAGS} -o ${FILE_OUT} -Wl-f${FILE_LINKFILE}`);
logOut(result, console.log);
} catch(e) {
logOut(e, console.error);
process.exit(1);
const fs = require('fs');
const path = require('path');
const process = require('process');
const { spawnSync, execSync } = require('child_process');
const { png2gb } = require('./png2gb');
const { string2gb } = require('./string2gb');
const DIR_BUILD = path.resolve('build');
const DIR_GENERATED = path.resolve(DIR_BUILD, 'generated');
const DIR_OBJ = path.resolve(DIR_BUILD, 'obj');
const DIR_SRC = path.resolve('src');
const DIR_GBDK = path.resolve(process.env['GBDKDIR']);
const DIR_ASSETS = path.resolve('assets');
const DIR_IMAGES = path.resolve(DIR_ASSETS, 'images');
const FILE_OUT = path.resolve(DIR_BUILD, 'Penny.gb');
const FILE_LINKFILE = path.join(DIR_BUILD, `linkflile.lk`);
const LCC = path.join(DIR_GBDK, 'bin', 'lcc');
const LCCFLAGS = `-I${DIR_GENERATED} -I${DIR_SRC}`;
const compiledSources = [];
// Create build dirs
[
DIR_BUILD, DIR_GENERATED, DIR_OBJ
].forEach(d => {
if(fs.existsSync(d)) return;
fs.mkdirSync(d);
});
// Scandir
const buildSourceFiles = directory => {
const sources = [];
fs.readdirSync(directory).forEach(file => {
const fullPath = path.join(directory, file);
const stats = fs.statSync(fullPath);
if(stats.isDirectory()) {
sources.push(...buildSourceFiles(fullPath));
return;
}
if(file.endsWith('.c')) sources.push(fullPath);
});
return sources;
}
const logOut = (out, buffer) => {
const str = [
out.stderr, out.stdout
].filter(n => n)
.map(n => n.toString())
.filter(n => n)
.join('')
;
if(!str || !str.length) return;
buffer(str);
}
const compileC = (cFile) => {
const fileNameOut = path.basename(cFile, '.c') + '.o';
const fileOut = path.join(DIR_OBJ, fileNameOut);
compiledSources.push(path.join(fileNameOut));
if(fs.existsSync(fileOut)) return;
let result;
try {
result = execSync(`${LCC} ${LCCFLAGS} -c -o ${fileOut} ${cFile}`);
logOut(result, console.log);
} catch(e) {
logOut(e, e => {
console.error(e);
process.exit(1);
});
throw e;
}
}
// Generate strings
// let dataStringH = '#pragma once\n#include "libs.h"\n';
// let dataStringC = '#include "STRINGS.h"\n';
// Object.entries(GAME_STRINGS).forEach(entry => {
// const [ name, str ] = entry;
// const { dataH, dataC } = string2gb(str, name);
// dataStringH += dataH+'\n', dataStringC += dataC+'\n';
// });
// fs.writeFileSync(path.join(DIR_GENERATED, 'STRINGS.h'), dataStringH);
// fs.writeFileSync(path.join(DIR_GENERATED, 'STRINGS.c'), dataStringC);
// compileC(path.join(DIR_GENERATED, 'STRINGS.c'));
// Gen imagery
fs.readdirSync(DIR_IMAGES).forEach(img => {
if(!img.endsWith(".png")) return;
const { fileC, fileH } = png2gb(
path.join(DIR_IMAGES, img), DIR_GENERATED,
path.basename(img, '.png').toUpperCase()
);
compileC(fileC);
})
// Get a list of sources and build each of them prior to linking.
const allSources = buildSourceFiles(DIR_SRC);
for(let i = 0; i < allSources.length; i++) {
compileC(allSources[i]);
}
// Generate a linkfile.
fs.writeFileSync(FILE_LINKFILE, compiledSources.map(cs=>{
return path.join(DIR_OBJ, cs);
}).join('\n'));
// Compile BIN
let result;
try {
result = execSync(`${LCC} ${LCCFLAGS} -o ${FILE_OUT} -Wl-f${FILE_LINKFILE}`);
logOut(result, console.log);
} catch(e) {
logOut(e, console.error);
process.exit(1);
}

View File

@@ -1,4 +1,4 @@
const rimraf = require('rimraf');
console.log('🔥 Cleaning');
const rimraf = require('rimraf');
console.log('🔥 Cleaning');
rimraf.sync('build');

View File

@@ -1,7 +1,7 @@
const TILE_WIDTH = 8;
const TILE_HEIGHT = 8;
module.exports = {
TILE_WIDTH,
TILE_HEIGHT
const TILE_WIDTH = 8;
const TILE_HEIGHT = 8;
module.exports = {
TILE_WIDTH,
TILE_HEIGHT
}

View File

@@ -1,88 +1,88 @@
const PNG = require('pngjs').PNG;
const fs = require('fs');
const {
TILE_WIDTH,
TILE_HEIGHT
} = require('./common');
const colorPixel = (id) => {
if(id === undefined) id = 3;
if(id === 3) return { r: 8, g: 24, b: 32 };
if(id === 2) return { r: 52, g: 104, b: 86 };
if(id === 1) return { r: 136, g: 192, b: 112 };
if(id === 0) return { r: 224, g: 248, b: 208 };
throw new Error();
}
const gb2png = (DATA, fileOut) => {
// Begin
const PIXELS = DATA.length / 2 * TILE_WIDTH;
const DATA_WIDTH = TILE_WIDTH;
const DATA_HEIGHT = PIXELS / DATA_WIDTH;
// Create output image
const imageData = new PNG({
width: DATA_WIDTH,
height: DATA_HEIGHT
});
// Convert data into pixels
const pixelsOut = [];
for(let i = 0; i < DATA.length; i += 2) {
const low = DATA[i];
const high = DATA[i+1];
for(let j = 0; j < 8; j++) {
const mask = 0x80 >> j;
const pixel = (low & mask ? 1 : 0) + (high & mask ? 2 : 0);
pixelsOut.push(pixel);
}
}
// Buffer data output
for(let y = 0; y < DATA_HEIGHT; y++) {
for(let x = 0; x < DATA_WIDTH; x++) {
const id = (DATA_WIDTH * y + x);
const color = colorPixel(pixelsOut[id]);
const idx = id << 2;
imageData.data[idx] = color.r;
imageData.data[idx+1] = color.g;
imageData.data[idx+2] = color.b;
imageData.data[idx+3] = 0xFF;
}
}
const buffer = PNG.sync.write(imageData, { });
fs.writeFileSync(fileOut, buffer);
}
// // Now work out tile data
// if(TILEMAP.length) {
// for(let i = 0; i < TILEMAP.length; i++) {
// const tileX = i % TILEMAP_WIDTH;
// const tileY = Math.floor(i / TILEMAP_WIDTH);
// const tile = TILEMAP[i];
// for(let j = 0; j < TILE_WIDTH*TILE_HEIGHT; j++) {
// const outI = (
// (tileX * TILE_WIDTH) + (tileY * TILE_HEIGHT * TILEMAP_PIXEL_WIDTH) +
// ((j % TILE_WIDTH) + (Math.floor(j / TILE_WIDTH) * TILEMAP_PIXEL_WIDTH))
// );
// const idx = outI << 2;
// const pixelI = (tile * TILE_WIDTH * TILE_HEIGHT) + j;
// const color = colorPixel(pixelsOut[pixelI]);
// tileData.data[idx] = color.r;
// tileData.data[idx+1] = color.g;
// tileData.data[idx+2] = color.b;
// tileData.data[idx+3] = 0xFF;
// }
// }
// const buffer2 = PNG.sync.write(tileData, { });
// fs.writeFileSync('out.png', buffer2);
// }
module.exports = {
gb2png
const PNG = require('pngjs').PNG;
const fs = require('fs');
const {
TILE_WIDTH,
TILE_HEIGHT
} = require('./common');
const colorPixel = (id) => {
if(id === undefined) id = 3;
if(id === 3) return { r: 8, g: 24, b: 32 };
if(id === 2) return { r: 52, g: 104, b: 86 };
if(id === 1) return { r: 136, g: 192, b: 112 };
if(id === 0) return { r: 224, g: 248, b: 208 };
throw new Error();
}
const gb2png = (DATA, fileOut) => {
// Begin
const PIXELS = DATA.length / 2 * TILE_WIDTH;
const DATA_WIDTH = TILE_WIDTH;
const DATA_HEIGHT = PIXELS / DATA_WIDTH;
// Create output image
const imageData = new PNG({
width: DATA_WIDTH,
height: DATA_HEIGHT
});
// Convert data into pixels
const pixelsOut = [];
for(let i = 0; i < DATA.length; i += 2) {
const low = DATA[i];
const high = DATA[i+1];
for(let j = 0; j < 8; j++) {
const mask = 0x80 >> j;
const pixel = (low & mask ? 1 : 0) + (high & mask ? 2 : 0);
pixelsOut.push(pixel);
}
}
// Buffer data output
for(let y = 0; y < DATA_HEIGHT; y++) {
for(let x = 0; x < DATA_WIDTH; x++) {
const id = (DATA_WIDTH * y + x);
const color = colorPixel(pixelsOut[id]);
const idx = id << 2;
imageData.data[idx] = color.r;
imageData.data[idx+1] = color.g;
imageData.data[idx+2] = color.b;
imageData.data[idx+3] = 0xFF;
}
}
const buffer = PNG.sync.write(imageData, { });
fs.writeFileSync(fileOut, buffer);
}
// // Now work out tile data
// if(TILEMAP.length) {
// for(let i = 0; i < TILEMAP.length; i++) {
// const tileX = i % TILEMAP_WIDTH;
// const tileY = Math.floor(i / TILEMAP_WIDTH);
// const tile = TILEMAP[i];
// for(let j = 0; j < TILE_WIDTH*TILE_HEIGHT; j++) {
// const outI = (
// (tileX * TILE_WIDTH) + (tileY * TILE_HEIGHT * TILEMAP_PIXEL_WIDTH) +
// ((j % TILE_WIDTH) + (Math.floor(j / TILE_WIDTH) * TILEMAP_PIXEL_WIDTH))
// );
// const idx = outI << 2;
// const pixelI = (tile * TILE_WIDTH * TILE_HEIGHT) + j;
// const color = colorPixel(pixelsOut[pixelI]);
// tileData.data[idx] = color.r;
// tileData.data[idx+1] = color.g;
// tileData.data[idx+2] = color.b;
// tileData.data[idx+3] = 0xFF;
// }
// }
// const buffer2 = PNG.sync.write(tileData, { });
// fs.writeFileSync('out.png', buffer2);
// }
module.exports = {
gb2png
};

View File

@@ -1,247 +1,247 @@
const fs = require('fs');
const { PNG } = require('pngjs');
const path = require('path');
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
const WHITE = { r: 255, g: 255, b : 255, a: 255 };
const RED = { r: 255, g: 0, b: 0, a: 255 };
const TILE_WIDTH = 8;
const TILE_HEIGHT = 8;
// Helpers
const pixelIsSame = (left, right, alpha) => {
if(left.r !== right.r) return false;
if(left.g !== right.g) return false;
if(left.b !== right.b) return false;
if(!alpha) return true;
return left.a === right.a;
}
const imageOut = (pixels, width, fileName) => {
const png = new PNG({
width,
height: pixels.length / width
});
pixels.forEach((pixel, i) => {
const x = i % width;
const y = (i - x) / width;
const idx = (width * y + x) << 2;
png.data[idx] = pixel.r;
png.data[idx+1] = pixel.g;
png.data[idx+2] = pixel.b;
png.data[idx+3] = pixel.a;
});
if(!fs.existsSync('out')) fs.mkdirSync('out');
const out = PNG.sync.write(png);
fs.writeFileSync(path.join('out', fileName), out);
}
const imageIn = fileName => {
const data = fs.readFileSync(fileName);
const png = PNG.sync.read(data);
const pixels = [];
for(let y = 0; y < png.height; y++) {
for (let x = 0; x < png.width; x++) {
let idx = (png.width * y + x) << 2;
const r = png.data[idx];
const g = png.data[idx+1];
const b = png.data[idx+2];
const a = png.data[idx+3];
let pixel = { r, g, b, a };
if(a === 0) {
pixel = { ...WHITE };
} else {
pixel.a = 255;
}
pixels.push(pixel);
}
}
return { pixels, width: png.width, height: png.height };
}
const tileFromPixel = (x, y, original) => {
const byEightX = Math.floor(x / 8);
const byEightY = Math.floor(y / 8);
const byEightWidth = Math.floor(original.width / 8);
const byEightId = byEightX + (byEightY * byEightWidth);
return { x: byEightX, y: byEightY, columns: byEightWidth, id: byEightId };
}
// Read Input File
const original = imageIn('bruh.png');
const columns = (original.width / TILE_WIDTH);
const rows = (original.height / TILE_HEIGHT);
// Foreach pixel
const palette = [];
const paletteByEight = [];
const withPaletteOverflows = [];
for(let y = 0; y < original.height; y++) {
for(let x = 0; x < original.width; x++) {
const id = x + (y * original.width);
const pixel = original.pixels[id];
let errorPixel = { ...pixel };
const tile = tileFromPixel(x, y, original);
// Handle palettes
if(pixel.a != 0) {
const pb8 = (paletteByEight[tile.id] = paletteByEight[tile.id] || []);
if(!pb8.some(p => pixelIsSame(pixel, p))) {
pb8.push(pixel);
}
// Handle palette overflow
if(pb8.length > 4) errorPixel = { ...RED };
// Append to palette
if(!palette.some(p => pixelIsSame(pixel, p))) palette.push(pixel);
}
withPaletteOverflows.push(errorPixel);
}
}
// Generate the palette set image
const outPaletteByEight = [];
let outByEightWidth = 1;
paletteByEight.forEach((pal,y) => {
pal.forEach((p,x) => {
outByEightWidth = Math.max(outByEightWidth, x+1);
})
});
paletteByEight.forEach((pal,y) => {
for(let x = 0; x < outByEightWidth; x++) {
outPaletteByEight.push(x >= pal.length ? TRANSPARENT : pal[x]);
}
});
// Now determine for each TILE what palette to use.
const paletteGroups = [];
const gbVersion = [];
const paletteImage = [];
for(let y = 0; y < original.height; y++) {
for(let x = 0; x < original.width; x++) {
const id = x + (y * original.width);
const pixel = original.pixels[id];
const tile = tileFromPixel(x, y, original);
// Get the palette
const paletteSet = paletteByEight[tile.id];
// Check for matching
let palId = paletteGroups.findIndex(pg => {
// Check for cases where one of the pallet group palettes may have
// less pixels than the current set we're checking, e.g. we do a tile that
// has only two colors, then we iterate over a tile with 4 colors that has
// two colors shared with that other tile. In that case we just add our
// two extra colors.
if(paletteSet.length > pg.length) {
return pg.every(p => paletteSet.some(pss => pixelIsSame(pss, p)));
} else {
return paletteSet.every(p => pg.some(pgs => pixelIsSame(pgs, p)));
}
});
if(palId === -1) {
palId = paletteGroups.length;
paletteGroups.push(paletteSet);
}
const paletteGroupSet = paletteGroups[palId];
// This is where we correct the missing pixels if we share that tileset from
// earlier
paletteSet.forEach(ps => {
const existing = paletteGroupSet.some(pgs => pixelIsSame(pgs, ps));
if(existing) return;
paletteGroupSet.push(ps);
});
// Sort the paletteGroupSet...
const pgsGetWeight = thing => {
return thing.r + thing.g + thing.b;
}
paletteGroupSet.sort((l,r) => {
return pgsGetWeight(l) - pgsGetWeight(r);
});
const examples = [
/* 0 */{ r: 0, g: 0, b: 0, a: 255 },
/* 1 */{ r: 255, g: 0, b: 0, a: 255 },
/* 2 */{ r: 0, g: 255, b: 0, a: 255 },
/* 3 */{ r: 0, g: 0, b: 255, a: 255 },
/* 4 */{ r: 255, g: 255, b: 0, a: 255 },
/* 5 */{ r: 255, g: 0, b: 255, a: 255 },
/* 6 */{ r: 0, g: 255, b: 255, a: 255 },
/* 7 */{ r: 255, g: 255, b: 255, a: 255 },
/* S */{ r: 100, g: 0, b: 0, a: 255 },
/* S */{ r: 0, g: 100, b: 0, a: 255 },
/* S */{ r: 0, g: 0, b: 100, a: 255 },
/* S */{ r: 100, g: 100, b: 0, a: 255 },
/* S */{ r: 0, g: 100, b: 100, a: 255 },
/* S */{ r: 100, g: 0, b: 100, a: 255 },
/* S */{ r: 100, g: 100, b: 100, a: 255 },
];
paletteImage.push(examples[palId]);
const pixelIndex = paletteGroupSet.findIndex(ps => pixelIsSame(ps, pixel));
const nonColor = [
{ r: 8, g: 24, b: 32, a: 255 },
{ r: 52, g: 104, b: 86, a: 255 },
{ r: 136, g: 192, b: 112, a: 255 },
{ r: 224, g: 248, b: 208, a: 255 }
];
gbVersion.push(nonColor[pixelIndex % nonColor.length]);
}
}
console.log('Found', paletteGroups.length, 'palettes');
imageOut(original.pixels, original.width, 'original.png');
imageOut(withPaletteOverflows, original.width, 'errors.png');
imageOut(palette, palette.length, 'palette.png');
imageOut(outPaletteByEight, outByEightWidth, 'paletteByEight.png');
imageOut(paletteImage, original.width, 'palettes.png');
imageOut(gbVersion, original.width, 'gameboy.png');
// Now generate the GB files
// let rearranged = [];
// let n = 0;
// for(let i = 0; i < columns * rows; i++) {
// const tileX = i % columns;
// const tileY = Math.floor(i / columns) % rows;
// for(let y = 0; y < TILE_HEIGHT; y++) {
// for(let x = 0; x < TILE_WIDTH; x++) {
// const px = (tileX * TILE_WIDTH) + x;
// const py = (tileY * TILE_HEIGHT) + y;
// const pi = (py * png.width) + px;
// rearranged[n++] = original.pixels[pi];
// }
// }
// }
// // Now turn into a tileset
// const bits = [];
// for(let i = 0; i < rearranged.length; i += TILE_WIDTH) {
// let lowBits = 0x00;
// let highBits = 0x00;
// for(let j = 0; j < TILE_WIDTH; j++) {
// const pixel = rearranged[i + j];
// lowBits = lowBits | ((pixel & 0x01) << (7-j));
// highBits = highBits | ((pixel & 0x02) >> 1 << (7-j));
// }
// bits.push(lowBits, highBits);
const fs = require('fs');
const { PNG } = require('pngjs');
const path = require('path');
const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
const WHITE = { r: 255, g: 255, b : 255, a: 255 };
const RED = { r: 255, g: 0, b: 0, a: 255 };
const TILE_WIDTH = 8;
const TILE_HEIGHT = 8;
// Helpers
const pixelIsSame = (left, right, alpha) => {
if(left.r !== right.r) return false;
if(left.g !== right.g) return false;
if(left.b !== right.b) return false;
if(!alpha) return true;
return left.a === right.a;
}
const imageOut = (pixels, width, fileName) => {
const png = new PNG({
width,
height: pixels.length / width
});
pixels.forEach((pixel, i) => {
const x = i % width;
const y = (i - x) / width;
const idx = (width * y + x) << 2;
png.data[idx] = pixel.r;
png.data[idx+1] = pixel.g;
png.data[idx+2] = pixel.b;
png.data[idx+3] = pixel.a;
});
if(!fs.existsSync('out')) fs.mkdirSync('out');
const out = PNG.sync.write(png);
fs.writeFileSync(path.join('out', fileName), out);
}
const imageIn = fileName => {
const data = fs.readFileSync(fileName);
const png = PNG.sync.read(data);
const pixels = [];
for(let y = 0; y < png.height; y++) {
for (let x = 0; x < png.width; x++) {
let idx = (png.width * y + x) << 2;
const r = png.data[idx];
const g = png.data[idx+1];
const b = png.data[idx+2];
const a = png.data[idx+3];
let pixel = { r, g, b, a };
if(a === 0) {
pixel = { ...WHITE };
} else {
pixel.a = 255;
}
pixels.push(pixel);
}
}
return { pixels, width: png.width, height: png.height };
}
const tileFromPixel = (x, y, original) => {
const byEightX = Math.floor(x / 8);
const byEightY = Math.floor(y / 8);
const byEightWidth = Math.floor(original.width / 8);
const byEightId = byEightX + (byEightY * byEightWidth);
return { x: byEightX, y: byEightY, columns: byEightWidth, id: byEightId };
}
// Read Input File
const original = imageIn('bruh.png');
const columns = (original.width / TILE_WIDTH);
const rows = (original.height / TILE_HEIGHT);
// Foreach pixel
const palette = [];
const paletteByEight = [];
const withPaletteOverflows = [];
for(let y = 0; y < original.height; y++) {
for(let x = 0; x < original.width; x++) {
const id = x + (y * original.width);
const pixel = original.pixels[id];
let errorPixel = { ...pixel };
const tile = tileFromPixel(x, y, original);
// Handle palettes
if(pixel.a != 0) {
const pb8 = (paletteByEight[tile.id] = paletteByEight[tile.id] || []);
if(!pb8.some(p => pixelIsSame(pixel, p))) {
pb8.push(pixel);
}
// Handle palette overflow
if(pb8.length > 4) errorPixel = { ...RED };
// Append to palette
if(!palette.some(p => pixelIsSame(pixel, p))) palette.push(pixel);
}
withPaletteOverflows.push(errorPixel);
}
}
// Generate the palette set image
const outPaletteByEight = [];
let outByEightWidth = 1;
paletteByEight.forEach((pal,y) => {
pal.forEach((p,x) => {
outByEightWidth = Math.max(outByEightWidth, x+1);
})
});
paletteByEight.forEach((pal,y) => {
for(let x = 0; x < outByEightWidth; x++) {
outPaletteByEight.push(x >= pal.length ? TRANSPARENT : pal[x]);
}
});
// Now determine for each TILE what palette to use.
const paletteGroups = [];
const gbVersion = [];
const paletteImage = [];
for(let y = 0; y < original.height; y++) {
for(let x = 0; x < original.width; x++) {
const id = x + (y * original.width);
const pixel = original.pixels[id];
const tile = tileFromPixel(x, y, original);
// Get the palette
const paletteSet = paletteByEight[tile.id];
// Check for matching
let palId = paletteGroups.findIndex(pg => {
// Check for cases where one of the pallet group palettes may have
// less pixels than the current set we're checking, e.g. we do a tile that
// has only two colors, then we iterate over a tile with 4 colors that has
// two colors shared with that other tile. In that case we just add our
// two extra colors.
if(paletteSet.length > pg.length) {
return pg.every(p => paletteSet.some(pss => pixelIsSame(pss, p)));
} else {
return paletteSet.every(p => pg.some(pgs => pixelIsSame(pgs, p)));
}
});
if(palId === -1) {
palId = paletteGroups.length;
paletteGroups.push(paletteSet);
}
const paletteGroupSet = paletteGroups[palId];
// This is where we correct the missing pixels if we share that tileset from
// earlier
paletteSet.forEach(ps => {
const existing = paletteGroupSet.some(pgs => pixelIsSame(pgs, ps));
if(existing) return;
paletteGroupSet.push(ps);
});
// Sort the paletteGroupSet...
const pgsGetWeight = thing => {
return thing.r + thing.g + thing.b;
}
paletteGroupSet.sort((l,r) => {
return pgsGetWeight(l) - pgsGetWeight(r);
});
const examples = [
/* 0 */{ r: 0, g: 0, b: 0, a: 255 },
/* 1 */{ r: 255, g: 0, b: 0, a: 255 },
/* 2 */{ r: 0, g: 255, b: 0, a: 255 },
/* 3 */{ r: 0, g: 0, b: 255, a: 255 },
/* 4 */{ r: 255, g: 255, b: 0, a: 255 },
/* 5 */{ r: 255, g: 0, b: 255, a: 255 },
/* 6 */{ r: 0, g: 255, b: 255, a: 255 },
/* 7 */{ r: 255, g: 255, b: 255, a: 255 },
/* S */{ r: 100, g: 0, b: 0, a: 255 },
/* S */{ r: 0, g: 100, b: 0, a: 255 },
/* S */{ r: 0, g: 0, b: 100, a: 255 },
/* S */{ r: 100, g: 100, b: 0, a: 255 },
/* S */{ r: 0, g: 100, b: 100, a: 255 },
/* S */{ r: 100, g: 0, b: 100, a: 255 },
/* S */{ r: 100, g: 100, b: 100, a: 255 },
];
paletteImage.push(examples[palId]);
const pixelIndex = paletteGroupSet.findIndex(ps => pixelIsSame(ps, pixel));
const nonColor = [
{ r: 8, g: 24, b: 32, a: 255 },
{ r: 52, g: 104, b: 86, a: 255 },
{ r: 136, g: 192, b: 112, a: 255 },
{ r: 224, g: 248, b: 208, a: 255 }
];
gbVersion.push(nonColor[pixelIndex % nonColor.length]);
}
}
console.log('Found', paletteGroups.length, 'palettes');
imageOut(original.pixels, original.width, 'original.png');
imageOut(withPaletteOverflows, original.width, 'errors.png');
imageOut(palette, palette.length, 'palette.png');
imageOut(outPaletteByEight, outByEightWidth, 'paletteByEight.png');
imageOut(paletteImage, original.width, 'palettes.png');
imageOut(gbVersion, original.width, 'gameboy.png');
// Now generate the GB files
// let rearranged = [];
// let n = 0;
// for(let i = 0; i < columns * rows; i++) {
// const tileX = i % columns;
// const tileY = Math.floor(i / columns) % rows;
// for(let y = 0; y < TILE_HEIGHT; y++) {
// for(let x = 0; x < TILE_WIDTH; x++) {
// const px = (tileX * TILE_WIDTH) + x;
// const py = (tileY * TILE_HEIGHT) + y;
// const pi = (py * png.width) + px;
// rearranged[n++] = original.pixels[pi];
// }
// }
// }
// // Now turn into a tileset
// const bits = [];
// for(let i = 0; i < rearranged.length; i += TILE_WIDTH) {
// let lowBits = 0x00;
// let highBits = 0x00;
// for(let j = 0; j < TILE_WIDTH; j++) {
// const pixel = rearranged[i + j];
// lowBits = lowBits | ((pixel & 0x01) << (7-j));
// highBits = highBits | ((pixel & 0x02) >> 1 << (7-j));
// }
// bits.push(lowBits, highBits);
// }

View File

@@ -1,93 +1,101 @@
const PNG = require('pngjs').PNG;
const path = require('path');
const fs = require('fs');
const { arrayToString } = require('./util');
const {
TILE_WIDTH,
TILE_HEIGHT
} = require('./common');
const getPixelValue = (pixel) => {
if(pixel.g === 188) return 0;
if(pixel.g === 172) return 1;
if(pixel.g === 98) return 2;
if(pixel.g === 56) return 3;
throw new Error();
}
const png2gb = (fileIn, dirOut, name) => {
const data = fs.readFileSync(fileIn);
const png = PNG.sync.read(data);
// Convert PNG pixels into 0x00-0x03
const pixels = [];
for(let y = 0; y < png.height; y++) {
for(let x = 0; x < png.width; x++) {
const id = x + (y * png.width);
const idx = id << 2;
const r = png.data[idx];
const g = png.data[idx+1];
const b = png.data[idx+2];
const value = getPixelValue({ r, g, b });
pixels.push(value);
}
}
// Now take these raw pixels and extract the tiles themselves
let rearranged = [];
const columns = (png.width / TILE_WIDTH);
const rows = (png.height / TILE_HEIGHT);
let n = 0;
for(let i = 0; i < columns * rows; i++) {
const tileX = i % columns;
const tileY = Math.floor(i / columns) % rows;
for(let y = 0; y < TILE_HEIGHT; y++) {
for(let x = 0; x < TILE_WIDTH; x++) {
const px = (tileX * TILE_WIDTH) + x;
const py = (tileY * TILE_HEIGHT) + y;
const pi = (py * png.width) + px;
rearranged[n++] = pixels[pi];
}
}
}
// Now turn into a tileset
const bits = [];
for(let i = 0; i < rearranged.length; i += TILE_WIDTH) {
let lowBits = 0x00;
let highBits = 0x00;
for(let j = 0; j < TILE_WIDTH; j++) {
const pixel = rearranged[i + j];
lowBits = lowBits | ((pixel & 0x01) << (7-j));
highBits = highBits | ((pixel & 0x02) >> 1 << (7-j));
}
bits.push(lowBits, highBits);
}
let outH = '';
outH += `#include "libs.h"\n\n`
outH += `#define ${name}_IMAGE_WIDTH ${png.width}\n`;
outH += `#define ${name}_IMAGE_HEIGHT ${png.height}\n`;
outH += `#define ${name}_IMAGE_COLUMNS ${png.width / TILE_WIDTH}\n`;
outH += `#define ${name}_IMAGE_ROWS ${png.height / TILE_HEIGHT}\n`;
outH += `#define ${name}_IMAGE_TILES ${columns * rows}\n`;
outH += `extern const uint8_t ${name}_IMAGE[];`;
let outC = `#include "${name}.h"\n`;
outC += `\nconst uint8_t ${name}_IMAGE[] = {\n${arrayToString(bits)}};`;
const fileH = path.join(dirOut, name + '.h');
const fileC = path.join(dirOut, name + '.c');
fs.writeFileSync(fileH, outH);
fs.writeFileSync(fileC, outC);
return { fileH, fileC };
}
module.exports = {
png2gb
}
const PNG = require('pngjs').PNG;
const path = require('path');
const fs = require('fs');
const { arrayToString } = require('./util');
const {
TILE_WIDTH,
TILE_HEIGHT
} = require('./common');
const getPixelValue = (pixel) => {
if(pixel.g === 188) return 0;
if(pixel.g === 172) return 1;
if(pixel.g === 98 || pixel.g === 145) return 2;
if(pixel.g === 56) return 3;
if(pixel.a === 0) return 0;
throw new Error();
}
const png2gb = (fileIn, dirOut, name) => {
const data = fs.readFileSync(fileIn);
const png = PNG.sync.read(data);
// Convert PNG pixels into 0x00-0x03
const pixels = [];
for(let y = 0; y < png.height; y++) {
for(let x = 0; x < png.width; x++) {
const id = x + (y * png.width);
const idx = id << 2;
const r = png.data[idx];
const g = png.data[idx+1];
const b = png.data[idx+2];
const a = png.data[idx+3];
const pixel = { r, g, b, a };
try {
const value = getPixelValue(pixel);
pixels.push(value);
} catch(e) {
console.error(`Failed to get color for `, x, y, idx, fileIn, pixel);
throw e;
}
}
}
// Now take these raw pixels and extract the tiles themselves
let rearranged = [];
const columns = (png.width / TILE_WIDTH);
const rows = (png.height / TILE_HEIGHT);
let n = 0;
for(let i = 0; i < columns * rows; i++) {
const tileX = i % columns;
const tileY = Math.floor(i / columns) % rows;
for(let y = 0; y < TILE_HEIGHT; y++) {
for(let x = 0; x < TILE_WIDTH; x++) {
const px = (tileX * TILE_WIDTH) + x;
const py = (tileY * TILE_HEIGHT) + y;
const pi = (py * png.width) + px;
rearranged[n++] = pixels[pi];
}
}
}
// Now turn into a tileset
const bits = [];
for(let i = 0; i < rearranged.length; i += TILE_WIDTH) {
let lowBits = 0x00;
let highBits = 0x00;
for(let j = 0; j < TILE_WIDTH; j++) {
const pixel = rearranged[i + j];
lowBits = lowBits | ((pixel & 0x01) << (7-j));
highBits = highBits | ((pixel & 0x02) >> 1 << (7-j));
}
bits.push(lowBits, highBits);
}
let outH = '';
outH += `#include "libs.h"\n\n`
outH += `#define ${name}_IMAGE_WIDTH ${png.width}\n`;
outH += `#define ${name}_IMAGE_HEIGHT ${png.height}\n`;
outH += `#define ${name}_IMAGE_COLUMNS ${png.width / TILE_WIDTH}\n`;
outH += `#define ${name}_IMAGE_ROWS ${png.height / TILE_HEIGHT}\n`;
outH += `#define ${name}_IMAGE_TILES ${columns * rows}\n`;
outH += `extern const uint8_t ${name}_IMAGE[];`;
let outC = `#include "${name}.h"\n`;
outC += `\nconst uint8_t ${name}_IMAGE[] = {\n${arrayToString(bits)}};`;
const fileH = path.join(dirOut, name + '.h');
const fileC = path.join(dirOut, name + '.c');
fs.writeFileSync(fileH, outH);
fs.writeFileSync(fileC, outC);
return { fileH, fileC };
}
module.exports = {
png2gb
}
// convert('images/sm.png', 'out.c', 'PENNY');

View File

@@ -1,32 +1,32 @@
const fs = require('fs');
const path = require('path');
const { arrayToString } = require('./util');
const FONT_CHARACTER_FIRST = 33;
const FONT_DATA_POSITION = 4;
const getCodeFrom = l => {
const cc = l.charCodeAt(0)
if(l == '\n' || l == ' ') return cc;
return cc - FONT_CHARACTER_FIRST + FONT_DATA_POSITION
}
const string2gb = (string, name) => {
const letters = [];
for(let i = 0; i < string.length; i++) {
letters.push(getCodeFrom(string[i]));
}
let dataH = `#define STR_${name}_LENGTH ${string.length}\n`;
dataH += `extern const uint8_t STR_${name}_DATA[];`;
let dataC = `const uint8_t STR_${name}_DATA[] = {\n`;
dataC += arrayToString(letters);
dataC += `\n};`;
return { dataH, dataC };
}
module.exports = {
string2gb
const fs = require('fs');
const path = require('path');
const { arrayToString } = require('./util');
const FONT_CHARACTER_FIRST = 33;
const FONT_DATA_POSITION = 4;
const getCodeFrom = l => {
const cc = l.charCodeAt(0)
if(l == '\n' || l == ' ') return cc;
return cc - FONT_CHARACTER_FIRST + FONT_DATA_POSITION
}
const string2gb = (string, name) => {
const letters = [];
for(let i = 0; i < string.length; i++) {
letters.push(getCodeFrom(string[i]));
}
let dataH = `#define STR_${name}_LENGTH ${string.length}\n`;
dataH += `extern const uint8_t STR_${name}_DATA[];`;
let dataC = `const uint8_t STR_${name}_DATA[] = {\n`;
dataC += arrayToString(letters);
dataC += `\n};`;
return { dataH, dataC };
}
module.exports = {
string2gb
};

View File

@@ -1,21 +1,21 @@
const arrayToString = arr => {
const b = arr.map(n => {
return '0x' + (n.toString(16).padStart(2, '0').toUpperCase());
});
let str = '';
for(let i = 0; i < b.length; i += 16) {
str += ' ';
for(let x = i; x < Math.min(i+16, b.length); x++) {
str += b[x];
str += ',';
}
str += '\n';
}
return str;
}
module.exports = {
arrayToString
const arrayToString = arr => {
const b = arr.map(n => {
return '0x' + (n.toString(16).padStart(2, '0').toUpperCase());
});
let str = '';
for(let i = 0; i < b.length; i += 16) {
str += ' ';
for(let x = i; x < Math.min(i+16, b.length); x++) {
str += b[x];
str += ',';
}
str += '\n';
}
return str;
}
module.exports = {
arrayToString
}