# Author: Jason Pratt (pratt@andrew.cmu.edu)
# Last Updated: 10/20/2003

#
# This project demonstrates how to use various types of
# lighting
#

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *
from direct.gui.DirectGui import *
from direct.showbase.DirectObject import *
import math, sys, colorsys

#Simple function to keep a value in a given range (by default 0 to 1)
def restrain(i, mn = 0, mx = 1): return min(max(i, mn), mx)

class World(DirectObject):
  #Macro-like function to reduce the amount of code needed to create the
  #onscreen instructions
  def makeStatusLabel(self, i):
    return OnscreenText(
      style=1, fg=(1,1,0,1), pos=(-1.3, 0.95 - (.05 * i)),
      align=TextNode.ALeft, scale = .05, mayChange = 1)

  def __init__( self ):
  #The main initialization of our class
    #This creates the on screen title that is in every tutorial
    self.title = OnscreenText(text="Panda3D: Tutorial - Lighting",
                              style=1, fg=(1,1,0,1),
                              pos=(0.87,-0.95), scale = .07)

    #Creates labels used for onscreen instructions
    self.ambientText = self.makeStatusLabel(0)
    self.directionalText = self.makeStatusLabel(1)
    self.spotlightText = self.makeStatusLabel(2)
    self.pointLightText = self.makeStatusLabel(3)
    self.spinningText = self.makeStatusLabel(4)
    self.ambientBrightnessText = self.makeStatusLabel(5)
    self.directionalBrightnessText = self.makeStatusLabel(6)
    self.spotlightBrightnessText = self.makeStatusLabel(7)
    self.spotlightExponentText = self.makeStatusLabel(8)

    self.disco = loader.loadModel("models/disco_hall")
    self.disco.reparentTo(render)
    self.disco.setPosHpr(0, 50, -4, 90, 0, 0)

    # To light a scene, we will first create a LightAttrib, which is a container
    # to manager our lights. We will then create the lights, add them to the
    # lightAttrib, and finally set them to affect a node and all nodes below it
    # in our scene

    #Default lightAttrib with no lights
    self.lightAttrib = LightAttrib.makeAllOff() 

    # First we create an ambient light. All objects are affected by ambient
    # light equally
    #Create and name the ambient light
    self.ambientLight = AmbientLight( "ambientLight" )
    #Set the color of the ambient light
    self.ambientLight.setColor( Vec4( .2, .2, .2, 1 ) )
    #add the newly created light to the lightAttrib
    self.lightAttrib = self.lightAttrib.addLight( self.ambientLight )

    # Now we create a directional light. Directional lights add shading from a
    # given angle. This is good for far away sources like the sun
    self.directionalLight = DirectionalLight( "directionalLight" )
    self.directionalLight.setColor( Vec4( .7, .7, .7, 1 ) )
    # The direction of a directional light is set as a 3D vector
    self.directionalLight.setDirection( Vec3( 1, 1, -2 ) )
    self.lightAttrib = self.lightAttrib.addLight( self.directionalLight ) 

    # Now we create a spotlight. Spotlights light objects in a given cone
    # They are good for simulating things like flashlights
    self.spotlight = Spotlight( "spotlight" )
    self.spotlight.setColor( Vec4( .9, .9, .9, 1 ) )
    #The cone of a spotlight is controlled by it's lens. This creates the lens
    self.spotlight.setLens( PerspectiveLens() )
    #This sets the Field of View (fov) of the lens, in degrees for width and
    #height. The lower the numbers, the tighter the spotlight.
    self.spotlight.getLens().setFov( 16, 16 )
    # Attenuation controls how the light fades with distance. The numbers are
    # The three values represent the three constants (constant, linear, and
    # quadratic) in the internal lighting equation. The higher the numbers the
    # shorter the light goes.
    self.spotlight.setAttenuation( Vec3( 1, 0.0, 0.0 ) ) 
    # This exponent value sets how soft the edge of the spotlight is. 0 means a
    # hard edge. 128 means a very soft edge.
    self.spotlight.setExponent( 60.0 )
    # Unlike our previous lights, the spotlight needs a position in the world
    # We are attaching it to the camera so that it will appear is if we are
    # holding a flashlight, but it can be attached to any NodePath
    #
    # When attaching a spotlight to a NodePath, you must use the
    # upcastToLensNode function or Panda will crash
    camera.attachNewNode( self.spotlight.upcastToLensNode() ) 
    self.lightAttrib = self.lightAttrib.addLight( self.spotlight )

    # Now we create three colored Point lights. Point lights are lights that
    # radiate from a single point, like a light bulb. Like spotlights, they
    # are given position by attaching them to NodePaths in the world
    self.redPointLight = PointLight( "redPointLight" )
    self.redPointLight.setColor( Vec4( .7, 0, 0, 1 ) )
    self.redPointLight.setAttenuation( Vec3( .1, 0.04, 0.0 ) ) 
    self.redHelper = loader.loadModelCopy('models/sphere')
    self.redHelper.setColor( Vec4( 1, 0, 0, 1 ) )
    # To attach a point light to the scene, you must use the upcastToPandaNode
    # Again, if you don't do this Panda will crash
    self.redHelper.attachNewNode( self.redPointLight.upcastToPandaNode() ) 
    self.redHelper.setPos( -6.5, -3.75, 0 )
    self.redHelper.setScale(.25)

    #The green point light and helper
    self.greenPointLight = PointLight( "greenPointLight" )
    self.greenPointLight.setAttenuation( Vec3( .1, .04, .0 ) ) 
    self.greenPointLight.setColor( Vec4( 0, .7, 0, 1 ) )
    self.greenHelper = loader.loadModelCopy('models/sphere')
    self.greenHelper.setColor( Vec4( 0, 1, 0, 1 ) )
    self.greenHelper.attachNewNode( self.greenPointLight.upcastToPandaNode() )
    self.greenHelper.setPos( 0, 7.5, 0 )
    self.greenHelper.setScale(.25)

    #The blue point light and helper
    self.bluePointLight = PointLight( "bluePointLight" )
    self.bluePointLight.setAttenuation( Vec3( .1, 0.04, 0.0 ) ) 
    self.bluePointLight.setColor( Vec4( 0, 0, .7, 1 ) )
    self.bluePointLight.setSpecularColor( Vec4( 1 ) )
    self.blueHelper = loader.loadModelCopy('models/sphere')
    self.blueHelper.setColor( Vec4( 0, 0, 1, 1 ) )
    self.blueHelper.attachNewNode( self.bluePointLight.upcastToPandaNode() ) 
    self.blueHelper.setPos( 6.5, -3.75, 0 )
    self.blueHelper.setScale(.25)

    #Create a dummy node so the lights can be spun with one command
    self.pointLightHelper = render.attachNewNode( "pointLightHelper" )
    self.pointLightHelper.setPos(0, 50, 11)
    self.redHelper.reparentTo( self.pointLightHelper )
    self.greenHelper.reparentTo( self.pointLightHelper )
    self.blueHelper.reparentTo( self.pointLightHelper )

    #Add the point lights we just made to our lightAttrib
    self.lightAttrib = self.lightAttrib.addLight( self.redPointLight ) 
    self.lightAttrib = self.lightAttrib.addLight( self.greenPointLight ) 
    self.lightAttrib = self.lightAttrib.addLight( self.bluePointLight ) 

    #Finally we set the light attrib to a node. In this case we are using render
    #so that the lights will effect everything, but you could put it on any
    #part of the scene
    render.node().setAttrib( self.lightAttrib )

    # Create and start interval to spin the lights, and a variable to
    # manage them.
    self.pointLightsSpin = self.pointLightHelper.hprInterval(6, Vec3(360, 0, 0))
    self.pointLightsSpin.loop()
    self.arePointLightsSpinning = True

    # listen to keys for controlling the lights
    self.accept( "escape", sys.exit)
    self.accept( "a", self.toggleLights, [[self.ambientLight]] )
    self.accept( "d", self.toggleLights, [[self.directionalLight]] )
    self.accept( "s", self.toggleLights, [[self.spotlight]] )
    self.accept( "p", self.toggleLights, [[self.redPointLight,
                                           self.greenPointLight,
                                           self.bluePointLight]] )
    self.accept( "r", self.toggleSpinningPointLights )
    self.accept( "z", self.addBrightness, [self.ambientLight, -.05] )
    self.accept( "x", self.addBrightness, [self.ambientLight, .05] )
    self.accept( "c", self.addBrightness, [self.directionalLight, -.05] )
    self.accept( "v", self.addBrightness, [self.directionalLight, .05] )
    self.accept( "b", self.addBrightness, [self.spotlight, -.05] )
    self.accept( "n", self.addBrightness, [self.spotlight, .05] )
    self.accept( "q", self.adjustSpotlightExponent, [self.spotlight, -1] )
    self.accept( "w", self.adjustSpotlightExponent, [self.spotlight, 1] )

    #Finally call the function that builds the instruction texts
    self.updateStatusLabel()

  # This function takes a list of lights and toggles their state. It takes in a
  # list so that more than one light can be toggled in a single command
  def toggleLights( self, lights ): 
    for light in lights:
      #If the given light is in our lightAttrib, remove it.
      #This has the effect of turning off the light
      if self.lightAttrib.hasLight(light):   
        self.lightAttrib = self.lightAttrib.removeLight( light )
      #Otherwise, add it back. This has the effect of turning the light on
      else:
        self.lightAttrib = self.lightAttrib.addLight( light )

    #Reset the lightAttrib on render to tell it that the lighting has changed
    render.node().setAttrib( self.lightAttrib )
    self.updateStatusLabel()

  # This function toggles the spinning of the point intervals by pausing and
  # resuming the interval
  def toggleSpinningPointLights( self ):
    if self.arePointLightsSpinning: self.pointLightsSpin.pause()
    else: self.pointLightsSpin.resume()
    self.arePointLightsSpinning = not self.arePointLightsSpinning
    self.updateStatusLabel()

  #This function changes the spotlight's exponent. It is kept to the range
  #0 to 128. Going outside of this range causes an error
  def adjustSpotlightExponent( self, spotlight, amount ):
    e = restrain(spotlight.getExponent() + amount, 0, 128)
    spotlight.setExponent( e )
    self.updateStatusLabel()

  #This function reads the color of the light, uses a built-in python function
  #(from the library colorsys) to convert from RGB (red, green, blue) color
  #representation to HSB (hue, saturation, brightness), so that we can get the
  #brighteness of a light, change it, and then convert it back to rgb to chagne
  #the light's color
  def addBrightness( self, light, amount ):
    color = light.getColor()
    h, s, b = colorsys.rgb_to_hsv( color[0], color[1], color[2] )
    brightness = restrain(b + amount)
    r, g, b = colorsys.hsv_to_rgb( h, s, brightness )
    light.setColor( Vec4( r, g, b, 1 ) )

    self.updateStatusLabel()

  #Builds the onscreen instruction labels
  def updateStatusLabel( self ):
    self.updateLabel(self.ambientText, "(a) ambient is",
                     self.lightAttrib.hasLight( self.ambientLight ) )
    self.updateLabel(self.directionalText, "(d) directional is",
                     self.lightAttrib.hasLight( self.directionalLight ) )
    self.updateLabel(self.spotlightText, "(s) spotlight is",
                     self.lightAttrib.hasLight( self.spotlight ) )
    self.updateLabel(self.pointLightText, "(p) point lights are",
                     self.lightAttrib.hasLight( self.redPointLight ) )
    self.updateLabel(self.spinningText, "(r) point light spinning is",
                     self.arePointLightsSpinning)
    self.ambientBrightnessText.setText(
      "(z,x) Ambient Brightness: " +
      self.getBrightnessString(self.ambientLight))
    self.directionalBrightnessText.setText(
      "(c,v) Directional Brightness: " +
      self.getBrightnessString(self.directionalLight))
    self.spotlightBrightnessText.setText(
      "(b,n) Spotlight Brightness: " +
      self.getBrightnessString(self.spotlight))
    self.spotlightExponentText.setText(
      "(q,w) Spotlight Exponent: " +
      str(int(self.spotlight.getExponent())))

  #Appends eitehr (on) or (off) to the base string based on the bassed value
  def updateLabel(self, obj, base, var):
    if var: s = " (on)"
    else:   s = " (off)"
    obj.setText(base + s)
    
  #Returns the brightness of a light as a string to put it in the instruction
  #labels
  def getBrightnessString( self, light ):
    color = light.getColor()
    h, s, b = colorsys.rgb_to_hsv( color[0], color[1], color[2] )
    return "%.2f" % b


#Make an instance of our class and run the world
w = World()
run()
