Normals from extruded curves


#1

I’m trying to extrude a pipe from a curve using extrude and nurbsTessellate, like the helper in bonus tools. A problem is that I often end up with a mesh with inverted normals. Every thread about this is just somebody saying “just flip the mesh normals”, which isn’t much help since I want this to work automatically as the curve deforms.

I tried orienting the profile curve to the normal of the start of the curve (using a pointOnCurveInfo and an aim constraint), but that only half-works and still flips if I move CVs around.

Does anyone know how the extrude node decides which direction to point normals, or another generic way to fix this? It’s definitely related to the orientation of the profile curve (if it’s flipped, rotating the profile curve always fixes it), but I’m not sure what the other piece is.


#2

I don’t have a real answer, but the workaround I finally found was to sample the normal with a closestPointOnMesh node and see if it’s flipped (pointing towards the curve instead of away from it). That goes through a condition node and sets the nodeState of a polyNormal node to turn it on only if needed. I’m not sure about using nodeState that way, but it seems to work so far.

It doesn’t work if the mesh is self-intersecting, since closestPointOnMesh can find the wrong point, but that’s not a problem for me.


#3

Ugh. Yeah, I run into this problem all the time, when I’m even bothering with Maya’s NURBS. It can be really tedious if you’re doing some complex modeling, too.

In Rhino, the normals are determined by the direction of the input curves. It’s predictable and always works this way, plus of course Rhino’s NURBS are far better implemented and easier to work with. Maya seems to be the same at first, but like you said, in practice it’s never really predictable.

Glad you devised a workaround, even if it is a pain in the ass.


#4

In case it’s useful to anyone, here’s what I ended up with. It’s originally based on the “curve to tube” tool in bonus tools, though not much is left (stripped almost everything I don’t need away). “pointOnCurveInfo” is where the actual normals wrangling begins (which takes as much code as creating the tube in the first place).


import pymel.core as pm

def go(curve):
    # Create the profile curve.
    profile_curve = pm.circle(
            normal=(0,1,0), radius=1, degree=3,
            constructionHistory=True,
            sections=8,
            name='profileCurve#')[0]
    profile_curve.attr('visibility').set(False)
    
    # Extrude the curve.
    extruded_surface, extrude_node = pm.extrude(profile_curve, curve,
            constructionHistory=True,
            range=0, polygon=1, extrudeType=2,
            useComponentPivot=1, # component pivot
            fixedPath=True, useProfileNormal=True,
            reverseSurfaceIfPathReversed=True,
            name='tube#')
    extruded_surface = extruded_surface.getShape()
                
    # Configure the nurbsTesselate node.
    nurbs_tessellate = pm.listConnections(extrude_node)[0]
    nurbs_tessellate.attr('format').set(2)
    nurbs_tessellate.attr('uType').set(1) # uniform
    nurbs_tessellate.attr('polygonType').set(1)

    extruded_surface.addAttr('widthDivisions', min=4, at='long', defaultValue=5)
    extruded_surface.setAttr('widthDivisions',e=1, k=1)
    extruded_surface.attr('widthDivisions').connect(nurbs_tessellate.attr('uNumber'))

    extruded_surface.addAttr('lengthDivisions', min=1, at='long', defaultValue=7)  
    extruded_surface.setAttr('lengthDivisions', e=1, k=1)
    extruded_surface.attr('lengthDivisions').connect(nurbs_tessellate.attr('vNumber'))

    extruded_surface.addAttr('lengthDivisionSpacing', at='enum', enumName='uniform=1:non-uniform=2', dv=1)  
    extruded_surface.setAttr('lengthDivisionSpacing', e=1, k=1)
    extruded_surface.attr('lengthDivisionSpacing').connect(nurbs_tessellate.attr('vType'))

    # The extrude node often creates flipped normals, and it's not clear when this
    # happens.  Work around this by looking at the normal at the mesh at the start of
    # the curve, and enabling a polyNormal node only if it appears to be flipped.
    #
    # Find the position at the start of the curve.
    point_info = pm.createNode('pointOnCurveInfo')
    point_info.attr('parameter').set(0)
    curve.getShape().attr('worldSpace[0]').connect(point_info.attr('inputCurve'))
    
    # Find the closest point on the mesh we just generated to that point.  Connect
    # directly to the nurbsTesselate output, not to the output mesh.
    point_on_mesh = pm.createNode('closestPointOnMesh')
    point_info.attr('position').connect(point_on_mesh.attr('inPosition'))
    nurbs_tessellate.attr('outputPolygon').connect(point_on_mesh.attr('inMesh'))

    # Subtract (curve point) - (closest mesh point) to get a vector from the mesh
    # towards the center.
    vector_towards_center = pm.createNode('plusMinusAverage', n='vectorTowardsCenter')
    vector_towards_center.attr('operation').set(2) # subtract
    point_on_mesh.attr('inPosition').connect(vector_towards_center.attr('input3D[0]')) # curve point
    point_on_mesh.attr('position').connect(vector_towards_center.attr('input3D[1]')) # mesh point

    # Find the angle between the vectors (mesh -> center point) and (mesh -> normal).
    angle_between = pm.createNode('angleBetween', name='normalAngle')
    vector_towards_center.attr('output3D').connect(angle_between.attr('vector1'))
    point_on_mesh.attr('result').attr('normal').connect(angle_between.attr('vector2'))

    # If the normals are correct, the angle should be near 180.  If they're reversed, it'll be
    # near 0 instead.  Create a condition node to check this.
    condition = pm.createNode('condition', name='checkNormalAngle')
    angle_between.attr('angle').connect(condition.attr('firstTerm'))
    condition.attr('secondTerm').set(90)
    condition.attr('operation').set(2)

    # Create a polyNormal node to reverse the normals.
    reverse_normals = pm.polyNormal(extruded_surface)[0]
    output_mesh = reverse_normals.attr('output').listConnections(p=True)[0].node()

    # We only want to reverse the normals if they're flipped.  Connect the condition node to
    # the polyNormal node and only enable it if needed.
    condition.attr('colorIfTrue').set((1,0,0))
    condition.attr('colorIfFalse').set((0,0,0))
    condition.attr('outColorR').connect(reverse_normals.attr('nodeState'))

    return output_mesh