Thank you for coming back for part two of my blog series about my experience at PhET. If you haven't already read part 1, you can find that here!
Otherwise, I'm going to jump right into the rest of the work I did at PhET!
Once Vector Addition was published in September of 2019, I was assigned to "convert" another legacy Adobe Flash Player simulation: Collision Lab!
"Conversion" - Again?
One common question I get asked is: why couldn't you have built some sort of automated system to automatically "convert" legacy simulations?
My response is always a firm no. There's a lot that goes into "converting" a simulation. From the design perspective, every feature is carefully discussed, and pedagogy is prioritized.
For example, for the rewrite of Collision Lab, one of the newly added features was the "Change in Momentum" vectors.
These vectors briefly show up in the "Intro" screen for a simple rigid balls collision. But they were added for a pedagogical purpose: in a collision, rigid bodies exert an equal and opposite change of momentum on each other. This feature was designed to be a crucial stepping stone to understanding the law of conservation of momentum.
From the implementation perspective, newer versions of simulations are entirely different from their legacy counterparts. The legacy code for Collision Lab would not even come close to passing any code review or quality control checks. The way code is compartmentalized has drastically changed. If you compared the new implementation versus the legacy, you couldn't tell that one was adapted from the other.
Converting simulations to HTML5 is a whole engineering process — something that cannot be automated. As the implementer, I treat the HTML5 version as an entirely different end product.
Here's a brief overview of the screens for "Collision Lab":
- Intro - visualize and interact with a simple 1D ball-to-ball collision with varying elasticity, and understand the conservation of linear momentum.
- Explore 1D - visualize collisions in 1D. Understand that ball to ball collisions conserve momentum, but a ball colliding into a wall does not conserve the momentum of the system of balls.
- Explore 2D - visualize collisions in 2D with varying elasticity (except perfectly inelastic collisions).
- Inelastic - discover perfectly inelastic collisions in 2D and introduce the conservation of rotational momentum.
Implementing a Physics Engine
Many of the patterns (like MVC) that I used for Vector Addition also applied to this simulation. For the sake of space, I'll be focusing on how I implemented the physics engine for this blog.
What is a physics engine?
Simply put, a physics engine is a model that ensures that the physics of the simulation is correct. You've probably seen these in video games or computer graphics. For an educational simulation, it's essential that the physics engine is precise and exactly matches real-world behavior.
For Collision Lab, the physics engine implements collision detection and collection responses for all collisions between the balls and the border walls.
Collision detection identifies when two objects in motion collide. When a collision has been detected between two objects, collision response determines what effect that collision has on their motion.
Collision Detection: Discrete (first approach)
A discrete physics engine (also known as a posteriori) is where collisions are detected after the collision visibly occurs. On each frame, the physics engine checks to see if any two bodies are physically overlapping, which would mean a collision has occurred.
This was my first approach for Collision Lab. Mainly because a discrete engine is relatively easy to implement (due to the discrete nature of computers), and the legacy version of the simulation used a similar approach.
However, there were a variety of issues with this approach:
When a collision is detected, the bodies are already overlapping, meaning their positions are slightly inaccurate. The positions of the bodies are important for collision response. We noticed this small inaccuracy would magnify as the simulation ran over time. Additionally, since the browser controls the frame rate, we saw some non-deterministic behavior (since the frame rate is equal to the rate at which overlapping bodies are checked).
We also noticed that when the balls were traveling at high velocities or the frame rate dropped, balls would occasionally tunnel through each other. On each frame, the physics engine progresses every ball by an amount proportional to their velocity and how long the frame is. In extreme situations, balls can be moved past each other without detection.
Continuous Collision Detection
To solve the issues outlined above, I decided to switch to a continuous collision engine.
A continuous physics engine (also known as a priori) is where collisions are detected before the collision occurs (opposite of discrete). All collisions are accurately predicted beforehand so that regardless of the framerate or velocities, all collisions are handled. Unfortunately, a continuous collision detector is much more challenging to implement, and predicting collisions is non-trivial.
Predicting Collisions Beforehand
Predicting collisions beforehand is different depending on what types of bodies are colliding.
For predicting collisions, the physics engine needs to be able to compute:
- If two bodies will collide at all.
- If they do, what time in the future will they collide.
Luckily, for the first three screens (all screens excluding the "Inelastic" screen), collisions only involve either a ball with another ball or a ball colliding into some border wall. In both cases, balls are moving at a fixed velocity and aren't accelerating. It turns out, predicting this type of collision has a closed-form solution!
Ball To Ball Collision Derivation
For predicting ball-to-ball collisions, we first proceed with the known quantities.
- - the position of the first ball involved in the collision before the collision occurred.
- - the position of the second ball involved in the collision before the collision occurred.
- - the velocity of the first ball involved in the collision before before the collision occurred.
- - the velocity of the second ball involved in the collision before before the collision occurred.
- radius1 - the radius of the first ball involved in the collision.
- radius2 - the radius of the second ball involved in the collision.
- - the position of ball1 when the balls exactly contacted each other (in between the previous and current frame).
- - the position of ball2 when the balls exactly contacted each other (in between the previous and current frame).
- - the time the balls collide.
Since balls are undergoing uniform-motion, they are traveling in a straight line and aren't accelerating. Thus:
Additionally, based on this picture, we know that when the Balls are exactly colliding, the distance between the centers of the balls is equal to the sum of the radii of the ball:
Now use the Vector property:
which is quadratic in-form for .
In summary, computing when two balls will collide reduces to solving the roots of a quadratic. If the quadratic has real roots, pick the smaller of the two. Of course, if the quadratic doesn't have any real roots, then the collision never occurs!
Ball Colliding Into Wall Derivation
- - the position of the ball involved in the collision before the collision occurred.
- - the velocity of the ball involved in the collision before before the collision occurred.
- radius - the radius of the ball involved in the collision.
- () - the boundaries of the border wall.
- - the time the ball collides with the wall, if it does at all.
It turns out, a ball will always collide with the border (assuming it doesn't collide with something else beforehand), unless it's velocity is . For the rest of the derivation, we can assume that the ball's speed isn't 0.
Since there are four border walls , the physics engine must consider each of the sides. Consider the time at which the ball will collide with one of these walls, one at a time:
This is because either or will be negative, and either or will be negative, since the ball is only traveling towards one of the respective sides.
A quick clarification that I want to make is that for Collision Lab, there's a button to run the simulation in reverse. This means that collision detection needs to also work for reverse collisions. Luckily, the two methods derived above work generally for both forwards and reverse collisions; for reverse scenarios, we can simply negate the velocity vectors for the balls (since balls are moving in the opposite direction of their velocity).
Now that the physics engine knows when collisions occur, it must know how to respond to them. For the first three screens, the only thing that happens when a collision occurs is that the velocities of the balls involved in the collision change. For this section, we can assume that we handle the collisions at the exact moment it occurs (i.e. there is no overlapping inaccuracy).
When a ball collides with the border, the response is trivial: it simply scales the velocity component in the opposite direction. You can see how this is implemented here.
It turns out, ball-to-ball collisions can also be easily handled with a coordinate transformation of the velocities.
Since the velocity along y' stays the same, it reduces down to a simple 1D collision involving the velocity component along x'.
Putting it together - collision response/detection
The physics engine now can detect the exact time at which all possible collisions occur (except for the inelastic screen). Now, to put these two algorithms together:
In the first frame, for every possible collision involving a ball or the walls, we compute the time that the collision occurs, assuming nothing happens in-between that would knock something off-course. We store these times (and the involved bodies) in a simple, lightweight data structure called a Collision.
Then, on subsequent frames, if the elapsed time is greater than any of the collision times, it means a collision occurred between the previous frame and the current frame. We start from the first stored time and work forwards, moving balls to the exact moment of impact each time. This fully ensures that collisions are simulated correctly — even with large time steps.
After each collision is handled, we delete every Collision data structure involving any of the two bodies of the collision. This ensures that a ball that has changed course due to a collision is correctly handled. Then, before the next collision is handled, all collisions involving the two bodies are redetected. This detection-response loop is repeated until there are no collisions detected within the time step.
The benefits of this strategy are two-fold:
- Tunneling has been eliminated.
- Minor inaccuracies have been eliminated: having all collision times, we can handle the collision response at the exact time that a collision occurs, which is when the bodies are tangentially touching.
- Performance: the physics engine doesn't have to check overlapping bodies on every frame. On average, this version of the physics engine usually doesn't have to do anything other than advancing the balls' positions! And only occasionally does it have to handle a collision. We saw a huge performance boost after the continuous collision engine was implemented.
Performance especially matters for PhET simulations because they must run smoothly on older or newer resourced devices and browsers, such as Chromebooks and iPads, which are popular in classroom environments.
Unfortunately, there’s a third type of collision that I hadn’t discussed before, which occurs in the "Inelastic" screen.
The "Inelastic" screen was a completely new screen that was added to HTML5.
It features collisions of two balls that are perfectly inelastic.
In 1D, when two perfectly inelastic balls collide, they stick together and travel in the same direction, and rotational momentum is conserved.
In 2D (assuming they collide at an angle), this drastically changes. There are two physically "correct" possibilities for what happens:
- "Slide" - the two balls don't stick together, and the resulting velocities of balls are computed along each component. This is how 2D inelastic collisions were implemented in the flash version.
- "Stick" - this is an entirely new feature for HTML5. The balls stick together but must rotate around their center of mass to conserve rotational momentum. To see how balls are rotated around their center of mass, see RotatingBallCluster.js.
Predicting these collisions is the same as the other screens. However, a new collision type is introduced, which also must be predicted: when a rotating "cluster" of balls collide with one of the borders.
Bisection Root Finder
This type of collision is much more complicated. Mainly because the cluster of balls rotate around their center of mass and have an associated faze. It turns out, there is no closed-form solution to predicting when a rotating ball cluster will collide with the border.
The prediction is made when the balls first collide and start rotating around their center of mass. Since there is no closed-form solution, a bisection root-finder is used. This "binary searches" to find the time when the rotating cluster will collide with the border.
Every binary search must have a lower and upper bound. The lower bound of when the cluster will collide with the border (the earliest possible time) is when the bounding circle of the cluster collides with the border. A rough upper bound is when the center of mass collides with the border. Since Collisions are "saved', this computation is only executed once and doesn't affect performance!
Final Note on the Implementation of Collision Lab
I had so much fun implementing Collision Lab. Mainly because this was not the first time I had implemented a collision simulation. Before I started any work at PhET (around March of 2019), I had written my own simulation called Collision Theory. At the time, I had only written it for practice. It was fitting that I got to hone my original physics engine into something that is actually used for a production PhET simulation.
If you would like to create a collision simulation for yourself, I'd like to point you to a few files that contain most of the implementation logic for the physics engine: CollisionEngine, RotatingBallCluster, and InelasticCollisionEngine.
Why PhET Sims matter
With the rapidly developing technological age we live in, there's an enormous opportunity to leverage technology for social good. PhET embodies this mentality, working to create educational technology that brings equity for every student.
Online simulations provide a great opportunity to visualize a concept and interact with it. PhET has created simulations that are cheaper (free!) than laboratories, which require expensive equipment. And even in a laboratory, it takes a long time to set up experiments. The experiments are often inaccurate because of the inability to idealize and eliminate other variables, like creating a frictionless surface or an ideal axis of rotation. Idealizing parameters are often a crucial first step to understanding a physics or math concept, and simulations can aid this process.
In recent pandemic times, the importance of educational technology has become even more important; the COVID-19 virus has forced millions of students to attend class virtually from their homes. Many homes are under-resourced and unfit for student learning, emphasizing the need for a broader societal investment in educational equity through technology.
Through educational technology, we can efficiently empower global personalized education, enabling following generations of students to solve other global issues. And PhET has truly made strides to work towards globalized education for everybody:
Firstly, all of their simulations are translatable. On the implementation side, this just means extracting all of the displayed strings into a translation file. Communities across the globe in different countries can access simulations in their native languages. Since Collision Lab was published, it has been translated into 28 languages. Vector Addition has also been translated into 43 languages.
One of the other main motivations PhET moved to HTML5 was accessibility. The goal was to give equitable access for everyone to learn from a simulation, including students with disabilities. PhET has researched and implemented various inclusive design patterns, including sonification, vibration haptics, and screen readers. You can read more about it here.
PhET has shown time and time again that they are willing to do anything to maximize global educational impact. And all of this has certainly paid off. PhET delivers hundreds of millions of simulation runs every year. Since publication (for HTML5), Collision Lab has been used 3 million times every year, and Vector Addition has been used over 2 million times per year.
PhET is also open source! Anybody can help and contribute to the PhET project. If you're interested in creating your own simulation or helping out in any way, you can find more about contributing here.
Once again, I want to give a huge thank you to Ariel Paul, my manager. It’s worth noting that my work on Collision Lab took place during the height of the worldwide COVID-19 pandemic, and Ariel did a fantastic job of making sure the logistics of working remotely went smoothly.
I’d also like to give a huge shout-out to Jonathan Olsen for answering so many of my math questions and giving me pointers for implementing Collision Lab.
That’s all for now!