Skip to content

135. Particles – Smoke (part 2)

November 4, 2013

In this post, I create a nice column of realistic smoke, gently rising upward.

I’ll begin by saying that there is no great science or art behind this code. I have simply played and fiddled, and fiddled and played, until it came out looking reasonable. It’s not fast enough to use with a lot of other stuff at the same time, so if I were looking to actually use it in a game, I’d work on speed some more.

But it does look realistic, and even has wind effects.

When I drew explosions in the last post, each was completely separate. But now I want the particles to move together so they make a column of smoke.

So I’ve created two classes

  • SmokeParticle – for individual particles of smoke
  • Smoke – to manage all the particles in SmokeParticle

Let’s start with how you actually use the Smoke class, because that’s the easy part. It’s only two lines of code.

    function setup()
    --this is the important line, creating a smoke column
    --at an x,y position in the middle bottom of screen
    s=Smoke(vec2(WIDTH/2,50))
end

function draw()
    background(167, 188, 204, 255)
    s:draw()  --draw smoke
    end

The reason it’s so simple to use is that the Smoke class makes all the decisions for you – you could of course change that if you wanted, to give the user more options.

Smoke class

The Smoke class initialises like this. See the numbered lines and notes underneath.

function Smoke:init(pos)
    self.pos=pos
    --particle settings --
    self.p={} --particle properties are held in here
    --[1] particle variations
    self.size=vec2(200,300)  --pixels
    self.expandTime=vec2(70,80)  --seconds
    self.fadeTime=vec2(10,20)    --seconds
    -texture image--
    local w=512 --size of image used as texture
    self.img=Smoke.createImage(w) --[2] get a piece of the big image
    --[3] create base particle that will just sit at the source
    --and mask the new particles
    self.base=SmokeParticle(self.pos,self.img,vec2(15,15),
                             vec2(5,5),vec2(0,0))
    --[4] set time to first new particle
    self.timer=0
    self.wind=math.random()*0.3-0.15 --[5] horizontal wind speed
    self.windShift=math.random(10,20) --[6] time to next wind shift
end

[1] We want particles to vary in size and in how fast they expand and shrink, so the smoke doesn’t look too artificial. So we provide a minimum and maximum value for each of size, time spent expanding, and time spent contracting. When we create individual particles, random values will be chosen in these ranges of values. I’ve stored the values as vec2.

[2] createImage is exactly the same as in the previous post. It creates the noise image we will use as the texture for all our particles.

[3] When I started playing with this, I had the problem that my particles started out really small and grew gradually, which is fine, except the very bottom of my fire was obviously very small and looked odd. So here, I’m creating a particle which I’m putting at the starting point of the fire, and setting it so it never fades. This means all the other particles will start off inside this particle, and then drift upward out of it as they get bigger. So I will always have a decent sized smoke ball at the bottom of the smoke column.

[4] We’ll start adding new particles using a timer, as you will see.

[5] The wind acts like a force (as discussed in earlier physics posts), pushing the particles left or right. Here I choose a random wind strength and direction.

[6] The wind can change direction every now and then, so I set a timer to the next change.

Now the drawing part. It’s not too complicated. Again, see the notes.

function Smoke:draw(wind)
    --create new particle when timer gets to 0
    self.timer=self.timer-DeltaTime [1]
    if self.timer<0 then
        local p=SmokeParticle(self.pos,self.img,self.size,
                              self.expandTime,self.fadeTime)
        table.insert(self.p,p)
        self.timer=2+math.random()*2  [2]
    end
    --reset wind shift when timer reaches 0
    self.windShift=self.windShift-DeltaTime [3]
    if self.windShift<0 then
        self.wind=math.random()*0.3-0.15 --horizontal wind speed
        self.windShift=math.random()*10+20
    end
    self.base:draw(vec2(0,0)) --draw base particle
    --draw other particles
    --y value is vertical drift speed
    local velocity=vec2(self.wind,0.5)
    --remove particle if faded completely [4]
    for i,p in pairs(self.p) do
        if p:draw(velocity)==false then table.remove(self.p,i) end
    end
end

[1] We reduce the timer by the number of seconds since we last drew, and if it gets to zero, it’s time for a new particle

[2] we reset the timer after adding a new particle

[3] If the timer for the wind gets to zero, change direction randomly

[4] remove particles when they fade

SmokeParticle class

This chunk of code looks rather long, but most of it is just creating vertices for the mesh, and if you’ve done that before, the rest of the code is pretty easy, and the comments in the code should make things clear.

--pos = vec2(x,y) starting position of particle
--img = noise image we are going to take a piece from
--vecSize = vec2(x,y) = size of the image piece
--vecExpand = vec2(x,y) = min/max expand time
--vecFade = vec2(x,y) = min/max fade time
function SmokeParticle:init(pos,img,vecSize,vecExpand,vecFade)
    self.pos=vec2(pos.x,pos.y) --current x,y position
    self.source=vec2(pos.x,pos.y) --starting position
    --particle settings --
    --choose random size and timing based on ranges given
    local size=math.random(vecSize.x,vecSize.y)
    self.expandTime=math.random(vecExpand.x,vecExpand.y)
    self.fadeTime=math.random(vecFade.x,vecFade.y)
    --create mesh --
    self.m=mesh()
    self.m.texture=img
    local w=img.width
    local x1,y1,x2,y2=-size/2,-size/2,size/2,size/2
    --choose a random piece of the noise picture as texture
    local tx1=math.random()*(1-size/w)
    local ty1=math.random()*(1-size/w)
    local tx2,ty2=tx1+size/w,ty1+size/w
    v,t={},{}
    v[1]=vec2(x1,y1) t[1]=vec2(tx1,ty1)
    v[2]=vec2(x2,y1) t[2]=vec2(tx2,ty1)
    v[3]=vec2(x2,y2) t[3]=vec2(tx2,ty2)
    v[4]=vec2(x1,y2) t[4]=vec2(tx1,ty2)
    v[5]=vec2(x1,y1) t[5]=vec2(tx1,ty1)
    v[6]=vec2(x2,y2) t[6]=vec2(tx2,ty2)
    self.m.vertices=v
    self.m.texCoords=t
    self.m:setColors(color(255))
    self.m.shader=shader(smokeShader.vertexShader,
                         smokeShader.fragmentShader)
    self.m.shader.size=size/w
    --see how fade and centre (below) are used in draw function
    self.m.shader.fade=1 --0=invisible, 1=full alpha
    self.m.shader.centre=vec2((tx1+tx2)/2,(ty1+ty2)/2)
    self.timer=0
end

Finally, the drawing of each particle. This is the important function. I think you should be able to follow the logic. And you are welcome to improve any of it.

function SmokeParticle:draw(velocity)
    self.timer=self.timer+DeltaTime
    --calculate how big it has got
    self.m.shader.frac=math.max(0.04,math.min(1,self.timer/
                            (self.expandTime+self.fadeTime)))
    --if fading, calculate fade and return false if fade=0
    if self.timer>self.expandTime and self.fadeTime>0 then
        self.m.shader.fade=1-(self.timer-self.expandTime)
                              /self.fadeTime
        if self.m.shader.fade<0 then return false end
    end
    pushMatrix()
    --wind takes effect as smoke rises, phase over 100 pixels
    self.pos.x=self.pos.x+velocity.x*
               math.min(1,(self.pos.y-self.source.y)/100)
    --make smoke drift upwards slowly, then increasing
    self.pos.y=self.pos.y+velocity.y*math.min(1,self.timer/50)
    if self.pos.y>HEIGHT then return false end
    translate(self.pos.x,self.pos.y)
    self.m:draw()
    popMatrix()
    return true
end

And here is the full code

Advertisement
Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: