Tutorial: Layering (Generative Art 1)

September 1, 2020 tutorial 16 minutes, 58 seconds

You can find the code to copy and paste from this gist.

Generative Art 1

Layered Approach

sample generative output

Let's take a look at the code I've written for this assignment. Remember that the code was (obviously) not written in the order it is presented here: It comes together in bits and pieces, some working and others broken. This is one of problems with so many tutorials as they exist on YouTube, for example: Instructors spend a few hours building the code in haphazard and unpredictable ways -- then they present it as though they're writing it 'live', giving the impression that they just start coding with line one at the top and move to the bottom, like an essay or a research article. But if a program has more than, say, five lines of code, the odds are very good that they were not written in the order 1,2,3,4 and 5: We make mistakes, we change our minds, we hit upon different ways to solve problems half-way through. "Writing" code is almost nothing like "writing" an essay.

Tabs

In the version below, I put the code into three different TABS, one for each of PICO-8's main Functions. (You can put more than one function on a single tab, but you should never try to spread one function across two tabs.)

function _init()

function _init() is where we start, in this case by defining all of the variables that I want to have ready access to later on. I've commented the variables in the code, below.

As I suggested above, function _init() tends to grow and shrink throughout the process: Whenever I decide to add a new feature, etc., I almost always have to go back and create new GLOBAL variables by defining these inside function _init(). By the same token, when I remove code that just didn't work, I'll usually need to go back to function _init() and get rid of those variables that I don't need any more.

function _init()
        cls(1)
        counter = 0
        updatescreen = true
        colqty = 9
        rowqty = 9
        colwidth = 128/colqty
        rowheight = 128/rowqty
        layers=3
end

counter = 0

As before, I'll use a counter variable to slow down my code.

updatescreen = true

This time, however, I'm spreading the "slow down!" feature across two variables, which you'll see in the function _update() section. updatescreen is a BOOLEAN variable (as in Boolean algebra), sometimes called a FLAG: It is always either TRUE or FALSE (YES or NO, 1 or 0). What I'm trying to do here is break my code into discreet little parts. You can think of flags like updatescreen like lightswitches in a house.

layers = 3

My big challenge is to figure out how to put more than one shape inside each quadrant: e.g., sometimes my XY coordinates move, but sometimes they stay the same. If there are four quadrants, and two shapes per quadrant, that's a total of eight different shapes -- which is manageable by hand, I guess. But at, say, 4x4 sections, with three shapes per cell, that would be 16x3=48 separate shapes, which becomes hard to manage.

My solution (one of many possible!) is to think in terms of layers: I draw my biggest shapes on the bottom layer, then I start a new layer, drawing smaller shapes, then a last layer, drawing smaller shapes still. Computers never tire of repeating the same series of actions again and again: Just like before, I'm using for / do loops to count over cols and rows, but now I've put that inside a bigger loop: This one counts through each of my layers.

function _update()

Next, the function _update() function is where I put the code that will slow my program down. The counter variable gets increased with every loop, but that increase only matters once it hits a multiple of 120 (e.g., 120, 240, 360, etc.).

I chose to set the timer's trigger at about four seconds (an arbitrary choice on my part). At 30 frames per second, 4 seconds is about 120 cycles or repetitions. So when counter bumps up to a perfect multiple of 120 (counter % 120 == 0), the conditional statement inside this function suddenly evaluates as TRUE: counter divided by 120 suddenly gives us an even number without a remainder! For 119 cycles -- because of lots of ugly remainders -- we'd be consigned to the else part of the conditional: Setting screenupdate to false, over and over.

But now counter % 120 == 0 is true, so the computer flows into the previously off-limits section of the conditional: It sets the updatescreen variable to true. Of course, as soon as we come back to this function, and add 1 to counter, those remainders will pop up again, and we'll be back in the else part of the conditional, setting updatescreen to false again. The question is: What happens elsewhere in the code during this brief, shining moment (1/30th of a second, or 33.3 milliseconds)1 when updatescreen actually equals true?

Note that I don't have to stick with only one variable like updatescreen. In games, I'll typically use a lot of these "flags": update_high_score, show_instructions, show_title_screen, and so on. Because they are BOOLEAN VARIABLES (only TRUE or FALSE) they take up no practically no memory, are fast to read and write, and help me simplify my code.

function _update()
        counter=counter+1
        if (counter%100==0) then
                updatescreen=true
        else
                updatescreen=false
        end
end

function _draw()

Here we are, at the start of the biggest function. It is hard to talk about it usefully without breaking it up somewhat, so the format will change a bit below. I'll put a link to the code after all these comments. Let's go!

function _draw()

    if (updatescreen==true) then
        cls(1)

Here's the fruition of that 2-part approach we discussed earlier. See how I'm not asking about counter at all, but instead I'm interested in whether we updatescreen or not? That makes my code MUCH easier to read and understand, I think: We'll improve further on this approach later in the semester.

For now, notice that everything that happens in this function depends on updatescreen being set to true. We don't even clear the screen (cls(1)) until we know for sure that this is the one frame out of every 120 where we get to make a new piece of art. (See sections above for information about 120 frames, etc.).

(function _draw continues)

        radius = flr(colwidth/2)

        for pass=layers,1,-1 do
            scale=pass/layers

In the code above, I implement my layers approach (which took a little bit of trial and error, FWIW). You can think of it this way: If I had been using columns and rows to put stuff on the screen (that is, using X and Y), then with layers, I just added a Z-axis: Up and down.

Before, it looked a bit like this:

Count X from 1 to 4:
        Count Y from 1 to 4:
                Calculate exact position
                Draw something
        End of Y Loop
End of X Loop

Remember that if we were to peek in at how the computer handled this code, we'd see something like this:

X1 Y1
X1 Y2
X1 Y3
X1 Y4
X2 Y1
X2 Y2
X2 Y3
X2 Y4
X3 Y1
X3 Y2
X3 Y3
X3 Y4
X4 Y1
X4 Y2
X4 Y3
X4 Y4

Clearly, the outermost loop, in this case the X loop (width, columns, etc.) has a much easier job: It only runs four times. But the interior Y loop (height, rows, etc.) ends up running a total of 16 times. NOW we're going to just add one more loop to our project (one more Matroska doll). While some approaches are better than others, we could probably do it any way we pleased: We could put the layers loop on the top, so that X and Y are inside it, OR we could put the layers loop on the bottom, so that it is actually inside both X and Y. In effect, we're asking: which of these two approaches should I take?

Should I do it like this?

  1. Find the specific X, Y coordinates of the cell you're working on;
  2. Now at that position, draw 3 different shapes, each a bit smaller than the one before.

Or should we go this route?

  1. Get ready to draw shapes of size S:
  2. Counting X by Y, draw shapes of size S centered in every quadrant;
  3. Now reduce the size of S. Go back and do everything again.

(Note that the 'pseudocode' here doesn't present us with an exact parallel.)

Here's a more abstracted, esoteric, but perhaps more satisfying version of the question:

Columns x Rows x Layers?

or

Layers x Columns x Rows?

For our purposes? Six of one, half-dozen of the other: They're basically the same at this point. In the end, I chose to put the layer-counting loop as the outer-most loop, which means that it repeats just a couple of times, while X works harder than before, and Y (the inner-most loop) works hardest of all.

BONUS: One perk of doing these shapes by looping through them with a for / do loop? I can actually turn them off without causing trouble. I can do this by setting layers variable (in function _init()) to 1, so that (in effect) my outermost loop ends up counting from 1 to 1. It never ends up running that loop more than once.

OK. That's a lot of info. I'm going to add just a word or two more, and then post this. We can talk more about things as you like.

Remember that since I've added the layers loop, all of this stuff is inside it. The most important part here is a new variable that I introduced just inside the layers loop, called scale. Remember how that loop looked:

for pass = layers, 1, -1 do
        scale = pass / layers

So I'm inventing a variable here called pass, which keeps track of which pass I'm doing over the screen (e.g., first pass, second pass, etc.). The syntax that I've used, though, means that pass doesn't start from 1, like I normally would: It starts from whatever the value of layers is (which was set in the _init() function). That third argument, "-1", is not usually there, but that's because we're normally counting from 1 to n, rather than from n to 1. "-1" lets us count backwards ("decrement by 1"). So if I have asked for 4 layers total, pass will be 4, 3, 2, and then 1.

Why? Look at the next line. Again, millions of approaches to this, but this is a simple one: In order to resize the shapes on each subsequent layer, I compute scale. If there is only one layer total, then scale is always 1.0 (i.e., 100%). Two layers? 100% and 50%. Three? 100%, 66%, and 33%. See how that works? I then use scale to adjust the size of each rectangle, and/or each circle, in turn.

So why count backwards? Because layers can only work if the biggest is on the bottom -- otherwise they'll obscure subsequent layers. It works out nicely, as each subsequent layer is a smaller and smaller portion of 100% (the full cell).

            for col=0,colqty-1 do
            for row=0,rowqty-1 do

                x1=col * colwidth
                x2=x1+colwidth-1
                xcenter=(x1+x2)/2

                y1=row * rowheight
                y2=y1+rowheight-1
                ycenter = (y1+y2)/2

                mycolor=rnd(1)*15+1
                mycolor=flr(mycolor)
                mycolor=mycolor%6+1

                -- pick a shape
                shape=rnd(1)

                if (shape>0.49) then

                    xdist=(colwidth)/2
                    ydist=(rowheight)/2

                    xdist=xdist-1
                    ydist=ydist-1

                    xdist=xdist*scale
                    ydist=ydist*scale

                    x1=xcenter-xdist
                    x2=xcenter+xdist

                    y1=ycenter-ydist
                    y2=ycenter+ydist

                    rectfill(x1, y1, x2, y2, mycolor)

                else

                    circfill(xcenter, ycenter, radius * scale, mycolor)
                end
            end
        end
    end
end

Last observation

Did you spot this in the code?

mycolor = mycolor % 6 + 1

There are a lot of choices we could talk about. The program runs fine without this little bit of extra code. But what does it change, and why?

Finishing-Up

Completing your code is always (ALWAYS) a process of returning to it and tweaking a line here, fixing the spacing there, renaming a variable throughout, etc. It is gradual, and accomplished over time. If you are new to programming: The fact that it works (or almost works!) is good enough for now.


  1. (Actually, 33.3 milliseconds is a pretty long time. But we'll worry about that later!) 

alt