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)
bpy.context.collection.objects.link(line_obj)
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}")