Code archives/Graphics/2D Metaballs

This code has been declared by its author to be Public Domain code.

Download source code

2D Metaballs by TWH2008
Original was in Python. Look here for a description the method and the Python PyGame example:

http://www.niksula.cs.hut.fi/~hkankaan/Homepages/metaballs.html


Quite a few things are divided over far too many lines. Haven't bothered fixing this. I was having trouble converting Pythons equation-like expressions to BMX.
[code]
SuperStrict
Framework BRL.GlMax2D
Import brl.Random

'src idea:
'http://www.niksula.hut.fi/~hkankaan/Homepages/metaballs.html
AppTitle = "fast metaballs. "
'HideMouse()
Global resX:Int=800, resY:Int=600
SetGraphicsDriver GLMax2DDriver()
Graphics resX,resY,0,0

SeedRnd(0)'MilliSecs() 

Local numBalls:Int = 10
Local balls:tball[numBalls]
For Local i:Int=0 Until numBalls
	balls[i] = TBall.Create( TPoint.Create( Rand(50,resX-50), Rand(50,resY-50) ), Rand(1,4) )
Next

Local threshold:Float = 0.001'0.003 '0.0004 'higher value -> smaller metaballs
Local mbs:tmetaballsystem = TMetaballsystem.Create(balls, 2, threshold) 'balls, gooeyness, and potential field threshold

Local iterations:Int = 0
Local fps:Int = 0
Local lastSec:Long = MilliSecs() + 1000
Local frames:Int = 0

Global bench1:Int = 0
Local closestBall:TBall = Null

While(Not KeyHit(KEY_ESCAPE) And Not AppTerminate()	)
	Cls

	If(MouseDown(1) And closestBall <> Null)
		
		closestBall.pos.x = MouseX()
		closestBall.pos.y = MouseY() 
		
	Else
		'Find ball closest to mouse
		Local minDist# = 1e32
		For Local b:TBall = EachIn balls
			Local distSq# = (MouseX() - b.pos.x)^2 + (MouseY() - b.pos.y)^2
			If(distSq < minDist)
				closestBall = b
				minDist = distSq
			EndIf
		Next
	EndIf
	
	Local t# = MilliSecs() / 1000.0
	
	If MouseDown(2)
		balls[1].pos.x = resX/2.0 + 60 * Cos(2*t * 180/Pi)
		balls[1].pos.y = resY/2.0 + 60 * Sin(2*t * 180/Pi)
	EndIf
	If(closestBall <> Null) Then DrawLine(MouseX(), MouseY(), closestBall.pos.x, closestBall.pos.y )
	
	mbs.drawBalls(20) 'sim_step size, dt in pixels to move 
	
	SetColor 255,255,0
	DrawText("iterations: "+iterations+"    fps: "+fps, 0,0)
	'DrawText("bench1: "+(bench1/1000.0),0,15)
	bench1 = 0
	
	If(lastSec < MilliSecs() )
		lastSec = MilliSecs() + 1000
		fps = frames
		frames = 0
	EndIf
	
	iterations:+1
	frames :+ 1
	Flip
	
Wend
End

Type TPoint
	Field x:Double, y:Double
	
	Function Create:TPoint(x:Double, y:Double)
		Local tmp:TPoint = New TPoint
		tmp.x = x
		tmp.y = y
		Return tmp
	End Function
End Type

Type TBall
	
	Field pos:TPoint
	Field pos0:TPoint
	Field edgePos:tpoint
	Field size:Double
	Field tracking:Int 'Bool
		
	Function Create:TBall(pos:tpoint, size:Double)
		Local tmp:TBall = New TBall
		tmp.pos = pos
		tmp.pos0 = pos
		tmp.edgePos = pos
		tmp.size = size
		tmp.tracking = False
		Return tmp
	End Function
End Type

Type TMetaballsystem
	Const borderStepSize:Double = 0.01'0.01 'number of pixel to move in search of border offset from center
	Field balls:tball[]
	Field goo:Double
	Field threshold:Double
	Field minSize:Double
	Field oneOverGoo:Double
	Field minOverThresPowGoo:Double
	
	Function Create:TMetaballsystem(balls:TBall[], goo:Double, threshold:Double)
		Local tmp:TMetaballsystem = New TMetaballsystem
		
		tmp.balls = balls
		tmp.goo = goo
		tmp.threshold = threshold
		tmp.minSize = 1e32 'smallest ball
		tmp.oneOverGoo = 1.0/goo
		For Local b:TBall = EachIn balls
			If b.size < tmp.minSize Then tmp.minSize = b.size
		Next
		tmp.minOverThresPowGoo = (tmp.minSize/threshold)^(tmp.oneOverGoo)
		
		Return tmp
	End Function 
	
	Method calcForce:Double(pos:tpoint) 'called by stepOnceTowardsBorder
		'return the metaball fields force at point "pos"
		Local forceAtPoint:Double = 0.0
		
		For Local ball:tball = EachIn balls
			'### Formula (1)
			Local tmp:TPoint = TPoint.Create(ball.pos.x-pos.x,ball.pos.y-pos.y)
			Local div:Double= Sqr(tmp.x*tmp.x + tmp.y*tmp.y)^goo
			'Local div:Double = tmp.x^2 + tmp.y^2
			If( div<>0 ) 'prevent div by zero
				forceAtPoint :+ ball.size / div	
			Else
				forceAtPoint :+ 1e32'"big number"	
			EndIf
		Next
		Return forceAtPoint
	End Method
	
	Method calcNormal:tpoint(pos:tpoint) 'return a normalized (magnitude==1) tangent at points "pos"
		Local np:TPoint=TPoint.Create(0,0)
		
		For Local ball:tball = EachIn balls
			' ### Formula (3)
			Local fromPointToBall:TPoint = TPoint.Create(ball.pos.x-pos.x, ball.pos.y-pos.y)
			Local centerDist:Double= Sqr(fromPointToBall.x*fromPointToBall.x + fromPointToBall.y*fromPointToBall.y)
			Local rDiv:Double = 1.0 / centerDist^(2.0 + goo)
			np.x :+ -goo * ball.size * fromPointToBall.x * rDiv
			np.y :+ -goo * ball.size * fromPointToBall.y * rDiv
		Next
		Local rLen:Double = 1.0 / Sqr(np.x*np.x + np.y*np.y)
		
		Local retval:TPoint = TPoint.Create(np.x*rLen, np.y*rLen)
		'?debug
		'SetColor 0,255,0
		'DrawLine(pos.x, pos.y, pos.x+retval.x*10, pos.y+retval.y*10) 'draw normal
		'?
		
		Return retval
	End Method
	
	Method calcTangent:tpoint(pos:tpoint)
		'return a normalized (magnitude==1) tangent at points "pos"
		Local np:tpoint=Self.calcNormal(pos)
		'###Formula(7)
		Return TPoint.Create( -np.y, np.x )
	End Method
	
	Method stepOnceTowardsBorder:tpoint(pos:tpoint, forceAtPoint:Double)
		'Local timeStart:Int = MilliSecs()
		
		'step once towards the border of the metaballs field, return
		'new coordinates and force at old coordinates
		
		Local np:TPoint = calcNormal(pos)
		'### Formula(5)
		Local stepsize:Double = (minSize/threshold)^(oneOverGoo) - (minSize / forceAtPoint)^(oneOverGoo) + borderStepSize
		'Local stepsize:Double =  minOverThresPowGoo - (minSize / forceAtPoint)^(oneOverGoo) + borderStepSize
		'bench1 = bench1 + (MilliSecs() - timeStart)
		
		Return TPoint.Create( pos.x + np.x*stepsize , pos.y + np.y*stepsize )
	End Method
	
	Method trackTheBorder:tpoint(pos:tpoint)
		'track the border of the metaball field and return new coords
		Local force:Double = 1e7 '9999999.0
		'loop until force is weaker than the desired threshold
		
		'TODO: may loop forever
		Local iters:Int = 0;
		While(force > threshold)
			force = calcForce(pos)
			pos = stepOnceTowardsBorder(pos, force)
			
			?debug
			SetColor 255,255,0
			Plot pos.x, pos.y 'show a little debug output i.e. yellow pixels
			?
			
			iters :+1
			If(iters > 10) 'if it takes to long to find the border/threshold point, break
				Exit
				'Local s# = 5.0
				'DrawOval(pos.x-s,pos.y-s, s, s)
				'Flip
				'DebugStop()
			EndIf
		Wend
		Return pos
	End Method
	
	Method euler:TPoint(pos:TPoint, h:Double)
		Local t1:TPoint = calcTangent(pos)
		Return TPoint.Create( pos.x + h*t1.x, pos.y + h*t1.y )
	End Method
	
	Method rungeKutta2:TPoint(pos:TPoint, h:Double)
	 ' PYTHON: pos + h * Self.calcTangent(pos + Self.calcTangent(pos) * h / 2)
	Local t1:TPoint = calcTangent(pos)
	Local t2:TPoint = calcTangent( TPoint.Create(pos.x + t1.x * h/2, pos.y + t1.y * h/2 ) )
	Return TPoint.Create( pos.x + h*t2.x, pos.y + h*t2.y )
	End Method
	
	Method rungeKutta4:TPoint(pos:TPoint, h:Double)
    't1 = func(pos)
    't2 = func(pos + t1 * h / 2)
    't3 = func(pos + t2 * h / 2)
    't4 = func(pos + t3 * h)
    'Return pos + (h / 6) * (t1 + 2*t2 + 2*t3 + t4)
	Local t1:TPoint = calcTangent(pos)
	Local t2:TPoint = calcTangent( TPoint.Create(pos.x + t1.x * h/2, pos.y + t1.y * h/2 ) )
	Local t3:TPoint = calcTangent( TPoint.Create(pos.x + t2.x * h/2, pos.y + t2.y * h/2 ) )
	Local t4:TPoint = calcTangent( TPoint.Create(pos.x + t3.x * h, pos.y + t3.y * h ) )
	Return TPoint.Create( pos.x + (h/6)*(t1.x + 2*t2.x + 2*t3.x + t4.x), pos.y + (h/6)*(t1.y + 2*t2.y + 2*t3.y + t4.y ) )
	End Method
	
	Method drawBalls(stepping:Double)
		'First track the border for all balls and store
		'it to pos0 and edgePos. The latter will move along the border,
		'pos0 stays at the initial coordinates
		For Local b:TBall = EachIn balls
			Local borderPlusOne:TPoint = TPoint.Create(b.pos.x, b.pos.y-1) '(b.pos.x, b.pos.y-1 ) 'b.pos.x+Rnd(-.25,.25), b.pos.y+Rnd(-.25,.25) 
			b.pos0 = trackTheBorder(borderPlusOne)
			b.edgePos = b.pos0
			b.tracking = True
		Next
		
		'count how many times we have tracked a single ball. 
		'no need To do it too many times...
		Local loopIndex:Int = 0 
		
		For Local ball:tball = EachIn balls
			Local siz# = 6
			DrawOval(ball.pos.x-siz, ball.pos.y-siz,siz,siz)
		Next
			
		While(loopIndex < 200)
			loopIndex :+ 1
			
			
			For Local ball:tball = EachIn balls
				
				If(Not ball.tracking) Then Continue  'skip if tracking
				
				'store the old coordinates for drawing
				Local old_pos:TPoint = ball.edgePos
				
				'walk along the tangent, using chosen differential method
				'ball.edgePos = euler(ball.edgePos, stepping )
				ball.edgePos = rungeKutta2(ball.edgePos, stepping )
				'ball.edgePos = rungeKutta4(ball.edgePos, stepping )
				
				
				'correction step towards the border
				Local forceOnEdge:Double = calcForce(ball.edgePos)
				ball.edgePos = stepOnceTowardsBorder(ball.edgePos, forceOnEdge)
				
				'glVertex2f(old_pos.x, old_pos.y)
				'glVertex2f(ball.edgePos.x, ball.edgePos.y)
				SetColor 255,255,255
				'SetLineWidth 2
				DrawLine(old_pos.x, old_pos.y, ball.edgePos.x, ball.edgePos.y)
				'SetLineWidth 1
				'Plot (old_pos.x, old_pos.y)
				
				'check if we've gone a full circle or hit some other edge tracker
				For Local ob:TBall = EachIn balls
					Local delta:TPoint = TPoint.Create(ob.pos0.x - ball.edgePos.x, ob.pos0.y - ball.edgePos.y)
					Local distanceSq:Double = (delta.x*delta.x) + (delta.y*delta.y)
					
					If( (ob <> ball Or loopIndex > 3)  And distanceSq < (stepping*stepping) )
						ball.tracking = False
					EndIf
					
				Next 'eof check circle loop
				
				
			Next 'eof For ball loop
			
			Local tracking:Int = 0 'count how many balls are being tracked.
			For Local ball:TBall = EachIn balls
				If(ball.tracking) Then tracking :+ 1		
			Next
			
			If tracking = 0 Then Exit
			
						
		Wend 'eof while loopIndex

		
	End Method
End Type

[/code]
Edit: 15.08-2010
-Fixed a few silly mistakes.
-Added a counter that prevents trackTheBorder() for looping infinitely if the border/threshold cant be found.

Comments

plash2008
Interesting..


TWH2008
Perhaps one could use it for territory borders in a strategy game? It has to be made "stable" tho. The lines disappear in some cases... I haven't managed to convert it properly.


schilcote2009
Up untill now, I was wondering what this had to do with meatballs.


Nate the Great2009
nice! this is what ive been looking for for a particle based fluid rendering system... it needs to be multithreaded tho...


Code Archives Forum