# Can I create real/editable curves with Geometry Nodes?

I am creating some curves here with Geometry Nodes:

Now I want to apply the modifier so I can process these elsewhere as Bézier curves outside of Geometry Nodes. This works fine as soon as I convert them into a mesh with the node Curve to Mesh.

The problem I have here is, that I can only create a mesh with Geometry Nodes, but not curves. However, I need the output as real and editable curves, because I also change the values for Tilt and Radius with Geometry Nodes and then want to process them afterwards.

Is there currently a way to achieve this?

• maybe writing the tilt + radius in custom attributes and then a little python script which reads that attributes and set tilt + radius? Jun 8, 2022 at 19:02
• Deep in Blender, I think? . All constructive modifiers act, not on the curves, but on the polygonal approximation to them ( else some of the maths would be mind-bending :) ) You can't apply constructive modifiers at all to curves without converting the object to the mesh those modifiers have secretly been using Sep 9, 2022 at 18:58
• @RobinBetts Sometimes I wish Blender were Houdini ;-) Sep 9, 2022 at 19:04
• @quellenform Then houdini would be blender... ;) Jul 24, 2023 at 8:02
• @lajawi No, unfortunately this does not work (hence this question). Geometry Nodes only output a mesh once you apply the modifier, and if there are curves in it, they are simply removed. Sep 4, 2023 at 18:07

Since Geometry Nodes can only be applied as a mesh, we can't get a Curve object from it, but what we can do is transfer the attributes and data from the splines to a mesh in a way that a python script can read and recreate the splines in a Curve object.

the first obvious step is create a vertex for each spline point so that we can store the data of each point in them. The data being:

Name Data Type Optional Used by
position Vector False Nurbs, Bezier, Poly
radius Float True Nurbs, Bezier, Poly
tilt Float True Nurbs, Bezier, Poly
handle_type_left Integer False Bezier
handle_left Vector False Bezier
handle_type_right Integer False Bezier
handle_right Vector False Bezier
nurbs_weight Float False Nurbs

Optional means that the attribute may not be present, and we avoid wasting memory by not writing a custom attribute that will contain only the default value. Also, we can pack bézier handle types in a single attribute since they range from 0 to 3 (two bits for handle_type_left and two more for handle_type_right).

But there is more data than the ones per point, the splines themselves have data that we need, these being:

Name Data Type Optional Used by
resolution Integer False Nurbs, Bezier, Poly
cyclic Boolean True Nurbs, Bezier, Poly
nurbs_order Integer False Nurbs
knots_mode Integer False Nurbs

We will have to store these also in the vertices representing the spline points, or at least on the first vertex of each spline.

Now we have all the data that will end on the Curve object, but wait! To convert these vertices to splines we need to know when a spline ends and another starts, for that we will store the index of the spline for each vertex, that way, when iterating through the vertices, if the spline index changes, a new spline has to be created.

Node tree that stores the required attributes in a mesh line:

$$\text{Code (with comments explaining)}$$ $$_\text{also probably not that efficient}$$

#version 1.1.1

import bpy

#handle_type enums for int to str conversion
handle_types = ['FREE', 'AUTO', 'VECTOR', 'ALIGNED']

#function to display a popup message
def popup(message = "", title = "Message Popup", icon = 'INFO'):
bpy.context.window_manager.popup_menu(lambda self, context : self.layout.label(text=message), title=title, icon=icon)

#no action function to be used when an optional attribute isn't present
def no_setter(bez, attr, index):
pass

#function to set the cyclic attribute of a spline
def cyclic_setter(spl, attr, index):
spl.use_cyclic_u = attr.data[index].value

#function to set the radius attribute of a spline point
def radius_setter(spl_pt, attr, index):

#function to set the tilt attribute of a spline point
def tilt_setter(spl_pt, attr, index):
spl_pt.tilt = attr.data[index].value

#sets nurbs point data for the specified point
def set_nurbs_data(pt, attIndex):
pt.weight = nurbs_weightAttr.data[attIndex].value
set_poly_data(pt, attIndex)

#sets bezier point data for the specified point
def set_bezier_data(bez_pt, attIndex):
handle_type_packed = handle_typesAttr.data[attIndex].value #handle types are packed in a single attribute

bez_pt.co = data.vertices[attIndex].co #positoin
bez_pt.handle_left_type = handle_types[handle_type_packed >> 2]
bez_pt.handle_right_type = handle_types[handle_type_packed & 3]
bez_pt.handle_left = handle_leftAttr.data[attIndex].vector
bez_pt.handle_right = handle_rightAttr.data[attIndex].vector
radius_set(bez_pt, radiusAttr, attIndex) #using function reference for optional attribute
tilt_set(bez_pt, tiltAttr, attIndex) #using function reference for optional attribute

#sets poly point data for the specified point
def set_poly_data(pt, attIndex):
pt.co = data.vertices[attIndex].co.to_4d()
radius_set(pt, radiusAttr, attIndex) #using function reference for optional attribute
tilt_set(pt, tiltAttr, attIndex) #using function reference for optional attribute

target = bpy.context.active_object

if target == None:
popup("Selection is empty!", "Error", 'ERROR')
else:
if target.type not in {'CURVE', 'MESH'}:
popup("Invalid Selection!", "Error", 'ERROR')
elif bpy.context.mode != 'OBJECT': #will not work if in edit mode, so make sure we are in object mode
popup("Invalid Context! Please exit edit mode.", "Error", 'ERROR')
else:
#get object with geometry nodes evaluated
eval_target = target.evaluated_get(bpy.context.evaluated_depsgraph_get())
#creates mesh data if object is a curve
data = eval_target.to_mesh() if target.type == 'CURVE' else eval_target.data
total = len(data.vertices) #total amount of vertices

if total == 0:
popup("Selected Mesh is invalid!", "Error", 'ERROR')
elif total == 1: #empty curve
else:
attribs = data.attributes #getting the custom atributes
meta = data.vertices[0].co #the node tree adds a first vertex to show the curve types (with specific data) present
hasBezier = meta.x == 1
hasNurbs = meta.y == 1

#spline attributes
spline_IDAttr = attribs.get("spline_ID")
spline_typeAttr = attribs.get("curve_type")
resolutionAttr = attribs.get("resolution")
cyclicAttr = attribs.get("cyclic") #optional

#bezier point specific attributes
handle_typesAttr = attribs.get("handle_types")
handle_leftAttr = attribs.get("handle_left")
handle_rightAttr = attribs.get("handle_right")

#nurbs specific attributes
nurbs_orderAttr = attribs.get("nurbs_order") #per spline
knots_modeAttr = attribs.get("knots_mode") #per spline
nurbs_weightAttr = attribs.get("nurbs_weight") #per point

#shared point attributes
tiltAttr = attribs.get("tilt") #optional

#if required attributes are missing, show error popup, else continue normally
if None in {spline_IDAttr, spline_typeAttr, resolutionAttr}:
popup("Selected Mesh is invalid! Failed to find common required attributes.", "Error", 'ERROR')
elif hasBezier and None in {handle_typesAttr, handle_leftAttr, handle_rightAttr}:
popup("Selected Mesh is invalid! Bezier curves are present, but failed to find bezier attributes.", "Error", 'ERROR')
elif hasNurbs and None in {nurbs_orderAttr, knots_modeAttr, nurbs_weightAttr}:
popup("Selected Mesh is invalid! Nurbs curves are present, but failed to find nurbs attributes.", "Error", 'ERROR')
else:
#creates curve data
curveData = bpy.data.curves.new(name='Curve', type='CURVE')
curveData.dimensions = '3D'

#defines the action for each optional attribute, refers to a function that does nothing if missing, else refers to a function that sets the attribute value in the curve
cyclic_set = no_setter if cyclicAttr == None else cyclic_setter #function
radius_set = no_setter if radiusAttr == None else radius_setter #function
tilt_set = no_setter if tiltAttr == None else tilt_setter #function

v_index = 1 #index of current mesh vertex, starts at 1 since the index 0 is used for storing the presence of bezier curves
spl_index = 0 #index of current spline

while True:
type = spline_typeAttr.data[v_index].value
spline = curveData.splines.new('NURBS' if type == 3 else 'BEZIER' if type == 2 else 'POLY') #creates a spline inside the curve data

spl_index = spline_IDAttr.data[v_index].value
spline.resolution_u = resolutionAttr.data[v_index].value
cyclic_set(spline, cyclicAttr, v_index) #using function reference for optional attribute

if type == 3: #nurbs
nurbs_order = nurbs_orderAttr.data[v_index].value
knots = knots_modeAttr.data[v_index].value

spline.use_bezier_u = knots >> 1 & 1 #bezier boolean is on second bit
spline.use_endpoint_u = knots & 1 #endpoints boolean is on first bit
print(nurbs_orderAttr.data[v_index].value)
set_nurbs_data(spline.points[0], v_index)
v_index += 1

more_verts = True

while spline_IDAttr.data[v_index].value == spl_index: #while vertex has same spline index
set_nurbs_data(spline.points[-1], v_index)
v_index += 1

if v_index == total:
more_verts = False
break

spline.order_u = nurbs_order #order is set after creating points

if more_verts: #continues to create a new spline if vertices haven't ended
continue
elif type == 2: #bezier
set_bezier_data(spline.bezier_points[0], v_index)
v_index += 1

while spline_IDAttr.data[v_index].value == spl_index: #while vertex has same spline index
set_bezier_data(spline.bezier_points[-1], v_index)
v_index += 1

if v_index == total:
break
else:
continue #continues to create a new spline if vertices haven't ended
else: #poly
set_poly_data(spline.points[0], v_index)
v_index += 1

while spline_IDAttr.data[v_index].value == spl_index: #while vertex has same spline index
set_poly_data(spline.points[-1], v_index)
v_index += 1

if v_index == total:
break
else:
continue #continues to create a new spline if vertices haven't ended

break #stops main loop when there's no more vertices

if target.type == 'CURVE':
eval_target.to_mesh_clear()

#creates and add curve object to scene
obj_new = bpy.data.objects.new(target.name + ".Geo_Curve", curveData)

obj_new.matrix_world = target.matrix_world


Test curves generated via geometry nodes (some are cyclic, with random radius, tilt and handle rotation), and a few others of different type:

Mesh generated via Curve Mesh Prepare node group:

Curve object generated via script:

There is no need to apply the Geometry Nodes modifier, and the active can also be a Curve, just make sure that the desired object is the active object and then run the script. The script does not modify the selected object.

Please comment if you find bugs

(Blender 3.6.1)

Edit: Changes 1.1:

• Fixed wrong curve_type check in node tree, was comparing to $$2$$, but should be $$3$$. (Pointed by Quellenform).
• If in edit mode, a popup will now alert the user instead of later throwing an error for missing data.

Changes 1.1.1: Probably last update. I'm creating an addon for this functionality.

• Node tree will now work if there's more than curves in the input geometry
• Naming changes and variable reference fixes
• Created object now copies source's transforms
• What the ...!? How cool is that!? Please give me some time to try this out, it looks freaking wow, and exactly what I asked for. Thanks in advance! Aug 25, 2023 at 23:34
• +1 nice approach. btw does this also work for Quadratic Beziers? I tried a very simple setup with Quadratic Bezier node to Bezier Mesh Prepare node. This one gives a weird output when you run the script. Probably a limitation, it only works for Cubic bezier curves? Aug 26, 2023 at 5:10
• @quellenform done, I updated the answer, it is now valid for Nurbs, Bézier and Poly splines. Aug 27, 2023 at 20:51
• Small bugfix: NURBS curves have for curve_type the value $3$ Aug 31, 2023 at 10:51
• @lajawi Yes, you can have the node tree on a curve, but no - you cannot apply it. You get the error message: "Cannot apply constructive modifiers on curve. Convert curve to mesh in order to apply." But be aware that when you convert the curve object, the GN modifier gets automatically applied. But even if you have the modifier on a mesh object - if the only generated output is intances, curves or volumes (everything that is not a mesh), the modifier cannot be applied. Sep 5, 2023 at 8:33