top of page

DUEL OF BEAKS:

AN ANIMATED HUMMINGBIRD SIMULATION

#----------------------------------
class Hummingbird():
    def __init__(self, i=-1, pos=np.zeros(3), vel=np.zeros(3),
                 aggr=0.5, pb_rad=1.0, ab_rad=1.5, goal=np.zeros(3),
                 color=np.ones(3), startFrame=0, mayaShape=None):
...

...

...


    #---------------------------------------
    #Possible states are "seek," "hover," "attack," "evade," or "feed."
    #"hover" and "feed" pair with the Maya animation cycles/states of "hover" and "feed."
    #The other 3 states require movement in any velocity, so they pair up with
    #   the "forward," "backward," "right," and "left" animations.
    def updateState(self, distToGoal, closestPos, closestDist, distToFeeder, frame, fps):
        num = random.random()
        framesSinceChange = frame-self.lastStateChangeFrame
        changeTime = 5*fps     #seconds * fps
        print("frame = ", frame)
        print("self.lastStateChange = ", self.lastStateChangeFrame)
        print("frames since change = ", framesSinceChange)

        #Continue attacking if still close to goal
        #    or have been attacking for short amount of time.
        if self.state == "attack":
            if distToGoal > 10 or framesSinceChange <= changeTime:
                self.setState_seek(frame, self.feederPos)

        #Continue fleeing?
        elif self.state == "evade":
            if framesSinceChange > changeTime:
                self.setState_seek(frame, self.feederPos)

        #Change things up after seeking current goal?
        elif self.state == "seek":
            #If close to feeder and not evading, start feeding.
            if distToFeeder < self.PB_rad:
                self.setState_feed(frame)

            #If at least changeTime frames passed since last state change.
            elif framesSinceChange > changeTime:
                if closestDist < self.PB_rad:
                    self.setState_evade(frame)

                #Some chance based on aggression level to go off and attack another bird.
                elif num < self.aggression:
                    self.setState_attack(frame, closestPos)

                elif num < self.aggression*1.5:   #Some chance to hover if not going to attack.
                    self.setState_hover(frame)

        #Both feeding and hovering are more stationary motions, so they can be treated similarly here.
        elif self.state == "feed" or self.state == "hover":
            if framesSinceChange > changeTime:  #Feed or hover longer than staying in another state.
                if closestDist < self.PB_rad:
                    self.setState_evade(frame)

                #Equally can take off somewhere, attack nearest bird, or continue feeding.
                else:
                    if num < 0.33:   #Seek in a random direction.
                        self.setState_seek(frame, randomVector())
                    elif num < 0.67:
                        self.setState_attack(frame, closestPos)
                    #else, continue feeding or hovering.

    #---------------------------------------
    def computeForces(self, neighbors, frame, fps):
        dist = magnitude(self.goalPos-self.pos)
        avoid, closestPos, closestDist = self.calcAvoidForce(neighbors)
        avoidFeeder, fPos, distToFeeder = self.calcAvoidForce([feeder])
        self.updateState(dist, closestPos, closestDist, distToFeeder, frame, fps)   #Can change the goal

        if dist*dist > self.goalRadiusSq:   #Not close enough to goal.
            self.goalForce = self.seek()
        else:
            self.goalForce = np.zeros(3)

        #Accumulate forces for each behavior.
        f = np.zeros(3)
        f += self.goalForce         #First aim for the goal (whether feeder, random exploration destination, or another agent).
        f += avoidFeeder            #Don't collide with the feeder.
        dontAvoid = ["attack", "hover", "feed"]
        if self.state not in dontAvoid:
            f += avoid

        #Restrict force by a maximum force.
        if magnitude(f) <= self.maxForce:
            self.forces = f
        else:
            self.forces = normalize(f) * self.maxForce

    #-----------------------------
    def update(self, dt):
        #Update the velocity whether at goal or not but cap by the maximum speed.
        newVel = self.vel + self.forces*dt
        if magnitude(newVel) <= self.maxSpeed:
            self.vel = newVel
        self.vel = self.vel + (self.forces*dt)

        #Update position based on the new velocity.
        self.pos = self.pos + (self.vel*dt)

...

...

...

#---------------------------------------
def mainLoop(writeTime):
    fps = 24               #Animation cycles play at a slow flight for 120 frames at 24 FPS.
    dt = 1.0               #Half second
    step = int(fps * dt)
    simLength = fps * 60   #fps * seconds
    ittr = 0
    cycleLength = 120           #120 frames per animation cycle to access.
    animInd = 0
    lastAnimInd = 0
    currSegmentLength = step    #When blending animations, this is the minimum duration of the blended cycle.
    currSegmentStart = 0
    lastFwd = np.array([0.0, 0.0, 1.0, 0.0])

    for currTime in range(writeTime, writeTime+simLength, step):
        currCycleFrame = (currTime-writeTime+1) % cycleLength    #A frame within [1,120] to access within animation cycles.
        print("time = ", currTime)

        #Calculate forces that will act upon the birds during the update.
        for humm in birds:
            humm.computeForces(birds, currTime, fps)

        #Update to find a new simulation position and velocity, and set Maya attributes accordingly.
        for humm in birds:
            humm.update(dt)

          #----------------
          #Select the appropriate object per bird per timestep.
            birdName = mayaAssetGroupName + str(humm.id)
            ctrlName = birdName + "|Skeleton_Ctrl"   #Pipe char '|' when ':' already in string?
            maya.select(ctrlName)
            maya.currentTime(currTime)

          #----------------
          #Change the object's orientation.
            newVel = (humm.vel[0], humm.vel[1], humm.vel[2])
            fwd = normalize(newVel)
            fwd = np.array([fwd[0], fwd[1], fwd[2], 0.0])
            angles = cartesianToSpherical(fwd)              #Spherical coords that can be used as a GLOBAL rotation.
            angles = (angles[0], angles[1]%180, angles[2])  #Cap rotation about X-axis within upper hemisphere.
            maya.xform(rotation = angles)
            maya.setKeyframe(ctrlName, time=currTime, attribute="rotate")

          #----------------
          #Change position.
            newPos = (humm.pos[0], humm.pos[1], humm.pos[2])
            maya.xform(translation = newPos)
            maya.setKeyframe(ctrlName, time=currTime, attribute="translate")

          #----------------
          #Change color of material.
            matName = "birdMaterial"+str(humm.id)+".color"
            maya.setAttr(matName,
                         humm.color[0], humm.color[1], humm.color[2],
                         type = "double3")
            maya.setKeyframe(matName, time=currTime, attribute="color")

            #----------------
            #Change animation cycle according to horizontal rotation.
            #Angle between the new and the old forward vector only in horizontal X- and Z-components.
            turnAngle, sign = angleBetween( np.array([lastFwd[0], lastFwd[2]]),
                                            np.array([fwd[0], fwd[2]]) )
            lastAnimState = humm.animState
            humm.setAnimStateOnTurnAngle(turnAngle, sign)
            lastFwd = fwd

            print("humm id="+str(humm.id))
            print("\tsim state="+humm.state)

            if lastAnimState != humm.animState:
                animInd = animNames.index(humm.animState)

              #animNames and animStarts are lists in the BlendAnimations script.
              #runBlendAnimations() is also from that script.
                runBlendAnimations(birdName, lastAnimInd, animInd, currSegmentStart, currSegmentLength, step)
                lastAnimInd = animInd
                currSegmentStart = currTime
                currSegmentLength = step

            else:
                #Changes the current time, so do last after all the other keyframes!
                setAnimFrames(birdName, animInd, currCycleFrame, currTime)
                currSegmentLength += 1

          #----------------
        ittr += 1

#---------------------------------------

Language:  Python (with Maya Python Commands)

Date:  Spring 2018

Classes:  Technical Character Animation (Graduate) and Motion Planning (Graduate)

This project is a simulation of hummingbirds interacting with each other around a feeder.  There are 3 main stages of the development of the project:  asset preparation, flight animation, and simulation control.  The first part entails my gaining modeling and rigging experience in creating the hummingbird asset.  The second includes research on hummingbird flight and creating and editing animation cycles of different modes of flight.  The last focuses on designing an algorithm to handle multiple birds with positions, velocities, orientations, simulation states, et cetera as they aggressively interact with one another around a hummingbird feeder.  The final, animated simulation is keyframed within a Maya scene via Python code.  The code to the left is a sample of the code, particularly from the simulation loop and Hummingbird class.

The video provided is a half-minute highlight of components of the project. The first section plays through 6 keyframed, flight-animation cycles of a hummingbird asset I created. The right-turning flight cycle was generated via a Python script reflecting the left-turning flight about the YZ-plane. The next section of the video shows the hovering/idle and feeding animations again before displaying spherical interpolation from the former to the latter; I had implemented quaternions in Python and control interpolating between the hummingbird's joints' transformations. The rest of the video is a sampling of overall simulation results, which are new keyframed animations with global transformations controlled by a simulation algorithm I designed and keyframes set on joints based on the simulation state of each bird.

The following are two documents describing this project.  The discussion in the first focuses on the flight animation components:

The second discusses the simulation aspect of the project:


 

bottom of page