Saturday, 8 August 2015

3D Pipes - Maya edition!

If you haven't already watched our video for Delta Heavy's track Ghost then...do.

Lots of fun to make, one of the parts I was most excited by when Chris walked us through the animatic was working out how to recreate some classic Windows screensavers in Maya. I know that anyone who used a computer in the late 90s spent an unhealthy amount of time staring at these so I knew they'd have to feel authentic.

Firstly, the most famous one is probably 3D Pipes:

Now obviously this would be fairly straightforward to just build and animate, it just some cylinders and spheres. However that's
A: No fun
B: Incredibly time consuming

We only had 4 weeks start to finish on this job and there were over 70 shots to get done, so I opted to write a nice script to build the pipes for me. Unfortunately, time constraints meant I ended up building an embarrassingly messy script to build the pipes for me... Well, that's the realities of production, and it was a one use script so, hey-ho.

Fortunately, it's possible to actually get the original source for all the windows screensavers by downloading the NT 4.0 SDK. This gave me a lot of the parameters I needed to get the pipes looking right.

  • We have 3 objects, longPipe, shortPipeJoint and endSphere. 
  • Step forward placing longPipes, 
  • Every step there's a 1/5 chance we'll turn in a random direction
  • If the next step would collide with an existing pipe we have to turn
  • When we turn there's a 1/3 chance that we place a sphere and a long pipe rather than a joint
  • If there's nowhere left to go, stop

Putting this into Maya I wanted to use some premade meshes I'd created, place them in space, and animate their visibilty. I also needed to record their positions so we could keep running the script to create new pipes.

So, to jump straight into the code:
def makePipe(steps=10, startpos=(0,0,0), starttime=0, timestep=1, length=7, turnChance=0.2):
    longPipe = "longPipe"
    shortPipeJoint = "shortPipeJoint"
    endSphere = "endSphere"
So, starting off the main function, we've got a bunch of default arguments and define the names of the objects we're going to place.
    pos = startpos
    DHPipes.append(pos)
    dir = random.choice(directions)
    prevdir=dir   
    t=starttime
    grp = cmds.createNode("transform", n="pipes1")
    last = grp
We initialise our variables we're going to update in the main loop with the arguments passed into the function. Also, we create a group to keep everything nice and tidy.
for s in range(steps):       
        mv = getMove(dir, length)
        nextPos = addTuple(pos, mv)

        if random.random()<turnChance or nextPos in DHPipes:
            prevdir=dir
            dir=random.choice(possibleDirs(dir))
            mv=getMove(dir, length)
            nextPos = addTuple(pos, mv)
 For each step we work out what the move will be. getMove just multiplies each item in dir by length. In hindsight I'm sure there's a neater way of doing that, maybe someone will tell me in the comments. Really it should just be called scaleTuple... Then, get the next position with addTuple (guess what that does)
while nextPos in DHPipes and i<20:
    dir=random.choice(possibleDirs(dir))
    mv=getMove(dir, length)
    nextPos = addTuple(pos, mv)
    i+=1 
Next we check that we're not about to hit an existing pipe. DHPipes is a list of all positions of existing pipes. If we are we get a random direction to turn in (possibleDirs returns a list of directions which aren't forward or back from our current direction). I iterate i and check against it in the while because...well. I tend to always mess up whiles, cause an infinite loop and crash Maya. I just have that for safety.
    if nextPos in DHPipes:
                print "Nowhere to go, so I'm stopping"
                break #give up, we can find a way to go 

            if random.random() < 0.3333:
                new = cmds.duplicate(endSphere)[0]
                cmds.parent(new, grp)
                cmds.setAttr(new+".translateX", pos[0])
                cmds.setAttr(new+".translateY", pos[1])
                cmds.setAttr(new+".translateZ", pos[2])
                cmds.setKeyframe(new+".visibility", v=0, t=t-1)
                cmds.setKeyframe(new+".visibility", v=1, t=t)
                obj = longPipe
                rot = longRot(dir)
            elif dir!=prevdir:
                obj = shortPipeJoint
                rot=elbowRot(dir, prevdir)
            else:
                obj = longPipe
                rot = longRot(dir)
        else:
            obj = longPipe
            rot = longRot(dir)
           
        new = cmds.duplicate(obj)[0]
        cmds.parent(new, grp)
        cmds.setAttr(new+".translateX", pos[0])
        cmds.setAttr(new+".translateY", pos[1])
        cmds.setAttr(new+".translateZ", pos[2])
        cmds.setAttr(new+".rotateX", rot[0])
        cmds.setAttr(new+".rotateY", rot[1])
        cmds.setAttr(new+".rotateZ", rot[2])
        cmds.setKeyframe(new+".visibility", v=0, t=t-1)
        cmds.setKeyframe(new+".visibility", v=1, t=t)
        pos = nextPos
        DHPipes.append(pos)
        t+=timestep
This last bit does the meat, it decides which object we're going to place based on the direction we're now moving in, then duplicates the base object, moves it to the right position and rotates it. longRot and elbowRot gets the rotation needed. I'd like to say I did the maths for allthat, but it was quicker to just do it by hand and hard code it. Deadlines!

That's about it for 3D Pipes. I'm slightly concerned I just wrote quite a long, boring blog post on what is quite a simple script, but... it's my first time! Hope someone finds it useful/interesting. I'll try to get better at this. I'm write up a quickie about the flowerbox screensaver shortly. That one's less code, more Maya, and it's pretty straightforward so should be a shorter post.

No comments:

Post a Comment