The Skeleton of a Physics Engine
Stack Five Boxes and Settle Them
Goal
Undo penetrated overlap with position correction, adjust how solid a pile is with the number of iterations, and leave the process of five boxes stacking and settling in pictures. This lab is the wrap-up of Course 2.
Why it matters
Impulse only fixes velocity and cannot undo an overlap that has already penetrated. This is because the moment you notice the overlap is already after it has overlapped. So you have to push the position separately, and there are two knobs here. They are how much to undo per step (percent) and the margin to ignore (slop). If you push everything at once, it bounces, and if you leave no margin, it jitters slightly. The core of this layer is that not trying to make the overlap exactly 0 is actually more stable.
And when there are several contacts, they are entangled with each other. If you fix the top, the bottom is squashed, and if you fix the bottom, the top floats up. Solving exactly needs simultaneous equations, which are expensive, so engines approximate by repeating the same pass several times. The number of iterations decides how solid a pile looks.
Steps
- Put the toolbox in
/root/stack. /root/stack/correct.py— position correction./root/stack/out/03-sink.txt— the depth it sinks to without correction./root/stack/out/04-fixed.txt— the depth with correction on./root/stack/out/05-iters.txt— iteration counts 1, 4 and 10./root/stack/out/06-stack.txtandstack.png— stacking five./root/stack/out/f00.pngtof11.pngandindex.html— the stacking process.
Notes
- Start a server with
nohup python3 -m http.server 8080 -d /root/stack/out &and openhttp://localhost:8080/in the Web preview. If the twelve pictures play in order, you can see the boxes fall and stack. - Keep the order of a step exactly — gravity, iterative velocity solve, position update, position correction. If the order differs, the values change.
- Common mistake 1: changing velocity as well in position correction. The correction becomes a force and the pile bounces up by itself.
- Common mistake 2: trying to make the overlap 0 without slop. A tiny correction keeps happening at every step and the boxes jitter.
Put the drawing toolbox in place
Save /root/stack/gfxlib.py exactly as in the example, and use /root/stack/check.py to draw a test pattern and make /root/stack/out/00-check.png. The pattern is a 64x64 black background with a white (255,255,255) diagonal line from (0,0) to (63,63), and over it a red (255,0,0) horizontal line from (0,32) to (63,32).
From this lab on, you do not rebuild the PNG encoder. We hand you the same code you built by hand in the first lab as a tool — because file formats are not what you learn here.
The lab Pod has no volume, so the files you made in the previous lab are not kept. That is why each lab starts by putting the toolbox in place again.
Create Canvas(w, h, bg), draw the two lines with line(x0, y0, x1, y1, rgb), and then save with write_png(path). Draw the horizontal line later, so that the intersection (32,32) becomes red.
In this lab you use it to leave the process of the box pile stacking up in twelve pictures.
Position correction
In /root/stack/correct.py, make correct(depth, slop, percent, inv_m1, inv_m2). Find 보정량 = max(depth - slop, 0) / (inv_m1 + inv_m2) * percent and return the two real numbers (보정량*inv_m1, 보정량*inv_m2) (the placeholder is the correction amount). It means object 1 is moved by that much in the -n direction and object 2 in the +n direction. If the sum of the inverse masses is 0, return (0.0, 0.0).
max(depth - slop, 0) is the whole of the slop. If the overlap is shallower than the margin, nothing is done. Without it, a correction that tries to make the overlap exactly 0 keeps happening at every step with a tiny value and the object jitters slightly.
percent is how much of the overlap to undo per step. At 1.0 everything is pushed out at once, and then the object is thrown out and hits again in the next step, producing an oscillation. Usually you use 0.2 to 0.8.
The reason you divide by the inverse masses and then multiply by each one's inverse mass again is to make the lighter side move more. If one side is an immovable object (inverse mass 0), the other side moves all of it.
Without correction it sets while sunk
Drop one box with half-height 0.5 from y=1.5, v=0 and run it for 600 steps with g=-9.8 and dt=1/60. One step is (1) v += g*dt, (2) with 10 iterations, if 0.5 - y > 0 and v < 0 then v = 0, (3) y += v*dt, and you do not do position correction. Write the final penetration depth 0.5 - y to /root/stack/out/03-sink.txt as depth_final= (0 if negative).
Even if you make the velocity 0, the amount that has already penetrated stays as it is. This is because the moment you notice the overlap is already after it has overlapped.
And in that state, gravity in the next step gives velocity again, the position goes a little lower, and it becomes 0 again. The depth stops at some value, and that is the answer of this step.
After 600 steps the depth is greater than 0.02. That is a value equal to a few percent of the box height, and if you stack several boxes, this error accumulates and the pile gets noticeably lower.
Setting v = 0 is the same as an impulse with a coefficient of restitution of 0. The floor's inverse mass is 0, so only the box side changes.
With correction on, it comes back to the slop
Add a (4) position correction step to the same conditions — after the position update, find depth = 0.5 - y, and if depth > slop, then y += percent*(depth - slop) (slop=0.01, percent=0.2). Write the depth after 600 steps to /root/stack/out/04-fixed.txt as depth_final=, and the difference from the step 3 value as improved=.
The correction moves only the position. If you change velocity as well, the correction becomes a force that pushes the object and the pile bounces up by itself.
The depth stops near the slop. It is normal that it does not become 0 — because leaving it in a very thinly overlapped state is actually more stable.
improved is 3단계 깊이 - 4단계 깊이 (the depth of step 3 minus the depth of step 4). It should be positive, and it is roughly more than half of the step 3 depth.
The floor does not move, so only the box side is moved. For boxes against each other, you must push both sides, divided by the inverse mass ratio.
The number of iterations decides how solid the pile is
Drop three boxes with half-height 0.5 from y = 0.5 + i + 1.0 (i=0,1,2) and run for 600 steps, changing only the number of iterations to 1, 4 and 10, and write each one's maximum penetration depth to /root/stack/out/05-iters.txt as iters1=, iters4= and iters10=. Turn position correction on (slop=0.01, percent=0.2).
There are three contacts — floor to box 0, box 0 to box 1, and box 1 to box 2. Solve them in this order inside an iteration.
The contact between boxes goes like this. If the overlap 1.0 - (y[i+1] - y[i]) is greater than 0 and the relative velocity v[i+1] - v[i] is negative, find j = -(v[i+1]-v[i]) / (inv_i + inv_{i+1}) and do v[i] -= j*inv_i and v[i+1] += j*inv_{i+1}. The inverse mass of every box is 1.
Apply position correction to all three contacts too. Between boxes, find m = percent*(depth-slop)/(inv_i+inv_{i+1}) and lower the lower one by m*inv_i and raise the upper one by m*inv_{i+1}.
The maximum penetration depth is the largest of the depths of the three contacts. It should get smaller as you increase the iterations — that is what this step is meant to show.
Stack five
Drop five boxes from y = 0.5 + 2.0 + i*1.6 (i=0..4), run 900 steps with 15 iterations, and write the final five heights and the maximum penetration depth to /root/stack/out/06-stack.txt as y1= to y5= and max_depth=. Then draw the final state on /root/stack/out/stack.png (256x256, background (20,20,28)) — a box has a horizontal half-width of 1.5, the screen coordinates are sx = int(x*25.6 + 128) and sy = int(255 - y*25.6), the colors from the bottom are (230,120,110), (230,190,110), (150,220,120), (120,190,230) and (190,140,230), and at the end you mark the floor by drawing the sy=255 line in white.
If you place the initial heights staggered, you get the boxes coming down and stacking one after another. If you place them at equal spacing, they all fall at once and there is nothing to see.
One box is a rectangle on the screen from 89 to 166 horizontally and from int(255-(y+0.5)*25.6) to int(255-(y-0.5)*25.6) vertically. With fill_rect of Canvas, it is one line.
The bottom box has penetrated by the slop, so its rectangle comes down to sy=255. So if you draw the floor line first, the box covers it. Draw the floor last.
The final heights come out near 0.47, 1.44, 2.41, 3.38 and 4.37. The reason they are not exactly 0.5, 1.5, 2.5 is that it settles while overlapping by the slop.
The maximum penetration depth should not exceed 0.05. If it does, the number of iterations is insufficient or the position correction is missing.
The stacking process in twelve pictures
In the same simulation, capture the state every 15 steps starting from step 0 to make twelve pictures, /root/stack/out/f00.png through f11.png, create /root/stack/out/index.html containing all twelve names, and check it with python3 -m http.server 8080 -d /root/stack/out. The drawing rules are the same as in step 6.
Save the states at steps 0, 15, 30, ... 165 and draw them all at once afterwards. You do not need to rerun the simulation twelve times.
The last two pictures should be almost the same. That is exactly the way to confirm in numbers that the pile has stabilized — whether the difference between frames converges to 0.
Conversely, the first and last pictures should be clearly different. In the first, the boxes are scattered in the air, and in the last, they are stacked on the floor.
The HTML is one <img> and setInterval, the same way as in the previous lab.