1
$\begingroup$

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! enter image description here

$\endgroup$
6
  • 1
    $\begingroup$ Related blender.stackexchange.com/questions/47359/… $\endgroup$ Commented Oct 30, 2017 at 2:28
  • $\begingroup$ This may help : a primer on Bézier curves $\endgroup$
    – dr. Sybren
    Commented Oct 30, 2017 at 13:48
  • 1
    $\begingroup$ @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. $\endgroup$
    – patmo141
    Commented Feb 25, 2018 at 16:10
  • $\begingroup$ github.com/patmo141/odc_public/blob/… $\endgroup$
    – patmo141
    Commented Feb 25, 2018 at 16:11
  • $\begingroup$ @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 $\endgroup$
    – John
    Commented Mar 3, 2018 at 6:27

2 Answers 2

5
$\begingroup$

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]
bpy.ops.mesh.primitive_ico_sphere_add(size=0.05, location=(0, 0, 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
    scene.objects.link(s)
    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)
$\endgroup$
1
  • $\begingroup$ Nice, can this be done with a custom count? $\endgroup$ Commented Jan 20, 2021 at 5:35
2
$\begingroup$

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}")
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .