Bezier curves

17 Ⅰ 2010

Ok, so once again, this is mostly here so I can remember…

For my Entoforms project I’ve been using sine waves to calculate nice “increases” and “decreases”, but… a sine wave is only so flexible. Thus I had a look at using beziers in stead. Now… I am an artist, and not a math wizz, so it was tricky to get my head around…

The concept

Because I’m just using this bit of math to calulate how much I want to transform stuff, I’m only doing it in 2D. And to keep it simple only with curves with a single segment (though you can combine multiple curves). Below here are a few nice ones.

Now in my case I only care about the “inside” of the curve… so we’re ignoring the black (unselected) handles. This results in each curve being defined by 4 points… 2 nodes (where the curve starts and ends) and 2 handles (that define the shape of the curve). So we have p0 (the first node), p1 (the handle for the first node), p2 (the handle for the second node), p3 (the second node).

The math

I was lucky enough to find some very basic math for this on the net… I really don’t know much about what it actually does, but it works great! So here’s a simple script that places empties along a bezier curve.

import bpy, math, mathutils

print('-- starting --')

# Kappa is the position of the handle on a circular curve
kappa = ((math.sqrt(2)-1)/3)*4

# A series of nice 2D bezier curves
beziers = {
	'linear': {
		'p0': mathutils.Vector((0.0,0.0)),
		'p1':mathutils.Vector((0.5,0.5)),
		'p2':mathutils.Vector((0.5,0.5)),
		'p3':mathutils.Vector((1.0,1.0)),
		},
	'increasing': {
		'p0': mathutils.Vector((0.0,0.0)),
		'p1':mathutils.Vector((kappa,0.0)),
		'p2':mathutils.Vector((1.0,(1.0-kappa))),
		'p3':mathutils.Vector((1.0,1.0)),
		},
	'decreasing': {
		'p0': mathutils.Vector((0.0,0.0)),
		'p1':mathutils.Vector((0.0,kappa)),
		'p2':mathutils.Vector(((1.0-kappa),1.0)),
		'p3':mathutils.Vector((1.0,1.0)),
		},
	'swoosh': {
		'p0': mathutils.Vector((0.0,0.0)),
		'p1':mathutils.Vector((kappa,0.0)),
		'p2':mathutils.Vector(((1.0-kappa),1.0)),
		'p3':mathutils.Vector((1.0,1.0)),
		},
	}

	
# Find a point along a bezier curve
def findBezierPoint(r, curve):
	c = 3 * (curve['p1'] - curve['p0'])
	b = 3 * (curve['p2'] - curve['p1']) - c
	a = curve['p3'] - curve['p0'] - c - b

	r2 = r * r
	r3 = r2 * r

	return a * r3 + b * r2 + c * r + curve['p0']
	
	
# Lets make a curve go from 1.0 to 0.0
def reverseCurve(curve):
	bezier = {
		'p0': mathutils.Vector(((1.0-curve['p3'][0]),curve['p3'][1])),
		'p1': mathutils.Vector(((1.0-curve['p2'][0]),curve['p2'][1])),
		'p2': mathutils.Vector(((1.0-curve['p1'][0]),curve['p1'][1])),
		'p3': mathutils.Vector(((1.0-curve['p0'][0]),curve['p0'][1])),
		}
	return bezier
	
	
# Negate a curve
def negateCurve(curve):
	bezier = {
		'p0': mathutils.Vector((curve['p0'][0],-curve['p0'][1])),
		'p1': mathutils.Vector((curve['p1'][0],-curve['p1'][1])),
		'p2': mathutils.Vector((curve['p2'][0],-curve['p2'][1])),
		'p3': mathutils.Vector((curve['p3'][0],-curve['p3'][1])),
		}
	return bezier
	
	
# Make the intensity of a curve bigger or smaller
# Intensity has to be between 0.0 and 2.0 (1.0 is default)
def intensifyCurve(curve, intensity=1.0):

	bezier = {
		'p0': curve['p0'],
		'p1': curve['p1'],
		'p2': curve['p2'],
		'p3': curve['p3'],
		}

	if intensity > 1.0:
		if bezier['p1'][0]:
			dif = 1.0 - bezier['p1'][0]
			dif *= (intensity - 1.0)
			bezier['p1'][0] += dif
			
		if bezier['p1'][1]:
			dif = 1.0 - bezier['p1'][1]
			dif *= (intensity - 1.0)
			bezier['p1'][1] += dif
			
		if bezier['p2'][0] != 1.0:
			dif = bezier['p2'][0]
			dif *= (intensity - 1.0)
			bezier['p2'][0] -= dif
			
		if bezier['p2'][1] != 1.0:
			dif = bezier['p1'][1]
			dif *= (intensity - 1.0)
			bezier['p1'][1] -= dif
			
	elif intensity < 1.0:
		if bezier['p1'][0]:
			bezier['p1'][0] *= intensity
			
		if bezier['p1'][1]:
			bezier['p1'][1] *= intensity
			
		if bezier['p2'][0] != 1.0:
			dif = 1.0 - bezier['p2'][0]
			dif *= intensity
			bezier['p2'][0] += dif
			
		if bezier['p2'][1] != 1.0:
			dif = 1.0 - bezier['p2'][1]
			dif *= intensity
			bezier['p2'][1] += dif
			
	return bezier
	
	
# Now lets do something usefull!
# Lets say we want to scale something * 5.0 in 10 steps

# So we add a cube to do this to
bpy.ops.mesh.primitive_cube_add(view_align=False, enter_editmode=False, location=(0.0, 0.0, 0.0), rotation=(0, 0, 0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False))

# So now lets do multiple bezier curves
curves = [beziers['swoosh'],beziers['decreasing'],beziers['increasing'],beziers['increasing']]
intensities = [1.0,1.0,1.0,1.0]

# The number of steps we want to do this in
iterations = 40

# The size we start with is always 1 because we do it relative
startSize = 1.0 

# The size at the end of the process should be the start multiplied by this
scaleUp = 5.0

# We get the difference in scale so we can figure out each step
scaleDif = scaleUp - startSize

# This is here to see if the progression is correct
checkSize = startSize

# The number of iterations per curve
split = iterations / len(curves)
stepSize = (1.0/iterations) * len(curves)

# Lets do 10 steps
for i in range(iterations):

	# Before we do anything we find out where we should be at this point in time
	step = i
	
	# Figure out what curve we need to use
	curveId = math.floor(step/split)
	curve = curves[curveId]
	
	# Set the intensity for a curve
	curve = intensifyCurve(curve, intensities[curveId])
	
	# Make sure each curve is interpreted start to finish
	step -= (curveId * split)
	
	# The current curve number for reversing and inversing should be 1-2-3-4
	# 1 is regular
	curveNr = (curveId +1) - (curveId//4)

	# The second and third curves are reversed (the third makes things smaller)
	if curveNr is 2 or curveNr is 3:
		curve = reverseCurve(curve)
		
	# Before we do anything we find out where we should be at this point in time
	curvePoint = stepSize * step
	
	# Find the point on the curve for the previous iteration in the loop
	currentPoint = findBezierPoint(curvePoint, curve)
	
	# This should tell us what the current size of the object is
	currentOffset = (scaleDif * currentPoint[1]) + startSize
	
	# Now lets find out how much bigger we need to make it
	# We do i+1 because then we start with 1 and end with 10 (0 is nothing anyway)
	step += 1

	# The point along the curve is always a nr between 0.0 and 1.0
	# So we divide 1 by the number of iterations to find the stepsize
	curvePoint =  stepSize * step

	# Find the point on the curve for this iteration in the loop
	newPoint = findBezierPoint(curvePoint, curve)
	
	# This should be the size we want to achieve
	newOffset = (scaleDif * newPoint[1]) + startSize
	
	scaleFactor = newOffset / currentOffset
	
	checkSize *= scaleFactor
	
	# Lets print out some values to check
	print('')
	print((i+1),step,'curve',curveNr)
	#print('  stepSize',stepSize)
	print('  currentPoint', round(currentPoint[1],5),'newPoint',round(newPoint[1]))
	#print('  currentOffset',currentOffset)
	#print('  newOffset', newOffset)
	print('  scaleFactor', scaleFactor)
	print('  checkSize',checkSize)
	
	# lets add some meshes so we can see the effect
	bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked":True, "mode":1}, TRANSFORM_OT_translate={"value":(0, 0, 0), "constraint_axis":(False, False, False), "constraint_orientation":'GLOBAL', "mirror":False, "proportional":'DISABLED', "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "release_confirm":False})
	
	xPos = ((newPoint[0]*iterations*2)+(curveId*iterations*2))
	zPos = (newPoint[1]*iterations*2)
	
	if curveNr == 3 or curveNr == 4:
		zPos -= iterations*2
	
	bpy.context.active_object.location = (xPos,0.0,zPos)
	bpy.ops.transform.resize(value=(scaleFactor, scaleFactor, scaleFactor), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1, snap=False, snap_target='CLOSEST', snap_point=(0, 0, 0), snap_align=False, snap_normal=(0, 0, 0), release_confirm=False)