It is the second article in “Creativity through limitation” series. Check out the first one: 8-bit demoscene. In this article, I’m going to tell you about the fantasy console PICO-8 and recreate two classic demoscene effects with it.

When was the last time you coded something just for fun? If you’re like me, then it hasn’t happened for years. However, about a year ago, I learned about PICO-8, bought it, and I have to say that these were probably the most worthy $15 I spent on myself last year!

What is PICO-8?

PICO-8 is a fantasy console for making, sharing and playing tiny games and other computer programs. When you turn it on, the machine greets you with a command line and simple built-in tools for creating your own cartridges and exploring the PICO-8 cartverse. — PICO-8 official page

Do you remember the era of 8-bit video game consoles and computers? PICO-8 returns you to those times. It is an “emulator” of a non-existing video game console with Lua “CPU” developed by Lexaloffle. It has a set of wisely designed constraints (speed, color palette, screen resolution, sound, and memory) which may look ridiculous, but it stimulates your creative muscle. Also, to let you start creating right away, PICO-8 gives you simple but convenient built-in tools: code, graphics, and sound/music editors.

PICO-8 Built-in tools

PICO-8 has a fantastic community. Many people create games, demos, and tutorials. Teachers use it at schools to teach children programming. Professional game developers use it for experiments. One thing unites them all — it’s fun!

There’s also a pretty unique creative genre on PICO-8 — Tweetcart. Tweetcart is a program that fits in a Tweet (up to 280 characters) and produces some impressive visual effect. The idea is very similar to demoscene intro.

You can find more on Twitter by #tweetcart or at Museum of Tweetcart History.


Creating classic demoscene effects on PICO-8

If you don’t know what demoscene is, read the first article in this series. Many classic effects are recurring in different demos, and I’m going to re-create two of them on PICO-8: Vertical raster bars and Rotozoomer. PICO-8 uses the Lua programming language. If you‘re not familiar with it, it shouldn‘t be a problem, because Lua syntax is simple and straightforward.

PICO-8 can run in a browser. Try it!

Vertical raster bars

Let’s start with Vertical raster bars (also known as Kefrens bars, even though the first implementation was done not by Kefrens demo group). They look impressive on old computers because you could think — wow, they draw so many vertical lines! In reality, this effect doesn’t require drawing vertical lines at all. The classic implementation utilized Amiga’s ability to run some code for every scanline and change video memory address. You create a buffer for just one line and draw it repeatedly, adding a small “bar” every time in a different position.

You can use the same approach on PICO-8. There are no horizontal blank interrupts, but you don’t need it; instead, you use memcpy function. Let’s create a template first.

-- PICO-8 init function, create a coroutine here
function _init()
  cfx = cocreate(fx)
end

-- The effect can run in 60 FPS, use _update60() instead of _update()
function _update60()
end

-- PICO-8 frame draw function
function _draw()
  -- Draw the frame
  coresume(cfx)
  -- Uncomment the following lines to see CPU load
  --cursor(0, 0)
  --color(15)
  --print(stat(1))
end

function fx()
  -- FX code will be here
end

I like to use coroutines for both drawing and updating state variables because they allow me to write code linearly and encapsulate all variables in a function. The downside of this approach is the slowdown if your drawing code occasionally takes more than one frame to complete. When you use _update() + _draw() approach, you only get frame rate drop, because PICO-8 always executes _update()/_update60() at the constant rate (30/60 times per second), but may call _draw() at the lower rate (30/15 times per second).

So, here’s the code with comments:

-- Raster bars
function fx()
  -- Raster bars variables: phase counters
  local ph1, ph2, ph3 = 0, 0, 0
  local pht1, pht2, pht3, x

  while true do
    -- Clear the line buffer. I use the last line of the screen
    -- for it to use PSET function to draw.
    memset(0x7fc0, 0, 0x40)
    -- Set temporary variables
    pht1, pht2, pht3 = ph1, ph2, ph3

    -- Loop over all 128 screen lines
    for i = 0, 127 do
      -- Vertical raster bars
      -- 1. Calculate the position for the next bar piece
      x = flr(63 + sin(pht1) * 19 + sin(pht2) * 9 + sin(pht3) * 7)
      -- 2. Draw the bar by plotting points around the calculated X position
      pset(x - 1, 127, 0)
      pset(x, 127, 5)
      pset(x + 1, 127, 13)
      pset(x + 2, 127, 13)
      pset(x + 3, 127, 5)
      -- 3. Update temporary phase variables for the next line
      pht1 += 0.01
      pht2 += 0.02
      pht3 += 0.04

      -- Copy the line buffer
      memcpy(0x6000 + i * 64, 0x7fc0, 0x40)
    end

    -- Update phase counters
    ph1 += 0.002
    ph2 += 0.004
    ph3 += 0.03

    -- We're done, wait for the next frame
    yield()
  end
end

Now let’s add a roto-zoomer. I tried implementing a full-screen roto-zoomer on PICO-8 in 60 FPS and ran out of CPU time. However, if you combine a roto-zoomer with raster-bars, you don’t need to do it full-screen — You can draw in empty space only! It’s easy to track empty space with raster bars: the bar gets only wider every line, it can’t get narrower, so you can keep simply track the lowest and the highest X coordinates.

Roto-zoomer usually is a rotating and zooming picture, but I don’t want to deal with images in this example and will create a rotating and zooming checkerboard. Here’s the code:

-- Raster bars + roto-zoomer
function fx()
  local ph1, ph2, ph3 = 0, 0, 0
  local pht1, pht2, pht3, x
  -- Roto-zoomer variables: rotation angle; zoom, X and Y phases
  local zph, zzoom, zpx, zpy = 0, 1, 0, 0
  -- Temporary variables
  local xr, lx, hx, ztx, zty, zdx, zdy, zsx, zsy

  while true do
    memset(0x7fc0, 0, 0x40)
    pht1, pht2, pht3 = ph1, ph2, ph3
    -- Set roto-zoomer temporary variables
    lx = 64
    hx = 0
    -- zdx, zdy — rotated and scaled step vector
    zdx = cos(zph) * (1 + sin(zzoom) * 0.5)
    zdy = sin(zph) * (1 + sin(zzoom) * 0.5)
    -- zsx, zsy — position on the checkerboard
    zsx = sin(zpx) * 20
    zsy = sin(zpy) * 20

    for i = 0, 127 do
      -- Vertical raster bars
      x = flr(63 + sin(pht1) * 19 + sin(pht2) * 9 + sin(pht3) * 7)
      pset(x - 1, 127, 0)
      pset(x, 127, 5)
      pset(x + 1, 127, 13)
      pset(x + 2, 127, 13)
      pset(x + 3, 127, 5)
      pht1 += 0.01
      pht2 += 0.02
      pht3 += 0.04

      -- Roto-zoomer
      -- 1. Update left and right boundaries
      xr = flr(x / 2)
      if (xr < lx) lx = xr
      if (xr + 2 > hx) hx = xr + 2
      -- 2. Coordinates at the beginning of the line
      ztx = zsx + i / 2 * zdy
      zty = zsy + i / 2 * zdx
      -- 3. Draw left part
      for c = 0, lx - 1 do
        poke(0x7fc0 + c, (band(bxor(zty, ztx), 8) == 8) and 0xaa or 0)
        ztx += zdx
        zty -= zdy
      end
      -- 4. Update coordinates to skip drawn raster bar
      ztx += zdx * (hx - lx + 1)
      zty -= zdy * (hx - lx + 1)
      -- 5. Draw right part
      for c = hx + 1, 63 do
        poke(0x7fc0 + c, (band(bxor(zty, ztx), 8) == 8) and 0xaa or 0)
        ztx += zdx
        zty -= zdy
      end
      -- 6. Clean some artifacts
      if pget(x + 4, 127) == 10 then pset(x + 4, 127, 0) end
      if pget(x + 5, 127) == 10 then pset(x + 5, 127, 0) end

      -- Copy line
      memcpy(0x6000 + i * 64, 0x7fc0, 0x40)
    end

    -- Update raster bars variables
    ph1 += 0.002
    ph2 += 0.004
    ph3 += 0.03

    -- Update roto-zoomer variables
    zph += 0.001
    zpx += 0.002
    zpy += 0.003
    zzoom += 0.001

    yield()
  end
end

Explanation of the main variables.

Instead of using texture look-up, I use bitwise operations to create checkerboard. band(ztx, 8) gives a series of 0’s and 8’s every 8 pixels, bxor(zty, ztx) creates a checkerboard. To speed up the code and make both effects run in 60 FPS, I reduce the horizontal resolution and plot 2 points with a single poke (write a byte to memory) function instead of pset.

That’s it! You can find the full code and some other effect I create in my PICO-8 repository on GitHub.


As I recall, it’s the first time I’ve tried to explain a demo effect, and I’m afraid that it may not be clear enough. I tried to add as many comments as possible, but if you’ve got any questions, let me know, I’m ready to improve and add more details. Thanks for your attention!