content: #!/usr/bin/env python
  
  # Author: Shao Zhang, Phil Saltzman, and Eddie Canaan
  # Last Updated: 2015-03-13
  #
  # This tutorial will demonstrate some uses for intervals in Panda
  # to move objects in your panda world.
  # Intervals are tools that change a value of something, like position,
  # rotation or anything else, linearly, over a set period of time. They can be
  # also be combined to work in sequence or in Parallel
  #
  # In this lesson, we will simulate a carousel in motion using intervals.
  # The carousel will spin using an hprInterval while 4 pandas will represent
  # the horses on a traditional carousel. The 4 pandas will rotate with the
  # carousel and also move up and down on their poles using a LerpFunc interval.
  # Finally there will also be lights on the outer edge of the carousel that
  # will turn on and off by switching their texture with intervals in Sequence
  # and Parallel
  
  from direct.showbase.ShowBase import ShowBase
  from panda3d.core import AmbientLight, DirectionalLight, LightAttrib
  from panda3d.core import NodePath
  from panda3d.core import LVector3
  from direct.interval.IntervalGlobal import *  # Needed to use Intervals
  from direct.gui.DirectGui import *
  
  # Importing math constants and functions
  from math import pi, sin
  
  
  class CarouselDemo(ShowBase):
  
      def __init__(self):
          # Initialize the ShowBase class from which we inherit, which will
          # create a window and set up everything we need for rendering into it.
          ShowBase.__init__(self)
  
          # This creates the on screen title that is in every tutorial
          self.title = OnscreenText(text="Panda3D: Tutorial - Carousel",
                                    parent=base.a2dBottomCenter,
                                    fg=(1, 1, 1, 1), shadow=(0, 0, 0, .5),
                                    pos=(0, .1), scale=.1)
  
          base.disableMouse()  # Allow manual positioning of the camera
          camera.setPosHpr(0, -8, 2.5, 0, -9, 0)  # Set the cameras' position
                                                  # and orientation
  
          self.loadModels()  # Load and position our models
          self.setupLights()  # Add some basic lighting
          self.startCarousel()  # Create the needed intervals and put the
                                # carousel into motion
  
      def loadModels(self):
          # Load the carousel base
          self.carousel = loader.loadModel("models/carousel_base")
          self.carousel.reparentTo(render)  # Attach it to render
  
          # Load the modeled lights that are on the outer rim of the carousel
          # (not Panda lights)
          # There are 2 groups of lights. At any given time, one group will have
          # the "on" texture and the other will have the "off" texture.
          self.lights1 = loader.loadModel("models/carousel_lights")
          self.lights1.reparentTo(self.carousel)
  
          # Load the 2nd set of lights
          self.lights2 = loader.loadModel("models/carousel_lights")
          # We need to rotate the 2nd so it doesn't overlap with the 1st set.
          self.lights2.setH(36)
          self.lights2.reparentTo(self.carousel)
  
          # Load the textures for the lights. One texture is for the "on" state,
          # the other is for the "off" state.
          self.lightOffTex = loader.loadTexture("models/carousel_lights_off.jpg")
          self.lightOnTex = loader.loadTexture("models/carousel_lights_on.jpg")
  
          # Create an list (self.pandas) with filled with 4 dummy nodes attached
          # to the carousel.
          # This uses a python concept called "Array Comprehensions."  Check the
          # Python manual for more information on how they work
          self.pandas = [self.carousel.attachNewNode("panda" + str(i))
                         for i in range(4)]
          self.models = [loader.loadModel("models/carousel_panda")
                         for i in range(4)]
          self.moves = [0] * 4
  
          for i in range(4):
              # set the position and orientation of the ith panda node we just created
              # The Z value of the position will be the base height of the pandas.
              # The headings are multiplied by i to put each panda in its own position
              # around the carousel
              self.pandas[i].setPosHpr(0, 0, 1.3, i * 90, 0, 0)
  
              # Load the actual panda model, and parent it to its dummy node
              self.models[i].reparentTo(self.pandas[i])
              # Set the distance from the center. This distance is based on the way the
              # carousel was modeled in Maya
              self.models[i].setY(.85)
  
          # Load the environment (Sky sphere and ground plane)
          self.env = loader.loadModel("models/env")
          self.env.reparentTo(render)
          self.env.setScale(7)
  
      # Panda Lighting
      def setupLights(self):
          # Create some lights and add them to the scene. By setting the lights on
          # render they affect the entire scene
          # Check out the lighting tutorial for more information on lights
          ambientLight = AmbientLight("ambientLight")
          ambientLight.setColor((.4, .4, .35, 1))
          directionalLight = DirectionalLight("directionalLight")
          directionalLight.setDirection(LVector3(0, 8, -2.5))
          directionalLight.setColor((0.9, 0.8, 0.9, 1))
          render.setLight(render.attachNewNode(directionalLight))
          render.setLight(render.attachNewNode(ambientLight))
  
          # Explicitly set the environment to not be lit
          self.env.setLightOff()
  
      def startCarousel(self):
          # Here's where we actually create the intervals to move the carousel
          # The first type of interval we use is one created directly from a NodePath
          # This interval tells the NodePath to vary its orientation (hpr) from its
          # current value (0,0,0) to (360,0,0) over 20 seconds. Intervals created from
          # NodePaths also exist for position, scale, color, and shear
  
          self.carouselSpin = self.carousel.hprInterval(20, LVector3(360, 0, 0))
          # Once an interval is created, we need to tell it to actually move.
          # start() will cause an interval to play once. loop() will tell an interval
          # to repeat once it finished. To keep the carousel turning, we use
          # loop()
          self.carouselSpin.loop()
  
          # The next type of interval we use is called a LerpFunc interval. It is
          # called that becuase it linearly interpolates (aka Lerp) values passed to
          # a function over a given amount of time.
  
          # In this specific case, horses on a carousel don't move contantly up,
          # suddenly stop, and then contantly move down again. Instead, they start
          # slowly, get fast in the middle, and slow down at the top. This motion is
          # close to a sine wave. This LerpFunc calls the function oscillatePanda
          # (which we will create below), which changes the height of the panda based
          # on the sin of the value passed in. In this way we achieve non-linear
          # motion by linearly changing the input to a function
          for i in range(4):
              self.moves[i] = LerpFunc(
                  self.oscillatePanda,  # function to call
                  duration=3,  # 3 second duration
                  fromData=0,  # starting value (in radians)
                  toData=2 * pi,  # ending value (2pi radians = 360 degrees)
                  # Additional information to pass to
                  # self.oscialtePanda
                  extraArgs=[self.models[i], pi * (i % 2)]
              )
              # again, we want these to play continuously so we start them with
              # loop()
              self.moves[i].loop()
  
          # Finally, we combine Sequence, Parallel, Func, and Wait intervals,
          # to schedule texture swapping on the lights to simulate the lights turning
          # on and off.
          # Sequence intervals play other intervals in a sequence. In other words,
          # it waits for the current interval to finish before playing the next
          # one.
          # Parallel intervals play a group of intervals at the same time
          # Wait intervals simply do nothing for a given amount of time
          # Func intervals simply make a single function call. This is helpful because
          # it allows us to schedule functions to be called in a larger sequence. They
          # take virtually no time so they don't cause a Sequence to wait.
  
          self.lightBlink = Sequence(
              # For the first step in our sequence we will set the on texture on one
              # light and set the off texture on the other light at the same time
              Parallel(
                  Func(self.lights1.setTexture, self.lightOnTex, 1),
                  Func(self.lights2.setTexture, self.lightOffTex, 1)),
              Wait(1),  # Then we will wait 1 second
              # Then we will switch the textures at the same time
              Parallel(
                  Func(self.lights1.setTexture, self.lightOffTex, 1),
                  Func(self.lights2.setTexture, self.lightOnTex, 1)),
              Wait(1)  # Then we will wait another second
          )
  
          self.lightBlink.loop()  # Loop this sequence continuously
  
      def oscillatePanda(self, rad, panda, offset):
          # This is the oscillation function mentioned earlier. It takes in a
          # degree value, a NodePath to set the height on, and an offset. The
          # offset is there so that the different pandas can move opposite to
          # each other.  The .2 is the amplitude, so the height of the panda will
          # vary from -.2 to .2
          panda.setZ(sin(rad + offset) * .2)
  
  demo = CarouselDemo()
  demo.run()
  