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
@@ -1210,6 +1210,10 @@ from page 0, which is, and a tile left in another page is invisible unless a map
|
|||||||
that page - and the map is given back or cleared. What a program puts in page 1 is its own
|
that page - and the map is given back or cleared. What a program puts in page 1 is its own
|
||||||
and nobody is looking at it afterwards.
|
and nobody is looking at it afterwards.
|
||||||
|
|
||||||
|
**The depth buffer is not touched at all.** It only matters to a sprite that has a depth, and
|
||||||
|
the table is cleared, so there are none - a program that wants one writes it, and a program
|
||||||
|
that leaves one behind has left something nobody is asking about.
|
||||||
|
|
||||||
**The sprite table is cleared at exit, not saved and restored.** It lives in the atlas at
|
**The sprite table is cleared at exit, not saved and restored.** It lives in the atlas at
|
||||||
0xC000, and the pages the screen save walks run to the end of the map and pick up again at the
|
0xC000, and the pages the screen save walks run to the end of the map and pick up again at the
|
||||||
palette, with the sprites in the gap between. That is deliberate: nothing the shell draws is a
|
palette, with the sprites in the gap between. That is deliberate: nothing the shell draws is a
|
||||||
|
|||||||
@@ -5170,10 +5170,10 @@ screenSane:
|
|||||||
RSTA
|
RSTA
|
||||||
OUTA 0xE5
|
OUTA 0xE5
|
||||||
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
||||||
INIA 0x08
|
INIA 0x10
|
||||||
OUTA 0xE6
|
OUTA 0xE6
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0xE7 ; 0x0800, which is 256 entries of eight bytes.
|
OUTA 0xE7 ; 0x1000, which is 256 entries of sixteen bytes.
|
||||||
INIA 0x02
|
INIA 0x02
|
||||||
OUTA 0xE8
|
OUTA 0xE8
|
||||||
RET
|
RET
|
||||||
|
|||||||
+69
-40
@@ -377,6 +377,7 @@ uint8_t videoRead(uint8_t port) {
|
|||||||
// nothing say so in a byte and skipping them costs one test.
|
// nothing say so in a byte and skipping them costs one test.
|
||||||
static void drawSprites(uint8_t *pixels, int width, int height) {
|
static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
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--) {
|
for (int n = VIDEO_SPRITE_COUNT - 1; n >= 0; n--) {
|
||||||
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
|
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
|
||||||
const int wide = (entry[VIDEO_SPRITE_SIZE] >> 4) & 0x0F;
|
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) {
|
if (wide == 0 || tall == 0) {
|
||||||
continue;
|
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.
|
// 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]
|
const int left = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_X]
|
||||||
| (entry[VIDEO_SPRITE_X + 1] << 8));
|
| (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 mirrored = (flags & VIDEO_SPRITE_HFLIP) != 0;
|
||||||
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
|
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
|
||||||
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
|
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
|
||||||
|
const uint8_t depth = entry[VIDEO_SPRITE_DEPTH];
|
||||||
|
|
||||||
for (int downTile = 0; downTile < tall; downTile++) {
|
// ---- Only the part that lands on the screen is walked ----
|
||||||
for (int acrossTile = 0; acrossTile < wide; acrossTile++) {
|
//
|
||||||
// ---- Flipping moves the tiles as well as the pixels ----
|
// 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
|
||||||
// A mirrored sprite is not each of its tiles mirrored in place; the tile at
|
// tall would otherwise be sixty thousand turns of a loop that drew eight rows.
|
||||||
// the left end has to come out at the right end too, or a thing made of more
|
const int fromDown = (top < 0) ? -top : 0;
|
||||||
// than one tile turns inside out instead of round.
|
const int toDown = (drawTall < height - top) ? drawTall : height - top;
|
||||||
const int readAcross = mirrored ? (wide - 1 - acrossTile) : acrossTile;
|
const int fromAcross = (left < 0) ? -left : 0;
|
||||||
const int readDown = inverted ? (tall - 1 - downTile) : downTile;
|
const int toAcross = (drawWide < width - left) ? drawWide : width - left;
|
||||||
// In reading order from the first, and wrapping, because a byte plus a byte
|
|
||||||
// is a byte and the tile number is one.
|
// ---- 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]
|
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);
|
const uint8_t *art = tileArt(entry[VIDEO_SPRITE_ATTRIBUTE], tile);
|
||||||
|
const uint8_t pixel = art[(sourceDown % VIDEO_CELL_PIXELS) * VIDEO_CELL_PIXELS
|
||||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
+ (sourceAcross % VIDEO_CELL_PIXELS)];
|
||||||
const int atY = top + downTile * VIDEO_CELL_PIXELS + y;
|
// Nought is not a colour here, it is the absence of one, and it is tested
|
||||||
if (atY < 0 || atY >= height) {
|
// 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;
|
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-5
@@ -129,13 +129,17 @@
|
|||||||
// character is four tiles and a program that wants it in the background too just names the
|
// character is four tiles and a program that wants it in the background too just names the
|
||||||
// same four.
|
// same four.
|
||||||
//
|
//
|
||||||
// The table is 256 entries of 8 bytes. Eight so that entry n begins at n times eight, which
|
// The table is 256 entries of 16 bytes. Sixteen so that entry n begins at n times sixteen,
|
||||||
// is a shift - the same no-multiply argument that makes a palette entry four bytes and a map
|
// which is a shift - the same no-multiply argument that makes a palette entry four bytes and
|
||||||
// row a page. It sits above the tiles with the whole of 0x4000 to 0xBFFF still clear beneath
|
// a map row a page.
|
||||||
// it, which is two more 16K pages of tiles if they are ever wanted.
|
//
|
||||||
|
// It was eight, and grew when scaling arrived: four bytes for a target size and one for a
|
||||||
|
// depth would not fit beside what was already there. Grown NOW rather than later, because
|
||||||
|
// the cost of moving it is a rebuild of the two programs that use it, and the cost of moving
|
||||||
|
// it once somebody has written a game on top of it is not.
|
||||||
#define VIDEO_SPRITE_BASE 0xC000
|
#define VIDEO_SPRITE_BASE 0xC000
|
||||||
#define VIDEO_SPRITE_COUNT 256
|
#define VIDEO_SPRITE_COUNT 256
|
||||||
#define VIDEO_SPRITE_BYTES 8
|
#define VIDEO_SPRITE_BYTES 16
|
||||||
|
|
||||||
// Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute
|
// Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute
|
||||||
// means: its low nibble times sixteen is added to every index in the art.
|
// means: its low nibble times sixteen is added to every index in the art.
|
||||||
@@ -169,6 +173,51 @@
|
|||||||
// below for what "nothing" means.
|
// below for what "nothing" means.
|
||||||
#define VIDEO_SPRITE_BEHIND 0x04
|
#define VIDEO_SPRITE_BEHIND 0x04
|
||||||
|
|
||||||
|
// ---- How big to draw it, which is not the same as how big it is ----
|
||||||
|
//
|
||||||
|
// Bytes 8 to 11: a target width and a target height in PIXELS, low byte first. The device
|
||||||
|
// stretches the m by n tiles to fill that, so a program says how big the thing should look
|
||||||
|
// and never works out a ratio.
|
||||||
|
//
|
||||||
|
// A TARGET IN PIXELS RATHER THAN A MULTIPLIER, which is the whole of why this is usable on
|
||||||
|
// a machine with no divide. A billboard at distance d wants to be k/d pixels tall, and that
|
||||||
|
// is a number the program has anyway - out of a lookup table, most likely. A multiplier
|
||||||
|
// would have to be a fixed point fraction, computed by dividing, which is the one thing
|
||||||
|
// this CPU cannot do.
|
||||||
|
//
|
||||||
|
// NOUGHT MEANS NATURAL SIZE, eight times the tile count on that axis. So every sprite
|
||||||
|
// written before scaling existed still means what it meant, and the common case - a thing
|
||||||
|
// drawn at the size it was drawn at - costs nothing to say.
|
||||||
|
#define VIDEO_SPRITE_WIDTH 8
|
||||||
|
#define VIDEO_SPRITE_HEIGHT 10
|
||||||
|
|
||||||
|
// ---- And how far away it is ----
|
||||||
|
//
|
||||||
|
// Byte 12. Nought means no depth test at all, which is what every ordinary sprite wants and
|
||||||
|
// what a cleared table already says.
|
||||||
|
//
|
||||||
|
// Otherwise it is compared against the DEPTH BUFFER below, one column at a time, and the
|
||||||
|
// sprite draws only in the columns it is in front of. That is the thing table order cannot
|
||||||
|
// do: a billboard can be nearer than the wall in one column and further in the next, and no
|
||||||
|
// amount of sorting the table expresses that.
|
||||||
|
#define VIDEO_SPRITE_DEPTH 12
|
||||||
|
// Bytes 13 to 15 are reserved and should be left at nought.
|
||||||
|
|
||||||
|
// ---- The depth buffer ----
|
||||||
|
//
|
||||||
|
// One byte a screen column, written by the program and read by the device. A wall pass says
|
||||||
|
// how far away the thing it drew in each column was; a sprite with a depth says how far away
|
||||||
|
// it is; and a sprite pixel is drawn only where it is nearer.
|
||||||
|
//
|
||||||
|
// NOUGHT IN A COLUMN MEANS NOTHING IS THERE, so a program that never writes this has a
|
||||||
|
// buffer of noughts and every sprite draws - which is the behaviour there was before it
|
||||||
|
// existed. It is not cleared between frames: it belongs to the program, and a program that
|
||||||
|
// draws walls rewrites all of it every frame anyway.
|
||||||
|
//
|
||||||
|
// It is in the atlas because that is where the things a program sets up live, and because
|
||||||
|
// the alternative is a port and this is 640 bytes.
|
||||||
|
#define VIDEO_DEPTH_BASE 0xD000
|
||||||
|
|
||||||
// ---- What a sprite does not cover ----
|
// ---- What a sprite does not cover ----
|
||||||
//
|
//
|
||||||
// A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no
|
// A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no
|
||||||
|
|||||||
@@ -624,8 +624,9 @@ Two rather than one because the two halves of a screen are written at completely
|
|||||||
| Atlas | 0x0000 - 0x3FFF | Tile page 0. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
|
| Atlas | 0x0000 - 0x3FFF | Tile page 0. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
|
||||||
| Atlas | 0x4000 - 0x7FFF | Tile page 1. |
|
| Atlas | 0x4000 - 0x7FFF | Tile page 1. |
|
||||||
| Atlas | 0x8000 - 0xBFFF | Tile page 2. |
|
| Atlas | 0x8000 - 0xBFFF | Tile page 2. |
|
||||||
| Atlas | 0xC000 - 0xC7FF | The sprite table. 256 entries of eight bytes - and tiles 0 to 31 of page 3. |
|
| Atlas | 0xC000 - 0xCFFF | The sprite table. 256 entries of sixteen bytes - and tiles 0 to 63 of page 3. |
|
||||||
| Atlas | 0xC800 - 0xFBFF | Free - and tiles 32 to 239 of page 3. |
|
| Atlas | 0xD000 - 0xD27F | The depth buffer. One byte a screen column - and tiles 64 to 73 of page 3. |
|
||||||
|
| Atlas | 0xD280 - 0xFBFF | Free - and tiles 74 to 239 of page 3. |
|
||||||
| Atlas | 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused - and tiles 240 to 255 of page 3. |
|
| Atlas | 0xFC00 - 0xFFFF | The palette. 256 entries of four bytes: red, green, blue, and one unused - and tiles 240 to 255 of page 3. |
|
||||||
| Screen | 0x0000 - 0x3FFF | Free in a tile mode. |
|
| Screen | 0x0000 - 0x3FFF | Free in a tile mode. |
|
||||||
| Screen | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
|
| Screen | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
|
||||||
@@ -683,7 +684,7 @@ A sprite's attribute is a cell's attribute, read the same way and by the same co
|
|||||||
|
|
||||||
**A sprite is m by n tiles**, taken in reading order from one index. That is the decision the rest follows from: it needs no pixel format of its own, no second kind of memory, and nothing its art can be that the map could not also show. A 16 by 16 character is four tiles, and a program that wants the same picture in the background just names the same four.
|
**A sprite is m by n tiles**, taken in reading order from one index. That is the decision the rest follows from: it needs no pixel format of its own, no second kind of memory, and nothing its art can be that the map could not also show. A 16 by 16 character is four tiles, and a program that wants the same picture in the background just names the same four.
|
||||||
|
|
||||||
The table is 256 entries of 8 bytes at **0xC000 in the atlas**. Eight so that entry n begins at n times eight, which is a shift - the same reason a palette entry is four bytes.
|
The table is 256 entries of 16 bytes at **0xC000 in the atlas**. Sixteen so that entry n begins at n times sixteen, which is a shift - the same reason a palette entry is four bytes.
|
||||||
|
|
||||||
| Byte | Holds |
|
| Byte | Holds |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -693,6 +694,10 @@ The table is 256 entries of 8 bytes at **0xC000 in the atlas**. Eight so that en
|
|||||||
| 4, 5 | Y, the same. |
|
| 4, 5 | Y, the same. |
|
||||||
| 6 | Size: tiles across in the high nibble, tiles down in the low. |
|
| 6 | Size: tiles across in the high nibble, tiles down in the low. |
|
||||||
| 7 | Flags. Bit 0 mirrors it, bit 1 turns it over, bit 2 puts it behind. |
|
| 7 | Flags. Bit 0 mirrors it, bit 1 turns it over, bit 2 puts it behind. |
|
||||||
|
| 8, 9 | Target width in pixels. Zero means the natural width, eight times the tiles across. |
|
||||||
|
| 10, 11 | Target height, the same. |
|
||||||
|
| 12 | Depth. Zero means no depth test. |
|
||||||
|
| 13 - 15 | Reserved. Leave at zero. |
|
||||||
|
|
||||||
The position is signed and sixteen bits because the larger mode is 640 by 400, so neither axis fits in a byte - and because a sprite has to be able to sit half off the left or the top rather than appearing whole at the edge.
|
The position is signed and sixteen bits because the larger mode is 640 by 400, so neither axis fits in a byte - and because a sprite has to be able to sit half off the left or the top rather than appearing whole at the edge.
|
||||||
|
|
||||||
@@ -706,6 +711,24 @@ The same rule read the other way is what **behind** means. A sprite marked behin
|
|||||||
|
|
||||||
Sprites are drawn over a bitmap as readily as over a map. A bitmap is what a program draws once and leaves; there is no reason the mode that cannot afford to redraw itself should be the one that cannot have things moving on it.
|
Sprites are drawn over a bitmap as readily as over a map. A bitmap is what a program draws once and leaves; there is no reason the mode that cannot afford to redraw itself should be the one that cannot have things moving on it.
|
||||||
|
|
||||||
|
### Scaling:
|
||||||
|
|
||||||
|
**Bytes 8 to 11 say how big to draw it**, in pixels, and the device stretches the m by n tiles to fill that. Zero on an axis means the natural size, so every sprite written before scaling existed still means what it meant, and a thing drawn at the size it was drawn at costs nothing to say.
|
||||||
|
|
||||||
|
**A target 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.
|
||||||
|
|
||||||
|
The two axes are independent, so a sprite can be stretched one way and not the other. 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, and it is the reason such a game is possible at all on this machine. Drawing 640 by 400 pixels of wall from the CPU is 256,000 writes, which is fifteen frames of cycles for one frame of screen. Writing sixteen bytes a column and letting the device do the pixels is about three thousand.
|
||||||
|
|
||||||
|
### Depth:
|
||||||
|
|
||||||
|
**Byte 12 says how far away a sprite is**, and the **depth buffer** at 0xD000 says how far away the scenery is: one byte a screen column, written by the program. A sprite pixel is drawn only in the columns it is in front of.
|
||||||
|
|
||||||
|
Zero in a sprite's depth means no depth test at all, which is what every ordinary sprite wants and what a cleared table already says. Zero in a column means nothing is there, so a program that never writes the buffer has one of zeroes and every sprite draws - which is exactly the behaviour there was before the buffer existed.
|
||||||
|
|
||||||
|
**Per column, and that is the point.** A billboard can be nearer than the wall at one end of itself and further at the other, and no ordering of the sprite table can say that. Table order settles sprites against each other; the buffer settles them against the scenery.
|
||||||
|
|
||||||
|
The buffer belongs to the program. It is not cleared between frames, because a program that draws scenery rewrites all of it every frame anyway.
|
||||||
|
|
||||||
`Programs/CosmOS/Apps/Sprite.asm` moves one across the shell's own text without writing a byte of the map.
|
`Programs/CosmOS/Apps/Sprite.asm` moves one across the shell's own text without writing a byte of the map.
|
||||||
|
|
||||||
### Cells:
|
### Cells:
|
||||||
|
|||||||
+133
-4
@@ -149,12 +149,15 @@ pokeAtlasRun() {
|
|||||||
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
|
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
|
||||||
}
|
}
|
||||||
|
|
||||||
# One entry of the sprite table, which is eight bytes at 0xC000 plus eight times its number.
|
# One entry of the sprite table, which is sixteen bytes at 0xC000 plus sixteen times its
|
||||||
# X and Y are signed and go in low byte first, so a negative one is written as its two's
|
# number. X and Y are signed and go in low byte first, so a negative one is written as its
|
||||||
# complement here rather than being worked out at every call.
|
# two's complement here rather than being worked out at every call.
|
||||||
|
#
|
||||||
|
# The last eight bytes are left alone, and mean what a cleared table means: natural size and
|
||||||
|
# no depth test. spriteSize below is what fills them in.
|
||||||
spriteAt() {
|
spriteAt() {
|
||||||
# spriteAt <n> <tile> <attribute> <x> <y> <size> <flags>
|
# spriteAt <n> <tile> <attribute> <x> <y> <size> <flags>
|
||||||
local base=$(( 0xC000 + $1 * 8 ))
|
local base=$(( 0xC000 + $1 * 16 ))
|
||||||
local x=$(( $4 & 0xFFFF ))
|
local x=$(( $4 & 0xFFFF ))
|
||||||
local y=$(( $5 & 0xFFFF ))
|
local y=$(( $5 & 0xFFFF ))
|
||||||
pokeAtlas "$base" "$2"
|
pokeAtlas "$base" "$2"
|
||||||
@@ -167,6 +170,25 @@ spriteAt() {
|
|||||||
pokeAtlas "$(( base + 7 ))" "$7"
|
pokeAtlas "$(( base + 7 ))" "$7"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# How big a sprite is to be drawn, in pixels, and how far away it is. Nought either way means
|
||||||
|
# what nought means to the device: natural size, and no depth test.
|
||||||
|
spriteSize() {
|
||||||
|
# spriteSize <n> <width> <height> [depth]
|
||||||
|
local base=$(( 0xC000 + $1 * 16 ))
|
||||||
|
pokeAtlas "$(( base + 8 ))" "$(( $2 & 0xFF ))"
|
||||||
|
pokeAtlas "$(( base + 9 ))" "$(( ($2 >> 8) & 0xFF ))"
|
||||||
|
pokeAtlas "$(( base + 10 ))" "$(( $3 & 0xFF ))"
|
||||||
|
pokeAtlas "$(( base + 11 ))" "$(( ($3 >> 8) & 0xFF ))"
|
||||||
|
pokeAtlas "$(( base + 12 ))" "$(( ${4:-0} & 0xFF ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# One column of the depth buffer, which is one byte a screen column at 0xD000. Nought means
|
||||||
|
# nothing is in that column, which is what a program that never writes it says everywhere.
|
||||||
|
depthAt() {
|
||||||
|
# depthAt <column> <depth>
|
||||||
|
pokeAtlas "$(( 0xD000 + $1 ))" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
# Every pixel of one tile the same index. Tile n begins at n times 64.
|
# Every pixel of one tile the same index. Tile n begins at n times 64.
|
||||||
solidTile() {
|
solidTile() {
|
||||||
# solidTile <tile> <index>
|
# solidTile <tile> <index>
|
||||||
@@ -616,6 +638,113 @@ echo "Checking what the video device draws."
|
|||||||
&& result ok "a sprite has pages too" "tile one of page one, not of page nought" \
|
&& result ok "a sprite has pages too" "tile one of page one, not of page nought" \
|
||||||
|| result no "a sprite has pages too" "got $(pixel spritepage 16 24)"
|
|| result no "a sprite has pages too" "got $(pixel spritepage 16 24)"
|
||||||
|
|
||||||
|
# ---- Bigger and smaller than it is ----
|
||||||
|
#
|
||||||
|
# The same one tile sprite drawn at 16 by 16 and at 4 by 4. What is checked is where it stops
|
||||||
|
# as much as where it starts: a stretch that got the ratio right and the extent wrong would
|
||||||
|
# put the right colour in the right corner and run off the end.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 16 24 0x11 0x00
|
||||||
|
spriteSize 0 16 16
|
||||||
|
spriteAt 1 1 0x00 64 24 0x11 0x00
|
||||||
|
spriteSize 1 4 4
|
||||||
|
epilogue
|
||||||
|
} | run spritescale || exit 1
|
||||||
|
[ "$(pixel spritescale 31 39)" = "255,0,0" ] && [ "$(pixel spritescale 32 39)" = "0,0,0" ] \
|
||||||
|
&& result ok "a sprite can be drawn bigger" "sixteen pixels of it, and not seventeen" \
|
||||||
|
|| result no "a sprite can be drawn bigger" "at 31 $(pixel spritescale 31 39), at 32 $(pixel spritescale 32 39)"
|
||||||
|
[ "$(pixel spritescale 67 27)" = "255,0,0" ] && [ "$(pixel spritescale 68 27)" = "0,0,0" ] \
|
||||||
|
&& result ok "and smaller" "four pixels of it, and not five" \
|
||||||
|
|| result no "and smaller" "at 67 $(pixel spritescale 67 27), at 68 $(pixel spritescale 68 27)"
|
||||||
|
|
||||||
|
# The axes are separate, which is the shape a wall column in a pseudo-3D game is: one tile
|
||||||
|
# wide at its own size, and stretched to whatever height the distance says.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 16 24 0x11 0x00
|
||||||
|
spriteSize 0 8 64
|
||||||
|
epilogue
|
||||||
|
} | run spritecolumn || exit 1
|
||||||
|
[ "$(pixel spritecolumn 23 87)" = "255,0,0" ] && [ "$(pixel spritecolumn 24 87)" = "0,0,0" ] \
|
||||||
|
&& [ "$(pixel spritecolumn 16 88)" = "0,0,0" ] \
|
||||||
|
&& result ok "the two axes scale apart" "eight wide and sixty four tall" \
|
||||||
|
|| result no "the two axes scale apart" "$(pixel spritecolumn 23 87) $(pixel spritecolumn 24 87) $(pixel spritecolumn 16 88)"
|
||||||
|
|
||||||
|
# Stretched art still comes out of the right tile. Two tiles across, drawn at four times the
|
||||||
|
# width: the halves have to stay halves rather than one of them winning.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 4 0x01; solidTile 5 0x02
|
||||||
|
spriteAt 0 4 0x00 16 24 0x21 0x00
|
||||||
|
spriteSize 0 64 8
|
||||||
|
epilogue
|
||||||
|
} | run spritewide || exit 1
|
||||||
|
[ "$(pixel spritewide 16 24)" = "255,0,0" ] && [ "$(pixel spritewide 47 24)" = "255,0,0" ] \
|
||||||
|
&& [ "$(pixel spritewide 48 24)" = "0,255,0" ] && [ "$(pixel spritewide 79 24)" = "0,255,0" ] \
|
||||||
|
&& result ok "a stretched group keeps its tiles" "each half of it is half of the result" \
|
||||||
|
|| result no "a stretched group keeps its tiles" "$(pixel spritewide 16 24) $(pixel spritewide 47 24) $(pixel spritewide 48 24) $(pixel spritewide 79 24)"
|
||||||
|
|
||||||
|
# ---- How far away it is ----
|
||||||
|
#
|
||||||
|
# The depth buffer is a byte a column. Half of this sprite has a nearer wall in front of it
|
||||||
|
# and half has a further one, which is the case NO ORDERING OF THE TABLE CAN EXPRESS - and
|
||||||
|
# the whole reason the buffer is per column rather than one number per sprite.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 16 24 0x11 0x00
|
||||||
|
spriteSize 0 0 0 0x05
|
||||||
|
for i in 16 17 18 19; do depthAt $i 0x03; done
|
||||||
|
for i in 20 21 22 23; do depthAt $i 0x09; done
|
||||||
|
epilogue
|
||||||
|
} | run spritedepth || exit 1
|
||||||
|
[ "$(pixel spritedepth 16 24)" = "0,0,0" ] && [ "$(pixel spritedepth 20 24)" = "255,0,0" ] \
|
||||||
|
&& result ok "a nearer column hides a sprite" "hidden in four columns and drawn in four" \
|
||||||
|
|| result no "a nearer column hides a sprite" "near $(pixel spritedepth 16 24), far $(pixel spritedepth 20 24)"
|
||||||
|
|
||||||
|
# A depth of nought is no depth test at all, which is what a cleared table says and what
|
||||||
|
# every sprite that is not in a pseudo-3D scene wants. The same buffer, the same wall.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 16 24 0x11 0x00
|
||||||
|
for i in 16 17 18 19; do depthAt $i 0x03; done
|
||||||
|
epilogue
|
||||||
|
} | run spritenodepth || exit 1
|
||||||
|
[ "$(pixel spritenodepth 16 24)" = "255,0,0" ] \
|
||||||
|
&& result ok "and a depth of nought never asks" "drawn straight over the nearer column" \
|
||||||
|
|| result no "and a depth of nought never asks" "got $(pixel spritenodepth 16 24)"
|
||||||
|
|
||||||
|
# And a column with nothing in it does not hide anything, which is what makes a buffer nobody
|
||||||
|
# has written the same as no buffer at all.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 16 24 0x11 0x00
|
||||||
|
spriteSize 0 0 0 0x05
|
||||||
|
epilogue
|
||||||
|
} | run spriteemptydepth || exit 1
|
||||||
|
[ "$(pixel spriteemptydepth 16 24)" = "255,0,0" ] \
|
||||||
|
&& result ok "an empty column hides nothing" "a buffer nobody wrote is no buffer" \
|
||||||
|
|| result no "an empty column hides nothing" "got $(pixel spriteemptydepth 16 24)"
|
||||||
|
|
||||||
|
# A target size is sixteen bits and the screen is not. Asking for sixty thousand pixels must
|
||||||
|
# draw the part that fits and take no longer than that part deserves - the loop is clipped
|
||||||
|
# before it runs rather than inside it.
|
||||||
|
{ prologue
|
||||||
|
spriteColours
|
||||||
|
solidTile 1 0x01
|
||||||
|
spriteAt 0 1 0x00 0 0 0x11 0x00
|
||||||
|
spriteSize 0 60000 60000
|
||||||
|
epilogue
|
||||||
|
} | run spritehuge || exit 1
|
||||||
|
[ "$(pixel spritehuge 0 0)" = "255,0,0" ] && [ "$(pixel spritehuge 319 199)" = "255,0,0" ] \
|
||||||
|
&& result ok "a size past the screen is clipped" "it filled the screen and stopped there" \
|
||||||
|
|| result no "a size past the screen is clipped" "$(pixel spritehuge 0 0) $(pixel spritehuge 319 199)"
|
||||||
|
|
||||||
# ---- Two screens, and the flip between them ----
|
# ---- Two screens, and the flip between them ----
|
||||||
#
|
#
|
||||||
# One red cell in each screen, in different rows: row one of the screen being shown, and the
|
# One red cell in each screen, in different rows: row one of the screen being shown, and the
|
||||||
|
|||||||
Reference in New Issue
Block a user