1

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

John
  • 127
  • 1
  • 1
  • 8
  • 1
    Related https://blender.stackexchange.com/questions/47359/how-to-evenly-space-vertices-along-several-segments-bezier-curve – Duarte Farrajota Ramos Oct 30 '17 at 02:28
  • 1
    @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. – patmo141 Feb 25 '18 at 16:10
  • https://github.com/patmo141/odc_public/blob/7cf31d1165e2e83de875b69cdcbad6b41f9045e8/common_utilities.py#L458 – patmo141 Feb 25 '18 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 Mar 03 '18 at 06:27
  • Yes, I do convert to mesh in this code. In Retopoflow we were actually using a custom bezier evaluation code. Same principle of evaluating the bezier at small T. I then I go back and re-space the verts evenly along the path I get back from Curve.to_mesh() – patmo141 Mar 03 '18 at 14:05

2 Answers2

5

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)
batFINGER
  • 84,216
  • 10
  • 108
  • 233
2

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}")

quellenform
  • 35,177
  • 10
  • 50
  • 133