Category Archives: Masters Project

What is my project?

I am working to develop a procedural weathering tool for Houdini.  I am accomplishing this via a two sided system.  Firstly, I am developing a system that utilizes a variety of new occlusion mapping types.  These are calculated in a similar way to ambient occlusion, but limit the raycast directions to approximate a variety of atmospheric affectors.  By blending these values in a variety of ways, I will be able to make an approximate simulation of real world environments.

For instance, by making a mapper that checks the occlusion of the solar ecliptic and blending it with the ambient occlusion map, I can approximate the light intensity on an object for a given day.  This method takes into account the Mie Scattering that occurs on a cloudy day as well as the direct sunlight.  By varying this blending with a float ramp I am able to simulate this process over a long term to get an approximate total light intensity on the mesh over the period.  This will allow an artist to accurately fade a texture based on how much light has struck the surface over the course of years.

The second prong of this project is an attribute propagation technique that utilizes the information from the occlusion mapping to determine which areas are most subjected to a particular environmental affector.  This will allow  the spread of a surface aspect such as rust, flaking of paint, and marring over the object.  Another use of this tool will be to spread precursor attributes over an area for placing and modifying procedural geometry in a more organic way.

Saving the Occlusion Map: Part 2

OcclusionExample1occlusion1MPoints

I was working until 3 AM this morning on this and I got it working! I now have a node that is designed to take any 3d vector or scalar from points and save them to a UV coordinate map. They being saved as an Alpha PNG. with enough points it becomes nearly solid. The next step is to make a block of code that will interpolate the data points to form a solid map from the sample points. I have found some ways using the Python library SciPy, but I have to discuss it with the school to get it installed on the computers here. I will be doing that this week. Once I use these libraries, they will be required for distributing this as a tool on Orbolt, but I will look into it. Anyway, here is the code:

#overall imports
import numpy as np

#-----------------------------------------------------------------------------------
#Credit for write_png goes to ideasman42 on StackOverflow.com
#Credit for saveAsPNG goes to Evgeni Sergeev in the same thread on StackOverflow.com
#Found here: http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
def write_png(buf, width, height):
""" buf: must be bytes or a bytearray in py3, a regular string in py2. formatted RGBARGBA... """
    import zlib, struct

    # reverse the vertical line order and add null bytes at the start
    width_byte_4 = width * 4
    raw_data = b''.join(b'\x00' + buf[span:span + width_byte_4]
    for span in range((height - 1) * width * 4, -1, - width_byte_4))

    def png_pack(png_tag, data):
    chunk_head = png_tag + data
    return (struct.pack("!I", len(data)) +
    chunk_head +
    struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)))

    return b''.join([
    b'\x89PNG\r\n\x1a\n',
    png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
    png_pack(b'IDAT', zlib.compress(raw_data, 9)),
    png_pack(b'IEND', b'')])

def saveAsPNG(array, f):
    import struct
    if any([len(row) != len(array[0]) for row in array]):
    raise ValueError, "Array should have elements of equal size"
    
    #First row becomes top row of image.
    flat = []; map(flat.extend, reversed(array))
    #Big-endian, unsigned 32-byte integer.
    
    buf = b''.join([struct.pack('>I', ((0xffFFff & int(i32,16))<<8)|int(i32,16)>>24) for i32 in flat]) #Rotate from ARGB to RGBA.
    
    data = write_png(buf, len(array[0]), len(array))
    #f = open(filename, 'wb')
    f.write(data)
    f.close()
#-----------------------------------------------------------------------------------

# Define the save directory. Future plans to make it check for diretory existence and create if it does not exist.
# The images are saved into a folder that matches the name of the attribute, that way they can be kept organized by
# user.

def defineSave():
    hipName = hou.hipFile.basename()
    attribName = hou.ch("attrib")
    workingDir = hou.hipFile.path()
    workingDir = workingDir[:len(workingDir)-len(hipName)]
    workingFileName = hou.ch("fileName") + ".png"
    workingFullPath = workingDir + attribName+ "/" + workingFileName
    f = open(workingFullPath, 'wb')
    return f

# Goes over the 2d int array that makes up the image and converts it into hexadecimal for so it can be processed by
# write_png.

def processImage(image, width, height):
    fmt = "0x{alpha}{r}{g}{b}"
    processedImage = np.ndarray((height, width)).astype(str)
    for i in range(len(image)):
        for j in range(len(image[i])):
            p = image[i,j]
            if type(p) == np.ndarray:
                #Process vector
                if np.all([p, [-1,-1,-1]]):
                    v = np.array([0,0,0], dtype=np.int16)
                    a = 0
                else:
                    v = p
                    a = 255
                hexA = hexValue(a)
                hexV = hexValue(v)
                hexString = fmt.format(alpha=hexA, r=hexV[0], g=hexV[1], b=hexV[2])
            else:
                #Process scalar
                if p == -1:
                    v = 0
                    a = 0
                else:
                    v = p
                    a = 255
                hexA = hexValue(a)
                hexV = hexValue(v)
                hexString = fmt.format(alpha=hexA, r=hexV, g=hexV, b=hexV)
            processedImage[i,j] = hexString
    return processedImage

# Rescale the values to an 8 bit int or vector of 8 bit ints. This is needed for creating the PNGs as
# PNGs are in 256 color with alpha.

def rescaleValue(data, min=0, max=0):
    if type(data) == float:
        normalized = (data - min)/(max-min)
        scaledValue = int(256*normalized)
    elif type(data) == tuple:
        data = np.array(data)
        normalized = data/np.sqrt((data*data).sum())
        scaledValue = [int(256*normalized[0]),int(256*normalized[1]),int(256*normalized[2])]
    return scaledValue

# Reformats the incoming int vector or int as hexadecimal. Again, needed to make the image.

def hexValue(value):
    if type(value) == int or type(value) == np.int16:
        return hex(value).replace("0x", "").zfill(2)
    elif type(value) == np.ndarray:
        out = []
        for i in range(3):
            out.append(hex(value[i]).replace("0x", "").zfill(2))
        return out

# Calculates the pixel position of a given uv for a point that is passed to the object. Requires the data
# input is a dictionary that contains the key uv that is a vector of minimu lenth 2. It will only process
# the U and V direction. The W direction is ignored.

def imageBucket(data, height, width):
    widthBucket = int(data["uv"][0]*width)
    heightBucket = int((1-data["uv"][1])*height)
    return {"x":widthBucket, "y":heightBucket}

# The real workhorse of the script. This is where the point data is processed into a Numpy ndarray which
# 1 for 1 stores the values for the map. This code also takes into account if multiple points are sharing
# a pixel bucket. This is accomplished by averaging the current pixel value with the new. Ostensibly the
# values should be identical, but small variations in topology and normals could have points that are very
# near to each other but on faces that are facing apart from each other, like at a corner. These can give
# very different values.

def processPoints(points):
    width = hou.ch("width")
    height = hou.ch("height")
    minMax = {"min":hou.ch("min"), "max":hou.ch("max")}

    if type(points[0].attribValue(hou.ch("attrib"))) == float:
        output = np.ndarray((height, width))
        output.fill(-1)
    elif len(points[0].attribValue(hou.ch("attrib"))) == 3:
        output = np.ndarray((height, width, 3))
        output.fill(-1)

    for point in points:
        pointUV = {"uv":point.attribValue("uv")}
        pixelPos = imageBucket(pointUV, height, width)
        pointValue = output[pixelPos["y"], pixelPos["x"]]

        valued = True
        if (type(pointValue) == np.ndarray and np.all([pointValue, [-1,-1,-1]])) or (type(pointValue) == np.float64 and np.all([pointValue, -1])):
            valued = False

        if valued:
            pointValue += rescaleValue(point.attribValue(hou.ch("attrib")), minMax["min"], minMax["max"])
            pointValue /= 2
        else:
            pointValue = rescaleValue(point.attribValue(hou.ch("attrib")), minMax["min"], minMax["max"])

        output[pixelPos["y"], pixelPos["x"]] = pointValue

    pngFile = defineSave()
    hexImage = processImage(output.astype(np.int16), width, height)
    saveAsPNG(hexImage, pngFile)

Solar Occlusion

solarOcclusion1
Utilizing an Attribute Wrangle SOP, I have been able to calculate the occlusion of the surface. To the node, the first input is my sample object, the second is my occluding geometry. The white line in the image is the ecliptic that is being tested against. The sample surface is densely scattered with points, then each one of those points is sampled against all of the points on the ecliptic for blockage. This algorithm is a discretization of the following integration:
ambient occlusion integral

f@occlusion = 0;
for(int i = 0; i<npoints(1); i++) {
    vector hitPoint, hitUVW, hit;
    vector relP = point(1, "P", i) - v@P;
    int test = intersect(2, @P, relP, hitPoint, hitUVW);
    hit = hitPoint - @P;
    float len = length(hit)/length(relP);
    if(test == -1) {
        float testFacing  = dot(relP, @N);
        if(testFacing>0)@occlusion += testFacing ;
    }
}
@occlusion /= npoints(1)*3.1415 ;

Saving the occlusion map [WIP]

Currently, there is no way that I have been able to locate to save a map that represents data stored directly on points. To tackle this problem, I am developing a node that will output the point data as a mapped PNG. This is accomplished by making a Python digital asset that reads the data from the input and processes it into a format that can be written by native python code. I am currently debugging the code, as there is a hang up between the saveAsPNG() and write_png() functions that is preventing it from writing any pixels

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

#--------------------------------------------------------------------------------------------------------------
#Credit for write_png goes to ideasman42 on StackOverflow.com
#Credit for saveAsPNG goes to Evgeni Sergeev in the same thread on StackOverflow.com
#Found here: http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
def write_png(buf, width, height):
    """ buf: must be bytes or a bytearray in py3, a regular string in py2. formatted RGBARGBA... """
    import zlib, struct

    # reverse the vertical line order and add null bytes at the start
    width_byte_4 = width * 4
    raw_data = b''.join(b'\x00' + buf[span:span + width_byte_4]
                        for span in range((height - 1) * width * 4, -1, - width_byte_4))

    def png_pack(png_tag, data):
        chunk_head = png_tag + data
        return (struct.pack("!I", len(data)) +
                chunk_head +
                struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)))

    return b''.join([
        b'\x89PNG\r\n\x1a\n',
        png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
        png_pack(b'IDAT', zlib.compress(raw_data, 9)),
        png_pack(b'IEND', b'')])
        
def saveAsPNG(array, f):
    import struct
    if any([len(row) != len(array[0]) for row in array]):
        raise ValueError, "Array should have elements of equal size"

                                #First row becomes top row of image.
    flat = []; map(flat.extend, reversed(array))
                                 #Big-endian, unsigned 32-byte integer.
                
    buf = b''.join([struct.pack('>I', ((0xffFFff & int(i32,16))<<8)|(int(i32,16)>>24) )
                   for i32 in flat])   #Rotate from ARGB to RGBA.

    data = write_png(buf, len(array[0]), len(array))
    #f = open(filename, 'wb')
    f.write(data)
    f.close()
#--------------------------------------------------------------------------------------------------------------

def defineSave():
        hipName = hou.hipFile.basename()
        workingDir = hou.hipFile.path() 
        workingDir = workingDir[:len(workingDir)-len(hipName)]
        workingFileName = hou.ch("fileName") + ".png"
        workingFullPath = workingDir + "occlusions/" + workingFileName
        f = open(workingFullPath, 'wb')
        return f

def processImage(image, width, height):
        from itertools import repeat
        import math
        fmt = "0x{alpha}{r}{g}{b}"
        processedImage = list(repeat(list(repeat(0,width)),height));
        for i in range(width*height):
                if image[i] is -1:
                        a = 0
                        v = 0
                else:
                        a = 255
                        v = image[i]
                hexA = hex(a).replace("0x", "").zfill(2)
                hexV = hex(v).replace("0x", "").zfill(2)
                
                hexValue = fmt.format(alpha = hexA, r = hexV, g = hexV, b = hexV)
                processedImage[int(math.floor(i/width))][i%width] = hexValue
        return processedImage
                
def rescaleValue(data, min, max):
        normalized = (data["occlusion"] - min)/(max-min)
        scaledValue = int(256*normalized)
        return scaledValue

def imageBucket(data, height, width):
        heightBucket = int(data["uv"][0]*height)
        widthBucket  = int(data["uv"][1]*width)
        return {"x":widthBucket, "y":heightBucket}
                
def processPoints(points):
        from itertools import repeat
        
        minMax = {"minOcclusion":10000000, "maxOcclusion":0} 
        output = {}
        for point in points:
                output[point.number()] = {"uv":point.attribValue("uv"), "occlusion":point.attribValue("occlusion")}
                if point.attribValue("occlusion") > minMax["maxOcclusion"]:
                        minMax["maxOcclusion"] = point.attribValue("occlusion")
                if point.attribValue("occlusion") < minMax["minOcclusion"]:
                        minMax["minOcclusion"] = point.attribValue("occlusion")
        
        width = hou.ch("width")
        height = hou.ch("height")
        image = list(repeat(-1, height*width))
                
        for key in output.keys():
                pixelValue = rescaleValue(output[key], minMax["minOcclusion"], minMax["maxOcclusion"])
                pixelPos = imageBucket(output[key], height, width)
                
                if image[pixelPos["x"]*pixelPos["y"]] != -1:
                    pixelValue = int((pixelValue + image[pixelPos["x"]*pixelPos["y"]])/2)

                image[pixelPos["x"]*pixelPos["y"]] = pixelValue
                
        pngFile = defineSave()
        saveAsPNG(processImage(image, width, height), pngFile)