Skip to content

Instantly share code, notes, and snippets.

@trbritt
Created August 16, 2022 19:47
Show Gist options
  • Save trbritt/6c0d6c98366b12df8330a2913c0b0d8b to your computer and use it in GitHub Desktop.
Save trbritt/6c0d6c98366b12df8330a2913c0b0d8b to your computer and use it in GitHub Desktop.
Generate parametric curve from a functional form in Blender. User defined functions are implemented.
"""
Create a Blender curve from a 3D parametric function.
This allows for a 3D plot to be made of the function, which can be converted into a mesh.
I have documented the inner workings here, but if you're not interested and just want to
suit this to your own function, scroll down to the bottom and edit the `f(t)` function and
the iteration count to your liking.
This code has been checked to work on Blender 2.79 and Blender 2.80b.
"""
import math
from math import sin, cos, pi, sqrt, exp
from numpy import cosh
import bmesh
from bmesh.ops import spin
import bpy
from mathutils import Vector
import utils
context = bpy.context
scene = context.scene
link_object = scene.collection.objects.link if bpy.app.version >= (2, 80) else scene.objects.link
unlink_object = scene.collection.objects.unlink if bpy.app.version >= (2, 80) else scene.objects.unlink
def lathe_geometry(bm, cent, axis, dvec, angle, steps, remove_doubles=True, dist=0.0001):
geom = bm.verts[:] + bm.edges[:]
# super verbose explanation.
spin(
bm,
geom=geom, # geometry to use for the spin
cent=cent, # center point of the spin world
axis=axis, # axis, a (x, y, z) spin axis
dvec=dvec, # offset for the center point
angle=angle, # how much of the unit circle to rotate around
steps=steps, # spin subdivision level
use_duplicate=0) # include existing geometry in returned content
if remove_doubles:
bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=dist)
def derive_bezier_handles(a, b, c, d, tb, tc):
"""
Derives bezier handles by using the start and end of the curve with 2 intermediate
points to use for interpolation.
:param a:
The start point.
:param b:
The first mid-point, located at `tb` on the bezier segment, where 0 < `tb` < 1.
:param c:
The second mid-point, located at `tc` on the bezier segment, where 0 < `tc` < 1.
:param d:
The end point.
:param tb:
The position of the first point in the bezier segment.
:param tc:
The position of the second point in the bezier segment.
:return:
A tuple of the two intermediate handles, that is, the right handle of the start point
and the left handle of the end point.
"""
# Calculate matrix coefficients
matrix_a = 3 * math.pow(1 - tb, 2) * tb
matrix_b = 3 * (1 - tb) * math.pow(tb, 2)
matrix_c = 3 * math.pow(1 - tc, 2) * tc
matrix_d = 3 * (1 - tc) * math.pow(tc, 2)
# Calculate the matrix determinant
matrix_determinant = 1 / ((matrix_a * matrix_d) - (matrix_b * matrix_c))
# Calculate the components of the target position vector
final_b = b - (math.pow(1 - tb, 3) * a) - (math.pow(tb, 3) * d)
final_c = c - (math.pow(1 - tc, 3) * a) - (math.pow(tc, 3) * d)
# Multiply the inversed matrix with the position vector to get the handle points
bezier_b = matrix_determinant * ((matrix_d * final_b) + (-matrix_b * final_c))
bezier_c = matrix_determinant * ((-matrix_c * final_b) + (matrix_a * final_c))
# Return the handle points
return (bezier_b, bezier_c)
def create_parametric_curve(
function,
*args,
min: float = 0.0,
max: float = 1.0,
use_cubic: bool = True,
iterations: int = 8,
resolution_u: int = 10,
**kwargs
):
"""
Creates a Blender bezier curve object from a parametric function.
This "plots" the function in 3D space from `min <= t <= max`.
:param function:
The function to plot as a Blender curve.
This function should take in a float value of `t` and return a 3-item tuple or list
of the X, Y and Z coordinates at that point:
`function(t) -> (x, y, z)`
`t` is plotted according to `min <= t <= max`, but if `use_cubic` is enabled, this function
needs to be able to take values less than `min` and greater than `max`.
:param *args:
Additional positional arguments to be passed to the plotting function.
These are not required.
:param use_cubic:
Whether or not to calculate the cubic bezier handles as to create smoother splines.
Turning this off reduces calculation time and memory usage, but produces more jagged
splines with sharp edges.
:param iterations:
The 'subdivisions' of the parametric to plot.
Setting this higher produces more accurate curves but increases calculation time and
memory usage.
:param resolution_u:
The preview surface resolution in the U direction of the bezier curve.
Setting this to a higher value produces smoother curves in rendering, and increases the
number of vertices the curve will get if converted into a mesh (e.g. for edge looping)
:param **kwargs:
Additional keyword arguments to be passed to the plotting function.
These are not required.
:return:
The new Blender object.
"""
# Create the Curve to populate with points.
curve = bpy.data.curves.new('Parametric', type='CURVE')
curve.dimensions = '3D'
curve.resolution_u = 2
# Add a new spline and give it the appropriate amount of points
spline = curve.splines.new('BEZIER')
spline.bezier_points.add(iterations)
if use_cubic:
points = [
function(((i - 3) / (3 * iterations)) * (max - min) + min, *args, **kwargs)
for i in range((3 * (iterations + 2)) + 1)
]
# Convert intermediate points into handles
for i in range(iterations + 2):
a = points[(3 * i)]
b = points[(3 * i) + 1]
c = points[(3 * i) + 2]
d = points[(3 * i) + 3]
bezier_bx, bezier_cx = derive_bezier_handles(a[0], b[0], c[0], d[0], 1 / 3, 2 / 3)
bezier_by, bezier_cy = derive_bezier_handles(a[1], b[1], c[1], d[1], 1 / 3, 2 / 3)
bezier_bz, bezier_cz = derive_bezier_handles(a[2], b[2], c[2], d[2], 1 / 3, 2 / 3)
points[(3 * i) + 1] = (bezier_bx, bezier_by, bezier_bz)
points[(3 * i) + 2] = (bezier_cx, bezier_cy, bezier_cz)
# Set point coordinates and handles
for i in range(iterations + 1):
spline.bezier_points[i].co = points[3 * (i + 1)]
spline.bezier_points[i].handle_left_type = 'FREE'
spline.bezier_points[i].handle_left = Vector(points[(3 * (i + 1)) - 1])
spline.bezier_points[i].handle_right_type = 'FREE'
spline.bezier_points[i].handle_right = Vector(points[(3 * (i + 1)) + 1])
else:
points = [function(i / iterations, *args, **kwargs) for i in range(iterations + 1)]
# Set point coordinates, disable handles
for i in range(iterations + 1):
spline.bezier_points[i].co = points[i]
spline.bezier_points[i].handle_left_type = 'VECTOR'
spline.bezier_points[i].handle_right_type = 'VECTOR'
# Create the Blender object and link it to the scene
curve_object = bpy.data.objects.new('Parametric', curve)
link_object(curve_object)
# Return the new object
return curve_object
def make_edge_loops(*objects):
"""
Turns a set of Curve objects into meshes, creates vertex groups,
and merges them into a set of edge loops.
:param *objects:
Positional arguments for each object to be converted and merged.
"""
mesh_objects = []
vertex_groups = []
# Convert all curves to meshes
for obj in objects:
# Unlink old object
unlink_object(obj)
# Convert curve to a mesh
if bpy.app.version >= (2, 80):
new_mesh = obj.to_mesh().copy()
else:
new_mesh = obj.to_mesh(scene, False, 'PREVIEW')
# Store name and matrix, then fully delete the old object
name = obj.name
matrix = obj.matrix_world
bpy.data.objects.remove(obj)
# Attach the new mesh to a new object with the old name
new_object = bpy.data.objects.new(name, new_mesh)
new_object.matrix_world = matrix
# Make a new vertex group from all vertices on this mesh
vertex_group = new_object.vertex_groups.new(name=name)
vertex_group.add(range(len(new_mesh.vertices)), 1.0, 'ADD')
vertex_groups.append(vertex_group)
# Link our new object
link_object(new_object)
# Add it to our list
mesh_objects.append(new_object)
mat = utils.create_material(metalic=0.5)
obj.data.materials.append(mat)
utils.set_smooth(obj, 2)
# Make a new context
ctx = context.copy()
# Select our objects in the context
ctx['active_object'] = mesh_objects[0]
ctx['selected_objects'] = mesh_objects
if bpy.app.version >= (2, 80):
ctx['selected_editable_objects'] = mesh_objects
else:
ctx['selected_editable_bases'] = [scene.object_bases[o.name] for o in mesh_objects]
# Join them together
bpy.ops.object.join(ctx)
utils.rainbow_lights(10, 100, 3, energy=300)
def gaussian_pulse(t, offset: float = 0.0):
"""
The function to plot.
:param t:
The parametric variable, from `min <= t <= max`.
:param offset:
An extra offset parameter. These can be passed to create_parametric_curve.
:return:
A tuple of the (x, y, z) coordinate of the parametric at this position.
"""
zz = (1/(3*sqrt(2*pi)))*exp(-t**2/(2*3**2))
zz *= sin(t*2*pi/2)
zz *= 30
zz += offset
return (
0,
t,
zz
)
def hyperbolic_beam(t, offset: float = 0.0):
"""
The function to plot.
:param t:
The parametric variable, from `min <= t <= max`.
:param offset:
An extra offset parameter. These can be passed to create_parametric_curve.
:return:
A tuple of the (x, y, z) coordinate of the parametric at this position.
"""
return (
0,
t,
cosh(0.225*t)
)
def hyperbolic_beam_inv(t, offset: float = 0.0):
"""
The function to plot.
:param t:
The parametric variable, from `min <= t <= max`.
:param offset:
An extra offset parameter. These can be passed to create_parametric_curve.
:return:
A tuple of the (x, y, z) coordinate of the parametric at this position.
"""
return (
0,
t,
-cosh(0.225*t)
)
if __name__ == '__main__':
# Plot the parametric.
# A higher iteration count here adds more points, increasing accuracy but also
# increasing calculation time and memory usage.
# Setting use_cubic to false skips calculating handles, which saves time and memory
# but results in sharp edges in the spline.
create_parametric_curve(hyperbolic_beam, offset=0.0, min=-8.0, max=8.0, use_cubic=True, iterations=1000)
# create_parametric_curve(hyperbolic_beam_inv, offset=0.0, min=-8.0, max=8.0, use_cubic=True, iterations=1000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment