12
$\begingroup$

I am creating some curves here with Geometry Nodes:

enter image description here

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?

$\endgroup$
10
  • 4
    $\begingroup$ maybe writing the tilt + radius in custom attributes and then a little python script which reads that attributes and set tilt + radius? $\endgroup$
    – Chris
    Jun 8, 2022 at 19:02
  • 1
    $\begingroup$ 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 $\endgroup$
    – Robin Betts
    Sep 9, 2022 at 18:58
  • 1
    $\begingroup$ @RobinBetts Sometimes I wish Blender were Houdini ;-) $\endgroup$
    – quellenform
    Sep 9, 2022 at 19:04
  • $\begingroup$ @quellenform Then houdini would be blender... ;) $\endgroup$ Jul 24, 2023 at 8:02
  • 1
    $\begingroup$ @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. $\endgroup$
    – quellenform
    Sep 4, 2023 at 18:07

1 Answer 1

11
+300
$\begingroup$

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 group to store spline data in a python convertible mesh

Node tree that stores the required attributes in a mesh line: node tree to store spline data in a python convertible mesh

$$\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):
    spl_pt.radius = attr.data[index].value
    
#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
            bpy.context.scene.collection.objects.link(bpy.data.objects.new("Geo_Curve", bpy.data.curves.new(name='Curve', type='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
            radiusAttr = attribs.get("radius") #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
                            spline.points.add(1)
                            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
                            spline.bezier_points.add(1)
                            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
                            spline.points.add(1)
                            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
                bpy.context.scene.collection.objects.link(obj_new)

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

Mesh generated via Curve Mesh Prepare node group: Mesh generated by custom node tree

Curve object generated via script: Resulting Curve via python 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
$\endgroup$
12
  • $\begingroup$ 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! $\endgroup$
    – quellenform
    Aug 25, 2023 at 23:34
  • $\begingroup$ +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? $\endgroup$ Aug 26, 2023 at 5:10
  • 2
    $\begingroup$ @quellenform done, I updated the answer, it is now valid for Nurbs, Bézier and Poly splines. $\endgroup$
    – Hulifier
    Aug 27, 2023 at 20:51
  • 1
    $\begingroup$ Small bugfix: NURBS curves have for curve_type the value $3$ $\endgroup$
    – quellenform
    Aug 31, 2023 at 10:51
  • 1
    $\begingroup$ @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. $\endgroup$ Sep 5, 2023 at 8:33

You must log in to answer this question.

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