PicoSystem has a 240x240 resolution screen and can either run in full resolution or a pixel doubled mode.
The decision of which resolution to use is both aesthetically and limitation driven - you may want the retro look of larger pixels, or need the extra RAM saved by having a smaller framebuffer, or need lots of cycles per pixel to calculate visual effects.
240x240 | 120x120 | |
---|---|---|
Pixel count | 57,600 | 14,400 |
Good for | Vector art | Pixel art |
Colour | 12bits / 4,096 colours | 12bits / 4,096 colours |
Alpha | 4bits / 16 levels | 4bits / 16 levels |
Framebuffer size | 112KB | 28KB |
Cycles per pixel @ 40Hz | ~110 | ~450 |
Our graphics API takes inspiration from many sources and adds a few pinches of our own inspiration into the mix. The end result is both familiar but has its own character.
Each pixel (either on screen or in a buffer like a spritesheet) is represented by a 16-bit value into which the three colour channels + alpha are packed with each occupying 4 bits. A 4 bit value is also known as a "nibble" and can represent numbers between 0 and 15 - the same as a single hex digit.
Because the colour and alpha channels are 4-bit all colour values passed into and returned by API functions are between 0 and 15!
For example the colour value 0xff00
is pure red with full alpha. The API has helper functions like pen(r, g, b, [a])
which will return the packed colour value but you can manipulate the colour values manually if you prefer.
This pixel format provides a palette of 4,096 different colours on screen - the same as the Original Chip Set (OCS) Amiga!
The screen coordinates start at 0,0
in the top left corner and increase across and down the display. For the framebuffer the dimensions are always either 120x120
or 240x240
to match the current resolution:
The PicoSystem API creates a buffer which contains the pixel data that will be sent to the screen. This buffer has the special name SCREEN
and contains information about the current resolution and a pointer to the start of the pixel data.
The frame buffer consists of a grid of pixels starting in the top left corner of the screen. The first entry in the frame buffer is the top-left corner and subsequent entries travel along the screen from left to right. Once the end of the first row of pixels is reached the next entry will be the first pixel on the next row.
The graphics API includes a few functions for setting and resetting the current drawing "state". This includes things like setting the pen colour, changing the global alpha blending value, and defining a clipping rectangle.
Once you've set a property of the drawing state all future drawing operations will adhere to it. For example if you set the pen to a red colour then all shapes drawn afterwards will be red until you set the pen to a different colour.
At the start of each cycle of the game loop the state (except for the current spritesheet and font) is reset to default values!
The following state properties exist:
sprite()
callstext()
callsAll state methods can be called with no parameters to reset the state to default values. For example:
// set pen to a red colour
pen(rgb(15, 0, 0));
// circle will be drawn in red
circle(10, 10, 5);
// reset the pen back to default (white)
pen();
The pen sets the colour that will be used for all drawing operations. Colours are constructed from four channels; red, green, blue, and alpha - each channel can have a value from 0
to 15
(4 bits per channel).
Pens can be created an stored for later use, or passed directly into the pen()
state setting function.
// opaque red
colour_t red = rgb(15, 0, 0);
// transparent bright green
colour_t green_glass = rgb(15, 0, 0, 4);
// set pen colour directly to blue
pen(0, 0, 15);
// set pen colour directly to transparent purple
pen(8, 0, 8, 10);
// set previously created pen
pen(red);
Sometimes it can be easier to think about colours in relation to hue, saturation, and brightness. We provide a helper function to create pens based on HSV values.
hsv()
is different in that it takes values from 0
to 1
for each parameter as floating point values instead of the 0
to 15
that all other colour functions use.
// orange create via hsv() function
colour_t orange = hsv(0.2f, 1.0f, 1.0f);
PicoSystem is a bit special in that it supports 4-bits of alpha blending within its pixel format. Historically microcontrollers aren't really fast enough to perform complicated operations like alpha blending on a per pixel basis but the RP2040 both runs at a high clock speed and has a high speed memory bus allowing for very efficient manipulation of pixel data.
The way the alpha channel information is used depends on the current blend mode but generally allows one drawing operation to overlap another while letting through part of the original information. A good example of this would be looking through stained glass where you can see what's behind it, but the coloured glass tints your view.
The global alpha value is multiplied by the source alpha when performing drawing operations. It allows you to change the opacity of a sprite when blitting.
// set alpha to 50%
alpha(8);
// sprite at index 4 in the spritesheet will be drawn at 50% opacity
sprite(4, 10, 10);
// reset global alpha back to default (100%)
alpha();
All drawing operations can be constrained to a clipping rectangle. In fact all drawing operations are clipped, it's just that the default clipping rectangle is the same size as the framebuffer.
// set the clipping rectangle to a small square
// in the top left corner
clip(10, 10, 50, 50);
// write text into the box with any excess being clipped
text("This is a long message that won't fit", 10, 10);
Clipping can be helpful to ensure that drawing doesn't bleed into areas that don't want to be affected. If, for example, the top 10% of the screen is being used to show the current score and player's health bar then you could clip that area out to ensure that when you draw the background of the play area that it doesn't overlap.
TODO: Write this section up!
Sets the target buffer for drawing operations to happen on.
All drawing operations (for example pixel(x, y)
) will act upon the currently selected target buffer which by default is SCREEN
(i.e. the framebuffer).
This can be useful when you want to draw into an off-screen buffer and then copy the result over to the framebuffer.
Offsets all future drawing operations by the camera coordinates. Useful for following the player character around and not having to manually offset all draw calls.
Set the spritesheet used as the source for calls to sprite()
. Spritesheets must be a multiple of 8 pixels wide and high because individual sprites are 8x8 pixels. Each sprite has an index in the spritesheet. The indices start at 0
and increase across and down the spritesheet.
For example in this 32x32
sprite sheet (4x4 sprites) the indices are:
PicoSystem comes with a default spritesheet which contains 256 (16x16 sprites) different icons, symbols, and glyphs:
This collection of handy icons and graphics was created for us by @s4m_ur4i - check out more of his work at https://s4m-ur4i.itch.io/! They are free for you to use in your own PicoSystem projects - have fun with them!
To draw sprites we work out the index of it in the spritesheet and where we want to put it on the screen.
// draw the third sprite from the default spritesheet (banana) at 10, 10
sprite(3, 10, 10);
The cursor is the location that text()
calls will print to if coordinates are not specified. The cursor position is automatically moved when drawing text so that subsequent text()
follow on from each other.
At the end of a line of text the cursor will move to the start of the next line, the following code will output three lines of text one after another.
text("a crash reduces");
text("your expensive computer");
text("to a simple stone.");
TODO: Write this section up!