draw pixel and 1bpp sprite

This commit is contained in:
sejo 2021-07-24 20:14:03 -05:00
parent 73607a8c72
commit ec578efac5
2 changed files with 488 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@ -14,6 +14,8 @@ if you haven't done it already, i recommend you read the previous section at <(u
before jumping right into drawing to the screen, we need to talk about bytes and shorts :)
## bytes and shorts
even though uxn is a computer that works natively with 8-bits-sized words (bytes), there are several occasions in which the amount of data that it is possible to store in one byte is not enough.
when we use 8 bits, we can represent 256 different values (2 to the power of 8). at any given time, one byte will store only one of those possible values.
@ -128,7 +130,9 @@ the DEO instruction needs a value (1 byte) to output, and an i/o address (1 byte
DEO ( value address -- )
```
now that we are at it, let's mention its counterpart DEI (device in); this instruction needs an i/o address (1 byte) in the stack, and it will push down onto the stack the value (1 byte) that corresponds to that input.
now that we are at it, let's mention its counterpart instruction: DEI (device in).
this instruction needs an i/o address (1 byte) in the stack, and it will push down onto the stack the value (1 byte) that corresponds to that input.
```
DEI ( address -- value )
@ -140,7 +144,7 @@ in the case of the short mode of DEO and DEI the short aspect applies to the val
remember that i/o addresses can be covered using one byte only already, so using one short for them would be redundant: the high byte would be always 00.
therefore, this is the behavior that we can expect: the DEO2 (device out in short mode) instruction needs a value (1 short) to output, and an i/o address (1 byte) in the stack, in order to output that value to that address.
therefore, this is the behavior that we can expect: the DEO2 instruction needs a value (1 short) to output, and an i/o address (1 byte) in the stack, in order to output that value to that address.
on the other hand, the DEI2 instruction needs an i/o address (1 byte) in the stack, and it will push down onto the stack the value (1 short) that corresponds to that input.
@ -155,7 +159,7 @@ the system device is the uxn device with an address of 00. its output addresses
in uxntal examples we can see its labels defined as follows:
```
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
```
we will ignore the first elements for the moment, and focus on the color components.
@ -176,7 +180,7 @@ the way we could write that would be as follows:
( hello-screen.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
( main program )
|0100
@ -201,6 +205,12 @@ try changing the values of color 0, and see what happens :)
# on-screen debugger
we will take a little detour in order to talk about the on-screen debugger, that we can use now thanks to setting the system colors.
if you prefer to jump right into drawing to the screen, feel free to skip this section :)
## the debugger
if you tried using the F2 key while running your program before today, you would have found that apparently nothing happened.
that was because the on-screen debugger that the F2 key shows uses the screen device, and therefore needs the system colors to be set.
@ -214,6 +224,8 @@ now that you have some system colors, run your program and press the F2 key: you
* there is a single byte drawn with color 2: it corresponds to the address of the top of the return stack (we'll talk about it on day 5)
* there is another set of 32 bytes, drawn with color 3; these show the contents of the first section of the zero page in the main memory.
remember: you can use the F1 key to switch between different zoom levels.
take a look at the representation of the stack: if you didn't change the values i suggested above, you'll the the following numbers at the top left:
``` 25 e5 0c
@ -222,15 +234,15 @@ take a look at the representation of the stack: if you didn't change the values
what are these numbers?
25e5 is the short we assigned to the blue components of the system colors, and 0c is the i/o address of the short corresponding to .System/b !
the highlight in the leftmost 25 implies that there's the "top" of the stack now (i.e., that the stack is empty)
25e5 is the short we assigned to the blue components of the system colors, and 0c is the i/o address of the short corresponding to .System/b ! (can you say what are the numerical addresses of each of the color components in the system device?)
we can think of the highlight in the leftmost 25, as an arrow pointing leftwards to the "top" of the stack. it current position implies that the stack is empty, as there are no more elements to its left.
tip: the stack memory is not erased when taking elements out of it, what changes is the value of the address that points to its top.
## stack debugging test
let's try adding to our program, after setting the system colors, the example code we discussed above:
let's try appending to our program, after setting the system colors, the example code we discussed above:
```
#0004 #0008 ADD2
@ -246,21 +258,482 @@ if everything went alright, you'll see:
00 0c [00] 08
```
that corresponds to some extent with the result that we wrote before!
if we think of the highlight as an arrow pointing left towards the top of the stack, we'll see that its position corresponds to some extent with the result that we wrote before!
```
00 0c <- top
```
000c is the result of the addition, stored in the stack!
000c is the result of the addition that was performed, that it is now stored in the stack!
the 00 with the highlight, and the 08 to its right, correspond to the 0008 of our second operand.they were used by the ADD2 instruction already, but they are left unused in the stack memory until overwritten.
the highlighted 00, and the 08 to its right, correspond to the 0008 of our second operand. they were used by the ADD2 instruction already, but they are left unused in the stack memory. they would stay there until overwritten.
in general, if our program is functioning alright, we will see the top of the stack always at the top left.
in general, if our program is functioning alright, we will see the highlight of the top of the stack always at the top left position.
otherwise, it means that our operations with the stack were left unbalanced: there were more elements added to it than element removed from it.
* draw pixels
# the screen device
we mentioned already that the screen device can only show four different colors at a given time, and that these colors are numbered from 0 to 3. we set these colors already with the system device.
let's discuss and start using the uxn screen device!
## inputs and outputs
you will be able to find the labels of the i/o memory address space of this device described in uxntal programs as follows:
```
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
```
the inputs that we can get from this device are:
* vector (2 bytes)
* width of the screen (2 bytes)
* height of the screen (2 bytes)
the output fields of this device are:
* x coordinate (2 bytes)
* y coordinate (2 bytes)
* memory address (2 bytes)
* color (1 byte)
## foreground and background
the screen device has two overlayed layers of the same size, the foreground and the background.
whatever is drawn over the foreground layer will cover anything that is drawn in the same position in the background layer.
in the foreground layer, color 0 is actually completely transparent: a process of alpha blending makes sure that we can see the background layer wherever color 0 is present in the foreground layer.
# drawing a pixel
the first and simpler way to draw into the screen, is drawing a single pixel.
in order to do this, we need to set a pair of x,y coordinates where we want the pixel to be drawn, and we need to set the color byte to actually perform the drawing.
## setting the coordinates
the x,y coordinates follow conventions that are common to other computer graphics software:
* x starts in 0 at the left, and increases towards the right of the screen
* y starts in 0 at the top, and increases towards the bottom of the screen
if we wanted to draw a pixel in coordinates ( 8, 8 ), we'd set its coordinates in this way:
```
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
```
alternatively, we could first push the values for the coordinates down onto the stack, and output them afterwards:
```
#0008 #0008 .Screen/x DEO2 .Screen/y DEO2
```
a question for you: if we wanted to set the coordinates as ( x: 4, y: 8 ), which one of the shorts in the code above you should change for 0004?
## setting the color
sending a byte to .Screen/color will perform the drawing in the screen.
the high nibble of that byte will determine the layer in which we'll draw:
* 0: draw a single pixel in the background
* 1: draw a single pixel in the foreground
and the low nibble of the byte will determine its color.
therefore, the 8 possible combinations of the color byte that we have for drawing a pixel are:
* 00: draw pixel with color 0 in the background layer
* 01: draw pixel with color 1 in the background layer
* 02: draw pixel with color 2 in the background layer
* 03: draw pixel with color 3 in the background layer
* 10: draw pixel with color 0 in the foreground layer
* 11: draw pixel with color 1 in the foreground layer
* 12: draw pixel with color 2 in the foreground layer
* 13: draw pixel with color 3 in the foreground layer
## hello pixel
let's try it all together! the following code will draw a pixel with color 1 in the foreground layer, at coordinates (8,8)
```
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
#11 .Screen/color DEO
```
the complete program would look as follows:
```
( hello-pixel.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( draw a pixel in the screen )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
#11 .Screen/color DEO
```
woohoo!
remember you can use F1 to switch between zoom levels, and F3 to take screenshots of your sketches :)
## hello pixels
the values we set to the x and y coordinates stay there until we overwrite them.
for example, we can draw multiple pixels in an horizontal line, setting the y coordinate only once:
```
( set y coordinate )
#0008 .Screen/y DEO2
( draw 6 pixels in an horizontal line )
#0008 .Screen/x DEO2
#11 .Screen/color DEO
#0009 .Screen/x DEO2
#11 .Screen/color DEO
#000a .Screen/x DEO2
#11 .Screen/color DEO
#000b .Screen/x DEO2
#11 .Screen/color DEO
#000c .Screen/x DEO2
#11 .Screen/color DEO
#000d .Screen/x DEO2
#11 .Screen/color DEO
```
note that we have to set the color for each pixel we draw; that operation signals the drawing.
we can define a macro to make it easier to repeat that:
```
%DRAW-PIXEL { #11 .Screen/color DEO } ( -- )
```
## reading and manipulating coordinates
we will not cover repetitive structures yet, but this is a good opportunity to start aligning our code towards that.
even though the x and y coordinates of the screen device are intended as outputs, we can also read them as inputs.
for example, in order to read the x coordinate, pushing its value down onto the stack, we can write:
```
.Screen/x DEI2
```
taking that into account, can you tell what would this code do?
```
.Screen/x DEI2
#0001 ADD2
.Screen/x DEO2
```
you guessed it right, i hope!
* the first line pushes the x coordinate as a short, down onto the stack.
* the second line pushes number 0001, adds it to the previous short, and pushes the result down onto the stack.
* the third line takes that result from the stack and writes it as the new x coordinate.
as that set of instructions increments the screen x coordinate by one, we could save it as a macro as well:
```
%INC-X { .Screen/x DEI2 #0001 ADD2 .Screen/x DEO2 } ( -- )
```
here's another question for you: how would you write a macro ADD-X that allows you to increment the x coordinate by an arbitrary amount you put in the stack?
```
%ADD-X { } ( increment -- )
```
## hello pixels using macros
using these macros we defined above, our code could end up looking as following:
```
( hello-pixels.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
( macros )
%DRAW-PIXEL { #11 .Screen/color DEO } ( -- )
%INC-X { .Screen/x DEI2 #0001 ADD2 .Screen/x DEO2 } ( -- )
( main program )
|0100
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( set initial x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( draw 6 pixels in an horizontal line )
DRAW-PIXEL INC-X
DRAW-PIXEL INC-X
DRAW-PIXEL INC-X
DRAW-PIXEL INC-X
DRAW-PIXEL INC-X
DRAW-PIXEL
```
nice, isn't it?
we'll see now how to leverage the built-in support for "sprites" in the uxn screen device, in order to draw many pixels at once!
# drawing 1bpp sprites
the uxn screen device allows us to use and draw tiles of 8x8 pixels (sprites), stored in the main memory.
these tiles can be either in a 1bpp (1 bit per pixel) format, 8 bytes in size, or in a 2bpp (2 bits per pixel) format, with a size of 16 bytes.
a 1bpp tile consists in a set of 8 bytes that encode the state of its 8x8 pixels.
each byte corresponds to a row of the tile, and each bit in a row corresponds to the state of a pixel: it can be either "on" (1) or "off" (0).
## encoding a sprite
for example, we could design a tile that corresponds to the outline of an 8x8 square, turning on or off its pixels accordingly.
``` the outline of a square marked with 1s, and its insides marked with 0s
11111111
10000001
10000001
10000001
10000001
10000001
10000001
11111111
```
as each of the rows is a byte, we can encode them as hexadecimal numbers instead of binary.
it's worth noting (or remembering) that groups of four bits correspond to a nibble, and each possible combination in a nibble can be encoded as an hexadecimal digit:
* 0000 is 0
* 0001 is 1
* 0010 is 2
* 0011 is 3
* 0100 is 4
* 0101 is 5
* 0110 is 6
* 0111 is 7
* 1000 is 8
* 1001 is 9
* 1010 is a
* 1011 is b
* 1100 is c
* 1101 is d
* 1110 is e
* 1111 is f
we could encode our square as follows:
``` the outline of a square marked with 1s, and its insides marked with 0s, and its equivalent in hexadecimal
11111111 ff
10000001 81
10000001 81
10000001 81
10000001 81
10000001 81
10000001 81
11111111 ff
```
## storing the sprite
in uxntal, we need to write and label the data corresponding to the sprite into the main memory, going from top to bottom:
```
@square ff81 8181 8181 81ff
```
note that we are not using the literal hex (#) rune here: we want to use the raw bytes in memory, and we don't need to push them down onto the stack.
to make sure that these bytes are not read as instructions by the cpu, it's a good practice to precede them with the BRK instruction: this will interrupt the execution of the program before arriving here, leaving the cpu "waiting" for inputs.
## setting the address
in order to draw the sprite, we need to set its address in memory to the screen device, and we need to assign an appropriate color byte.
to achieve the former, we write the following:
```
;square .Screen/addr DEO2
```
a new rune is here! the literal absolute address rune (;) lets us push down onto the stack the absolute address of the given label in main memory.
an absolute address would be 2-bytes long, and is pushed down onto the stack with LIT2, included by the assembler when using this rune.
because the address is 2-bytes long, we output it using DEO2.
## setting the color
as we saw already, sending a byte to .Screen/color will perform the drawing in the screen.
### high nibble
as in the case of drawing pixels, the high nibble of that byte will determine the layer in which we'll draw.
however, in this case we'll have other possibilities: we can flip the sprite in the horizontal and/or the vertical axis:
* 2: draw a 1bpp sprite in the background, original orientation
* 3: draw a 1bpp sprite in the foreground, original orientation
* 6: draw a 1bpp sprite in the background, flipped horizontally
* 7: draw a 1bpp sprite in the foreground, flipped horizontally
* a: draw a 1bpp sprite in the background, flipped vertically
* b: draw a 1bpp sprite in the foreground, flipped vertically
* e: draw a 1bpp sprite in the background, flipped horizontally and vertically
* f: draw a 1bpp sprite in the foreground, flipped horizontally and vertically
### low nibble
the low nibble of the byte color will determine the colors that are used to draw the "on" and "off" pixels of the tiles.
* 0: "on" with color 0, "off" with color 0
* 1: "on" with color 1, "off" with color 0
* 2: "on" with color 2, "off" with color 0
* 3: "on" with color 3, "off" with color 0
* 4: "on" with color 0, "off" with color 1
* 5: "on" with color 1, "off" with color 0
* 6: "on" with color 2, "off" with color 1
* 7: "on" with color 3, "off" with color 1
* 8: "on" with color 0, "off" with color 2
* 9: "on" with color 1, "off" with color 2
* a: "on" with color 2, "off" with color 0
* b: "on" with color 3, "off" with color 2
* c: "on" with color 0, "off" with color 3
* d: "on" with color 1, "off" with color 3
* e: "on" with color 2, "off" with color 3
* f: "on" with color 3, "off" with color 0
## hello sprite(s)
let's do this! the following program will draw our sprite once:
```
( hello-sprite.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
( draw sprite in the background )
( using color 1 for the outline )
#21 .Screen/color DEO
BRK
@square ff81 8181 8181 81ff
```
and the following code will draw it with all 16 combinations of color, with an output that looks like this image:
=> ./img/screenshot_uxn-tiles.png screenshot of the output of the program, showing 16 squares colored with different combinations of outline and fill.
```
( hello-sprite.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &color $1 ]
( macros )
%INIT-X { #0008 .Screen/x DEO2 } ( -- )
%INIT-Y { #0008 .Screen/x DEO2 } ( -- )
%INC-X { .Screen/x DEI2 #0008 ADD2 .Screen/x DEO2 } ( -- )
%INC-Y { .Screen/y DEI2 #0008 ADD2 .Screen/y DEO2 } ( -- )
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( set initial x,y coordinates )
INIT-X INIT-Y
( set sprite address )
;square .Screen/addr DEO2
#20 .Screen/color DEO INC-X
#21 .Screen/color DEO INC-X
#22 .Screen/color DEO INC-X
#23 .Screen/color DEO INC-Y
INIT-X
#24 .Screen/color DEO INC-X
#25 .Screen/color DEO INC-X
#26 .Screen/color DEO INC-X
#27 .Screen/color DEO INC-Y
INIT-X
#28 .Screen/color DEO INC-X
#29 .Screen/color DEO INC-X
#2a .Screen/color DEO INC-X
#2b .Screen/color DEO INC-Y
INIT-X
#2c .Screen/color DEO INC-X
#2d .Screen/color DEO INC-X
#2e .Screen/color DEO INC-X
#2f .Screen/color DEO
BRK
@square ff81 8181 8181 81ff
```
note that in this case, the INC-X and INC-Y macros increment each coordinate by 0008: that's the size of the tile.
* sprites: chr format, nasu
* draw sprites
* stack operations
@ -276,7 +749,7 @@ new mode: short mode
these are the instructions we covered today:
new instructions: DEI, MUL, DIV, SWP, OVR, ROT, DUP, POP
new instructions: DEI, BRK, MUL, DIV, SWP, OVR, ROT, DUP, POP
* ADD: take the top two elements from the stack, add them, and push down the result ( a b -- a+b )
@ -290,5 +763,5 @@ stay tuned for the next sections!
# support
if you found this tutorial to be helpful, consider sharing it and giving it your (support) :)
if you found this tutorial to be helpful, consider sharing it and giving it your <(support)> :)