Knife II

After losing some work due to HDD problems I ran into a lot of issues with the previously posted Knife SOP
So I tried again using the normal Knife and got the same issues as it does not work iteratively and then I bugfixed (or rewrote largely) the previous knife I made, hopefully more functional this time. It retains face order, it does now calculate point attributes for new points and it transfters custom prim attributes (the split prims duplicate the attribute vaues). Consider this a snippet dump…

node = hou.pwd()
geo = node.geometry()


#parse parameters
target = node.evalParm("target")
origin = hou.Vector3( node.evalParm("originx"), node.evalParm("originy"), node.evalParm("originz") )
distance = node.evalParm("dist")
direction = hou.Vector3( node.evalParm("dirx"), node.evalParm("diry"), node.evalParm("dirz") ).normalized()
#distance really just moves the origin
origin += direction*distance


def rayPlaneIntersect(rayorigin, in_raydirection, planeorigin, in_planenormal):  
    ''''' 
    @returns: Vector3, intersectionPoint-rayOrigin 
    '''  
    raydirection = in_raydirection.normalized()  
    planenormal = in_planenormal.normalized()  
    distanceToPlane = (rayorigin-planeorigin).dot(planenormal)  
    triangleHeight = raydirection.dot(-planenormal)  
    if not distanceToPlane: #ray origin lies in the plane
        return rayorigin-planeorigin  
    if not triangleHeight: #ray is parallel to plane
        return None
    return raydirection * distanceToPlane * (1.0/triangleHeight)


def getPolygonsWithEdge(geom, edgeids):
    '''''
    @param geo: hou.Geometry, geometry to search
    @param edgeids: tuple of 2 ints, point numbers
    describing the edge to find shared faces for
    
    @returns: list of hou.Prim, all primitives sharing this edge
    
    if the points are not connected by an edge
    (adjacent in the vertex list of any primitive)
    the result is an empty list
    '''
    out = []
    for poly in geom.prims():  
        verts = poly.vertices()
        for i in range(poly.numVertices()):
            if verts[i].point().number() in edgeids and\
               verts[(i+1)%poly.numVertices()].point().number() in edgeids:
                out.append(poly)
    return out


#stub primitive, I use this for the cut faces so I can actually add the polygons in the end to not disturb primitive order / numbers
class notPrim():
    def __init__(self):
        self.points = []

    def addVertex( self, inPoint ):
        self.points.append( inPoint )

    def addToGeo( self, inGeo ):
        poly = inGeo.createPolygon()
        for point in self.points:
            poly.addVertex(point)
        return poly


### Cut target ###  
verts = geo.iterPrims()[target].vertices()  
nverts = len(verts)  
cutEdges = []
adjFaces = []
#foreach edge
for i in range(nverts):  
    pt0 = verts[i].point()
    pt1 = verts[(i+1)%nverts].point()
    edgedirection = pt1.position()-pt0.position()
    #find interseciton on edge
    intersectpt = rayPlaneIntersect(pt0.position(), edgedirection, origin, direction)  
    if not intersectpt: #edge is parallel to cutting plane  
        continue
    #check if intersection is on the edge (line-segment)
    param = intersectpt.dot(edgedirection.normalized())
    if param > 0 and param < edgedirection.length():  
        #store the cut
        pt = geo.createPoint()

        #propagate point attribs
        for attrib in geo.pointAttribs():
            val0 = pt0.attribValue(attrib)
            val1 = pt1.attribValue(attrib)
            if type(val0) in (int, float):
                pt.setAttribValue( attrib, val0*(1-param)+val1*param )
            if type(val0) == tuple:
                val = []
                for i in range(len(val0)):
                    if type(val0[i]) in (int, float):
                        val.append( val0[i]*(1-param)+val1[i]*param )
                pt.setAttribValue( attrib, val )

        pt.setPosition( intersectpt+pt0.position() )  
        cutEdges.append( [pt0, pt1, pt] )

        #store the face(s) influenced by this cut
        adjFaces.extend( getPolygonsWithEdge( geo, (pt0.number(), pt1.number()) ) )


### Rebuild geometry ###
delete = []
polys = []
#rebuild all prims
for i in range(len(geo.iterPrims())):
    prim = geo.iterPrims()[i]
    delete.append(prim) #remove all old prims

    poly = geo.createPolygon() #create new prim

    #duplicate prim attribs
    for attrib in geo.primAttribs():
        val = prim.attribValue(attrib)
        poly.setAttribValue( attrib, val )

    #build cut faces
    if i == target:
        ### Create cut face ###
        #iterate over edges to build new polygons  
        cuts = 0
        wrap = False
        polys.append( poly ) #the first split primitive keeps the original primitive nr
        for j in range(prim.numVertices()):
            cut = None
            vtx = prim.vertices()[j]
            #all cuts added, finish the first polygon
            if wrap:
                polys[0].addVertex(vtx.point())  
                continue

            #find edge points
            nxtvtx = prim.vertices()[(j+1)%prim.numVertices()]
            polys[-1].addVertex(vtx.point())

            for edge in cutEdges:
                if vtx.point() in edge and nxtvtx.point() in edge:
                    cut=edge

            #if edge is a cut edge
            if cut:
                polys[-1].addVertex(cut[2])  
                cuts += 1
                if cuts == len(cutEdges): #wrap to first polygon at last cut  
                    wrap = True  
                    polys[0].addVertex(cut[2])
                    continue
                polys.append(notPrim()) #add stub primitive and start building vertex list for it
                polys[-1].addVertex(cut[2])
    else: #or just build the primitive again
        for j in range(prim.numVertices()):
            vtx = prim.vertices()[j]
            poly.addVertex(vtx.point())
            if prim in adjFaces: #if influenced by cut, then cut the right edge
                for edge in cutEdges:
                    #when at start of the split edge, known because next vertex is it's end point
                    if vtx.point() in edge:
                        nxtvtx = prim.vertices()[(j+1)%prim.numVertices()]
                        if nxtvtx.point() in edge: 
                            #add the intersection point
                            poly.addVertex(edge[2])
                            break


### append stub split faces at the very end###
for i in range(1,len(polys),1):
    poly = polys[i].addToGeo( geo )
    src = geo.iterPrims()[target]
    #duplicate prim attribs
    for attrib in geo.primAttribs():
        val = src.attribValue(attrib)
        poly.setAttribValue( attrib, val )

#remove all old prims
geo.deletePrims(delete, True)

Houdini clouds tool demo

Explanation on how to start making clouds here.

cloud_tool_1
The start of the tool allows to setup a number of copies, that are copied along the X axis, and to set up a min and max size. The size determines the endpoint and midpoint scale of the copied sphere and the radius in which they scatter. This results in always having a nice curvy cloud. The Show result button is for previewing the settings in a faster way, this button appears in several spots of the tool to be able to look at different stages of the cloud isolating certain calculations and increasing editing speed.

cloud_tool_2
Next there’s the stacking tabs. This primary stack scatters clouds onto the previously determined shape (which is first converted to metaballs to avoid invisible inside spheres). The radius varies randomly between the given points, the amount per area increases the number of spheres that are scattered in this specific stack. The merge node on the right of the network is solely for the preview to also show the input of the scatter.

cloud_tool_3
Next all stacks are applied, with ever decreasing radii and ever increasing amounts the cloud takes its final shape. Then the post-scale comes into play. The foreach loop at the end of the network scales every sphere individually from its own center point to the post-sphere scale.
Note that previewing nodes are painted out of the network to avoid confusion.

Because all scattered spheres are put with their center on the surface of the input geometry, they will never float loose and creases in the cloud volume are not very deep. By scaling afterwards, less densely scattered areas will have bigger gaps and this will create a more puffy cloud with more pieces hanging loose around the edges.

cloud_tool_4
Converted to a volume with a billow smoke shader results in this final picture.

Houdini Clouds

I was working on creating a day-night cycle using Unity and figured it would be best to generate volumetric cloud data rather than attempting to approximate cloud shading from a sky texture. Hence I googled and searched the Houdini site to find:

A cloud example, leading to this customer story.
Reading through that and finally finding this howto to have some steps to follow quickly led to a renderable set of clouds

Then I attempted to duplicate the Rio system (customer story), which I also slightly had in mind, but seeing their pictures gave me the great tip to not just scatter spheres on spheres, but to offset the secondary and tertiary spheres in Y, so that the clouds actually stack upwards for a much more cloud-like look. Even though this probably isn’t how clouds look from up close, this is what they appear to me from the ground (slightly stylized) which is all I require.

The final idea is to generate game-ready cubemaps with object space normal maps of clouds (with volumetric transparency) as well as a depth map, so when the sun is directly behind a cloud and the normal map is of no influence the cloud still looks properly (1-depth map = translucency). I imagine this in a shader as my current concept to create a good looking day-night cycle.

First step is to produce a decent looking cloud:
(Houdini Mantra render on the left, Composite on a blue background on the right)
cloud

I created a sphere to scatter metaballs on, merged it with a metaball the same size as the sphere and converted that to poly’s to base my volume on. Not cloudy at all but at least a wooly base look, then I followed the steps of
http://www.sidefx.com/docs/houdini9.5/howto/clouds
in Create clouds using Volumes. But I’ll go more into detail below.
cloud_step1

The IsoOffset has the output type to be set to SDF Volume and it requires Invert Sign to be set to true, this is a tickbox in the Construction tab! I started playing with the offset to see something, but coming to the volume mix this appeared unnecessary, so just leave it at zero.

After copying the formula given at the houdini howto I did increase the Uniform sampling to 30.
cloud_step2

Now for the rendering I just added a top-down distant light (from the Lights and Cameras shelf) and created a default camera to frame my cloud, paying attention to the volume box not the volume. DO use ray trace shadows, ignore the remark about depth maps as to me it decreases quality and speed with default rendering.

Drag on the Billowy Smoke shader, then in the out I add a default mantra node and in the render view I set the render node / camera and hit Render.
cloud_step3

Now for the cloud look all we have to do is adjust the geometry, for the ambient color we can just adjust the billowy smoke shader’s shadow density to something like 1.3. The smoke density can be adjusted to have more or less chunks separated on the edges, when increasing samples the smoke density should also increase to make the samples in fact count and be visible, it can also create a tighter/more cartoony look, but you can come back to this at any time.

So looking at the Rio image we can see a distinction between green (base spheres) and red (small secondary spheres). So I decided to scatter 4 or 5 points on a unit sphere, and copy more smaller spheres onto that. Editing the first sphere to have radii (1, 0.6, 1.2) to get a slightly flatter result.

Now instead of scattering points onto the sphere, I first create a combined mesh by copying metaballs using the spheres as template points (meaning that the metaballs come in the same volume as the input, to achieve this the metaball weight must be set to a high value; I use 100).

Then I can scatter onto that so that there are no spheres on the inside. Metaballs need to be converted to polygons, settings the level of detail U, V to 1, 1. Then again scattering 4 pts per area and copying smaller spheres on top, merging with the initial spheres, gives me a better cloud already.
cloud_step4

But it is still much too generic and blobby. After repeating the above process for even smaller spheres didn’t work I decided to randomize my secondary sphere radius, copy stamping the template points. You can see the difference between the previous (left) and this step (right) that the cloud has larger creases now.
Image5

This randomness is what I wish for, so the next step is to add a third iteration of spheres. First I convert the previous step to a subnet, so everything between the first copy node and the merge at the end becomes a subnet, then I append a copy of that subnet to itself for tertiary sphere. Inside the copy the sphere radius is scaled down (previously I randomized tpt*0.2+0.2 and now I randomize tpt*0.1+0.1). The problem is however, that the metaball copy in the second subnet does not match the sphere input, because the spheres have a varying radius. The trick is to add an attribute, pointradius, to the point level of the spheres. I do this already outside of the subnet at the first sphere so that I can keep both subnets the same, then the metaball in the subnet will get the point(input,tpt,pointradius,0) as radius and the copy node output matches the sphere input again.
cloud_step6

Then with the two subnets converted to a volume again, setting the second sphere radius to a smaller number and the scatter per area amount to a higher number creates this result (I did edit the shader to have a volume density of 30 and a shadow density of 1.3 now):
cloud_step7

Increasing the isooffset samples to 100, the volume mix formula to clamp($V*4, 0, 1) – just editing the multiplier to something much lower – and the billow smoke smoke density to 30 gives me somewhat what I want.
cloud_step8

As a final note it is also possible to set the isooffset sampling to non square, as for some clouds I got strange stretching in the noise, this can be solved by manually calculating or inputting a more suitable amount of samples, and/or oversampling the largest axis compared to the other axes.
cloud_step9
On the left the previous result with diagonal lines disturbing the look, on the right the non-square sample setup with this problem solved.

Knife in pyhon

I’m rather inexperienced with Python in houdini, so I didn’t bother with external files and a clean workflow for this one.

The knife tool bothered my because when cutting a single face it would separate the face, not cutting shared edges on other faces. Hence I wrote this to find faces sharing the cut edge and rebuild those faces too.

A bit of novice info first
Use File -> New Operator Type, set Style to Python and Network Type to Geometry, save it into an OTL and create the node. Then right click the node and at the bottom select Type properties. There add the following parameters under the Parameters tab:
int, Face nr (id)
float3, Origin (origin)
float, Distance (dist)
float3, Direction (dir)
Then go to the Code tab to start writing code.

Not knowing how to make a param that accepts a group or a pattern it currently only can cut one face, determined by the ‘Face nr’ parameter.

Writing the code
First I’ll need to know the parameter contents, so the basic setup of the node looks like this:

node = hou.pwd()
geo = node.geometry()
### Parse parameters ###
target = node.evalParm("id")
origin = hou.Vector3( node.evalParm("originx"), node.evalParm("originy"), node.evalParm("originz") )
dist = node.evalParm("dist")
dir = hou.Vector3( node.evalParm("dirx"), node.evalParm("diry"), node.evalParm("dirz") )

Then, matching the original Knife operator, I added a distance parameter, normally this is senseless because the distance simply moves the cutting plane’s origin into the direction vector, by that distance. Safely done by normalizing first, like this:

dir = dir.normalized()
origin += dir*dist

Ray – plane intersection

The next thing is to cut the target face where it intersects with the given plane. For this I raycast every edge of the face through the plane, any intersections found that still lie on the edge should become an inbetween point, splitting the edge in two. So first the raycasting part:

def rayPlaneIntersect(rayorigin, in_raydirection, planeorigin, in_planenormal):
    '''
    @returns: Vector3, intersectionPoint-rayOrigin
    '''
    raydirection = in_raydirection.normalized()
    planenormal = in_planenormal.normalized()
    distanceToPlane = (rayorigin-planeorigin).dot(planenormal)
    triangleHeight = raydirection.dot(-planenormal)
    if not distanceToPlane:
        return rayorigin-planeorigin
    if not triangleHeight:
        return None #ray is parallel to plane
    return raydirection * distanceToPlane * (1.0/triangleHeight)

It essentially dots the ray origin to the plane normal to find out the distance to the plane, then the point on the plane and the ray origin form one line with a known length, trigonometry will determine the third point for we know its direction (the ray direction) and only need to determine the length. More on this here.

Cutting the face
To cut the face I will extract the primitive with the given id (stored in target), then I’ll iterate over it’s vertices to define the edges and check for every edge if it should be split, if so I append the new point to both the polygon and the edge for later usage.

By dotting the intersection point vector with the ray (edge) direction I know the parameter, which should remain between 0 and the edgelength to still be on the edge.

### Cut target ###
verts = geo.iterPrims()[target].vertices()
nverts = len(verts)
edges = []
for i in range(nverts):
    edges.append( [verts[i].point(), verts[(i+1)%nverts].point()] )
    edgedirection = edges[-1][1].position()-edges[-1][0].position()
    intersectpt = rayPlaneIntersect(edges[-1][0].position(), edgedirection, origin, dir)
    if intersectpt == edges[-1][0].position()-origin:
        continue
    if not intersectpt: #edge is parallel to cutting plane
        continue
    param = intersectpt.dot(edgedirection.normalized())
    if param > 0 and param < edgedirection.length():
        pt = geo.createPoint()
        pt.setPosition( intersectpt+edges[-1][0].position() )
        edges[-1].append( pt )

Next is to create the split geometry, after that we'll have a replica of the knife tool working on just one face. For this I start adding vertices to the first polygon until a cut edge is reached, then I add a new polygon and start adding vertices to that. Beforehand I work back to the last cut so I don't put half of the first polygon (the points between the last cut and point 0) in another polygon.

### Create output polygon(s) ###
polys = [geo.createPolygon()]
#find last cut point for wrapping the first polygon
lastcut = None
for i in range(len(edges)-1,-1,-1):
    if len(edges[i]) > 2:
        lastcut = i
        break;

#iterate over edges to build new polygons
wrap = False
for i in range(len(edges)):
    if wrap:
        polys[0].addVertex(edges[i][0])
        continue
    polys[-1].addVertex(edges[i][0])
    if len(edges[i]) > 2:
        polys[-1].addVertex(edges[i][2])
        if i == lastcut: #wrap to first polygon at last cut
            wrap = True
            polys[0].addVertex(edges[i][2])
            continue
        polys.append(geo.createPolygon())
        polys[-1].addVertex(edges[i][2])

Getting polygons from an edge
Given two point numbers and a geometry object we iterate over all primitives' vertices, if any adjacent vertices (thus an edge) matches the given ids it shares this edge.

Error warning
Also I ran into an interesting issue, where storing len(verts) before the for loop to speed it up (not calling it during the module but using the variable instead) it didn't contain a valid number, resulting in i exceeding len(verts).

def getPolygonsWithEdge(geom, edgeids):
    '''
    @param geo: hou.Geometry, geometry to search
    @param edgeids: tuple of 2 ints, point numbers
    describing the edge to find shared faces for

    @returns: list of hou.Prim, all primitives sharing this edge

    if the points are not connected by an edge
    (adjacent in the vertex list of any primitive)
    the result is an empty list
    '''
    out = []
    for poly in geom.prims():
        verts = poly.vertices()
        for i in range(len(verts)):
            if verts[i].point().number() in edgeids and\
               verts[(i+1)%len(verts)].point().number() in edgeids:
                out.append(poly)
    return out

The last bit of code
Now to make this tool actually renewing I need to find any polgons sharing the renewed edges and rebuild them to include the new points as well. I simply go over the points and if I encounter the cut edge I insert the additional point before continuing. Here I also track the old primitives to delete at the end.

### Find polygons sharing the cut edges ###
deleteprims = [geo.iterPrims()[target]]

for i in range(len(edges)):
    if len(edges[i]) > 2:
        #rebuild polygons that share the cut edge
        edgeids = (edges[i][0].number(), edges[i][1].number())
        sharedprims = getPolygonsWithEdge(geo, edgeids)
        for prim in sharedprims:
            deleteprims.append(prim)
            split = False
            if prim.number() != target: #ignore the polygon we are cutting entirely
                poly = geo.createPolygon()
                for vert in prim.vertices():
                    poly.addVertex(vert.point())
                    if vert.point().number() in edgeids and split == False:
                        split = True
                        poly.addVertex(edges[i][2])

geo.deletePrims(deleteprims,True)

Here's the full code again, two issues remain, being the changing of prim numbers (and groups containing those prims will lose them, even if they're the adjacent faces) and concave faces get overlapping primitives that should actually be joined (looking at the knife tool).

If you draw a closed polygonal curve with these coordinates and cut it with both the knife and this tool the issue will become clear:
-2,1,0 -1,1,0 -0.5,-0.25,0 0.5,-0.25,0 1,1,0 2,1,0 2,-1,0 0,-2,0 -2,-1,0

'''
@todo: fix concave face errors
@todo: insert prim with right number (important on adjacent faces, optional on new cut faces); also maintain groups!
'''
node = hou.pwd()
geo = node.geometry()


def rayPlaneIntersect(rayorigin, in_raydirection, planeorigin, in_planenormal):
    '''
    @returns: Vector3, intersectionPoint-rayOrigin
    '''
    raydirection = in_raydirection.normalized()
    planenormal = in_planenormal.normalized()
    distanceToPlane = (rayorigin-planeorigin).dot(planenormal)
    triangleHeight = raydirection.dot(-planenormal)
    if not distanceToPlane:
        return rayorigin-planeorigin
    if not triangleHeight:
        return None #ray is parallel to plane
    return raydirection * distanceToPlane * (1.0/triangleHeight)


def getPolygonsWithEdge(geom, edgeids):
    '''
    @param geo: hou.Geometry, geometry to search
    @param edgeids: tuple of 2 ints, point numbers
    describing the edge to find shared faces for

    @returns: list of hou.Prim, all primitives sharing this edge

    if the points are not connected by an edge
    (adjacent in the vertex list of any primitive)
    the result is an empty list
    '''
    out = []
    for poly in geom.prims():
        verts = poly.vertices()
        for i in range(len(verts)):
            if verts[i].point().number() in edgeids and\
               verts[(i+1)%len(verts)].point().number() in edgeids:
                out.append(poly)
    return out


### Parse parameters ###
target = node.evalParm("id")
origin = hou.Vector3( node.evalParm("originx"), node.evalParm("originy"), node.evalParm("originz") )
dist = node.evalParm("dist")
dir = hou.Vector3( node.evalParm("dirx"), node.evalParm("diry"), node.evalParm("dirz") )
dir = dir.normalized()
origin += dir*dist

### Cut target ###
verts = geo.iterPrims()[target].vertices()
nverts = len(verts)
edges = []
for i in range(nverts):
    edges.append( [verts[i].point(), verts[(i+1)%nverts].point()] )
    edgedirection = edges[-1][1].position()-edges[-1][0].position()
    intersectpt = rayPlaneIntersect(edges[-1][0].position(), edgedirection, origin, dir)
    if intersectpt == edges[-1][0].position()-origin:
        continue
    if not intersectpt: #edge is parallel to cutting plane
        continue
    param = intersectpt.dot(edgedirection.normalized())
    if param > 0 and param < edgedirection.length():
        pt = geo.createPoint()
        pt.setPosition( intersectpt+edges[-1][0].position() )
        edges[-1].append( pt )

### Create output polygon(s) ###
polys = [geo.createPolygon()]
#find last cut point for wrapping the first polygon
lastcut = None
for i in range(len(edges)-1,-1,-1):
    if len(edges[i]) > 2:
        lastcut = i
        break;

#iterate over edges to build new polygons
wrap = False
for i in range(len(edges)):
    if wrap:
        polys[0].addVertex(edges[i][0])
        continue
    polys[-1].addVertex(edges[i][0])
    if len(edges[i]) > 2:
        polys[-1].addVertex(edges[i][2])
        if i == lastcut: #wrap to first polygon at last cut
            wrap = True
            polys[0].addVertex(edges[i][2])
            continue
        polys.append(geo.createPolygon())
        polys[-1].addVertex(edges[i][2])

### Find polygons sharing the cut edges ###
deleteprims = [geo.iterPrims()[target]]

for i in range(len(edges)):
    if len(edges[i]) > 2:
        #rebuild polygons that share the cut edge
        edgeids = (edges[i][0].number(), edges[i][1].number())
        sharedprims = getPolygonsWithEdge(geo, edgeids)
        for prim in sharedprims:
            deleteprims.append(prim)
            split = False
            if prim.number() != target: #ignore the polygon we are cutting entirely
                poly = geo.createPolygon()
                for vert in prim.vertices():
                    poly.addVertex(vert.point())
                    if vert.point().number() in edgeids and split == False:
                        split = True
                        poly.addVertex(edges[i][2])

geo.deletePrims(deleteprims,True)