Sprites that scale, and a depth buffer to hide them behind
A target size in PIXELS rather than a multiplier, which is the whole of why this is usable here. A billboard at distance d wants to be k/d pixels tall, and that is a number a program has anyway - out of a lookup table, most likely. A multiplier would have to be a fixed point fraction arrived at by dividing, and this CPU cannot divide. Zero on an axis means the natural size, so every sprite written before scaling existed still means what it meant. The two axes are independent, and that shape - one tile wide at its own size, stretched to whatever height a distance says - is a wall column in a pseudo-3D game. Measured: a DDA step costs 85 cycles, so 80 columns of ray casting is about 85,000 cycles, or 12fps. Drawing those walls from the CPU instead would be 256,000 writes, fifteen frames of cycles for one frame of screen. The device doing the pixels is what makes such a game possible at all here, not merely faster. And a depth buffer, one byte a screen column at 0xD000, written by the program. A sprite with a depth draws only in the columns it is in front of. PER COLUMN, and that is the point: a billboard is nearer than the wall at one end of itself and further at the other, and no ordering of the table can say that. Table order settles sprites against each other; the buffer settles them against the scenery. Zero means no test at both ends, so a program that never writes it behaves as it did before it existed. The entry grew from 8 bytes to 16 - now, while two programs use the table, rather than once a game is written on it. Bytes 0 to 7 kept their meanings, so Sprite.asm needed no change. The pass is rewritten to walk where a sprite is GOING rather than where it came from, which is what makes a stretch and a squash one operation. It also made flipping fall out: turning the source coordinate round mirrors the tile order and the pixels inside each tile in one step, where drawing tile by tile had to be told to do both. All 111 checks passed unchanged at natural size, which is what says the rewrite changed nothing it should not. Clipping moved out of the inner loop and had to: a target size is sixteen bits, so a sprite asked to be 60,000 pixels tall would have been sixty thousand turns of a loop that drew eight rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
f8c3db5d56
commit
cb898450b5
+69
-40
@@ -377,6 +377,7 @@ uint8_t videoRead(uint8_t port) {
|
||||
// nothing say so in a byte and skipping them costs one test.
|
||||
static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *depths = videoAtlas + VIDEO_DEPTH_BASE;
|
||||
for (int n = VIDEO_SPRITE_COUNT - 1; n >= 0; n--) {
|
||||
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
|
||||
const int wide = (entry[VIDEO_SPRITE_SIZE] >> 4) & 0x0F;
|
||||
@@ -384,6 +385,15 @@ static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||
if (wide == 0 || tall == 0) {
|
||||
continue;
|
||||
}
|
||||
// How big the art is, and how big it is being asked to look. Nought means the one
|
||||
// is the other, which is what every sprite written before scaling existed says.
|
||||
const int naturalWide = wide * VIDEO_CELL_PIXELS;
|
||||
const int naturalTall = tall * VIDEO_CELL_PIXELS;
|
||||
int drawWide = entry[VIDEO_SPRITE_WIDTH] | (entry[VIDEO_SPRITE_WIDTH + 1] << 8);
|
||||
int drawTall = entry[VIDEO_SPRITE_HEIGHT] | (entry[VIDEO_SPRITE_HEIGHT + 1] << 8);
|
||||
if (drawWide == 0) { drawWide = naturalWide; }
|
||||
if (drawTall == 0) { drawTall = naturalTall; }
|
||||
|
||||
// Signed, and low byte first like everything else this machine writes to a device.
|
||||
const int left = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_X]
|
||||
| (entry[VIDEO_SPRITE_X + 1] << 8));
|
||||
@@ -395,52 +405,71 @@ static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||
const int mirrored = (flags & VIDEO_SPRITE_HFLIP) != 0;
|
||||
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
|
||||
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
|
||||
const uint8_t depth = entry[VIDEO_SPRITE_DEPTH];
|
||||
|
||||
for (int downTile = 0; downTile < tall; downTile++) {
|
||||
for (int acrossTile = 0; acrossTile < wide; acrossTile++) {
|
||||
// ---- Flipping moves the tiles as well as the pixels ----
|
||||
//
|
||||
// A mirrored sprite is not each of its tiles mirrored in place; the tile at
|
||||
// the left end has to come out at the right end too, or a thing made of more
|
||||
// than one tile turns inside out instead of round.
|
||||
const int readAcross = mirrored ? (wide - 1 - acrossTile) : acrossTile;
|
||||
const int readDown = inverted ? (tall - 1 - downTile) : downTile;
|
||||
// In reading order from the first, and wrapping, because a byte plus a byte
|
||||
// is a byte and the tile number is one.
|
||||
// ---- Only the part that lands on the screen is walked ----
|
||||
//
|
||||
// Clipped BEFORE the loop rather than inside it, which used to be enough and is not
|
||||
// any more: a target size is sixteen bits, so a sprite asked to be 60,000 pixels
|
||||
// tall would otherwise be sixty thousand turns of a loop that drew eight rows.
|
||||
const int fromDown = (top < 0) ? -top : 0;
|
||||
const int toDown = (drawTall < height - top) ? drawTall : height - top;
|
||||
const int fromAcross = (left < 0) ? -left : 0;
|
||||
const int toAcross = (drawWide < width - left) ? drawWide : width - left;
|
||||
|
||||
// ---- Walked over where it is GOING, not over where it came from ----
|
||||
//
|
||||
// Every destination pixel asks which source pixel it is showing, which is what makes
|
||||
// a stretch and a squash the same operation and needs no accumulator carried between
|
||||
// rows. It also makes flipping fall out: turning the source coordinate round mirrors
|
||||
// the tile ORDER and the pixels inside each tile in one step, where drawing tile by
|
||||
// tile had to be told to do both.
|
||||
for (int down = fromDown; down < toDown; down++) {
|
||||
const int atY = top + down;
|
||||
int sourceDown = (int)((long)down * naturalTall / drawTall);
|
||||
if (inverted) {
|
||||
sourceDown = naturalTall - 1 - sourceDown;
|
||||
}
|
||||
for (int across = fromAcross; across < toAcross; across++) {
|
||||
const int atX = left + across;
|
||||
int sourceAcross = (int)((long)across * naturalWide / drawWide);
|
||||
if (mirrored) {
|
||||
sourceAcross = naturalWide - 1 - sourceAcross;
|
||||
}
|
||||
// Which tile of the group, in reading order, and where inside it. Wrapping,
|
||||
// because a byte plus a byte is a byte and the tile number is one.
|
||||
const uint8_t tile = (uint8_t)(entry[VIDEO_SPRITE_TILE]
|
||||
+ readDown * wide + readAcross);
|
||||
+ (sourceDown / VIDEO_CELL_PIXELS) * wide
|
||||
+ (sourceAcross / VIDEO_CELL_PIXELS));
|
||||
const uint8_t *art = tileArt(entry[VIDEO_SPRITE_ATTRIBUTE], tile);
|
||||
|
||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||
const int atY = top + downTile * VIDEO_CELL_PIXELS + y;
|
||||
if (atY < 0 || atY >= height) {
|
||||
const uint8_t pixel = art[(sourceDown % VIDEO_CELL_PIXELS) * VIDEO_CELL_PIXELS
|
||||
+ (sourceAcross % VIDEO_CELL_PIXELS)];
|
||||
// Nought is not a colour here, it is the absence of one, and it is tested
|
||||
// before the attribute is added so that it stays the same hole in all
|
||||
// sixteen schemes.
|
||||
if (pixel == 0) {
|
||||
continue;
|
||||
}
|
||||
if (behind && !backgroundEmpty[atY * width + atX]) {
|
||||
continue;
|
||||
}
|
||||
// ---- And whether anything nearer is already in this column ----
|
||||
//
|
||||
// Per column, which is the whole reason this is a buffer and not a number:
|
||||
// a billboard is in front of the wall at one end of itself and behind it at
|
||||
// the other, and no ordering of the table can say that.
|
||||
if (depth != 0) {
|
||||
const uint8_t there = depths[atX];
|
||||
if (there != 0 && depth >= there) {
|
||||
continue;
|
||||
}
|
||||
const int fromY = inverted ? (VIDEO_CELL_PIXELS - 1 - y) : y;
|
||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||
const int atX = left + acrossTile * VIDEO_CELL_PIXELS + x;
|
||||
if (atX < 0 || atX >= width) {
|
||||
continue;
|
||||
}
|
||||
const int fromX = mirrored ? (VIDEO_CELL_PIXELS - 1 - x) : x;
|
||||
const uint8_t pixel = art[fromY * VIDEO_CELL_PIXELS + fromX];
|
||||
// Nought is not a colour here, it is the absence of one, and it is
|
||||
// tested before the attribute is added so that it stays the same
|
||||
// hole in all sixteen schemes.
|
||||
if (pixel == 0) {
|
||||
continue;
|
||||
}
|
||||
if (behind && !backgroundEmpty[atY * width + atX]) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t index = (uint8_t)(pixel + bank);
|
||||
const uint8_t *colour = palette + index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = colour[0];
|
||||
out[1] = colour[1];
|
||||
out[2] = colour[2];
|
||||
}
|
||||
}
|
||||
const uint8_t index = (uint8_t)(pixel + bank);
|
||||
const uint8_t *colour = palette + index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = colour[0];
|
||||
out[1] = colour[1];
|
||||
out[2] = colour[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user