How would you approach the simulation of rain, well, or any other lengthy physical process?
Simulation, be it rain, air flow over the wing of the plane or falling down the steps of the link (remember the rainbow spring toy from childhood?), You can imagine if you know the following: ')
The state of everything at the time of the start of the simulation.
How does this state change from one time point to another?
By “state of everything,” I mean any variable data that either determines how the environment looks, or changes that occur over time. As an example of a state, one can cite the position of a raindrop, the direction of the wind, or the speed of each part of the spring of a link.
If we assume that all our state of the environment is one big vector v e c y , then we can formulate the data we need, mentioned above, in the following:
Find value y 0 satisfying y ( t 0 ) = y 0 ?
Find function f such that f r a c d y ( t ) d t = f ( t , y ( t ) ) .
Why do we need to store the state of all in one vector, I will explain a little later. This is one of those cases where it seems that we are overdoing the task summary, but I promise there are some interesting things about this approach.
Now let's see how you can store all the information about the environment in one vector on a simple example. Suppose we have 2 objects in a 2D simulation. Each object is determined by its position. v e c x and speed v e c v .
So, to get a vector v e c y we need to combine vector vecx1,vecv1,vecx2andvecy2 into one, consisting of 8 components, like this:
If you are confused, why do we want to find f(t,y(t)) , not the initial definition fracdy(t)dt , the meaning is that we need the derivative as an expression depending only on the current state, y(t) , constants and time itself. If this is not possible, then surely any information about the state was not taken into account.
Initial state
To determine the initial state of the simulation, you need to define a vector vecy0 . Thus, if the initial state of our example with two objects looks like this:
Then in a vector form it can be represented as follows:
vecy0 determines the initial state and now all we need is to find a way to transition from the initial state to the state what is happening moment after it, and from the obtained state a little further in time and so on.
To do this, we solve the equation fracdy(t)dt=f(t,y(t)) for f . Find the derivative of y(t) :
vecx1 and vecx2 bound by similar rules, as well as vecv1 with vecv2 , so despite a bunch of expressions on top, all we really need to find are the following 2 things:
fracdvecxdttextandfracdvecvdt
The quality of the simulation depends on the definition of these two derivatives, it is in them that the whole force lies. And so that the simulation does not resemble a program where everything happens in a chaotic way, you can turn to physics for inspiration.
Kinematics and Dynamics
Kinematics and dynamics - the necessary ingredients to create an interesting simulation. Let's start with the basics in our example.
Responsible for the position in space vecx , and the first derivative of the position of a point in time is its speed vecv . In turn, the derivative of speed over time is acceleration, veca .
It may seem that we have already found the function we need. f because we already know the following:
fracdvecxdt=vecvtextandfracdvecvdt=veca
And in fact, we coped brilliantly with fracdvecxdt because vecv this is part of our state vector vecy(t) but we need a little more to disassemble the second formula, because with veca not so simple.
Here Newton's second law will help us: vecF=mveca . If we assume that the mass of our objects is known, then by rearranging the variables in the expression, we get:
fracdvecvdt=veca=fracvecFm
So, wait a minute, veca and vecF not part vecy(t) therefore it is difficult to call it promotion (remember, we need a derivative function depending only on vecy(t) and t ). Nevertheless, we have moved forward because we have found all the useful formulas that determine the behavior of objects in our physical world.
Suppose that in our simple example, the only force that affects objects is gravitational attraction. In that case, we can determine vecF using the law of world wide Newton:
F=Gfracm1m2r2
Where G it's a gravitational constant 6.67times10−11fracNm2kg2 , but m1 and m2 these are the masses of our objects (which, we assume, are constants).
To create the simulation itself, we also need the direction and how to specify r through vector components vecy(t) . Assuming that vecF1 this is the force acting on the first object, then you can do it as follows:
To summarize The change of states in our system of two objects is fully expressed in terms of variables. vecx1,vecv1,vecx2,andvecv2 . And the changes are:
But how, having a strictly defined simulation, turn it into a beautiful animation?
If you have had experience writing a simulation or game, then maybe you offer something like this:
x += v * delta_tv += F/m * delta_t
But let's stop a little and see why it works.
Differential equations
Before we begin to implement, you need to decide what information we already have and what we need. We have value y0 which satisfies y(t0)=y0 there is also f satisfying fracdydt(t)=f(t,y(t)) . And we need a function that will give us the state of the system at any time. Mathematically, we need a function. y(t) .
Having this in mind and looking closer fracdydt(t)=f(t,y(t)) You can notice that this equation links y with its derivative fracdydt . This means that our equation is differential! Ordinary first-order differential equation, to be more precise. If solved, we will find the function y(t) .
Task of finding y(t) according to y0 and f called the Cauchy problem .
Numerical integration
For some examples of Cauchy problems, you can easily find the answer by an analytical method, but in complex simulations an analytical approach can be very difficult. Therefore, we will try to find a way to find an approximated solution of the problem.
For example, take the simple Cauchy problem. Given: y(0)=1 and fracdydt(t)=f(t,y(t))=y(t) . Find an approximate solution for y(t) .
Consider the problem from a geometric point of view and look at the value and tangent at the point t=0 . From what is given to us, we have y(0)=1 and fracdydt(t)=y(t)=1
We don't know what it looks like yet. y(t) but we know that near the point t=0 , value close to tangent. Now we will try to calculate y(0+h) for small value h taking advantage of the tangent. First try h=0.5 .
If we paint, then we approximate the value y(h) in the following way:
\ begin {aligned} y (h) \ approx y (0) + h \ frac {dy} {dt} (0) & = y (0) + hf (0, y (0)) \\ & = y (0) + hy (0) \\ & = 1 + h \ end {aligned}
So for h=0.5,y(h)approx1.5 . Go Now we can continue to calculate for other points. Although, of course, we found not the exact value y ( h ) , but if our approximate value is very close to exact, then the approximated tangent will also be very close to real!
$$ display $$ \ begin {aligned} f (t, y (t)) & = y (t) \\ f (0.5,1.5) & = 1.5 \ end {aligned} $$ display $$
Next, let's move on to h units to the right tangent.
Repeat the process and get the angular coefficient of the tangent f(t,y(t))=f(1,2.25)=2.25 :
The procedure can be performed recursively and for this we derive the formula:
\ begin {aligned} t_ {i + 1} & = t_i + h \\ y_ {i + 1} & = y_i + h f (t_i, y_i) \ end {aligned}
This numerical method for solving differential equations is called the Euler method. For the general case, the step is x += v * delta_t .
In our particular case, the step by step solution looks like this:
\ begin {aligned} t_ {i + 1} & = t_i + h \\ y_ {i + 1} & = y_i + h y_i \ end {aligned}
Using this method, it is convenient to present the results in the form of a table:
It turns out that our problem has a beautiful analytical solution. y(t)=et :
What do you think will happen if you reduce the step in the Euler method?
The difference between the approximated and exact solutions decreases with decreasing h ! In addition, in addition to the step reduction, you can use other methods of numerical integration, which can lead to a better result, such as the method of averages rectangles , the Runge-Kutta method and the Adams method .
It's time to code!
With the same success as we derived a mathematical representation of a description of a simulation, we can write the implementation of a simulation programmatically.
Since I'm most familiar with JavaScript, and I like the clarity that annotations add to the code; all the examples will be written in TypeScript .
And we will start with the version in which it meant that y This is a one-dimensional array of numbers, just like in our mathematical model.
It is not always convenient to operate with one-dimensional arrays; you can abstract the functions of addition and multiplication of the simulation process into an interface and get a brief generalized implementation of the simulation using TypeScript Generics .
The positive side of this approach is the ability to concentrate on the basis of a simulation: what exactly this simulation distinguishes from any other. We use the simulation example with the two objects mentioned above:
With the acceleration of free fall, a=−g=−9.8fracms2 .
But what happens when the ball touches the surface? The fact that the ball has reached the surface can be recognized by x=0 . But with numerical integration, at one point in time the ball can be above the surface, and already at the next moment below it: xi>0,xi+1<0 .
It would be possible to solve this problem by determining the moment of collision. tc(ti<tc<ti+1) . But even if this moment is found, how to determine the acceleration fracdvdt so that it changes in the opposite direction.
You can, of course, define a collision in a limited period of time and apply another force F for this length of time Deltat , but it is much easier to define a discrete constant bounding simulation.
And in order to reduce the amount of surface penetration by the ball, it is possible to calculate several simulation steps at once for one tick. Together with this, our simulation code will change like this:
functionrunSimulation<TextendsNumeric<T>>( y0: T, f: (t: number, y: T) => T, applyConstraints: (y: T) => T, iterationsPerFrame: number, render: (y: T) => void ) { const frameTime = 1 / 60.0const h = frameTime / iterationsPerFrame functionsimulationStep(yi: T, ti: number) { render(yi) requestAnimationFrame(function () { for (let i = 0; i < iterationsPerFrame; i++) { yi = yi.plus(f(ti, yi).times(h)) yi = applyConstraints(yi) ti = ti + h } simulationStep(yi, ti) }) } simulationStep(y0, 0.0) }
And now you can write the code of our bouncing ball:
Bouncing ball code
const g = -9.8; // m / s^2 const r = 0.2; // m class Ball implements Numeric<Ball> { constructor(readonly x: number, readonly v: number) { } plus(other: Ball) { returnnew Ball(this.x + other.x, this.v + other.v) } times(scalar: number) { returnnew Ball(this.x * scalar, this.v * scalar) } } function f(t: number, y: Ball) { const { x, v } = y returnnew Ball(v, g) } function applyConstraints(y: Ball): Ball { const { x, v } = y if (x <= 0 && v < 0) { returnnew Ball(x, -v) } return y } const y0 = new Ball( /* x */0.8, /* v */0 ) function render(y: Ball) { ctx.clearRect(0, 0, 400, 400) ctx.fillStyle = '#EB5757' ctx.beginPath() ctx.ellipse(200, 400 - ((yx + r) * 300), r * 300, r * 300, 0, 0, 2 * Math.PI) ctx.fill() } runSimulation(y0, f, applyConstraints, 30, render)
Attention developers!
Although this model has its advantages, it does not always lead to productive simulations. For me, such a framework is useful for representing the behavior of a simulation, even if a lot of extra things happen in it.
For example, a rain simulation at the beginning of this article [ approx. In the original article, at the beginning a beautiful interactive simulation of rain was inserted, I recommend to see firsthand] it was not created using the template described in the article. It was an experiment using Entity – component – system . Simulation sources can be found here: rain simulation on GitHub .
See you later!
I find the intersection of mathematics, physics and programming with something really impressive. Creating a working simulation, launching and rendering it is a special kind of something from nothing .
I was inspired by the materials of the lecture SIGGRAPH, just like in the simulation of the fluid . If you want to find more comprehensive information about the above, then take a look at the materials of the course SIGGRAPH 2001 “Introduction to physical modeling” . I give a link to the course in 1997, because Pixar seems to have removed version 2001.
I want to thank Maggie Cai for the wonderful illustration of the couple under the umbrella and for the patience with the painstaking selection of colors, while I can not distinguish blue from gray.
And if you are interested, the illustrations were created in Figma .