#!/usr/local/bin/perl

# Copyright (c) Cass Everitt, 1998

# This program is freely distributable without licensing fees 
# and is provided without guarantee or warrantee expressed or 
# implied. This program is -not- in the public domain. 

# This program was ported from Mark J. Kilgard's dinoshade.c (1997).

# Uncomment this to run the example without installing the
# built modules. 

#sub BEGIN
#  {
#    push @INC, ("./OpenGL",
#                "./OpenGL/blib/arch",
#                "./OpenGLU",
#                "./OpenGLU/blib/arch",
#                "./GLUT",
#                "./GLUT/blib/arch");
#  }

use OpenGL;
use OpenGLU;
use GLUT;

use vars qw(
	    $pi
	    $stencilReflection $stencilShadow  $offsetShadow
	    $renderShadow      $renderDinosaur $renderReflection
	    $linearFiltering   $useMipmaps     $useTexture
	    $reportSpeed
	    $animation
	    $lightSwitch
	    $directionalLight
	    $forceExtension
	    
	    $jump
	    $lightAngle $lightHeight
	    $angle
	    $angle2
	    
	    $moving $startx $starty
	    $lightMoving $lightStartX $lightStartY
	    
	    $bodyWidth
	    $body $arm $leg $eye
	    
	    @lightPosition
	    $lightColor
	    $skinColor
	    $eyeColor
	    
	    @circles
	    
	    $tobj
	   );

$pi = 3.14159265;
($stencilReflection, $stencilShadow,  $offsetShadow)     = (1,1,1);
($renderShadow,      $renderDinosaur, $renderReflection) = (1,1,1);
($linearFiltering,   $useMipmaps,     $useTexture)       = (0,0,1);
$reportSpeed = 0;
$animation = 1;
$lightSwitch = GL_TRUE;
$directionalLight = 1;
$forceExtension = 0;

#  Time varying or user-controled variables.
$jump = 0.0;
($lightAngle, $lightHeight) = (0.0, 20.0);
$angle = -150;  # in degrees
$angle2 = 30;   # in degrees

$lightMoving = 0;

$bodyWidth = 3.0;

$body = [ 
	 pack("f2", 0, 3),   pack("f2", 1, 1),     pack("f2", 5, 1),     pack("f2", 8, 4),
	 pack("f2", 10, 4),  pack("f2", 11, 5),    pack("f2", 11, 11.5), pack("f2", 13, 12),
	 pack("f2", 13, 13), pack("f2", 10, 13.5), pack("f2", 13, 14),   pack("f2", 13, 15),
	 pack("f2", 11, 16), pack("f2", 8, 16),    pack("f2", 7, 15),    pack("f2", 7, 13),
	 pack("f2", 8, 12),  pack("f2", 7, 11),    pack("f2", 6, 6),     pack("f2", 4, 3),
	 pack("f2", 3, 2),   pack("f2", 1, 2)
	];

$arm  = [
	 pack("f2", 8, 10),  pack("f2", 9, 9),     pack("f2", 10, 9),    pack("f2", 13, 8),
	 pack("f2", 14, 9),  pack("f2", 16, 9),    pack("f2", 15, 9.5),  pack("f2", 16, 10),
	 pack("f2", 15, 10), pack("f2", 15.5, 11), pack("f2", 14.5, 10), pack("f2", 14, 11),
	 pack("f2", 14, 10), pack("f2", 13, 9),    pack("f2", 11, 11),   pack("f2", 9, 11)
	];

$leg  = [
	 pack("f2", 8, 6),   pack("f2", 8, 4),     pack("f2", 9, 3),     pack("f2", 9, 2),
	 pack("f2", 8, 1),   pack("f2", 8, 0.5),   pack("f2", 9, 0),     pack("f2", 12, 0),
	 pack("f2", 10, 1),  pack("f2", 10, 2),    pack("f2", 12, 4),    pack("f2", 11, 6),
	 pack("f2", 10, 7),  pack("f2", 9, 7)
	];

$eye  = [
	 pack("f2", 8.75, 15),    pack("f2", 9, 14.7),      pack("f2", 9.6, 14.7),
	 pack("f2", 10.1, 15),    pack("f2", 9.6, 15.25),   pack("f2", 9, 15.25)
	];

$lightColor = pack("f*", (.8,  1,  .8,  1));
$skinColor  = pack("f*", (.1,  1,  .1,  1));
$eyeColor   = pack("f*", (1.0, .2, .2,  1));

@circles =  unpack("C*",
		   "....xxxx........".
		   "..xxxxxxxx......".
		   ".xxxxxxxxxx.....".
		   ".xxx....xxx.....".
		   "xxx......xxx....".
		   "xxx......xxx....".
		   "xxx......xxx....".
		   "xxx......xxx....".
		   ".xxx....xxx.....".
		   ".xxxxxxxxxx.....".
		   "..xxxxxxxx......".
		   "....xxxx........".
		   "................".
		   "................".
		   "................".
		   "................");

$tobj = undef;

@floorVertices = (
		  pack("f*",( -20.0, 0.0,  20.0 )),
		  pack("f*",(  20.0, 0.0,  20.0 )),
		  pack("f*",(  20.0, 0.0, -20.0 )),
		  pack("f*",( -20.0, 0.0, -20.0 )),
		 );

@floorPlane  = (0..3);
@floorShadow = (0..15);

&main;

sub makeFloorTexture
  {
    my $floorTexture;

    my $i;
    foreach $i (@circles)
      {
	if(chr $i eq 'x') { $floorTexture .= pack("C3", (0x1f,0x8f,0x1f)); } 
	else          { $floorTexture .= pack("C3", (0xaa,0xaa,0xaa)); }
      }
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    if($useMipmaps)
      {
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
			GL_LINEAR_MIPMAP_LINEAR);
	gluBuild2DMipmaps(GL_TEXTURE_2D, 3, 16, 16,
			  GL_RGB, GL_UNSIGNED_BYTE, $floorTexture);
      }
    else
      {
	if ($linearFiltering) 
	  { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); }
	else
	  { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); }
	
	glTexImage2D(GL_TEXTURE_2D, 0, 3, 16, 16, 0,
		     GL_RGB, GL_UNSIGNED_BYTE, $floorTexture);
      }
  }

# Create a matrix that will project the desired shadow.

sub X { 0 } sub Y { 1 } sub Z { 2 } sub W { 3 }
sub A { 0 } sub B { 1 } sub C { 2 } sub D { 3 }

sub shadowMatrix
  {
    my ($shadowMat, $groundplane, $lightpos) = @_;
    my $dot;
    
    
    # Find dot product between light position vector and ground plane normal.
    $dot = ($groundplane->[X]*$lightpos->[X] +
	    $groundplane->[Y]*$lightpos->[Y] +
	    $groundplane->[Z]*$lightpos->[Z] +
	    $groundplane->[W]*$lightpos->[W] );
    
    $shadowMat->[4*0 + 0] = $dot - $lightpos->[X] * $groundplane->[X];
    $shadowMat->[4*1 + 0] = 0.0  - $lightpos->[X] * $groundplane->[Y];
    $shadowMat->[4*2 + 0] = 0.0  - $lightpos->[X] * $groundplane->[Z];
    $shadowMat->[4*3 + 0] = 0.0  - $lightpos->[X] * $groundplane->[W];
    
    $shadowMat->[4*0 + 1] = 0.0  - $lightpos->[Y] * $groundplane->[X];
    $shadowMat->[4*1 + 1] = $dot - $lightpos->[Y] * $groundplane->[Y];
    $shadowMat->[4*2 + 1] = 0.0  - $lightpos->[Y] * $groundplane->[Z];
    $shadowMat->[4*3 + 1] = 0.0  - $lightpos->[Y] * $groundplane->[W];
    
    $shadowMat->[4*0 + 2] = 0.0  - $lightpos->[Z] * $groundplane->[X];
    $shadowMat->[4*1 + 2] = 0.0  - $lightpos->[Z] * $groundplane->[Y];
    $shadowMat->[4*2 + 2] = $dot - $lightpos->[Z] * $groundplane->[Z];
    $shadowMat->[4*3 + 2] = 0.0  - $lightpos->[Z] * $groundplane->[W];
    
    $shadowMat->[4*0 + 3] = 0.0  - $lightpos->[W] * $groundplane->[X];
    $shadowMat->[4*1 + 3] = 0.0  - $lightpos->[W] * $groundplane->[Y];
    $shadowMat->[4*2 + 3] = 0.0  - $lightpos->[W] * $groundplane->[Z];
    $shadowMat->[4*3 + 3] = $dot - $lightpos->[W] * $groundplane->[W];
    
  }


# Find the plane equation given 3 points.
sub findPlane
  {
    my ($plane, $v0, $v1, $v2) = @_;
    
    my $vec0 = [0,0,0];
    my $vec1 = [0,0,0];
    
    # Need 2 vectors to find cross product.
    $vec0->[X] = $v1->[X] - $v0->[X];
    $vec0->[Y] = $v1->[Y] - $v0->[Y];
    $vec0->[Z] = $v1->[Z] - $v0->[Z];
    
    $vec1->[X] = $v2->[X] - $v0->[X];
    $vec1->[Y] = $v2->[Y] - $v0->[Y];
    $vec1->[Z] = $v2->[Z] - $v0->[Z];

  # find cross product to get A, B, and C of plane equation
    $plane->[A] =   $vec0->[Y] * $vec1->[Z] - $vec0->[Z] * $vec1->[Y];
    $plane->[B] = -($vec0->[X] * $vec1->[Z] - $vec0->[Z] * $vec1->[X]);
    $plane->[C] =   $vec0->[X] * $vec1->[Y] - $vec0->[Y] * $vec1->[X];

    $plane->[D] = -($plane->[A] * $v0->[X] + 
		    $plane->[B] * $v0->[Y] +
		    $plane->[C] * $v0->[Z]);
    
}

$mydata;

sub myTessError
  {
    my ($v) = @_;
    print STDERR "Tesselator error: ".gluErrorString($v)."\n";
  }

sub extrudeSolidFromPolygon
{
  my ($data, $count, $thickness, $side, $edge, $whole) = @_;
  my ($vertex, $dx, $dy, $len);
  my $i;
  my $zero = pack("d", 0);
  
  $mydata = $data;

  if (! defined $tobj) 
    {
      $tobj = gluNewTess();  # new GLU polygon tesselation object
      gluTessCallback($tobj, GLU_ERROR, \&myTessError);
      gluTessCallback($tobj, GLU_BEGIN, \&glBegin);
      gluTessCallback($tobj, GLU_VERTEX, \&glVertex2fv); # semi-tricky 
      gluTessCallback($tobj, GLU_END, \&glEnd);
    }
  
  glNewList($side, GL_COMPILE);
  glShadeModel(GL_SMOOTH);  # smooth minimizes seeing tessellation

  gluBeginPolygon($tobj);
  for ($i = 0; $i < $count; $i++)
    {
      $vertex = pack("d2", unpack("f2", $data->[$i])) . $zero;
      # print STDERR "Vertex: ", join(", ", unpack("d3", $vertex)), "\n"; 
      #gluTessVertex($tobj, $vertex, $data->[$i]);
      gluTessVertex($tobj, $vertex, $data->[$i]);
    }
  gluEndPolygon($tobj);
  glEndList();
  glNewList($edge, GL_COMPILE);
  glShadeModel(GL_FLAT);  # flat shade keeps angular hands from being "smoothed"
  glBegin(GL_QUAD_STRIP);


  for ($i = 0; $i < $count+1; $i++)
    {
      my @d = unpack("f2", $data->[$i     % $count]);
      my @e = unpack("f2", $data->[($i+1) % $count]);
      
      # mod function handles closing the edge
      glVertex3f($d[0], $d[1], 0.0);
      glVertex3f($d[0], $d[1], $thickness);
      # Calculate a unit normal by dividing by Euclidean
      # distance. We * could be lazy and use
      # glEnable(GL_NORMALIZE) so we could pass in * arbitrary
      # normals for a very slight performance hit.
      $dx = $e[1] - $d[1];
      $dy = $d[0] - $e[0];
      $len = sqrt($dx * $dx + $dy * $dy);
      glNormal3f($dx / $len, $dy / $len, 0.0);
    }
  glEnd();
  glEndList();
  glNewList($whole, GL_COMPILE);
  glFrontFace(GL_CW);
  glCallList($edge);
  glNormal3f(0.0, 0.0, -1.0); # constant normal for side
  glCallList($side);
  glPushMatrix();
  glTranslatef(0.0, 0.0, $thickness);
  glFrontFace(GL_CCW);
  glNormal3f(0.0, 0.0, 1.0);  # opposite normal for other side
  glCallList($side);
  glPopMatrix();
  glEndList();
}

sub BODY_SIDE { 1  }  sub BODY_EDGE { 2  }  sub BODY_WHOLE { 3  }
sub ARM_SIDE  { 4  }  sub ARM_EDGE  { 5  }  sub ARM_WHOLE  { 6  } 
sub LEG_SIDE  { 7  }  sub LEG_EDGE  { 8  }  sub LEG_WHOLE  { 9  }
sub EYE_SIDE  { 10 }  sub EYE_EDGE  { 11 }  sub EYE_WHOLE  { 12 } 

sub makeDinosaur
  {
    extrudeSolidFromPolygon($body, scalar @{$body}, $bodyWidth,
			    BODY_SIDE, BODY_EDGE, BODY_WHOLE);
    extrudeSolidFromPolygon($arm,  scalar @{$arm},   $bodyWidth / 4,
			    ARM_SIDE,  ARM_EDGE,  ARM_WHOLE);
    extrudeSolidFromPolygon($leg,  scalar @{$leg},   $bodyWidth / 2,
			    LEG_SIDE,  LEG_EDGE,  LEG_WHOLE);
    extrudeSolidFromPolygon($eye,  scalar @{$eye}, $bodyWidth + 0.2,
			    EYE_SIDE, EYE_EDGE, EYE_WHOLE);
  }

sub drawDinosaur
{
  glPushMatrix();
  # Translate the dinosaur to be at (0,8,0).
  glTranslatef(-8, 0, - $bodyWidth / 2);
  glTranslatef(0.0, $jump, 0.0);
  glMaterialfv(GL_FRONT, GL_DIFFUSE, $skinColor);
  glCallList(BODY_WHOLE);
  glTranslatef(0.0, 0.0, $bodyWidth);
  glCallList(ARM_WHOLE);
  glCallList(LEG_WHOLE);
  glTranslatef(0.0, 0.0, -$bodyWidth - $bodyWidth / 4);
  glCallList(ARM_WHOLE);
  glTranslatef(0.0, 0.0, -$bodyWidth / 4);
  glCallList(LEG_WHOLE);
  glTranslatef(0.0, 0.0, $bodyWidth / 2 - 0.1);
  glMaterialfv(GL_FRONT, GL_DIFFUSE, $eyeColor);
  glCallList(EYE_WHOLE);
  glPopMatrix();
}


# Draw a floor (possibly textured). 
sub drawFloor
{
  glDisable(GL_LIGHTING);
  
  if ($useTexture) {
    glEnable(GL_TEXTURE_2D);
  }

  glBegin(GL_QUADS);
  {
    glTexCoord2f(0.0, 0.0);
    glVertex3fv($floorVertices[0]);
    glTexCoord2f(0.0, 16.0);
    glVertex3fv($floorVertices[1]);
    glTexCoord2f(16.0, 16.0);
    glVertex3fv($floorVertices[2]);
    glTexCoord2f(16.0, 0.0);
    glVertex3fv($floorVertices[3]);
  }
  glEnd();

  if ($useTexture) {
    glDisable(GL_TEXTURE_2D);
  }

  glEnable(GL_LIGHTING);
}

sub redraw
  {
    my ($start, $end);
    
    if ($reportSpeed) { $start = glutGet(GLUT_ELAPSED_TIME); }
    
    # Clear; default stencil clears to zero. 
    if (($stencilReflection && $renderReflection) || ($stencilShadow && $renderShadow))
      { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); }
    else
      {
	# Avoid clearing stencil when not using it.
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      }
    # Reposition the light source.
    $lightPosition[0] = 12*cos($lightAngle);
    $lightPosition[1] = $lightHeight;
    $lightPosition[2] = 12*sin($lightAngle);
    if   ($directionalLight) { $lightPosition[3] = 0.0; }
    else                     { $lightPosition[3] = 1.0; }
    
    shadowMatrix(\@floorShadow, \@floorPlane, \@lightPosition);
    
    glPushMatrix();
    {
      # Perform scene rotations based on user mouse input.
      glRotatef($angle2, 1.0, 0.0, 0.0);
      glRotatef($angle,  0.0, 1.0, 0.0);
      
      # Tell GL new light source position. 
      glLightfv(GL_LIGHT0, GL_POSITION, pack("f*", @lightPosition));
      
      if ($renderReflection)
	{
	  if ($stencilReflection)
	    {
	      # We can eliminate the visual "artifact" of seeing the "flipped"
	      # dinosaur underneath the floor by using stencil.  The idea is
	      # draw the floor without color or depth update but so that 
	      # a stencil value of one is where the floor will be.  Later when
	      # rendering the dinosaur reflection, we will only update pixels
	      # with a stencil value of 1 to make sure the reflection only
	      # lives on the floor, not below the floor.
	      
	      # Don't update color or depth.
	      glDisable(GL_DEPTH_TEST);
	      glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
	      
	      # Draw 1 into the stencil buffer.
	      glEnable(GL_STENCIL_TEST);
	      glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
	      glStencilFunc(GL_ALWAYS, 1, 0xffffffff);
	      
	      # Now render floor; floor pixels just get their stencil set to 1.
	      drawFloor();
	      
	      # Re-enable update of color and depth.
	      glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
	      glEnable(GL_DEPTH_TEST);
	      
	      # Now, only render where stencil is set to 1.
	      glStencilFunc(GL_EQUAL, 1, 0xffffffff);  # draw if == 1
	      glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
	    }
	  
	  glPushMatrix();
	  {	
	    # The critical reflection step: Reflect dinosaur through the floor
	    # (the Y=0 plane) to make a relection. 
	    glScalef(1.0, -1.0, 1.0);
	    
	    # Reflect the light position. 
	    glLightfv(GL_LIGHT0, GL_POSITION, pack("f*", @lightPosition));
	    
	    # To avoid our normals getting reversed and hence botched lighting
	    # on the reflection, turn on normalize. 
	    glEnable(GL_NORMALIZE);
	    glCullFace(GL_FRONT);
	    
	    # Draw the reflected dinosaur.
	    drawDinosaur();
	    
	    # Disable noramlize again and re-enable back face culling.
	    glDisable(GL_NORMALIZE);
	    glCullFace(GL_BACK);
	  }
	  glPopMatrix();
	  
	  # Switch back to the unreflected light position.
	  glLightfv(GL_LIGHT0, GL_POSITION, pack("f*", @lightPosition));
	  
	  if ($stencilReflection) { glDisable(GL_STENCIL_TEST); }
	}
      
      # Back face culling will get used to only draw either the top or the
      # bottom floor.  This let's us get a floor with two distinct
      # appearances.  The top floor surface is reflective and kind of red.
      # The bottom floor surface is not reflective and blue.
      
      # Draw "bottom" of floor in blue. 
      glFrontFace(GL_CW);  # Switch face orientation.
      glColor4f(0.1, 0.1, 0.7, 1.0);
      drawFloor();
      glFrontFace(GL_CCW);
      
      if ($renderShadow)
	{
	  if ($stencilShadow)
	    {
	      # Draw the floor with stencil value 3.  This helps us only 
	      # draw the shadow once per floor pixel (and only on the
	      # floor pixels).
	      glEnable(GL_STENCIL_TEST);
	      glStencilFunc(GL_ALWAYS, 3, 0xffffffff);
	      glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
	    }
	}
      
      # Draw "top" of floor.  Use blending to blend in reflection. 
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
      glColor4f(0.7, 0.0, 0.0, 0.3);
      glColor4f(1.0, 1.0, 1.0, 0.3);
      drawFloor();
      glDisable(GL_BLEND);
      
      if ($renderDinosaur)
	{
	  # Draw "actual" dinosaur, not its reflection. 
	  drawDinosaur();
	}
      
      if ($renderShadow)
	{
	  # Render the projected shadow.
	  if ($stencilShadow)
	    {
	      
	      # Now, only render where stencil is set above 2 (ie, 3 where
	      # the top floor is).  Update stencil with 2 where the shadow
	      # gets drawn so we don't redraw (and accidently reblend) the
	      # shadow).
	      glStencilFunc(GL_LESS, 2, 0xffffffff);  # draw if == 1
	      glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
	    }
	  
	  # To eliminate depth buffer artifacts, we use polygon offset
	  # to raise the depth of the projected shadow slightly so
	  # that it does not depth buffer alias with the floor.
	  if ($offsetShadow) { glEnable(GL_POLYGON_OFFSET_FILL); }
	
	  
	  # Render 50% black shadow color on top of whatever the
	  # floor appareance is.
	  glEnable(GL_BLEND);
	  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	  glDisable(GL_LIGHTING);  # Force the 50% black. 
	  glColor4f(0.0, 0.0, 0.0, 0.5);
	  
	  glPushMatrix();
	  {
	    # Project the shadow. */
	    glMultMatrixf(pack("f*", @floorShadow));
	    drawDinosaur();
	  }
	  glPopMatrix();
	  
	  glDisable(GL_BLEND);
	  glEnable(GL_LIGHTING);

	  if ($offsetShadow) { glDisable(GL_POLYGON_OFFSET_FILL); }
	  if ($stencilShadow) { glDisable(GL_STENCIL_TEST); }
	}
      
      
      
      glPushMatrix();
      {
	glDisable(GL_LIGHTING);
	glColor3f(1.0, 1.0, 0.0);
	if ($directionalLight)
	  {
	    # Draw an arrowhead.
	    glDisable(GL_CULL_FACE);
	    glTranslatef($lightPosition[0], $lightPosition[1], $lightPosition[2]);
	    glRotatef($lightAngle * -180.0 / $pi, 0, 1, 0);
	    glRotatef(atan2($lightHeight,12) * 180.0 / $pi, 0, 0, 1);
	    
	    glBegin(GL_TRIANGLE_FAN);
	    {
	      glVertex3f(0, 0, 0);
	      glVertex3f(2, 1, 1);
	      glVertex3f(2, -1, 1);
	      glVertex3f(2, -1, -1);
	      glVertex3f(2, 1, -1);
	      glVertex3f(2, 1, 1);
	    }
	    glEnd();
	    # Draw a white line from light direction. 
	    glColor3f(1.0, 1.0, 1.0);
	    glBegin(GL_LINES);
	    {
	      glVertex3f(0, 0, 0);
	      glVertex3f(5, 0, 0);
	    }
	    glEnd();
	    glEnable(GL_CULL_FACE);
	  }
	else
	  {
	    # Draw a yellow ball at the light source.
	    glTranslatef($lightPosition[0], $lightPosition[1], $lightPosition[2]);
	    glutSolidSphere(1.0, 5, 5);
	  }
	
	glEnable(GL_LIGHTING);
      }
      glPopMatrix();
    }
    glPopMatrix();
    
    if ($reportSpeed) 
      {
	glFinish(); 
	$end = glutGet(GLUT_ELAPSED_TIME);
	printf("Speed %.3g frames/sec (%d ms)\n", 1000.0/($end-$start), $end-$start);
      }
    glutSwapBuffers();
  }

sub mouse
{
  my ($button, $state, $x, $y) = @_;
  
  if ($button == GLUT_LEFT_BUTTON)
    {
      if ($state == GLUT_DOWN)
	{
	  $moving = 1;
	  $startx = $x;
	  $starty = $y;
	}
      if ($state == GLUT_UP) { $moving = 0; }
    }
  if ($button == GLUT_MIDDLE_BUTTON)
    {
      if ($state == GLUT_DOWN) 
	{
	  $lightMoving = 1;
	  $lightStartX = $x;
	  $lightStartY = $y;
	}
      if ($state == GLUT_UP) { $lightMoving = 0; }
    }
}

sub motion
{
  my ($x, $y) = @_;
  if ($moving) 
    {
      $angle  += $x - $startx;
      $angle2 += $y - $starty;
      $startx = $x;
      $starty = $y;
      glutPostRedisplay();
    }
  if ($lightMoving)
    {
      $lightAngle  += ($x - $lightStartX)/40.0;
      $lightHeight += ($lightStartY - $y)/20.0;
      $lightStartX = $x;
      $lightStartY = $y;
    glutPostRedisplay();
  }
}

# Advance time varying state when idle callback registered.

sub idle
{
  my $time;

  $time = glutGet(GLUT_ELAPSED_TIME) / 500.0;

  $jump = 4.0 * abs(sin($time)*0.5);
  if (! $lightMoving) { $lightAngle += 0.03; }
  glutPostRedisplay();
}


sub M_NONE          {  0 } sub M_MOTION             {  1 }  sub M_LIGHT          {  2 }
sub M_TEXTURE       {  3 } sub M_SHADOWS            {  4 }  sub M_REFLECTION     {  5 }
sub M_DINOSAUR      {  6 } sub M_STENCIL_REFLECTION {  7 }  sub M_STENCIL_SHADOW {  8 }
sub M_OFFSET_SHADOW {  9 } sub M_POSITIONAL         { 10 }  sub M_DIRECTIONAL    { 11 }
sub M_PERFORMANCE   { 12 } 


sub controlLights
{
  my $value = shift;

  if($value == M_NONE)
    {
      return;
    }
  elsif($value == M_MOTION)
    {
      $animation = ! $animation;
      if   ($animation) { glutIdleFunc(\&idle); }
      else              { glutIdleFunc(undef); }
    }
  elsif($value == M_LIGHT)
    {
      $lightSwitch = ! $lightSwitch;
      if   ($lightSwitch) { glEnable(GL_LIGHT0); }
      else                { glDisable(GL_LIGHT0);}
    }
  elsif($value == M_TEXTURE)
    {      
      $useTexture = ! $useTexture;
    }
  elsif($value == M_SHADOWS)
    {
      $renderShadow = ! $renderShadow;
    }
  elsif($value == M_REFLECTION)
    {
      $renderReflection = 1 - $renderReflection;
    }
  elsif($value == M_DINOSAUR)
    {
      $renderDinosaur = 1 - $renderDinosaur;
    }
  elsif($value == M_STENCIL_REFLECTION)
    {
      $stencilReflection = 1 - $stencilReflection;
    }
  elsif($value == M_STENCIL_SHADOW)
    {
      $stencilShadow = 1 - $stencilShadow;
    }
  elsif($value == M_OFFSET_SHADOW)
    {
      $offsetShadow = 1 - $offsetShadow;
    }
  elsif($value == M_POSITIONAL)
    {
      $directionalLight = 0;
    }
  elsif($value == M_DIRECTIONAL)
    {
      $directionalLight = 1;
    }
  elsif($value == M_PERFORMANCE)
    {
      $reportSpeed = 1 - $reportSpeed;
    }

  glutPostRedisplay();
}

# When not visible, stop animating.  Restart when visible again. 
sub visible
{
  my $vis = shift;

  if ($vis == GLUT_VISIBLE) 
    { if ($animation)   { glutIdleFunc(\&idle); } }
  else
    { if (! $animation) { glutIdleFunc(undef);  } }
}

# Press any key to redraw; good when motion stopped and
# performance reporting on.

sub key
{
  my ($c, $x, $y) = @_;
  if ($c == 27) { exit(0); } # IRIS GLism, Escape quits.
  glutPostRedisplay();
}

# Press any key to redraw; good when motion stopped and
# performance reporting on. 

sub special
{
  my ($k, $x, $y) = @_;
  glutPostRedisplay();
}


sub main
{
  my $i;

  glutInit;

  for ($i = 0; $i < $#ARGV+1; $i++) 
    {
      if    ( $ARGV[$i] eq "-linear" ) { $linearFiltering = 1; }
      elsif ( $ARGV[$i] eq "-mipmap" ) { $useMipmaps = 1;      } 
      elsif ( $ARGV[$i] eq "-ext")     { $forceExtension = 1;  }
    }
  glutInitDisplayString("stencil>=2 rgb double depth");
  glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE | GLUT_STENCIL);

  glutCreateWindow("Shadowy Leapin' Lizards");


  if (glutGet(GLUT_WINDOW_STENCIL_SIZE) <= 1) 
    {
      print "dinoshade.pl: Sorry, I need at least 2 bits of stencil.\n";
      exit(1);
    }

  # Register GLUT callbacks.
  glutDisplayFunc    (\&redraw);
  glutMouseFunc      (\&mouse);
  glutMotionFunc     (\&motion);
  glutVisibilityFunc (\&visible);
  glutKeyboardFunc   (\&key);
  glutSpecialFunc    (\&special);

  glutCreateMenu(\&controlLights);

  glutAddMenuEntry("Toggle motion",                M_MOTION);
  glutAddMenuEntry("-----------------------",      M_NONE);
  glutAddMenuEntry("Toggle light",                 M_LIGHT);
  glutAddMenuEntry("Toggle texture",               M_TEXTURE);
  glutAddMenuEntry("Toggle shadows",               M_SHADOWS);
  glutAddMenuEntry("Toggle reflection",            M_REFLECTION);
  glutAddMenuEntry("Toggle dinosaur",              M_DINOSAUR);
  glutAddMenuEntry("-----------------------",      M_NONE);
  glutAddMenuEntry("Toggle reflection stenciling", M_STENCIL_REFLECTION);
  glutAddMenuEntry("Toggle shadow stenciling",     M_STENCIL_SHADOW);
  glutAddMenuEntry("Toggle shadow offset",         M_OFFSET_SHADOW);
  glutAddMenuEntry("----------------------",       M_NONE);
  glutAddMenuEntry("Positional light",             M_POSITIONAL);
  glutAddMenuEntry("Directional light",            M_DIRECTIONAL);
  glutAddMenuEntry("-----------------------",      M_NONE);
  glutAddMenuEntry("Toggle performance",           M_PERFORMANCE);
  glutAttachMenu(GLUT_RIGHT_BUTTON);

  makeDinosaur();

  glPolygonOffset(-2.0, -1.0);

  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_2D);
  glLineWidth(3.0);

  glMatrixMode(GL_PROJECTION);
  gluPerspective( 40.0,   # field of view -- in degrees
		  1.0,    # aspect ratio
		  20.0,   # z near
		  100.0); # z far
  glMatrixMode(GL_MODELVIEW);
  gluLookAt(0.0, 8.0, 60.0,   # eye is at (0,8,60) 
	    0.0, 8.0, 0.0,    # center is at (0,8,0) 
	    0.0, 1.0, 0.0  ); # up is in postivie Y direction

  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, $lightColor);
  glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 0.1);
  glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 0.05);
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHTING);
  
  makeFloorTexture();

  # Setup floor plane for projected shadow calculations.
  {
    my @fv1 = unpack("f*", $floorVertices[1]);
    my @fv2 = unpack("f*", $floorVertices[2]);
    my @fv3 = unpack("f*", $floorVertices[3]);
    findPlane(\@floorPlane, \@fv1, \@fv2, \@fv3);
  }
  
  glutMainLoop();
}
