# Bezier curve interpolation not uniform

I generated a bunch of points using mathutils.geometry.interpolate_bezier() but noticed the points are not uniformly distributed on the curve. Please see attached screenshot.

I think it has to do with the interpolation function processing each segment individually which can be dense or sparse depending on how stretched the segment is.

Is there is a better way to interpolate bezier curve and obtain a list of uniformly distributed points?

Thanks!

• Commented Oct 30, 2017 at 2:28
• This may help : a primer on Bézier curves Commented Oct 30, 2017 at 13:48
• @John, Bezier curves are evaluated on a non space based paramter (sometiems called 't'). You can think of it as time, and moving along the curve at different speeds. They way I work around this is that I evaluate the enture curve at small t steps, then re-evaluate/interpolate my points to space them evenly along the evaluated curve. Commented Feb 25, 2018 at 16:10
• github.com/patmo141/odc_public/blob/… Commented Feb 25, 2018 at 16:11
• @patmo141 Clever. But I end up converting to mesh and subdividing it to generate enough vertices. I thought that is what you did in your code too. Anyways Thanks
– John
Commented Mar 3, 2018 at 6:27

Use offset factor of follow path constraint.

Use the offset factor of follow curve constraint.

Test script, select the curve in object mode and run script.

import bpy
context = bpy.context
scene = context.scene
curve_obj = context.object
spline = curve_obj.data.splines[0]
sphere = context.object
fp = sphere.constraints.new(type='FOLLOW_PATH')
fp.target = curve_obj
fp.use_fixed_location = True

res = len(spline.bezier_points)

spheres = [sphere]
o = 0 if spline.use_cyclic_u else 1
r = spline.resolution_u + 1
pts = (res + o) * r
for i in range(1, pts + o):
s = sphere.copy()
sc = s.constraints[0]
sc.offset_factor = i / pts
spheres.append(s)


To remove the constraints, and keep their constraint location

# remove constraints
scene.update()
for s in spheres:
sc = s.constraints[0]
s.location = s.matrix_world.translation
s.constraints.remove(sc)

• Nice, can this be done with a custom count? Commented Jan 20, 2021 at 5:35

I got this working the hard way since I could not use geometry nodes for my purpose. I used numeric approximation methods to compensate for the variable curve velocity. Appologies the solution is a bit messy, it switches back and forth to numpy arrays, but numpy comes installed with blender 4.0 anyway.. and I think older versions too.

import bpy
import bmesh
from mathutils import Vector, Matrix
import numpy as np

def cubic_bezier_points_extended(control_points, t_values):
# Define the characteristic matrix for cubic Bézier curve
M = np.array([
[1, 0, 0, 0],
[-3, 3, 0, 0],
[3, -6, 3, 0],
[-1, 3, -3, 1]
])

# Calculate the number of segments based on the control points
n = (len(control_points) - 1) // 3

# Initialize the list to hold the computed points
bezier_points = []

for t in t_values:
# Determine which segment this t value falls into
segment_index = int(t)
if segment_index >= n:
segment_index = n - 1  # Clamp to the last segment for t values out of range

# Normalize t to the local coordinate system of the current segment [0, 1]
local_t = t - segment_index

# Select the appropriate control points for the current segment
cp_index = segment_index * 3
segment_control_points = control_points[cp_index:cp_index+4]

# Compute the T vector for the cubic Bézier curve
T = np.array([1, local_t, local_t**2, local_t**3])

# Compute the point on the curve for the current t value
point = T @ M @ segment_control_points  # Matrix multiplication to get the point
bezier_points.append(point)

return np.array(bezier_points)

def numeric_distance_integration(control_points, resolution=1000):
n_segments = (len(control_points) - 1) // 3
t_values = np.linspace(0, n_segments, resolution+1)
bezier_points = cubic_bezier_points_extended(control_points, t_values)
distance = np.sqrt(np.sum(np.power(bezier_points[:-1,:] - bezier_points[1:,:],2),axis=-1))
return distance

def cubic_bezier_points_equdistant(control_points, count=20, resolution=1000):
n_segments = (len(control_points) - 1) // 3
x = np.linspace(0, n_segments, resolution)
y = numeric_distance_integration(control_points, resolution=resolution)
length = np.sum(y)
t_values_equidistant = np.interp(np.linspace(0, 1, count),y.cumsum()/length,x,)
return cubic_bezier_points_extended(control_points, t_values_equidistant)

def resample_curve(obj, count=20):
if obj.type != 'CURVE':
raise ValueError("Object is not a curve in custom function resample_curve().")

spline = obj.data.splines[0]
control_points = []
for point_index in range(len(spline.bezier_points)-1):
a = spline.bezier_points[point_index]
b = spline.bezier_points[point_index+1]
if point_index == 0:
control_points.append(a.co.xyz)
control_points.extend([
a.handle_right.xyz,
b.handle_left.xyz,
b.co.xyz,
])
control_points = [obj.matrix_world @ p for p in control_points]
# Convert control points to a numpy array
control_points = np.array(control_points)
equidistant_points_np = cubic_bezier_points_equdistant(control_points, count=count)
equidistant_points = [Vector(p) for p in equidistant_points_np]
return equidistant_points


then use it like

# select a curve object in the viewport then run

context = bpy.context
obj = context.active_object
resampled_points = resample_curve(obj)


to quickly view the result dump the points out as a mesh;

def mesh_line_from_points(points, name="MeshLine"):
mesh = bpy.data.meshes.new(name=name)
line_obj = bpy.data.objects.new(name, mesh)
bm = bmesh.new()

for point in points:
bm.verts.new((point[0], point[1], point[2]))

if len(bm.verts) > 1:
bm.verts.ensure_lookup_table()
for i in range(len(bm.verts)-1):
bm.edges.new((bm.verts[i], bm.verts[i+1]))

bm.to_mesh(mesh)
bm.free()

return line_obj

mesh_line_from_points(resampled_points, f"Proj_{obj.name}")