Page 1 of 2

Cheap-Ass Metaballs

Posted: Tue May 01, 2012 4:06 pm
by timmeh42
Something I did a while ago in Gamemaker and recently decided to port to Löve (only actually ported it this afternoon) was a small demo of metaballs. Now, I have absolutely no idea how metaballs are meant to be made, but I hear that it involves maths. And I hate maths. Therefore, when I was working on it in GM, I found that a similar effect could be achieved with gradiented images being additively drawn onto a surface, which is then put through a threshold function reducing it to pure black-and-white. The threshold function in GM I used was a really hacked-together thing using recursive rendering in certain blend modes, but when I first thought of doing it in Löve I realised that PixelEffects would be perfect for the job.
SO today I looked through some simple PixelEffect tutorials, and made a very simple BnW shader (it rounds the rgb values to the nearest whole number).
Using this I then went and coded up the rest of the stuff needed for the demo.
I therefore present to you
the Cheap-Ass Metaball System (no, I'm not going to work a Löve-pun into this. EDIT: "Cheap, Ugly Metaballs"?)
so-called because it is a cheap-ass, hackish method of faking metaballs.

Here is a shot showing it giving a cool 985 FPS.
Image

Note that the edges are not anti-aliased at all; this could be remedied by using a more advanced shader. Someone else can do that part.

The way I achieve this is I draw a 64x64 image of a radial sinusoidal gradient from black to white at the points where the metaballs are, using additive blending. This is then passed through the afore-mentioned shader. The gradient must be sinusoidal to achieve the proper effect of the metaballs interacting; a straight-line gradient will give flat edges where they interact.

If you look at the code, be aware that the for-loop is made for a table of only 10 metaballs. This was out of laziness, so just expand it or make it detect the size of the table if you want to use it. Auto-detects table size now, it just assumes the xvalue table is the same length as the yvalue table.

UPDATE:
+ Auto-detects the size of the tables, not hard-coded anymore.
+ The shader now uses 'floor' instead of 'round', which isn't supported by some computers.

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 4:35 pm
by slime
It looks like your shader code uses a function which isn't officially supported in GLSL 1.20 (which LÖVE uses). http://www.opengl.org/sdk/docs/manglsl/xhtml/round.xml
It will possibly work on some of the more lenient graphics drivers, but it doesn't seem to for me in OSX with an ATI video card and an intel one (both of which do support later GLSL versions).

Adding this to the top of the shader code made it work for me, but I'm not sure what will happen on systems where the driver allows access to the built-in round, so you might want to rename it.

Code: Select all

float round(float x)
{
	return floor(x + 0.5);
}

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 5:16 pm
by timmeh42
Ah, I didn't realise Löve used such an old version. Too much trouble to use newer version?
Works on my pc, which uses ATI video card (HD4670) but pretty up-to-date drivers (updated about a month ago). Thanks for that bit of code tho, will add it when I get time.

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 5:25 pm
by bartbes
timmeh42 wrote:Ah, I didn't realise Löve used such an old version. Too much trouble to use newer version?
It's a matter of compatibility, by aiming low, you include as many people as possible.

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 6:22 pm
by timmeh42
Right, updated.

UPDATE:
+ Auto-detects the size of the tables, not hard-coded anymore.
+ The shader now uses 'floor' instead of 'round', which isn't supported by some computers.

Also added some line spacing and changed the names of some variables.

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 8:24 pm
by Tesselode
Why are people so obsessed with metaballs?

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 10:49 pm
by ishkabible
I want to use this for a fluid simulator; get a bunch of physics balls and display them using meatballs that you created and presto; instance fluid simulator ;)

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 10:59 pm
by Tesselode
Haha, "meatballs."

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 11:05 pm
by slime
Yeah, portal 2's gels use 3d metaballs for visualization I think.

Re: Cheap-Ass Metaballs

Posted: Tue May 01, 2012 11:39 pm
by ishkabible
proof of concept: this looks just a lot like portal 2's goo. I'm still tweaking the numbers. maybe adding surface tension(attraction between close balls) will give it a better effect.

Code: Select all

function love.load()
	mb_img = love.graphics.newImage("metaball.png")
	canv = love.graphics.newCanvas(800,600)
	effect = love.graphics.newPixelEffect [[
        vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords) {
			vec4 pixel = Texel(texture, texture_coords);
			pixel.r = floor(pixel.r+0.5);
			pixel.g = floor(pixel.g+0.5);
			pixel.b = floor(pixel.b+0.5);
			return  pixel;
		}
	]]

	love.physics.setMeter(64) --the height of a meter our worlds will be 64px
	world = love.physics.newWorld(0, 9.81*64, true) --create a world for the bodies to exist in with horizontal gravity of 0 and vertical gravity of 9.81

	--let's create the ground
	ground = {}
	ground.body = love.physics.newBody(world, 800/2, 600-50/2) --remember, the shape (the rectangle we create next) anchors to the body from its center, so we have to move it to (650/2, 650-50/2)
	ground.shape = love.physics.newRectangleShape(800, 50) --make a rectangle with a width of 650 and a height of 50
	ground.fixture = love.physics.newFixture(ground.body, ground.shape); --attach shape to body

	--let's create a ball
	balls = {}
	for i=1, 300, 1 do
		local x = math.random(-100, 100)
		local y = math.random(-100, 100)
		balls[i] = {}
		balls[i].body = love.physics.newBody(world, 800/2 + x, 600/2 + y, "dynamic") --place the body in the center of the world and make it dynamic, so it can move around
		balls[i].body:setMass(75) --give it a mass of 15
		balls[i].shape = love.physics.newCircleShape(2.75) --the ball's shape has a radius of 10
		balls[i].fixture = love.physics.newFixture(balls[i].body, balls[i].shape, 10) --attach shape to body and give it a friction of 1
		balls[i].fixture:setRestitution(.9) --let the ball bounce
	end
end


function love.update(dt)
	world:update(dt) --this puts the world into motion
end

function love.draw()
	canv:clear(0,0,0,0)
	love.graphics.setBlendMode("additive")
	love.graphics.setCanvas(canv)
	
	for i=1, 100, 1 do
		love.graphics.draw(mb_img, balls[i].body:getX(), balls[i].body:getY())
	end

	love.graphics.setBlendMode("alpha")
	love.graphics.setCanvas()

	love.graphics.setPixelEffect(effect)
	love.graphics.draw(canv,0,0,0,1,1,0,0,0,0)
	love.graphics.setPixelEffect()

	love.graphics.setColor(255,255,255,255)
	love.graphics.print(love.timer.getFPS(),32,32,0,1,1,0,0,0,0)
end
if this was really going to be done right; I would use a faster fluid dynamics algorithm that had a 2D array of density for a given area of the screen; the more pressure the more intense the color(alpha value) and the larger the circle. that would give a REALLY nice effect.