iigs-game-engine/tools/mkrot.js
2021-07-30 08:01:16 -05:00

169 lines
5.8 KiB
JavaScript

/**
* Generated data tables for BG1 rotation
*
* The trickiest issue to address is that, when calculating the rotation indices, at
* a 45 degree angle, the mapped address for a fixed rectangle can be outside of the
* pixel buffer. To fix this we make a compromise.
*
* To keep speed, image data is drawn one word at a time, so the effective width of the
* rotation is 82 units wide. Since each work contains 4 pixels, we will only rotate
* a quarter of the vertical image -- an effective 52 units -- and display the same offset
* for four consecutive lines.
*
* Further, the image data will be the center of the BG1 buffer, so the middle 52 lines.
*
* When rotating we may still calculate address "outside" of the buffer by a factor of
* sqrt(2) (~40%) -- or 32 words horizontally and 21 lines vertically. There is extra
* data vertically to fill this and, since the BG1 buffer is stored with a stride of
* 256 bytes (128 words) there are an extra 46 words of empty space that can be zeroed
* out or filled with content to improve the rotation visuals.
*/
const fs = require('fs').promises;
const process = require('process');
const { Buffer } = require('buffer');
// When calculating addresses, do we use the floor() function or round()?
const USE_FLOOR = true;
const NUM_ANGLES = 64;
const BUFFER_HEIGHT = 208;
const BUFFER_WIDTH = 164; // In bytes
const BUFFER_STRIDE = 256; // In bytes
const TEXTURE_WIDTH = BUFFER_WIDTH / 2; // Full width (in words)
const TEXTURE_HEIGHT = BUFFER_HEIGHT / 4; // Quarter height
const TEXTURE_STRIDE = BUFFER_STRIDE;
const BUFFER_START = 0x1800;
const BUFFER_END = BUFFER_START + BUFFER_STRIDE * BUFFER_HEIGHT;
console.log(`; The BG1 buffer lives at [${toHex(BUFFER_START)}, ${toHex(BUFFER_END)}]`);
// The texture portion of BG starts at the left edge of line 77 and
// extends down to line
const TEXTURE_START = BUFFER_STRIDE * (BUFFER_HEIGHT - TEXTURE_HEIGHT) / 2;
const TEXTURE_END = BUFFER_STRIDE * (BUFFER_HEIGHT + TEXTURE_HEIGHT) / 2;
const TEXTURE_CENTER = BUFFER_START + TEXTURE_START + Math.floor(TEXTURE_HEIGHT / 2) * BUFFER_STRIDE + Math.floor(BUFFER_WIDTH / 2);
console.log(`; The texture is this range of the BG1 buffer [${toHex(TEXTURE_START)}, ${toHex(TEXTURE_END)}]`);
// Define some other constants
const x_half = Math.floor(TEXTURE_WIDTH / 2);
const y_half = Math.floor(TEXTURE_HEIGHT / 2);
// Calculate some bias values to keep everything positive
BIAS_X = Math.floor(TEXTURE_CENTER / 2) + 0x200;
BIAS_Y = TEXTURE_CENTER - BIAS_X;
const angles = Array.from({ length: NUM_ANGLES}).map((x, i) => (i * 2 * Math.PI) / NUM_ANGLES);
main(process.argv.slice(2)).then(
() => process.exit(0),
(e) => {
console.error(e);
process.exit(1);
}
);
function toHex(n) {
return '$' + n.toString(16).toUpperCase().padStart(4, '0');
}
function f_x(x, angle) {
// Calculate x in units of bytes
// return Math.floor(a(x - x_half, angle)) + x_half + BIAS_X;
return a(x - x_half, angle) + BIAS_X;
}
function f_y(y, angle) {
// return Math.floor(b(y - y_half, angle)) + (y_half * TEXTURE_STRIDE) + BIAS_Y;
return b(y - y_half, angle) + BIAS_Y;
}
function check_sample(_a, x, y) {
const angle = angles[_a];
const degrees = Math.round(360 * angle / (2 * Math.PI));
const fx = f_x(x, angle);
const fy = f_y(y, angle);
const ptr = fx + fy;
if (fx < 0 || fy < 0 || ptr < 0x1800 || ptr >= 0xE800) {
console.log(`(a = ${degrees}, x = ${x}, y = ${y}) : f_x = ${toHex(fx)}, f_y = ${toHex(fy)}, p = ${toHex(ptr)}`);
process.exit();
}
}
async function main(argv) {
// Inspired by https://www.youtube.com/watch?v=glWIf0gfWSE&t=1196s
//
// We will support 64 rotation angles (~5.5 degree increments) which gives nice
// power-of-2 values from the common angles or 45, 90, 135, etc.
// Do a brute force check to make sure that we can generate addresses that stay within
// a proper range
for (let a = 0; a < NUM_ANGLES; a += 1) {
for (let x = 0; x < TEXTURE_WIDTH; x += 1) {
for (let y = 0; y < TEXTURE_HEIGHT; y += 1) {
check_sample(a, x, y);
}
}
const degrees = Math.round(360 * angles[a] / (2 * Math.PI));
}
// Now generate the tables to stdout as merlin source code
const _ = console.log;
_("ANGLEBNK\tENT");
_("x_angles\tENT");
for (let a = 0; a < NUM_ANGLES; a += 1) {
_(`\tdw\t:x_a_${a}`);
}
for (let a = 0; a < NUM_ANGLES; a += 1) {
const angle = angles[a];
const label = `:x_a_${a}`;
const fx = [];
for (let x = 0; x < TEXTURE_WIDTH; x += 1) {
fx.push(f_x(x, angle));
}
const arr = fx.map(toHex).join(',');
// Double every array for fast copies
_(`${label}\tdw\t${arr}`);
_(`\tdw\t${arr}`);
}
_("y_angles\tENT");
for (let a = 0; a < NUM_ANGLES; a += 1) {
_(`\tdw\t:y_a_${a}`);
}
for (let a = 0; a < NUM_ANGLES; a += 1) {
const angle = angles[a];
const label = `:y_a_${a}`;
const fy = [];
for (let y = 0; y < TEXTURE_HEIGHT; y += 1) {
const value = f_y(y, angle);
fy.push(value);
}
const arr = fy.map(toHex).join(',');
// Double every array for fast output
_(`${label}\tdw\t${arr}`);
_(`\tdw\t${arr}`);
}
}
function a(x, angle) {
if (USE_FLOOR) {
return (Math.floor(2 * x * Math.cos(angle)) & ~1) + Math.floor(x * Math.sin(angle)) * TEXTURE_STRIDE;
}
return (Math.round(2 * x * Math.cos(angle)) & ~1) + Math.round(x * Math.sin(angle)) * TEXTURE_STRIDE;
}
function b(y, angle) {
if (USE_FLOOR) {
return Math.floor(y * Math.cos(angle)) * TEXTURE_STRIDE - (Math.floor(2 * y * Math.sin(angle)) & ~1);
}
return Math.round(y * Math.cos(angle)) * TEXTURE_STRIDE - (Math.round(2 * y * Math.sin(angle)) & ~1);
}