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
+133
-4
@@ -149,12 +149,15 @@ pokeAtlasRun() {
|
||||
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.
|
||||
# X and Y are signed and go in low byte first, so a negative one is written as its two's
|
||||
# complement here rather than being worked out at every call.
|
||||
# One entry of the sprite table, which is sixteen bytes at 0xC000 plus sixteen times its
|
||||
# number. X and Y are signed and go in low byte first, so a negative one is written as its
|
||||
# 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 <n> <tile> <attribute> <x> <y> <size> <flags>
|
||||
local base=$(( 0xC000 + $1 * 8 ))
|
||||
local base=$(( 0xC000 + $1 * 16 ))
|
||||
local x=$(( $4 & 0xFFFF ))
|
||||
local y=$(( $5 & 0xFFFF ))
|
||||
pokeAtlas "$base" "$2"
|
||||
@@ -167,6 +170,25 @@ spriteAt() {
|
||||
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.
|
||||
solidTile() {
|
||||
# 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 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 ----
|
||||
#
|
||||
# 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