#-----------------------------------------------------------------------------
#           Name: ogl_lighting.cpp
#         Author: Kevin Harris (kevin@codesampler.com)
#  Last Modified: 02/01/05
#    Description: This sample demonstrates the three basic types of lights
#                 that are available in OpenGL: directional, spot, and point. 
#
#   Control Keys: l - Changes the light's type
#                 w - Toggles wire frame mode
#-----------------------------------------------------------------------------
#
# Original C++ code by Kevin Harris (kevin@codesampler.com)
# See www.codesampler.com for the original files
# OpenGL samples page 5: Lighting
# http://www.codesampler.com/oglsrc/oglsrc_5.htm#ogl_lighting
#
# Modified for Tcl3D by Paul Obermeier 2008/05/01
# See www.tcl3d.org for the Tcl3D extension.

package require Tk
package require tcl3d

# Font to be used in the Tk listbox.
set g_listFont {-family {Courier} -size 10}

set g_WinWidth  640
set g_WinHeight 480

set g_StopWatch [tcl3dNewSwatch]

set LIGHT_TYPE_DIRECTIONAL 0
set LIGHT_TYPE_SPOT        1
set LIGHT_TYPE_POINT       2

set g_lightType $LIGHT_TYPE_DIRECTIONAL
set g_bRenderInWireFrame false
set g_bAnimStarted false

# Mesh properties...
set g_nNumVertsAlongX   32
set g_nNumVertsAlongZ   32
set g_fMeshLengthAlongX 10.0
set g_fMeshLengthAlongZ 10.0

# GL_C4F_N3F_V3F
set R  0
set G  1
set B  2
set A  3
set NX 4
set NY 5
set NZ 6
set X  7
set Y  8
set Z  9
set SIZE 10

set g_nMeshVertCount [expr {($g_nNumVertsAlongX-1) * \
                            ($g_nNumVertsAlongZ-1) * 6}]

# Show errors occuring in the Togl callbacks.
proc bgerror { msg } {
    tk_messageBox -icon error -type ok -message "Error: $msg\n\n$::errorInfo"
    exit
}

# Print info message into widget a the bottom of the window.
proc PrintInfo { msg } {
    if { [winfo exists .fr.info] } {
        .fr.info configure -text $msg
    }
}

proc ChangeLightType { lightType } {
    set ::g_lightType $lightType
    .fr.toglwin postredisplay
}

proc ToggleWireframe {} {
    set ::g_bRenderInWireFrame [expr ! $::g_bRenderInWireFrame]
    if { $::g_bRenderInWireFrame } {
        glPolygonMode GL_FRONT GL_LINE
    } else {
        glPolygonMode GL_FRONT GL_FILL
    }
    .fr.toglwin postredisplay
}

proc InitLights {} {
    set mat_ambient { 1.0 1.0 1.0 1.0 }
    set mat_diffuse { 1.0 1.0 1.0 1.0 }
    glMaterialfv GL_FRONT GL_DIFFUSE $mat_diffuse
    glMaterialfv GL_FRONT GL_AMBIENT $mat_ambient
 
    # Set light 0 to be a simple, bright directional light to use 
    # on the mesh that will represent light 2
    set diffuse_light0  { 1.0 1.0   1.0 1.0 }
    set position_light0 { 0.5 -0.5 -0.5 0.0 }
    glLightfv GL_LIGHT0 GL_DIFFUSE  $diffuse_light0
    glLightfv GL_LIGHT0 GL_POSITION $position_light0

    # Set light 1 to be a simple, faint grey directional light so 
    # the walls and floor are slightly different shades of grey
    set diffuse_light1  { 0.25  0.25 0.25 1.0 }
    set position_light1 { 0.3  -0.5  0.2  0.0 }
    glLightfv GL_LIGHT1 GL_DIFFUSE  $diffuse_light1
    glLightfv GL_LIGHT1 GL_POSITION $position_light1

    # Light #2 will be the demo light used to light the floor and walls. 
    # It will be set up in DisplayCallback since its type can be changed at 
    # run-time.

    # Enable some dim, grey ambient lighting so objects that are not lit 
    # by the other lights are not completely black.
    set ambient_lightModel { 0.25 0.25 0.25 1.0 }
    glLightModelfv GL_LIGHT_MODEL_AMBIENT $ambient_lightModel
}

proc CreateMesh {} {
    # Compute position deltas for moving down the X, and Z axis during mesh 
    # creation
    set dX [expr { (1.0 / ($::g_nNumVertsAlongX-1))}]
    set dZ [expr {-(1.0 / ($::g_nNumVertsAlongZ-1))}]

    # These are all the same...
    #              R   G   B   A    NX  NY  NZ   X   Y   Z
    set constList {1.0 1.0 1.0 0.0  0.0 1.0 0.0  0.0 0.0 0.0}
    for {set i 0 } { $i < $::g_nMeshVertCount } { incr i } {
        lappend meshList $constList
    }
    # Make a flat list.
    set meshList [join $meshList]

    # Create all the vertex points required by the mesh...
    # Note: Mesh tesselation occurs in the X,Z plane.

    # For each row of our mesh...
    set i 0
    for {set z 0 } { $z < [expr {$::g_nNumVertsAlongZ-1}] } { incr z } {
        # Fill the row with quads which are composed of two triangles each...
        for {set x 0 } { $x < [expr {$::g_nNumVertsAlongX-1}] } { incr x } {
            # First triangle of the current quad
            #   ___ 2
            #  |  /|
            #  |/__|
            # 0     1

            # 0
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * $x * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * $z * $dZ}]
            incr i

            # 1
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * ($x+1.0) * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * $z * $dZ}]
            incr i

            # 2
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * ($x+1.0) * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * ($z+1.0) * $dZ}]
            incr i

            # Second triangle of the current quad
            # 2 ___ 1
            #  |  /|
            #  |/__|
            # 0

            # 0
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * $x * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * $z * $dZ}]
            incr i

            # 1
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * ($x+1.0) * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * ($z+1.0) * $dZ}]
            incr i

            # 2
            set off [expr {$i*$::SIZE}]
            lset meshList [expr {$off + $::X}] \
                          [expr {$::g_fMeshLengthAlongX * $x * $dX}]
            lset meshList [expr {$off + $::Z}] \
                          [expr {$::g_fMeshLengthAlongZ * ($z+1.0) * $dZ}]
            incr i
        }
    }
    set ::g_meshVertices [tcl3dVectorFromList GLfloat $meshList]
}

proc CreateCallback { toglwin } {
    glClearColor 0.0 0.0 0.0 1.0
    glEnable GL_LIGHTING
    glEnable GL_TEXTURE_2D
    glEnable GL_DEPTH_TEST

    glMatrixMode GL_PROJECTION
    glLoadIdentity
    gluPerspective 45.0 [expr double($::g_WinWidth)/double($::g_WinHeight)] \
                   0.1 100.0

    InitLights
    CreateMesh
}

proc ReshapeCallback { toglwin { w -1 } { h -1 } } {
    set w [$toglwin width]
    set h [$toglwin height]

    glViewport 0 0 $w $h
    glMatrixMode GL_PROJECTION
    glLoadIdentity
    gluPerspective 45.0 [expr double($w)/double($h)] 0.1 100.0
    glMatrixMode GL_MODELVIEW
}

proc DisplayCallback { toglwin } {
    # Create light 2 at run-time based on the light type the user has
    # selected.

    if { ! [info exists ::fStartTime] } {
      set ::fStartTime [tcl3dLookupSwatch $::g_StopWatch]
    }
    set fElapsedTime [expr {[tcl3dLookupSwatch $::g_StopWatch] - $::fStartTime}]

    set x [expr {  sin ($fElapsedTime * 2.000 )}]
    set y [expr {  sin ($fElapsedTime * 2.246 )}]
    set z [expr {-(sin ($fElapsedTime * 2.640 ))}]

    # Since we want to reuse GL_LIGHT2 to demonstrate the different types of 
    # lights at run-time, we'll push the original or default lighting 
    # attributes of the OpenGL state machine before we start modifying 
    # anything. This way, we can pop and restore the default setup before we 
    # setup a new light type.
    
    glPushAttrib GL_LIGHTING_BIT

    # While both Direct3D and OpenGL use the same formula for lighting 
    # attenuation, they call the variables by different names when setting 
    # them through the API. The following two formulas are the same and 
    # only differ by the API names used for each variable.
    #
    # Direct3D:
    #
    # attenuation = 1 / ( Attenuation0 +
    #                     Attenuation1 * d +
    #                     Attenuation2 * d2 )
    #
    # OpenGL:
    #
    # attenuation = 1 / ( GL_CONSTANT_ATTENUATION  +
    #                     GL_LINEAR_ATTENUATION    * d +
    #                     GL_QUADRATIC_ATTENUATION * d2 )
    #
    # Where: d  = Distance from vertex position to light position
    #        d2 = d squared

    # You should note that GL_POSITION is used for both spot lights and 
    # directional lights in OpenGL.
    # 
    # If the w component of the position is 0.0, the light is treated 
    # as a directional source and x, y, and z represent a direction vector.
    #
    # If the w component of the position is 1.0, the light is treated 
    # as a positionable light source and x, y, and z represent the lights 
    # position in eye coordinates as the light's position will be transformed 
    # by the modelview matrix when glLight is called.

    if { $::g_lightType == $::LIGHT_TYPE_DIRECTIONAL } {
        set diffuse_light2  [list 1.0 1.0 1.0 1.0]
        set position_light2 [list $x $y $z 0.0]

        glLightfv GL_LIGHT2 GL_DIFFUSE  $diffuse_light2
        glLightfv GL_LIGHT2 GL_POSITION $position_light2

    } elseif { $::g_lightType == $::LIGHT_TYPE_SPOT } {
        set diffuse_light2  [list 1.0 1.0 1.0 1.0]
        set position_light2 [list [expr {2.0*$x}] [expr {2.0*$y}] [expr {2.0*$z}] 1.0]
        set spotDirection_light2 [list $x $y $z]

        glLightfv GL_LIGHT2 GL_DIFFUSE        $diffuse_light2
        glLightfv GL_LIGHT2 GL_POSITION       $position_light2
        glLightfv GL_LIGHT2 GL_SPOT_DIRECTION $spotDirection_light2
        glLightf  GL_LIGHT2 GL_CONSTANT_ATTENUATION 1.0
        glLightf  GL_LIGHT2 GL_SPOT_CUTOFF 45.0
        glLightf  GL_LIGHT2 GL_SPOT_EXPONENT 25.0

    } elseif { $::g_lightType == $::LIGHT_TYPE_POINT } {
        set diffuse_light2  [list 1.0 1.0 1.0 1.0]
        set position_light2 [list [expr {4.5*$x}] [expr {4.5*$y}] [expr {4.5*$z}] 1.0]

        glLightfv GL_LIGHT2 GL_DIFFUSE  $diffuse_light2
        glLightfv GL_LIGHT2 GL_POSITION $position_light2
        glLightf  GL_LIGHT2 GL_LINEAR_ATTENUATION 0.4
    }

    glClear [expr {$::GL_COLOR_BUFFER_BIT | $::GL_DEPTH_BUFFER_BIT}]

    # Viewport command is not really needed, but has been inserted for
    # Mac OSX. Presentation framework (Tk) does not send a reshape event,
    # when switching from one demo to another.
    glViewport 0 0 [$toglwin width] [$toglwin height]
    
    glMatrixMode GL_MODELVIEW
    glLoadIdentity

    # Camera position, Look-at point, Up vector
    gluLookAt -12.0 12.0 12.0 \
                0.0 -1.2  0.0 \
                0.0  1.0  0.0

    # The first thing we draw is our walls. The walls are to be lit by 
    # lights 1 and 2 only, so we need to turn on lights 1 and 2, and turn off 
    # light 0. Light 0 will be used later for the 3D primitives.

    glDisable GL_LIGHT0
    glEnable  GL_LIGHT1
    glEnable  GL_LIGHT2

    # Draw the floor
    glPushMatrix
    glTranslatef -5.0 -5.0 5.0
    glInterleavedArrays GL_C4F_N3F_V3F 0 $::g_meshVertices
    glDrawArrays GL_TRIANGLES 0 $::g_nMeshVertCount
    glPopMatrix

    # Draw the back wall
    glPushMatrix
    glTranslatef 5.0 -5.0 5.0
    glRotatef 90.0 0.0 0.0 1.0
    glInterleavedArrays GL_C4F_N3F_V3F 0 $::g_meshVertices
    glDrawArrays GL_TRIANGLES 0 $::g_nMeshVertCount
    glPopMatrix

    # Draw the side wall
    glPushMatrix
    glTranslatef -5.0 -5.0 -5.0
    glRotatef 90.0 1.0 0.0 0.0
    glInterleavedArrays GL_C4F_N3F_V3F 0 $::g_meshVertices
    glDrawArrays GL_TRIANGLES 0 $::g_nMeshVertCount
    glPopMatrix

    # We're finshed drawing the walls, we'll now draw a simple 
    # 3D primitive to represent the light's type. We'll use a little cone 
    # for a directional or spot light and a little sphere for a point light.
    # Light 0 is just for our primitives, so turn on light 0, and 
    # turn off lights 1 and 2 before rendering.

    glEnable  GL_LIGHT0
    glDisable GL_LIGHT1
    glDisable GL_LIGHT2

    # Draw the correct 3d primitve representing the current light type...
    if { $::g_lightType == $::LIGHT_TYPE_DIRECTIONAL } {
        glPushMatrix

        set position_light2 [tcl3dVector GLfloat 4]
        glGetLightfv GL_LIGHT2 GL_POSITION $position_light2

        # Light's position (add a 0.25f offset to center our light's cone)
        gluLookAt 0.0 0.0 0.25 \
                  [$position_light2 get 0] \
                  [$position_light2 get 1] \
                  [$position_light2 get 2] \
                  0.0 1.0 0.0

        glutSolidCone 0.2 0.6 15 15

        glPopMatrix
        $position_light2 delete

    } elseif { $::g_lightType == $::LIGHT_TYPE_SPOT } {
        glPushMatrix
        glLoadIdentity

        set position_light2 [tcl3dVector GLfloat 4]
        set spotDirection_light2 [tcl3dVector GLfloat 4]
        glGetLightfv GL_LIGHT2 GL_POSITION       $position_light2
        glGetLightfv GL_LIGHT2 GL_SPOT_DIRECTION $spotDirection_light2

        gluLookAt [$position_light2 get 0] \
                  [$position_light2 get 1] \
                  [$position_light2 get 2] \
                  [expr {[$position_light2 get 0] + [$spotDirection_light2 get 0]}] \
                  [expr {[$position_light2 get 1] + [$spotDirection_light2 get 1]}] \
                  [expr {[$position_light2 get 2] + [$spotDirection_light2 get 2]}] \
                  0.0 1.0 0.0

        set modelViewMat    [tcl3dVector GLfloat 16]
        set invModelViewMat [tcl3dVector GLfloat 16]

        glGetFloatv GL_MODELVIEW_MATRIX $modelViewMat

        tcl3dMatfInvert $modelViewMat $invModelViewMat
        set invModelViewMatAsList [tcl3dVectorToList $invModelViewMat 16]
        glLoadMatrixf $invModelViewMatAsList

        glutSolidCone 0.2 0.6 15 15

        glPopMatrix

        $position_light2 delete
        $spotDirection_light2 delete
        $modelViewMat delete
        $invModelViewMat delete

    } elseif { $::g_lightType == $::LIGHT_TYPE_POINT } {
        glPushMatrix
        glLoadIdentity

        set position_light2 [tcl3dVector GLfloat 4]
        glGetLightfv GL_LIGHT2 GL_POSITION $position_light2
        glTranslatef [$position_light2 get 0] \
                     [$position_light2 get 1] \
                     [$position_light2 get 2]

        glutSolidSphere 0.25 15 15

        glPopMatrix
        $position_light2 delete
    }

    # We're finshed with our current light type. Restore the default setup
    # for our next frame, which may use a different light type.
    glPopAttrib

    $toglwin swapbuffers
}

proc StartStopAnimation {} {
    if { $::g_bAnimStarted == false } {
        StartAnimation
    } else {
        StopAnimation
    }
}

proc StartAnimation {} {
    tcl3dStartSwatch $::g_StopWatch
    .fr.toglwin postredisplay
    set ::animId [tcl3dAfterIdle StartAnimation]
    set ::g_bAnimStarted true
}

proc StopAnimation {} {
    if { [info exists ::animId] } {
        after cancel $::animId 
        unset ::animId
    }
    set ::g_bAnimStarted false
    tcl3dStopSwatch $::g_StopWatch
}

proc Cleanup {} {
    $::g_meshVertices delete
    tcl3dDeleteSwatch $::g_StopWatch

    foreach var [info globals g_*] {
        uplevel #0 unset $var
    }
}

proc ExitProg {} {
    exit
}

frame .fr
pack .fr -expand 1 -fill both
togl .fr.toglwin -width $g_WinWidth -height $g_WinHeight \
                 -double true -depth true \
                 -createcommand CreateCallback \
                 -reshapecommand ReshapeCallback \
                 -displaycommand DisplayCallback 
listbox .fr.usage -font $::g_listFont -height 6
label   .fr.info
grid .fr.toglwin -row 0 -column 0 -sticky news
grid .fr.usage   -row 1 -column 0 -sticky news
grid .fr.info    -row 2 -column 0 -sticky news
grid rowconfigure .fr 0 -weight 1
grid columnconfigure .fr 0 -weight 1
wm title . "Tcl3D demo: CodeSampler's Lighting Demo"

# Watch For ESC Key And Quit Messages
wm protocol . WM_DELETE_WINDOW "ExitProg"
bind . <Key-Escape> "ExitProg"
bind . <Key-s>      "StartStopAnimation"
bind . <Key-d>      "ChangeLightType $::LIGHT_TYPE_DIRECTIONAL"
bind . <Key-o>      "ChangeLightType $::LIGHT_TYPE_SPOT"
bind . <Key-p>      "ChangeLightType $::LIGHT_TYPE_POINT"
bind . <Key-w>      "ToggleWireframe"

.fr.usage insert end "Key-Escape Exit"
.fr.usage insert end "Key-s      Start|Stop Animation"
.fr.usage insert end "Key-d      Change to directional light"
.fr.usage insert end "Key-o      Change to spot light"
.fr.usage insert end "Key-p      Change to point light"
.fr.usage insert end "Key-w      Toggle wireframe mode"
.fr.usage configure -state disabled

PrintInfo [tcl3dOglGetInfoString]
