This week, you’ll be making another interactive program, but with a focus on simulating a large number of objects called particles.
What is this?
A particle system is a way to simulate collections of small objects called “particles” which all behave the same way. Each particle on its own is not too interesting, but put enough of them together, and you can get something really beautiful. In the animation to the right, it looks like we’re spraying droplets of water or something. Each droplet is a particle.
Particle systems are ubiquitous in computer graphics in video games, animation, visual effects for film and TV, and even graphical interfaces in apps and on websites. They’re used to simulate things like rain, snow, fog, smoke, fire, water, steam, clouds, swarms of insects, flocks of birds, and so much more. They’re also nice because they require only a small amount of code to make something that looks impressive.
0. Getting started
Here are the starting files (plural) for this lab, as a zip file.
- download it.
- if it’s still compressed, right-click and “Decompress” or “Extract” or whatever.
- make a folder for lab 4 and move all the
.asm
files that came out of the zip file into it. - replace the
abc123
inabc123_lab4.asm
with your username. - open your
_lab4.asm
file in MARS.
Good lord, what IS all this stuff?
On labs 2 and 3, you were given some very simplified include files that had only what was needed to do those labs. This time, you have been given the entire display driver.
display_2251_0925.asm
is the main file. it has a ton of functions. most of them, you won’t even use in this lab. but they’re all there if you want to play around with it on your own time!display_vars_2251_0925.asm
declares all the MMIO variables for interacting with the display.display_constants_2251_0925.asm
is a bunch of useful constants, including all the keyboard keys.macros.asm
is our growing collection of useful macros, likelstr
andprint_str
.- and finally,
lab4_graphics.asm
is the graphical assets needed, just like lab 3.- there are a lot more graphics this time.
What’s in _lab4.asm
First are some named constants (the .eqv
lines). What these constants are used for will be explained as we need them.
Then there are the variables and arrays. Again, they’ll be explained as needed.
Then there is the main
function, which is written for you, though some lines inside the main loop are commented out. You’ll be uncommenting them later.
Then there is the load_graphics
function. You did that stuff on lab 3 already, so having you do it again would be kind of pointless.
Finally there is the find_free_particle
function. This is a helper for a function you’ll write later.
1. Drawing the emitter
Try assembling and running your program. It should assemble and run just fine, but the display will just show a black screen. Boring. But this program runs as an infinite loop, just like lab 3! So you need to hit the “stop” button in MARS to make it stop running.
The emitter is the point on the screen from which the particles will be created. If we’re going with a water analogy, the emitter is like the end of the hose where water comes out. The user will control the position of the emitter in this program by using the mouse.
Just like on lab 3 where we had cursor_x
and cursor_y
, we have emitter_x
and emitter_y
here. And again, just like lab 3, we need to draw a sprite to see the emitter.
Fortunately, I’ve given you a new function, display_draw_sprite
that draws a sprite to the screen for you. All you have to do is call it with 4 arguments. So:
- Make a new function called
draw_emitter
. It will have no arguments and return nothing.- Remember what all functions must begin and end with?
- Uncomment the
jal draw_emitter
line in the loop inmain
.- At this point, you can assemble and run your code to make sure nothing has changed. (No news is good news.)
-
Inside
draw_emitter
, you’ll do this:display_draw_sprite(emitter_x - 3, emitter_y - 3, EMITTER_TILE, 0x40);
This is your goal. Done correctly, when you run your program you should now see this on your screen. If you don’t, something is wrong, and you should fix it before moving on.
If the circle is a little further to the right and down compared to this image, be sure you’re subtracting 3 from the X and Y arguments before calling display_draw_sprite
.
If the circle is moving, that means you’re changing emitter_x/y
. Don’t. There are no stores in that line of code above.
2. Moving the emitter around
Since we’re using the mouse this time, it’s actually even simpler than lab 3.
There are two variables for getting the mouse position:
display_mouse_x
display_mouse_y
If you lw
from them, you will get the coordinates of the mouse measured from the top-left of the display. Both X and Y range [0, 127].
However, if the mouse cursor is outside of the display, both variables will instead be -1
. (You only have to check one: either they’re both -1
or neither is.)
So now:
- Uncomment the
jal check_input
and make an emptycheck_input
function. - In
check_input
:- If
display_mouse_x
is -1, return from the function. (That means, branch down to the firstpop
instruction.) - Set
emitter_x = display_mouse_x
. - Set
emitter_y = display_mouse_y
.
- If
That’s literally it!
Now it should behave like in the animation to the right:
- it should follow your cursor as you move it around over the display
- if you move your cursor off the edges of the display, it should not snap to the top-left corner; it should instead just stop at the edge
As usual, get help if you can’t get this working.
Now the emitter is doing what it needs to; time to make some particles!
Tangent: how the particles are represented
In Java, we have classes to represent objects. You can give classes instance fields, and every time you new
, you get a new object with its own copies of these fields.
This isn’t really a great match for how assembly works. We’ll be using a different way of representing objects instead. Look up in the .data
segment. You’ll see these arrays being declared:
# parallel arrays of particle properties
particle_active: .byte 0:MAX_PARTICLES # "boolean" (0 or 1)
particle_x: .half 0:MAX_PARTICLES # signed
particle_y: .half 0:MAX_PARTICLES # signed
particle_vx: .half 0:MAX_PARTICLES # signed
particle_vy: .half 0:MAX_PARTICLES # signed
The syntax 0:MAX_PARTICLES
means “make an array MAX_PARTICLES
items long, and fill it with 0s”. It would be like doing new short[MAX_PARTICLES]
or so in Java.
All of the particle_
arrays are parallel arrays. They’re all the same length (MAX_PARTICLES
). An “object” is just the set of values with the same index. For example, particle 0’s active
field is particle_active[0]
; its position is particle_x[0]
and particle_y[0]
; and its velocity vector is particle_vx[0]
and particle_vy[0]
.
The particle_active
array is an array of “booleans” (bytes, but only ever the values 0 for false
or 1 for true
). All the particles start off inactive (0). When we need a new particle, we search this array for an inactive particle. Then we can mark it active (1), fill in its other properties, and the particle is now “alive.” Later, to “get rid of” a particle, we just set its particle_active
back to 0.
A downside to this system is that we have a fixed maximum number of particles available. We can never have more than MAX_PARTICLES
particles at one time. But the upside is it’s extremely simple to implement, and is actually very fast.
3.1. Spawning particles
Now things are going to get more interesting. What we want to happen is when the clicks the mouse, a new particle will “spawn” (be created) at the emitter’s position.
Checking mouse buttons
Mouse buttons work a little differently from keyboard input. There is a display_mouse_held
variable, but you never store into it - you only load from it, and it gives you the state of all three mouse buttons but packed into a single integer.
This is a technique known as bitsets or bitflags that we will learn about a little after the exam, but all you need to know about how to check for a button is:
lw t0, display_mouse_held # get the state of *all* buttons
and t0, t0, MOUSE_LBUTTON # use bitwise AND to extract just the left button
beq t0, 0, _endif # 0 means not held, not-0 means held
# here, the left mouse button is held. respond to it!
_endif:
Spawning a particle
- In
check_input
after setting theemitter_x/y
variables, you’ll make anif
to check if the user is holdingMOUSE_LBUTTON
(see above), and if so, callspawn_particle
. - Make the
spawn_particle
function. - In
spawn_particle
:- call
find_free_particle
.- This is the function that searches for an inactive particle. It returns
-1
to mean “all particles are active.” Otherwise, the number it returns is an index into theparticle_
arrays that is currently inactive.
- This is the function that searches for an inactive particle. It returns
- Move the return value into
s0
.- What do you have to do to “ask for permission” to use
s0
inspawn_particle
? Do that.
- What do you have to do to “ask for permission” to use
- If
s0
is not -1:- Set
particle_active[s0]
to 1. The constant 1. (This marks it as active.)- What type of array is
particle_active
? - So what type of store will you use? And do you even need to multiply the index?
- What type of array is
- Print out
s0
followed by a space, for testing.- You have to
move a0, s0
then do syscall 1 to print it out. print_str
is in the include file, so you can doprint_str " "
- You have to
- Set
- call
Now to test: assemble, run, and click on the display. As long as you hold the mouse button down, it should print numbers like this, stopping after it prints 99:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99
If you get an alignment error, wrong kinda store bucko
If you get the output 16 16 16...
, you didn’t move s0
into the argument register before doing the print syscall
If you get the output 0 1 1 1 1...
, there are a few possible issues:
- you might be incorrectly multiplying the index by 4 before indexing
particle_active
- you might not be indexing
particle_active
at all - it’s an array, you have to index it
Tangent: we need more digits!
If we represented the particle positions and velocities using the same coordinate system as the emitter (X and Y both in the range [0, 127]), we would run into a problem: we don’t have enough precision for smooth movement.
If we want our particles to behave something like real physical objects, we need to be able to keep track of fractions of a pixel. Fractions usually mean we have to reach for floats, but there’s another technique called fixed-point numbers.
The idea is simple: we use an integer, but we pretend that some places are fractional. For this lab, we scale all the numbers up so that we have more digits of precision. For our particles, we’ll be using a scale of 100 to give us 2 digits of precision after the decimal point.
Instead of the particle coordinates ranging from 0 to 127, they will range from 0 to 12799. 12799 represents “127.99” in our coordinate system. We perform all calculations on this larger range, and then when we draw the particles, we divide by 100 to truncate off the “decimal places”: 12799 / 100 == 127.
So that’s what’s going on with the weird numbers in the PARTICLE_X/Y_MIN/MAX
constants, and why we’re multiplying and dividing by 100 in the upcoming code!
3.2. Finishing spawn_particle
Okay, let’s finish this. In spawn_particle
, delete the printing code now that we’ve confirmed it works. But leave the code that sets particle_active
!
After setting particle_active
, you need to set the other particle variables. Since all the other arrays are .half
, you can multiply s0
by… what? (How many bytes is a .half
?) before accessing the other arrays.
Then set the properties as follows (remember, they’re .half
arrays):
particle_x[s0] = emitter_x * 100
particle_y[s0] = emitter_y * 100
particle_vx[s0] = 0
(you have a register that holds 0)particle_vy[s0] = 0
Now if you assemble, run, and hold down the mouse button… nothing should happen. Well, nothing visible should happen. Importantly, it should not crash!
If you’re wondering why we’re using
s0
, it’s cause we’ll need it for a later step.
4. Where are the particles?
We’re creating them, they’re just not visible yet. Just like we had to write draw_emitter
, we have to draw the particles too. So:
- Uncomment the
jal draw_particles
line in the loop inmain
. - Make the
draw_particles
function. You’re gonna needs0
in this function too. draw_particles
should do this:
for(int i = 0; i < MAX_PARTICLES; i++) { // s0 is i
if(particle_active[i] != 0) {
// particle_x and particle_y are SIGNED arrays.
// So which should you use: lh or lhu?
display_draw_sprite(particle_x[i] / 100 - 7, particle_y[i] / 100 - 7, 161, 0x88);
}
}
None of this code is new to you. You’ve done all this stuff before. Just remember to translate from the outside in. Write the for
loop first, then put the entire if
inside it, then put the call inside that. Do it!
I said, put the entire if
inside the for
loop. That means that you just skip inactive particles. you do not break out of the loop when you find an inactive particle.
Done correctly, you should now be able to “draw” on the display by clicking and dragging. The animation on the right shows this. You will eventually “run out of particles” to draw with, though, because you only have 100 of them. This is normal.
- If you just get an apple in the upper-left corner, or weird things like drawing diagonal lines:
- it’s most likely a problem in
spawn_particle
. draw_particles
just draws the particles whereverspawn_particle
placed them.
- it’s most likely a problem in
- If you can draw normally, but you get an extra apple in the upper-left corner:
- that’s probably all the inactive particles being drawn. Don’t draw them!
- Look at the pseudocode above - you should skip those by going to the next iteration of the loop.
You’re actually pretty close to being done!
Tangent: Apples??!???!
Yes, apples. Cause it’s fall. :)
The 161
argument you passed to display_draw_sprite
identifies which tile to use. Here is a diagram of all the possible fruit graphics. Go ahead and pick the fruit that speaks to you by changing that argument.
The other “mystery” argument, 0x88
, is the sprite’s flags. Try changing the first hex digit - the colors of the fruits get all messed up. Changing the second hex digit will have even weirder effects. Just leave it at 0x88
when you’re done playing around though.
5. Moving the particles
Here’s where the magic really happens. Once the particles start moving, it looks really cool.
Logical OR ||
In Java we can just write two conditions with ||
to check them both and do something if either one is true. We don’t have that in asm. We have to build it out of branches.
Let’s say I want to do something like if(x == 5 || x == 10)
. I’d write it like a weird switch-case:
lw t0, x
# it's like you're checking for two cases...
beq t0, 5, _then
beq t0, 10, _then
j _endif # and then you have a default that goes to the endif.
_then:
print_str "it's either 5 or 10!\n"
_endif:
Making the particles fall
- Uncomment the
jal update_particles
line in the loop inmain
. - Make the
update_particles
function. Yep, you needs0
here too. - Loop over all the particles, and only for the active ones (just like you did in
draw_particles
, I mean, it’s literally the samefor
loop andif
inside):particle_vy[i] += GRAVITY
particle_vx
andparticle_vy
are SIGNED, so which do you use,lh
orlhu
?
particle_x[i] += particle_vx[i]
particle_y[i] += particle_vy[i]
- Finally, if the particle’s position is “offscreen” (its X position is less than
PARTICLE_X_MIN
OR greater thanPARTICLE_X_MAX
OR its Y position is less thanPARTICLE_Y_MIN
OR greater thanPARTICLE_Y_MAX
):- set
particle_active[i] = 0
to “despawn” it - then this particle can be recycled in the future!
- set
I believe in you. You can do this. Once done, you should be able to produce an infinite stream of particles that fall from the emitter, as shown in the animation to the right.
- If the particles still appear but don’t move:
- you’re not adding
GRAVITY
to the Y velocity correctly.
- you’re not adding
- If the particles move downward very slowly:
- then you’re probably just setting the Y velocity to
GRAVITY
instead of addingGRAVITY
to it.
- then you’re probably just setting the Y velocity to
- If no particles appear now when they did on the previous step:
- it likely means you are always despawning the particles.
- Try commenting out the
sb
that setsparticle_active
and see if they reappear. If that’s the case, then yeah, your conditionals for despawning particles are wrong.
- If the particles loop around to the top of the screen when they get to the bottom:
- yes it looks cool, but it likely means you are never despawning the particles. Your conditionals for that are wrong somehow.
- If the particles do strange things like flickering in and out, freezing in place, only moving when you hold B, etc:
- you are breaking out of the loop when you encounter an inactive particle instead of just skipping to the next iteration. the
if
forparticle_active
should be completely INSIDE the loop. This applies to bothdraw_particles
andupdate_particles
.
- you are breaking out of the loop when you encounter an inactive particle instead of just skipping to the next iteration. the
- If the particles move in strange ways (e.g. diagonally):
- it’s a really common mistake to mix up X and Y in this kind of code… make sure you are adding X and VX, and Y and VY, and not X and VY or something.
6. Randomizing velocity (making them “spray”)
Now for a little “victory lap.” This step is very quick and just makes things look a little better.
spawn_particle
currently starts every particle with a velocity vector of (0, 0) (sitting still). This makes them fall straight down. But if we give each particle an initial “kick” in some random direction, we can make it look like they’re “spraying” out of the emitter instead.
We’re going to use syscall 42 to generate some random numbers. It works like this:
a0
should always be 0.a1
is the upper bound of the random range.- it returns a value in the range
[0, a1)
- so if you seta1
to 10, you’ll get random numbers in the range[0, 9]
.
So here’s what you do in spawn_particle
:
- Instead of setting
particle_vx
to 0…- Use syscall 42 with an upper bound of
VEL_RANDOM_MAX
- Subtract
VEL_RANDOM_MAX_OVER_2
from the return value - Store that as
particle_vx
instead
- Use syscall 42 with an upper bound of
- Similarly, for
particle_vy
:- Use syscall 42 with an upper bound of
VEL_RANDOM_MAX
- You must set the
a0
anda1
arguments again!
- You must set the
- Subtract
VEL_RANDOM_MAX_OVER_2
from the return value - Subtract
GRAVITY
from the return value (just makes it look a little better) - Store that as
particle_vy
instead
- Use syscall 42 with an upper bound of
Doing this may reveal problems in your logic for detecting when particles should despawn off the left/right/top sides. So you may have to go back to update_particles
to fix that.
And that’s it.
But how’d you get the blue background? (You don’t have to do this)
In main
, after jal load_graphics
, I did this:
li t0, 0x4287F5
sw t0, display_palette_ram
You’ll notice a few references to “palette” in the code. The palette is the set of colors that we can use for the display graphics. Palette entry 0 is used as the background color. So, whatever color you store there shows up as the background.
Google “color picker” and it will give you a color picker. Then you can copy the hex value and paste it in your program using the code above, changing #
to 0x
. Pick a color that compliments your fruit ;o
Submitting
Upload only your _lab4.asm
file to Gradescope once it’s open.
Going Further
None of this stuff is required. It’s just fun ideas if you want to go further.
If you had fun making this, the cool thing about particle systems is how easy it is to change their behavior! Here are some ideas of things you could do:
- multiple fruits
- add a
particle_tile
array, and give each particle a random tile when spawned.- the random tile would be something like
random(41) * 4 + 1
- the random tile would be something like
- then when drawing, you draw each particle using the tile in the array, instead of a fixed tile.
- add a
- particle physics
- when particles hit left/right sides, instead of despawning them, limit their coordinates to the range
[PARTICLE_X_MIN, PARTICLE_X_MAX]
and negate their X velocity - now they bounce off the walls! - you can make them bounce off the bottom in a similar way but…
- only negating the Y velocity will make them bounce infinitely. which is neat but not realistic
- so you can negate the Y velocity and then multiply by a “restitution” constant in the range
[0, 100]
to make them gradually bounce lower and lower - eventually you will run out of particles though, so you’ll also have to despawn them some other way
- maybe when the absolute value of the result of the above multiplication is below some threshold?
- or just keep track of the age of each particle and despawn when their age reaches some value?
- wind can be implemented by just adding something to the X velocity of every particle on each frame - similar to gravity
- when particles hit left/right sides, instead of despawning them, limit their coordinates to the range
- particle behavior
- instead of being passive things that just fall down, maybe they can be “active” and move towards a point (the emitter?)
- this gets real tricky real fast because you have to perform trigonometry…
- instead of being passive things that just fall down, maybe they can be “active” and move towards a point (the emitter?)