Sunday, November 10, 2013

Your own 3D Rotatable Cow!

I work at Tableau Software whose motto is “Tableau helps people see and understand their data.” But it can also be used artistically or even frivolously, such as the 3D rotatable cow I created (use the sliders or “<” and “>” icons to rotate the cow over the x, y and z axes):




How I created it:
  • I found a Wavefront OBJ file defining a wireframe of a cow from somewhere unremembered on the internet. Here’s a large collection of downloadable OBJ files. An OBJ file first lists the vertices of the object in 3D space, then a list of the each face of the object.
  • In Tableau, each edge of the object could be described as the endpoints of of a line, so Tableau needed as its source data an x, y, and z coordinate, and an edge name.
  • I wrote this Python script to convert an OBJ file into a CSV file that Tableau would be able to load. The script is at the end of this post.
  • From this Wikipedia page about the 3D Rotation Group I found the formulae to rotate an object around the x, y and z axis. In a Tableau calculated field, Here is how the x coordinate is changed by rotation on the y axis:
    [X:RotateZ] * cos( ([ThetaY] / 360) * 2 * PI() ) - [Z] * sin( ([ThetaY] / 360) * 2 * PI() )
  • If you download the workbook, it includes a rotatable dodecahedron on another tab. That one you can animate the rotation as there's a theta on the page shelf (as opposed to be a parameter). For the theta, for each row in the CSV file, I repeated the coordinate data with a different theta value at 5° intervals.
  • The previous mentioned Python script to convert an OBJ file to a CSV file suitable for Tableau: 
import sys

def print_line(vertex, edge):
    sys.stdout.write(('%s, %s, %s, "%d"\n') % (vertex[0], vertex[1], vertex[2], edge))

# Column names
print "X, Y, Z, Edge"

lineNum = 0
doneWithV = False
faces = []
vertices = [[]]

f = open(sys.argv[1])

for line in f:
    lineNum += 1
    line = line.rstrip('#')
    line = line.strip()
    if len(line) == 0:
        continue
    vals = line.split()
    if vals[0] == 'v':
        if doneWithV:
            print "V after F"
            sys.exit(0)
        if len(vals) != 4:
            print "Invalid line #", lineNum
            continue
        vertices.append(vals[1:])

    elif vals[0] == 'f':
        doneWithV = True
        faces.append(vals[1:])

edge = 0
for face in faces:
    
    # Add the first vertex to the end of the list of vertices
    # so there is a line from the last to the first vertex.

    face += [face[0]] 

    for i in range(len(face) - 1):
        print_line(vertices[int(face[i])], edge)
        print_line(vertices[int(face[i + 1])], edge)
        edge += 1;

2 comments:

  1. I sent a link to this to my son Nick, who's launched into a computer science major this fall at Rochester Institute of Technology.

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete