Code archives/Graphics/2D Metaballs

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

Download source code

2D Metaballs by TWH
(Posted 18 years ago)
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

AppTitle = "premature optimization is the root of all evil"
'HideMouse()
Global resX:Int=800, resY:Int=600
SetGraphicsDriver GLMax2DDriver()
Graphics resX,resY,0,0


Local balls:tball[4]
balls[0] = TBall.Create( TPoint.Create(350,100),3)
balls[1] = TBall.Create( TPoint.Create(20,200),2)
balls[2] = TBall.Create( TPoint.Create(280,140),4)
balls[3] = TBall.Create( TPoint.Create(400,440),3)

Local threshold:Float = 0.0003'0000100000000000
Local mbs:tmetaballsystem = TMetaballsystem.Create(balls, 2.0, threshold)

Local iterations:Int = 0
Local fps:Int = 0
Local lastSec:Long = MilliSecs() + 1000
Local frames:Int = 0
DrawText "if this stays long we have an infinite loop :(",0,0
Flip
While(Not KeyHit(KEY_ESCAPE) And Not AppTerminate()	)
	If(MouseDown(1))
		balls[0].pos.x = MouseX()
		balls[0].pos.y = MouseY() 
	End If
	mbs.drawBalls(20) 'sim_step
	
	
	SetColor 255,255,0
	DrawText("iterations: "+iterations+"    fps: "+fps, 0,0)
	DrawText("last force: ")+mbs.force, 0, 20
	If(lastSec < MilliSecs() )
		lastSec = MilliSecs() + 1000
		fps = frames
		frames = 0
	EndIf
	
	iterations:+1
	frames :+ 1
	Flip
	Cls
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
	Field balls:tball[]
	Field goo:Double
	Field threshold:Double
	Field minSize:Double
	Field force: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 = 9 'smallest ball
		
		Return tmp
	End Function 
	
	' fix 26/11/08
	Method calcForce:Double(pos:tpoint) 'called by stepOnceTowardsBorder
		'return the metaball fields force at point "pos"
		Local singleForce:Double = 0
		
		For Local ball:tball = EachIn balls
			'### Formula (1)
			Local tmp:TPoint = TPoint.Create(ball.pos.y-pos.y,ball.pos.x-pos.x)
			Local div:Double= Sqr(tmp.x*tmp.x + tmp.y*tmp.y)
			div = div*div '^2
			If(div <> 0)
				singleForce :+ ball.size / div	
			Else
				singleForce = 10000 '"big number"	
			EndIf
		Next
		Return singleForce
	End Method
	
	'checked  26.11.08
	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 tmp:TPoint = TPoint.Create(ball.pos.y-pos.y, ball.pos.x-pos.x)
			Local div:Double= Sqr(tmp.x*tmp.x + tmp.y*tmp.y)
			div = div*div*div*div
			np.x :+ -Self.goo * ball.size * (ball.pos.x-pos.x) /div
			np.y :+ -Self.goo * ball.size * (ball.pos.y-pos.y) /div
		Next
		Local lengthOfVector:Double = Sqr(np.x*np.x + np.y*np.y)
		Return TPoint.Create( np.x/lengthOfVector, np.y/lengthOfVector)
	End Method
	
	'checked  26.11.08
	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
	
	'checked  26.11.08
	Method stepOnceTowardsBorder:tpoint(pos:tpoint) 'called by drawBalls, trackTheB
		'called by trackTheBorder
		'step once towards the border of the metaballs field, return
		'new coordinates and force at old coordinates
		Local singleForce:Double = calcForce(pos)
		Local np:TPoint = calcNormal(pos)
		'### Formula(5)
		Local stepsize:Double = (minSize/threshold)
		
		stepsize = stepsize^(1/Self.goo)
		stepsize = stepsize - (minSize / singleForce)^(1/Self.goo)
		stepsize :+ 0.01
		Self.force = singleForce
		Return TPoint.Create( pos.x + np.x*stepsize , pos.y + np.y*stepsize )
	End Method
	
	'checked  26.11.08
	Method trackTheBorder:tpoint(pos:tpoint)
		'track the border of the metaball field and return new coords
		force = 9999999
		'loop until force is weaker than the desired threshold
		
		Local i:Int =0 
		While(force > threshold)
			pos = stepOnceTowardsBorder(pos)
			'show a little debug output i.e. yellow pixels
			' sz = screen size
			'If( 0 < pos.x And pos.x < resX And 0 < pos.y And pos.y < resY)
				SetColor 255,255,0
				DrawRect( pos.x, pos.y, 10, 10 )
			'EndIf
			'Print "i, force, posx,posy "+i+", "+force+", "+pos.x+","+pos.y
			'If i > 10 Then End
			'i:+1
			
		Wend
		Return pos
	End Method
	
	Method rungeKutta2:TPoint(pos:TPoint, h:Double)
	 ' PYTHON: pos + h * Self.calcTangent(pos + Self.calcTangent(pos) * h / 2)
	  Local tmp:TPoint = calcTangent(pos)
	  tmp.x = tmp.x * h / 2
	  tmp.y = tmp.y * h / 2
	  tmp.x :+ pos.x
	  tmp.y :+ pos.y
	  tmp = calcTangent( tmp )
	  tmp.x = tmp.x * h
	  tmp.y = tmp.y * h
	  tmp.x :+ pos.x
	  tmp.y :+ pos.y
        Return tmp
	End Method
	
	Method drawBalls(stepping:Double)
		'First track the border for all balls and store
		'it to pos0 and edgPos. The latter will move along the border,
		'pos0 stays at the initial coordinates
		For Local ball:TBall = EachIn balls
			Local borderPlusOne:TPoint = TPoint.Create(ball.pos0.x, ball.pos0.y+1)
			ball.pos0 = trackTheBorder(borderPlusOne)
			ball.edgePos = ball.pos0
			ball.tracking = True
		Next
		Local loopIndex:Int = 0
		Local tracking:Int=0
		
		
		SetColor 255,255,255
		SetLineWidth 5
		While(loopIndex < 1000)
			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 = rungeKutta2(ball.edgePos, stepping )
				
				'correction step towards the border
				ball.edgePos = stepOnceTowardsBorder(ball.edgePos)
				
				
				DrawLine(old_pos.x, old_pos.y, ball.edgePos.x, ball.edgePos.y)
				'SetColor 255,0,0
				'DrawOval old_pos.x-1, old_pos.y-1,2,2
				'SetColor 0,0,255
				'DrawOval ball.edgePos.x-2, ball.edgePos.y-2,4,4
				
				'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.y - ball.edgePos.y, ob.pos0.x - ball.edgePos.x)
					Local distance:Double = Sqr(delta.x*delta.x + delta.y*delta.y)
					If((ob <> ball Or loopIndex > 3) And distance < stepping)
						ball.tracking = False
					EndIf
				Next 'eof check circle loop
			Next 'eof For ball loop
			
			tracking=0
			For Local ball:TBall = EachIn balls
				If(ball.tracking) Then tracking :+ 1
				If tracking = 0 Then Exit
			Next
		Wend 'eof while loopIndex
	End Method
End Type


[/code]