# 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 PerspectiveLens
from pandac.PandaModules import NodePath,LightAttrib
from pandac.PandaModules import AmbientLight,DirectionalLight
from pandac.PandaModules import PointLight,Spotlight
from pandac.PandaModules import TextNode
from pandac.PandaModules import Vec3,Vec4
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
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()

