LÖVE Tutorial - Metaballs

Hi, everyone. This is my first tutorial haha. I always wanted to make some tutorial to help people with programming, well, with focus on gamedev. So i was thinking some thing that i can teach, and these days, i learn to make a simple metaballs effect, using blurred image and alpha threshold. And why not teach it to other people, right? haha This tutorial uses love2d as game engine/framework, but i think you can easily adapt to a game engine/framework you are using.

image

This is the image i’ll use for the metaball:

image

Let’s start our code. First, i’ll create a function to create our metaballs, and a table to hold them all

metaballs = {}
function createMetaball(x, y)
  local metaball = {
    x = x or 0,
    y = y or 0,
    vx = 0,
    vy = 0,
    size = 1
  }
  return metaball
end

function love.load()
end

function love.update(dt)
end

function love.draw()
end

The metaball properties:

Ok, now we have to load our image and create a canvas, how i’ll use the same image for all metaballs, i’ll create a global var for it.

metaballs = {}
function createMetaball(x, y)
  local metaball = {
    x = x or 0,
    y = y or 0,
    vx = 0,
    vy = 0,
    size = 1
  }
  return metaball
end

function love.load()
  metaball_image = love.graphics.newImage("metaball.png")
  canvas = love.graphics.newCanvas(canvas_width,canvas_height)
end

function love.update()
end

function love.draw()
end

Now, let’s setup the canvas, in this example i’ll draw a metaball on the mouse pos:

metaballs = {}
function createMetaball(x, y)
  local metaball = {
    x = x or 0,
    y = y or 0,
    vx = 0,
    vy = 0,
    size = 1
  }
  return metaball
end

function love.load()
  metaball_image = love.graphics.newImage("metaball.png")
  canvas = love.graphics.newCanvas(canvas_width,canvas_height)
end

function love.update()
  mx, my = love.mouse.getPosition()
end

function love.draw()
  love.graphics.setCanvas(canvas)
  love.graphics.clear(0,0,0,0)
  love.graphics.draw(metaball_image, mx, my)
  for i,v in ipairs(metaballs) do
    love.graphics.draw(metaball_image, v.x, v.y, 0, v.size, v.size)
  end
  love.graphics.setCanvas()
end

Let’s write our Alpha Threshold shader, and apply in the canvas. Basically an Alpha Threshold shader is a shader that limit our alpha to a certain value.

vec4 effect(vec4 color, Image texture, vec2 tex_coord, vec2 screen_coord) { 
  vec4 pixel = Texel(texture, tex_coord); 
  if (pixel.a <= threshold)
    pixel.a = 0.0;
  return pixel * color;
}

Where threshold is our alpha limit, it has to be a value between 0 and 1

So, if the pixel alpha (pixel.a) is minor or equal than the threshold value (threshold), pixel.a = 0

metaballs = {}
function createMetaball(x, y)
  local metaball = {
    x = x or 0,
    y = y or 0,
    vx = 0,
    vy = 0,
    size = 1
  }
  return metaball
end

function love.load() 
  metaball_image = love.graphics.newImage("metaball.png")
  canvas = love.graphics.newCanvas(canvas_width,canvas_height)
  shadersrc = [[ 
    vec4 effect(vec4 color, Image texture, vec2 tex_coord, vec2 screen_coord) { 
      vec4 pixel = Texel(texture, tex_coord); 
      if (pixel.a <= 0.6)
        pixel.a = 0.0;
      return pixel * color;
    } ]]
  shader = love.graphics.newShader(shadersrc)
end

function love.update(dt)
  mx, my = love.mouse.getPosition()
end

function love.draw()
  love.graphics.setCanvas(canvas)
  love.graphics.clear(0,0,0,0)
  love.graphics.draw(metaball_image, mx, my)
  for i,v in ipairs(metaballs) do
    love.graphics.draw(metaball_image, v.x, v.y, 0, v.size, v.size)
  end
  love.graphics.setCanvas()
  love.graphics.setShader(shader)
  love.graphics.draw(canvas)
  love.graphics.setShader()
end

I set the threshold value directly on shader, but you can create a uniform for it.

Well, now should be all working, all you have to do is add metaballs to the metaballs table. I’ll make an example to create metaballs on mouse click with random properties, and make metaballs kick on touch screen bounds.

metaballs = {}
function createMetaball(x, y)
  local metaball = {
    x = x or 0,
    y = y or 0,
    vx = 0,
    vy = 0,
    size = 1,
    update = function(self, dt)
      self.x = self.x + (self.vx * dt) 
      self.y = self.y + (self.vy * dt) 
      if self.x >= screen_width or self.x <= 0 then 
        self.vx = self.vx * -1 
      end 
      if self.y >= screen_height or self.y <=0 then
        self.vy = self.vy * -1
      end
    end
  }
  return metaball
end

function love.load()
  metaball_image = love.graphics.newImage("metaball.png")
  canvas = love.graphics.newCanvas(canvas_width,canvas_height)
  shadersrc = [[ 
    vec4 effect(vec4 color, Image texture, vec2 tex_coord, vec2 screen_coord) {
      vec4 pixel = Texel(texture, tex_coord);
      if (pixel.a <= 0.6)
        pixel.a = 0.0;
      return pixel * color;
      } ]]
  shader = love.graphics.newShader(shadersrc)
end

function love.update(dt)
  mx, my = love.mouse.getPosition()
  if love.mouse.isDown(1) then
    local meta = createMetaball(mx, my)
    meta.vx = love.math.random(-200, 200)
    meta.vy = love.math.random(-200, 200)
    meta.size = love.math.random(0.2, 0.4)
    table.insert(metaballs, meta)
  end
  for i,v in ipairs(metaballs) do
    v:update(dt)
  end
end

function love.draw()
  love.graphics.setCanvas(canvas)
  love.graphics.clear(0,0,0,0)
  love.graphics.draw(metaball_image, mx, my, 0, 1, 1, img_width/2, img_height/2)
  for i,v in ipairs(metaballs) do
    love.graphics.draw(metaball_image, v.x, v.y, 0, v.size, v.size)
  end
  love.graphics.setCanvas()
  love.graphics.setShader(shader)
  love.graphics.draw(canvas)
  love.graphics.setShader()
end

Okay, all done :D. Hope it would be helpful.