Building some rain

This tutorial comes out of an office hours session where we were thinking about how to draw ‘rain’ in Field / AR — a little bit like some of the “particle system” work that we’ve looked at in class.

There are two points to this page. The first is to throw a little bit more example working code at you; but the second is to show how to grow some complex code step-by-step, with each step being fairly easily tested and debugged.

Let’s draw some rain

No, first, let’s check that we can draw anything at all:

var f = new FLine()

f.moveTo(0,0,0)
f.lineTo(1,1,1)

stage.f = f

Yields

Great. We remember how to draw something.

Next lets consider how we are going to approach this. For rain we’ll need some number of droplets. Since we’re not sure how many let’s make a variable:

var f = new FLine()

f.moveTo(0,0,0)
f.lineTo(1,1,1)

stage.f = f

var num = 20

I’m sure 20 is wrong, but it seems like a good place to start. Next, let’s have the main structural insight: we’ll need to keep track of the position (at least) of all of the droplets as we animate them over time. Let’s have an array of droplets:

var num = 20
var droplets = []

And let’s set them going at a random position:

var num = 20
var droplets = []

for(var i=0;i<num;i++)
{
    droplets.push(vec(Math.random(),Math.random(),Math.random()))
}

We had to Google how to add things to Javascript arrays, because we don’t keep that kind of stuff in our heads.

Immediately, we ask did that work? No errors, but let’s check:

print(droplets)

Looks like it.

Now we’re short of exactly two things. Some code to draw our droplets and some code to update them.

var f = new FLine()
for(var i=0;i<num;i++)
{
    f.moveTo(droplets[i])
    f.lineTo(droplets[i]+vec(0,1,0))
}
stage.f = f

Yields:

Looks like our vec(0,1,0) is too big. Let’s make it a variable and vary it:

var rainDirection = vec(0,0.05,0)
var f = new FLine()
for(var i=0;i<num;i++)
{
    f.moveTo(droplets[i])
    f.lineTo(droplets[i]+rainDirection)
}
stage.f = f

Shorter now:

Ok, so that’s code to draw droplets, now we need some code to update it

for(var i=0;i<num;i++)
{
    droplets[i] = droplets[i] + rainDirection
}

If I execute that then I don’t see anything change. I need to execute that and the drawing code over and over to see them move

Perfect! except the rain is going up:

var rainDirection = vec(0,-0.05,0)

Now, and only now, we’re in a position to put our drawing and updating code inside a _r function which we can call every frame:

var num = 200
var droplets = []

for(var i=0;i<num;i++)
{
    droplets.push(vec(Math.random(),Math.random(),Math.random()))
}

var rainDirection = vec(0,-0.05,0)

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+rainDirection)
	}
	stage.f = f

	for(var i=0;i<num;i++)
	{
		droplets[i] = droplets[i] + rainDirection
	}
}

Now we have code that randomizes some rain (I couldn’t actually see the rain so I changed num to 200) once and then draws and updates droplets every frame. Of course, it falls forever. Let’s have the rain disappear when it hits the ground and start someplace else:

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+rainDirection)
	}
	f.color = vec(0,0,0, 1)
	stage.f = f

	for(var i=0;i<num;i++)
	{
		droplets[i] = droplets[i] + rainDirection

		// restart someplace when we go through the floor
		if (droplets[i].y<0)
			droplets[i] = vec(Math.random(),Math.random(),Math.random())
	}
}

I also changed the color, because I was still having a hard time seeing it:

Changing num to 1000 and moving the camera around gives us some nice, dense rain:

Extensions

So these droplets are all moving in a rather uniform way. What would I need for these droplets’ motion to be a little bit more organic? The first approach is to carefully deploy random numbers with varying degrees of finesse:

		// random!
		droplets[i] = droplets[i] + rainDirection * Math.random()

That means that not everything falls at the same speed, but the speed varies every frame for ever droplet — it’s not that some droplets are faster than others in general.

If we want the droplets to have more personality then they need to differ by more than just i — their index. For that we’ll need more storage. Let’s make a second array vel that will hold velocities.

var num = 200
var droplets = []
var vel = []

var rainDirection = vec(0,-0.05,0)

for(var i=0;i<num;i++)
{
    droplets.push(vec(Math.random(),Math.random(),Math.random()))
    vel.push(vec(rainDirection.x,rainDirection.y,rainDirection.z) + vec(Math.random()-0.5,Math.random()-0.5,Math.random()-0.5)* 0.01)
}

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+vel[i])
	}

	stage.f = f

	for(var i=0;i<num;i++)
	{
		droplets[i] = droplets[i] + vel[i]
		if (droplets[i].y<0)
			droplets[i] = vec(Math.random(),Math.random(),Math.random())
	}
}

We’re forming a random velocity for each point based on rainDirection and a little random vector vec(Math.random()-0.5,Math.random()-0.5,Math.random()-0.5)* 0.01. What’s with the -0.5? Math.random() yields a number between 0 and 1, so Math.random()-0.5 yields a number between -0.5 and 0.5. Our velocities are equally random left or right.

Increasing that 0.1 yields something that doesn’t look much like rain:

Now that we have a velocity per particle, we’re dangeously close to a high-school physics simulation:

for(var i=0;i<num;i++)
{
	droplets[i] = droplets[i] + vel[i]
	if (droplets[i].y<0)
	{
		droplets[i].y = 0
		vel[i].y *= -1
	}
}

This reads “when the droplet goes under the ground (y<0) move it to the ground and flip it’s downward velocity.

Now we have rain that bounces:

Finally, we remember that acceleration adds a small, downward amount to velocity with every frame:

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+vel[i]*4) // increased this for more drama
	}

	f.color=vec(0,0,0,1)
	stage.f = f

	for(var i=0;i<num;i++)
	{
		droplets[i] = droplets[i] + vel[i]
		if (droplets[i].y<0)
		{
			droplets[i].y = 0
			vel[i].y *= -1
		}
		
		vel[i].y -= 0.001 // gravity, neglecting air resistence
	}
}

The stage is set for more complex, “semi-physical” dynamics:

for(var i=0;i<num;i++)
{
	droplets[i] = droplets[i] + vel[i]
	if (droplets[i].y<0)
	{
		droplets[i].y = 0
		vel[i].y *= -1
	}
	
	vel[i].y -= 0.001
	
	droplets[i].z += droplets[i].x*0.01
	droplets[i].x += -droplets[i].z*0.01
	
	alpha = 0.999+vel[i].y
	droplets[i].x = droplets[i].x*alpha + Math.cos(droplets[i].z)*(1-alpha)
}

But now our velocity array isn’t really thevelocity any more, and it’s much more exciting if we conspire to draw lines from the droplet position back to where it was in a previous frame. So lets keep track of the ‘previous’ position:

var num = 1000
var droplets = []
var previous = []
var vel = []

var rainDirection = vec(0,-0.05,0)

for(var i=0;i<num;i++)
{
    droplets.push(vec(Math.random(),Math.random(),Math.random()))
	// a new copy of dropliets[i]
	previous.push(vec(droplets[i].x,droplets[i].y,droplets[i].z))
    vel.push(vec(rainDirection.x,rainDirection.y,rainDirection.z) + vec(Math.random()-0.5,Math.random()-0.5,Math.random()-0.5)* 0.02)
}

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+(previous[i]-droplets[i])*4)
	}

	f.color=vec(0,0,0,1)
	stage.f = f

	for(var i=0;i<num;i++)
	{
		previous[i] = droplets[i]
		droplets[i] = droplets[i] + vel[i]
		if (droplets[i].y<0)
		{
			droplets[i].y = 0
			vel[i].y *= -1
		}
		
		vel[i].y -= 0.001
		
		droplets[i].z += droplets[i].x*0.01
		droplets[i].x += -droplets[i].z*0.01
		
		alpha = 0.999+vel[i].y
		droplets[i].x = droplets[i].x*alpha + Math.cos(droplets[i].z)*(1-alpha)
		
	}
}

Finally, one last rummage through linear-algebra 101:

var closestPointToLine = (origin, direction, p) => {
	return direction * (p-origin).dot(direction) + origin
}


var origin = getCurrentCamera().position
var ray = getCurrentCamera().forward

_r = () => {
	var f = new FLine()
	for(var i=0;i<num;i++)
	{
		f.moveTo(droplets[i])
		f.lineTo(droplets[i]+(previous[i]-droplets[i])*4)
	}

	f.color=vec(0,0,0,1)
	stage.f = f

	for(var i=0;i<num;i++)
	{
		previous[i] = droplets[i]
		droplets[i] = droplets[i] + vel[i]
		if (droplets[i].y<0)
		{
			droplets[i].y = 0
			vel[i].y *= -1
		}
		
		vel[i].y -= 0.001
		
		var diff = closestPointToLine(origin, ray, droplets[i])-droplets[i]
		
		droplets[i] = droplets[i] + diff*0.05
	}
}

Gives (after 30 or 40 attempts!) a particle system following your camera gaze: