I’ve been working on a point cloud meshing method, incorporating this Paper
The algorithm has been extended using a custom nearest neighbour and Forward Facing Normal Flow operator written in VEX. I might be able to post those soon.
Anyway, the surface reconstruction is handled by a custom Python Operator, connecting neighbours, sampling edges, defining edge normals etc.
The most optimal distribution is searched for, empty spots are recursively filled. This is a work in progress and I won’t be able to post the complete functioning operator. But this snippet of code might help (tip of the iceberg ;)).
import Triangulate.hpoly
reload(Triangulate.hedge)
reload(Triangulate.hpoly)
from Triangulate.hedge import Edge
from Triangulate.hpoly import TriPoly
import sys
import math
# This code is called when instances of this SOP cook.
node = hou.pwd()
geo = node.geometry()
'''
Globals______________________________________________________________________________________________________
'''
# Set up some globals
neigh_name = hou.pwd().evalParm("neighbour_name") # neighbour attribute name
angle_name = hou.pwd().evalParm("angle_name") # angle attribute name
distance_name = hou.pwd().evalParm("distance_name") # distance attribute name
edge_group_name = hou.pwd().evalParm("edge_group_name") # group of initial edge points
neigh_amount = hou.pwd().evalParm("neighbours") # amount of neighbours
max_iter = hou.pwd().evalParm("max_iterations") # maximum amount of iterations
point_prior = hou.pwd().parm("point_priorization").evalAsString() # point sampling method
max_connections = hou.pwd().evalParm("max_connections") # max allowed point connections
conn_constrain = hou.pwd().parm("constrain_method").eval() # point constrain method
double_constrain = hou.pwd().parm("double_reduction").eval() # double prims constrain method
orient_polygons = hou.pwd().parm("orient_polygons").eval() # if polygons need to be oriented
predefined_edges = hou.pwd().evalParm("predefined") # if edges are predefined in groups
num_connections_name = hou.pwd().evalParm("connections_attrib") # name of number of connections
num_associations_name = hou.pwd().evalParm("association_attrib") # name of the association attribute
unresolved_name = hou.pwd().evalParm("unresolved_attribute") # name of the unresolved edge attribute
orient_name = hou.pwd().evalParm("orient_attribute") # name of the orientation attribute
orient_group_name = hou.pwd().evalParm("orient_export_name") # name of the orient polygons group
unresolved_group_name = hou.pwd().evalParm("unresolved_export_name")# name of the unresolved prims group
unresolved_edges_name = hou.pwd().evalParm("unresolved_edges_name")
DEBUG = hou.pwd().evalParm("debug")
neigh_attributes = [] # list of neighbour point attributes
connected_points = [] # list of point numbers that have been used!
active_edges = [] # holds the currently active edges
unresolved_edges = [] # holds the currently unresolved edges
boundary_edges = [] # holds if an edge is on a boundary
bound_polygons = [] # list of polygons created
geo = hou.pwd().geometry() # reference to incoming geo
points = geo.points() # original points
'''
Attributes___________________________________________________________________________________________________
'''
# Add the number of connections attribute
num_connections_attrib = geo.findPointAttrib(num_connections_name)
if num_connections_attrib is None:
num_connections_attrib = geo.addAttrib(hou.attribType.Point, num_connections_name, 0)
# Add the number of associations attribute
num_associations_attrib = geo.findPointAttrib(num_associations_name)
if num_associations_attrib is None:
num_associations_attrib = geo.addAttrib(hou.attribType.Point, num_associations_name, 0)
# Add the point resolved attribute
resolved_attrib = geo.findPointAttrib(unresolved_name)
if resolved_attrib is None:
resolved_attrib = geo.addAttrib(hou.attribType.Point, unresolved_name, 0)
resolved_prim_attrib = geo.findPrimAttrib(unresolved_name)
if resolved_prim_attrib is None:
resolved_prim_attrib = geo.addAttrib(hou.attribType.Prim, unresolved_name,0)
'''
Methods______________________________________________________________________________________________________
'''
#special debug method
def Console(inString):
"""
Helper function showing debug information if switched on
"""
if DEBUG:
print inString
def GetPoint(inNumber):
"""
Returns a point by index
"""
return points[inNumber]
def GetNumberOfAssociations(inPoint):
"""
Returns the number of point associations
"""
return inPoint.numberOfAssociations
def GetNumberOfConnections(inPoint):
"""
Returns the number of point connections
"""
return inPoint.numberOfConnections
def PolyAssociatedPresence(inEdge, inPoint):
"""
Checks by heritage if the poly is already created
"""
if inEdge.heritagePoint == inPoint:
return True
return False
def PolyPresence(inEdge, inPoint):
"""
Check in the bound polygon list if the poly was already created
"""
if TriPoly([inEdge.pointOne, inEdge.pointTwo, inPoint]) in bound_polygons:
return True
return False
def findPrimGroup(inName):
"""
Finds a primitive group with a specific name
Returns none if not found!
"""
for group in geo.primGroups():
if group.name() == inName:
return group.prims()
break
return None
# get's the edge group points
def GetSeedPoints(inGroupName):
"""
Returns the points of the specified point group
"""
return_group = None
for group in geo.pointGroups():
if group.name() == inGroupName:
if len(group.points()) > 2:
pgroup = []
for point in group.points(): # Search for the point in the original points and return those!
pgroup.append(points[point.number()])
return pgroup
else:
raise hou.NodeError( "Not enough points in edge group, minimum of two required!")
return -1
raise hou.NodeError( "Initial point group not found or not enough points: %s" % inGroupName)
return -1
# adds some sampling attributes
def AddPointAttributes(inPoints):
"""
Adds number of connections and isEdge attribute to points
"""
for point in inPoints:
point.numberOfConnections = 0 #set number of connections to 0
point.numberOfAssociations = 0 #set number of point associations to 0
point.isEdgePoint = False #see if it's an edge point or not
#point.isUnresolved = 1 #set's the point to be unresolved (share a min of 2 edges)
# creates seed edges
def CreateSeedEdges(inPoints):
"""
Based on the seed points, create some edges to start with
These edges are directly set to be active and to be an edge
"""
for pnum in range(len(inPoints)):
if pnum <= len(inPoints)-2:
edge = Edge(inPoints[pnum], inPoints[pnum+1])
else:
edge = Edge(inPoints[pnum], inPoints[0])
active_edges.append(edge) #add to active edge
boundary_edges.append(edge) #add to bound edge
connected_points.append(inPoints[pnum].number()) #add as connected point
inPoints[pnum].isEdgePoint = True #set it to be an edge point
inPoints[pnum].numberOfConnections = 2 #set it to already have 2 connections
inPoints[pnum].numberOfAssociations = 0 #set number of point associations to 0
def CreatePredefinedEdges(inGroupName):
"""
Creates a list of predefined edges that were submitted in groups
Based on the initial group name, appendices are sampled and added as an edge
"""
for group in geo.pointGroups():
if inGroupName in group.name():
# check to see if we're dealing with 2 points
group_points = group.points()
if len(group_points) != 2 :
raise hou.NodeError("Error in group: %s Only predefined groups of two points can be sampled" % group)
edge = Edge(group_points[0], group_points[1])
active_edges.append(edge)
boundary_edges.append(edge)
# initializes the neigbours
def InitializeNeighbourAttributes(inGeo, inName):
"""
initialized a list of neighbour attributes
"""
rlist = []
for n in range(neigh_amount):
attr = inGeo.findPointAttrib("%s%d" % (inName,(n+1)))
if attr != None:
rlist.append(attr)
else:
print "can't find neighbour attribute: %s%d" % (inName, (n+1))
return rlist
# initialize export group and attribute
def InitializeGroup(inName):
"""
Initializes the export group and orientation attribute
"""
if len(inName) < 1:
raise hou.NodeError("Please specify an export group, no name found")
if inName in geo.primGroups():
raise hou.NodeError("Primitive group '%s' already exists" % inName)
return geo.createPrimGroup(inName)
# initialize orientation attribute
def InitializeOrientAttrib(inName):
"""
Initializes the poly orientation attribute
"""
orient_attrib = geo.findPointAttrib(inName)
if orient_polygons and orient_attrib is None:
raise hou.NodeError("Can't find orientation attribute: %s" % orient_name)
return orient_attrib
# finalizes the attributes
def Finalize(inUnresolvedGroup):
"""
Transfers point properties on to attributes
"""
# Set the number of connections attribute
for point in points:
point.setAttribValue(num_connections_attrib, point.numberOfConnections)
point.setAttribValue(num_associations_attrib, point.numberOfAssociations)
edge_group_counter = 0
# Append unresolved primitives to unresolved group and set attribute
for edge in unresolved_edges:
Console( "Unresolved Edge: %s, Prim: %s" % (edge, edge.edgePrims))
# Create the edge group
group = geo.createPointGroup("%s_%d" % (unresolved_edges_name, edge_group_counter))
edge_group_counter += 1
for prim in edge.edgePrims:
prim = geo.iterPrims()[prim]
prim.setAttribValue(resolved_prim_attrib, 1)
inUnresolvedGroup.add(prim)
for point in edge.points:
point.setAttribValue(resolved_attrib, 1)
group.add(point)
# Append unresolved active primitives to unresolved group and set attribute
for edge in active_edges:
Console("Unresolved Active Edge: %s, Prim: %s" % (edge, edge.edgePrims))
group = geo.createPointGroup("%s_%d" % (unresolved_edges_name, edge_group_counter))
edge_group_counter += 1
for prim in edge.edgePrims:
prim = geo.iterPrims()[prim]
prim.setAttribValue(resolved_prim_attrib, 1)
inUnresolvedGroup.add(prim)
for point in edge.points:
point.setAttribValue(resolved_attrib, 1)
group.add(point)
# updates the edges
def UpdateActiveEdges(inEdges):
"""
Verifies if the incoming edges are already being used
If used, remove from the active edge list, as the poly is most likely closed
Also increment number of connections
"""
for edge in inEdges:
if edge in active_edges: #if in active_edges, remove
Console("edge was in active edges: %s" % str(edge))
active_edges.remove(edge)
continue
if edge in unresolved_edges: #if in unresolved edges, remove
Console("edge was in unresolved edges: %s" % str(edge))
unresolved_edges.remove(edge)
continue
active_edges.append(edge) #otherwise increment connections and allow for sampling
Console("Edge NOT in active edges: %s" % str(edge))
for point in edge.points:
point.numberOfConnections += 1
# update point associations
def UpdateAssociations(inPoints):
"""
increments the 3 vertices of the just created triangle
"""
for point in inPoints:
point.numberOfAssociations += 1
# orient polygons
def OrientPolygons(inPoints, inPoly):
"""
compares the point normals agains prim normal
"""
prim_point_normals = []
for point in inPoints:
vector = hou.Vector3(point.attribValue(orient_attrib))
vector.normalized()
prim_point_normals.append(vector)
# combine normals
accum = hou.Vector3(0,0,0)
for n in prim_point_normals:
for i in range(3):
accum[i]+=n[i]
# get the average
count = len(prim_point_normals)
for i in xrange(len(accum)):
accum[i] /= count
# get the prim normal
prim_normal = hou.Vector3(inPoly.normal())
inv_prim_normal = -1*prim_normal
# compare
if accum.angleTo(inv_prim_normal) < accum.angleTo(prim_normal):
export_group.add(inPoly)
# sort polygon verts
def SortVertices(inPoints):
"""
Sorts the incoming points clockwise
"""
order_array = [None,None,None] # start off with an empty array
xmin = sys.float_info.max
zmin = sys.float_info.max
for point in inPoints: # find first point
posx = point.position()[0]
if posx < xmin:
xmin = posx
order_array[0] = point
inPoints.remove(order_array[0])
for point in inPoints: # find second point
posz = point.position()[2]
if posz < zmin:
zmin = posz
order_array[1] = point
inPoints.remove(order_array[1]) # append third point
order_array[2] = inPoints[0]
return order_array
# Create a polygon out of a previous edge and new point
def CreatePolygon(inEdge, inPoint):
"""
Create A poly out of an edge and a point
TODO: Add a edge normal to every edge created, + point if necess ary
"""
poly = geo.createPolygon()
poly.setIsClosed(True)
if inEdge.pointOne.isEdgePoint and inEdge.pointTwo.isEdgePoint:
order_array = [inEdge.pointOne, inEdge.pointTwo, inPoint]
else:
order_array = SortVertices([inEdge.pointOne, inEdge.pointTwo, inPoint])
poly.addVertex(order_array[0])
poly.addVertex(order_array[1])
poly.addVertex(order_array[2])
poly.addVertex(order_array[0])
# add the poly as a TriPoly to our list of polygons
bound_polygons.append(TriPoly(order_array))
# check poly orientations
if orient_polygons:
OrientPolygons(order_array, poly)
# create the new edges
edge_list = [Edge(inEdge.pointOne, inPoint, inEdge.pointTwo), Edge(inEdge.pointTwo, inPoint, inEdge.pointOne), inEdge]
# The edge now needs it's associated polygon
for edge in edge_list:
edge.addPrim(poly.number())
# see if the edge already exists, if so, no active edges should be added
UpdateActiveEdges(edge_list)
# increase number of references to point
UpdateAssociations([inEdge.pointOne, inEdge.pointTwo, inPoint])
def FilterSharedNeighbour(inDictOne, inDictTwo, inEdge):
"""
iterates through the points in the dictionary, searching for the best shared neighbour.
sampling has two options: `minimum` or `closest`. Minimum = abs(x-y), closest = x+y
returns None if no suitable point is found
TODO: Add pointcheck that point is in between other points
"""
closest_point = None
current_dist = sys.float_info.max
conn_dict = {0: GetNumberOfConnections, 1: GetNumberOfAssociations} # holds the two connection comparison methods
double_dict = {0: PolyAssociatedPresence, 1: PolyPresence}
for key_point in inDictOne:
if inDictTwo.has_key(key_point):
sample_point = GetPoint(key_point) # returns the associated point
connections = conn_dict[conn_constrain](sample_point)
if sample_point.isEdgePoint or connections >= max_connections: # compares the neighbour based on edge, connectivity
Console("invalid point: %s" % key_point)
continue
if double_dict[double_constrain](inEdge, sample_point): # compares the neighbour based on poly existance
Console("creates similar poly: %s" % key_point)
continue
p2_dist = inDictTwo[key_point][0] # get the point two distance
p1_dist = inDictOne[key_point][0] # get the point one distance
if point_prior == "minimal": # choose sampling method
dist = abs(p1_dist - p2_dist)
else:
dist = p1_dist + p2_dist
Console("shared point: %s, distance: %s" % (key_point, dist))
if dist < current_dist:
closest_point = sample_point
current_dist = dist
return closest_point
def GetEdgeNeighbour(inEdge):
"""
Finds the most suitable edge point to create a triangle with
In case multiple points are found, the one with the lowest distributed distance
is chosen.
"""
# sample the values
nlist_one = {}
nlist_two = {}
point_one = inEdge.pointOne
point_two = inEdge.pointTwo
Console("sampling edge: %s" % str(inEdge))
# we want to construct a dict with: `pointnum:[dist, angle]`
counter = 0
for attribute in neigh_attributes:
# first get the point numbers as keys
pnum_one = point_one.attribValue(attribute)
pnum_two = point_two.attribValue(attribute)
# sample the distance and angle for every neighbour point
pdist_one = point_one.attribValue(dist_attributes[counter])
pdist_two = point_two.attribValue(dist_attributes[counter])
pangle_one = point_one.attribValue(angle_attributes[counter])
pangle_two = point_two.attribValue(angle_attributes[counter])
nlist_one[pnum_one] = [pdist_one, pangle_one]
nlist_two[pnum_two] = [pdist_two, pangle_two]
counter += 1
# now we have the point numbers + distance / angle, do the comparison
closest_point = FilterSharedNeighbour(nlist_one, nlist_two, inEdge)
if closest_point is not None:
Console("closest shared point: %s" % closest_point.number())
return closest_point
"""
Main------------------------------------------------------------------------------------------
"""
# Initialize the unresolved poly group
unresolved_group = InitializeGroup(unresolved_group_name)
# If an orientation is specified, we want sample the export group
if orient_polygons:
export_group = InitializeGroup(orient_group_name)
orient_attrib = InitializeOrientAttrib(orient_name)
# Initialize the points with the necessary attributes
AddPointAttributes(points)
# Initialize the neighbour attributes used for sampling best point to angle fit
neigh_attributes = InitializeNeighbourAttributes(geo, neigh_name)
dist_attributes = InitializeNeighbourAttributes(geo, distance_name)
angle_attributes = InitializeNeighbourAttributes(geo, angle_name)
# Now create a start list of active edges
if not predefined_edges:
CreateSeedEdges(GetSeedPoints(edge_group_name))
else:
CreatePredefinedSeedEdges(edge_group_name)
# Iterate over the active edges and connect
count = 0
while len(active_edges) > 0 and count < max_iter:
current_edge = active_edges[0]
closest_point = GetEdgeNeighbour(current_edge)
if closest_point != None:
CreatePolygon(current_edge, closest_point)
else:
unresolved_edges.append(current_edge)
active_edges.remove(current_edge)
count += 1
Console("\n")
Finalize(unresolved_group)