Everything is numbers.
In Pico-8, each of the 16 colors can be drawn using it’s index in the Pico-8 palette. Using pget() to get the color of a pixel on the screen will also get you an index from the palette. The peek() function even returns a byte value containing color indexes for two pixels. In Pico-8, colors are indexes.
So today, we’ll be treating them as such, thinking of colors as numbers!
Our first subject today is this great example of extended procedural dithering! (read about procedural dithering here, here and there)
In this doodle, the fire effect is produced by a form of cellular automaton. The algorithm takes random pixels from the screen, gets their color and finds new colors for them. While this could be done through a series of if and elseif, there is a much more elegant to way to do it, using a table.
This is what I call a link table. Each element in the table is an index for another element in the table, until an element links to itself.
And of course, color indexes can be used as indexes to this table! When our algorithm gets a pixel value, it will look up the value it links to in the link table and there’s the new pixel value. Here are the 6 lines of code producing the fire effect in this doodle:
for i=0,2999 do local x,y=rnd(128),rnd(128) local c=pget(x,y) c=fire[c] circ(x,y,1,c) end
This link table technique can also be made less linear, with multiple colors linking to the same one. The best example for this is the darkening table which links every color to a similar color at a darker tone in the palette. It’s super useful to do shading effects and fades!
You can read more about the Pico-8 colors in the Doodle Insights #5!
But let’s back away from the idea of indexes for now. Pixel values can lead to many things. One of the most obvious is pixel operations!
In this doodle inspired by Anders Hoff (@inconvergent)‘s amazing generative stuff and articles, a ring is being distorted and added onto the previous frame, every frame.
There were multiple ways to achieve this but I took the lazy way out here. The last frame is always stored in the sprite-sheet. Every frame, we draw the ring on the cleared screen. Then we look at every pixel on the screen and we add the value of the same pixel on the sprite-sheet to it, up to 15. And we use memcpy() to store the final frame for the next one.
That alone would give out an interesting result but also not very gracious, with colors clashing badly all over the screen. This is not the case in the doodle as you can see, and that’s because I used pal()!
And actually I used it quite badly, as we’ll see further on. But for this doodle the way I did it was to swap every color of the pico-8 palette to the corresponding color in a gradient stored in a table. And then draw the frame currently stored in the sprite-sheet on the screen. (again)
local grdk=#grds for i=0,15 do pal(i,grds[flr(i/16*grdk)+1]) end spr(0,0,0,16,16)
And now our screen looks good! But the CPU in very unhappy and we only get a frame once every 1/10 second or so.
This one runs at 60 fps!!
Here, the ring moves faster but also it is drawn directly with the highest value (15) and the gradient is actually double-sided, going from 0 to 7 and then to 0 again. And random pixel values get decreased.
First the color swaps! This time around we’re making much better use of the pal() function, by using its third parameter! The third parameter of pal() lets your color-swaps affect the whole screen when displaying the frame, rather than just replacing the original colors in your next draw calls. The code looks like this:
for i=0,15 do pal(i,plt[flr((i/16)%1*#plt)+1],1) end
And that’s in the _init()! No need to do it again until you use the third argument again or you call pal() without arguments at all, which resets all color swaps.
The second problematic thing is the decreasing of random pixels. We could use procedural dithering but I really wanted to do it on individual pixels here. In the intro I mentioned that peek() returned a byte value that contains two pixel values. peek() happens to be very fast and so is poke(). And tables.
So we’ll be making two look-up tables! Each look-up table will have a value for every pixel duo possible. (that’s 16*16=256 possibilities for each table) On one table, the left pixel of each duo gets decreased (down to 0) and on the other table, the right pixel gets decreased. (down to 0)
In our _draw() function, we have this:
for i=0,1699 do local a=flr(rnd(0x2000))+0x6000 local v=peek(a) if rnd(2)<1 then poke(a,cvmap1[v]) else poke(a,cvmap2[v]) end end
And this will decrease 1700 pixels every frame! (with possibility of double decreasings but that’s ok) At 60 fps!! (and with the CPU at >95%)
This one uses the same optimization tricks!
Here, the pixel values are binary! (until we add the shadow effect anyway) Every pixel on which we’re drawing a shape gets the other value than the one it had before, 0 becomes 1, 1 becomes 0!
To do this, we make one function that will serve all our drawing needs. That function draws horizontal lines with the behavior we just described. And it uses very similar tables than the ones in previous doodle, except the pixel values can only be 0 or 1 (that’s 2*2=4 possibilities for each table) and there’s one more table for when both pixels of each duo get changed!
Afterwards we only have to recode every drawing functions that we might want to call so that it uses our line drawing function! Every frame, use these custom functions to draw shapes on the blank screen, use our previous palette swapping trick to swap 1 for 7 (white) and there we are?
Well yeah, there we are, but y’know, let’s push it a bit further and add a shadow effect.
And of course there was no better (i.e. actually usable) way to do this other than bitwise magic! Here’s what it looks like:
for a=0x6000+64,0x6000+128*64-1 do local va=peek(a) local vb=peek(a+64) local v=va+band(va,vb) poke(a,v) end
Not explaining that today, hopefully you can figure it out!
Fact: Metaballs are cool.
In case you didn’t know, metaballs are these blobs that can be rendered by calculating the added presence of all the metaballs at any point, based on the distance from the metaballs’ centers to this point. Get a point on the screen, figure out how far it is to each ball, make the sum of these distances, draw a color (or nothing) based on that distance. Metaballs are pretty costly to render.
And so we’ll have to get tricky! As in the last doodles, we’ll have pixels serving as values from 0 to 15, which we’ll swap to a gradient using the third parameter of pal(). And to get these pixel values, we’ll use the sprite-sheet.
Because we’re all about performances, we’re using procedural dithering. For any random pixel on the screen, we will check for each ball if this pixel is in the bounding-box of that ball. (which is as easy as testing “if abs(x-b.x)<b.r and abs(y-b.y)<b.r then”) If it is in the bounding box, we get the relative position of the pixel in the bounding box and we adapt it to a position on the sprite-sheet. On the sprite-sheet we have one big metaball that we drew there in the _init(). We get the color at the adapted coordinates on the sprite-sheet with sget() and that’s the distance value between our random pixel and the center of that ball. With every ball, that distance is added to a value v, capped at 15, which will be the color to set on the screen.
We made pixel operations using the sprite-sheet as a reference sheet!
For our final doodle, we’re taking a look at a custom lighting system that simulates a normal map!
A normal map is a texture where the blue and red channels are used to describe a normal vector showing what direction the pixel faces for the lighting. It’s generally used with shaders and it’s really good to add a sense of depth and/or texture.
But we don’t have color channels in Pico-8, we only have 16 indexed colors. So instead, each of 9 colors will indicate a direction! Here’s a quick look at the sprite-sheet for this doodle:
And again we are using procedural dithering! For any random pixel, we’ll check the corresponding color/direction in the sprite-sheet and by comparing it to the relative position of the light and its distance, we’ll choose a shade of our light gradient! It’s actually fairly simple but the code is horrible as we check for each possible direction, for each pixel. (there’s not ‘switch’ in Lua)
Again with the sprite-sheet as a reference sheet, but with more processing on it this time. And the reference sheet was not made by code and it was kindof a pain to draw actually.
Seeing colors as values can allow formore complex effects which can easily turn out very interesting! Pixel values can be indexes for tables that may link to other pixel values. Or they can serve as a ranged value, to be translated to a gradient through the powers of color swapping. They can be the subject of simple maths operations. They may even be used as indicator of directions, simulating a normal map! The limit really is your thirst for experimenting here! Just keep in mind that colors are numbers and try things! The one drawback is that single-pixel operations tend to be quite costly, but luckily there’s always a way to optimize!
And that’s it for this Doodle Insights! I hope you liked it!
If you have any questions or remarks, please do put them in the comments down below or on the patreon post or send them to me on Twitter!
Next week we may finally talk about logic data generation! I wanted to do it this week but I realized I needed at least one more Pico-8 Doodle to make it really interesting, so I’ll do that first!
As usual, I want to thank my amazing Patreon patrons for supporting this series and all the other stuff I do! Here are the names of all my 3$+ supporters!
Ryan Malm, Joseph White, Adam M. Smith, Goose Ninja, Matthew, Giles Graham, Luke Davies, Jake Meanwell, Tim and Alexandra, Sasha Bilton, berkfrei, Nick Hughes, Christopher Mayfield, Jearl, Dave Hoffman, Thomas Wright, Morgan Jensen, Zach Bracken, Cole Smith, Marty Kovach, GucioDevs, Corey O’Connor, nanoplink, Joel Jorgensen, Andreas Bretteville, Raf, Anne Le Clech, Flo Devaux, Brent Werness, Ian Fare, babyjeans, Emerson Smith, Cathal O’Keeffe, Dan Sanderson, Andrew Reist, vaporstack, Dzozef, Tony Sarkees, Justin TerAvest and Vorzam!
You too can help me live on this stuff, simply by pledging one or more dollars to me on Patreon! You can get a whole bunch of cool rewards, including exclusive wallpapers, exclusive source-codes, exclusive articles about code and more! Help me make more cool stuff and you’ll get even more cool stuff!!
Thank you for reading and enjoy thinking of pixels as values!