File:  [Local Repository] / thwomper / thwomper
Revision 1.1.1.1 (vendor branch): download - view: text, annotated - select for diffs
Sat Mar 28 03:38:35 2009 UTC (15 years, 1 month ago) by nick
Branches: thwomper, MAIN
CVS tags: v1_0, HEAD
Initial import of thwomper

#!/usr/bin/perl -w

=begin comment info
Thwomper
Nicholas DeClario 2009
nick@declario.com

Thwomper is a fully interactive program for babies, stimulating many senses.  Thwomper allows babies and toddlers to safely hit the computer's keyboard and will display shapes, letters and numbers accompanied by different sounds with each keypress.  Written in Perl and taking advantage of the Perl OpenGL (pogl) libraries, Thwomper was written for Linux-based operating systems but should be portable to any OS supporting Perl.

Thwomper is based on the excellent program by Scott Hanselman, BabySmash!

=end comment info
=cut

use strict;
use Data::Dumper;
use OpenGL 0.5604 qw/ :all /;
use OpenGL::Image;
use Time::HiRes qw/ usleep setitimer ITIMER_REAL time /;
use SDL;
use SDL::Mixer;
use SDL::Sound;
####  use Festival::Client;

use constant PROGRAM_TITLE => "Thwomper";
use constant DO_TESTS 	   => 0;
use constant ESCAPE 	   => 27;
use constant ZMIN 	   => -100;
use constant ZMAX 	   => -12;

my $version	  = 0.1;
my ( $windowWidth, $windowHeight ) = &fetchScreenRes( );
my $screenRatio   = $windowWidth / $windowHeight;
my $imageBase	  = "images";
my $soundBase	  = "sounds";
my @objects       = ( );
my @LightAmbient  = ( 0.5, 0.5, 0.5, 1.0 );	## Ambient Light Values
my @LightDiffuse  = ( 1.0, 1.0, 1.0, 1.0 );	## Diffuse Light Values
my @LightPosition = ( 0.0, 0.0, 2.0, 1.0 );	## Light Position
my @textureFiles  = ( "characters-01.png", "characters-02.png", "characters-03.png" );
my @textureInfo = ( );
## my $festival;
my $objTextures;
my @characters = &populateCharacters( );
my @shapes = &populateShapes( );
my $texShift = 0.050;
my $fs = 0;
my $fadeInterval = 0.05;   # milliseconds
my $fullscreen = 0;
my $sdKey = 0;
my @soundFiles = glob( "$soundBase/*.[Ww][Aa][Vv]" );
my @sounds = ( );
my $lastKey = 0;

print "Starting thwomper version $version\n";

print "Initializing SDL Sound System\n";

my $mixer = new SDL::Mixer(
	-frequency	=> 44100,
	-channels	=> 2,
	-size		=> 1024
);

##
## Load all our sound effects up
##
foreach ( @soundFiles ) {
	my $sound = new SDL::Sound( $_ );
	push @sounds, $sound;	
}

print "Initializing GLUT\n";

##
## Confirm we have the proper image engines installed
##
my $engines = OpenGL::Image::GetEngines( ) || die( "OpenGL::Image error!\n" );
my $ok = OpenGL::Image::HasEngine( 'Magick' ) || die( "OpenGL::Image: Error: Missing Magick engine!\n" );

##
## Set up Glut
##
eval { glutInit( ); 1 } or die ( "ERROR: GLUT not found: $!\n" );

$SIG{CHLD} = 'IGNORE';
local $SIG{HUP}  = $SIG{INT} = $SIG{TERM} = \&shutdown;
local $SIG{ALRM} = \&fadeChecker;

&setitimer( ITIMER_REAL, 1, $fadeInterval );

glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH | GLUT_ALPHA );
glutInitWindowSize( $windowWidth, $windowHeight );
my $windowID = glutCreateWindow( PROGRAM_TITLE );

##
## Enter fullscreen game mode
##
my $gameModeString = $windowWidth . "x" . $windowHeight;
glutGameModeString( $gameModeString );
glutEnterGameMode();

&myGlutInit( );
init( $windowWidth, $windowHeight );

glutMainLoop( );

###############################################################################
##
##
##
###############################################################################
sub renderScene {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         ## Clear The Screen And The Depth Buffer

	foreach my $obj ( @objects ) {
		next if ( ! exists $obj->{'fade'} ); ## Object was deleted earlier

		glLoadIdentity();    
		glMatrixMode( GL_MODELVIEW );
		glEnable( GL_TEXTURE_2D );
		glEnable( GL_ALPHA_TEST );
		glAlphaFunc( GL_GREATER, 0.1 );
	
		if ( $obj->{'fade'} ) {
			glDepthMask( 0 );	
			glBlendFunc( GL_ONE, GL_ONE );
		}

		glTranslatef( $obj->{'locX'}, $obj->{'locY'}, $obj->{'zoom'} );

		glRotatef( $obj->{'xRot'}, 1.0, 0.0, 0.0);             ## Rotate On The X Axis
		glRotatef( $obj->{'yRot'}, 0.0, 1.0, 0.0);             ## Rotate On The Y Axis
		glRotatef( $obj->{'zRot'}, 0.0, 0.0, 1.0);             ## Rotate On The Z Axis

		&selectTexture( $obj->{'tfOffset'} );

		if ( $obj->{'fade'} ) {
			$obj->{'fadeValue'} -= 0.01;
			glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
			glEnable(GL_BLEND);
		}

		glColor4f( @{$obj->{'color'}}, $obj->{'fadeValue'} );
		glColor4f( 1.0, 1.0, 1.0, $obj->{'fadeValue'} );
	
		glBegin(GL_QUADS);                          ## begin drawing a square

			## Front Face (note that the texture's corners have to match the quad's corners)
			glTexCoord2f( ($texShift*$obj->{'offset'}), 0.0); 
			glVertex3f(-0.75, -0.75,  0.75);  ## Bottom Left Of The Texture and Quad
			glTexCoord2f(($texShift*$obj->{'offset'}+$texShift), 0.0); 
			glVertex3f( 0.75, -0.75,  0.75);  ## Bottom Right Of The Texture and Quad
			glTexCoord2f(($texShift*$obj->{'offset'}+$texShift), 1.0); 
			glVertex3f( 0.75,  0.75,  0.75);  ## Top Right Of The Texture and Quad
			glTexCoord2f(($texShift*$obj->{'offset'}), 1.0); 
			glVertex3f(-0.75,  0.75,  0.75);  ## Top Left Of The Texture and Quad

		glEnd();                                    

		glDepthMask( 1 ) if ( $obj->{'fade'} );

#		$obj->{'xRot'} += 5.3;
#		$obj->{'yRot'} += 5.3;

		if ( $obj->{'zoom'} <= ZMAX ) {
			$obj->{'zoom'} += 1.85;
			$obj->{'zRot'} += 30.0;
		}
	}

	foreach my $i ( 0..$#objects ) {
		next if ( ! defined $objects[$i]->{'fadeValue'} );
		delete $objects[$i] if $objects[$i]->{'fadeValue'} <= 0;
	}

	##
	## Display the text in the lower left
	##

        # Move back to the origin (for the text, below).
        glLoadIdentity( );

        # We need to change the projection matrix for the text rendering.
        glMatrixMode( GL_PROJECTION );

        # But we like our current view too; so we save it here.
        glPushMatrix( );

        glLoadIdentity( );
        glOrtho( 0, $windowWidth, 0, $windowHeight, -1.0, 1.0 );
	glDisable( GL_TEXTURE_2D );
	glDisable( GL_LIGHTING );
        glDisable( GL_DEPTH_TEST );
        glColor4f( 0.0, 0.0, 0.0, 0.75 );

        glRasterPos2i( 2, 1 );
        ourPrintString( GLUT_BITMAP_HELVETICA_12, "Thwomper v$version by Nicholas DeClario" );
        glRasterPos2i( 2, 14 );
        ourPrintString( GLUT_BITMAP_HELVETICA_12, "To quit press ESC, F5 and F12 in order." );

      
        glPopMatrix( );

        glMatrixMode( GL_MODELVIEW );

	glutSwapBuffers( );
	&usleep( 25000 );
}

###############################################################################
##
## String rendering routine; leverages on GLUT routine.
## 
##  Taken from pogl test cube code
##
###############################################################################
sub ourPrintString
{
  my ($font, $str) = @_;
  my @c = split '', $str;

  for(@c)
  {
    glutBitmapCharacter($font, ord $_);
  }
}


###############################################################################
###############################################################################
sub reshapeWindow {
	my ( $width, $height ) = @_;
	
	$height = 1 if ( ! $height );

	glViewport( 0 , 2, $width, $height );
	
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity( );	# Reset the Projection Matrix
	gluPerspective( 45.0, $width/$height, 0.1, 100.0 );
	
	glMatrixMode( GL_MODELVIEW );

	$windowWidth  = $width;
	$windowHeight = $height;
	$screenRatio  = $width / $height;
}


###############################################################################
###############################################################################
sub keyPressed {
	my ( $key, $x, $y ) = @_;

	usleep( 100 ); ## Avoid thrashing this function

	return 0 if ( $key == $lastKey );

	if ( $key eq ESCAPE ) {
		$sdKey = ESCAPE;
	}
	else {	## Any other key creates a new object
		$sdKey = 0;
		$lastKey = $key;
		&createObject( $key );
	}
}


###############################################################################
###############################################################################
sub specialKeyPressed {
        my ( $key, $x, $y ) = @_;

        usleep( 100 ); ## Avoid thrashing this function

	return 0 if ( $key == $lastKey );

	$lastKey = $key;

	if ( $key == 5 ) {
		$sdKey = $sdKey == ESCAPE ? 5 : 0;
		&createObject( );
	}
	elsif ( $key == 12 ) {
		&shutdown if ( $sdKey == 5 );
		&createObject( );
	}
	else {
		$sdKey = 0;
		&createObject( );
	}
}

###############################################################################
###############################################################################
sub createObject {
	my $key = shift || "RAND";
	my $newObj = { };

	$newObj->{'zoom'}  = ZMIN;
	$newObj->{'locX'}  = rand(10);
	$newObj->{'locY'}  = rand(9);
	$newObj->{'xRot'}  = 0;
	$newObj->{'yRot'}  = 0;
	$newObj->{'zRot'}  = 0;
	$newObj->{'timer'} = 3;
	$newObj->{'fade'}  = 0;
	$newObj->{'fadeValue'}  = 0.80;
	$newObj->{'color'} = [ 1, 1, 1 ];

	if ( $key ne "RAND" ) {
		my $f = 0;
		foreach my $i ( 0..$#characters ) {
			$f++ if ( ( $characters[$i]->{'code'}[0] == $key ) || 
 			          ( $characters[$i]->{'code'}[1] == $key ) );
		}
		$key = "RAND" if ( ! $f );
	}

	if ( $key eq "RAND" ) {
		$newObj->{'key'}      = int rand( ( $#shapes + 1 ) );
		$newObj->{'offset'}   = 0;
		$newObj->{'tfOffset'} = 2;
		
		foreach my $i ( 0 .. $#shapes ) {
			$newObj->{'offset'} = $i if ( $i == $newObj->{'key'} );
		}
	}	
	else {
		$newObj->{'key'}   = $key;
		$newObj->{'offset'} = 1;
		$newObj->{'tfOffset'} = 0;

		foreach my $i ( 0 .. $#characters ) {
			if ( ( $characters[$i]->{'code'}[0] == $key ) || 
 			     ( $characters[$i]->{'code'}[1] == $key ) ) {
				$newObj->{'offset'} = $i;
				$newObj->{'tfOffset'} = $characters[$i]->{'offset'};
			}
		}
	}

	&playSound( );
#	&playSpeech( $characters[$newObj->{'offset'}]->{'speak'} );

	##
	## Determine characters final destination
	##
	$newObj->{'locX'} -= 10 if ( $newObj->{'locX'} > 5 );
	$newObj->{'locY'} -= 10 if ( $newObj->{'locY'} > 5 );

	$newObj->{'locX'} *= ( $screenRatio / 1.25 );  ## Compensate for wide-screen displays

	push @objects, $newObj;
}

###############################################################################
###############################################################################
sub LoadTextures {
	foreach my $file ( sort @textureFiles ) {
		my $ti = ( );

		$ti->{'fileName'} = "images/" . $file;

		( $ti->{'texID_image'}, $ti->{'texID_FBO'} ) = glGenTextures_p( 2 );

		if ( -e $ti->{'fileName'} ) {
			my $img = new OpenGL::Image( source => $ti->{'fileName'} );
			( $ti->{'texWidth'}, $ti->{'texHeight'} ) = $img->Get( 'width', 'height' );
			my $alpha = $img->Get( 'alpha' ) ? 'has' : 'no';
			print "Loading texture: " . $ti->{'texWidth'} .
                              " x " . $ti->{'texHeight'} . ", " . $alpha . " alpha\n";

			( $ti->{'texType'}, $ti->{'texFormat'}, $ti->{'texSize'} ) = 
				$img->Get( 'gl_internalformat', 'gl_format', 'gl_type');
		
			$ti->{'texImage'}  = $img;
	 		$ti->{'texPixels'} = $img->GetArray( );

			glBindTexture( GL_TEXTURE_2D, $ti->{'texID_image'} );

			glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
			glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
			glTexImage2D_c( GL_TEXTURE_2D, 0, $ti->{'texType'}, $ti->{'texWidth'}, 
			       		$ti->{'texHeight'}, 0, $ti->{'texFormat'}, 
			       		$ti->{'texSize'}, $ti->{'texPixels'}->ptr( ) );
		}

		push @textureInfo, $ti;
	}

	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_BLEND);
}

###############################################################################
###############################################################################
sub selectTexture {
	my $offset = shift || 0;

	glBindTexture( GL_TEXTURE_2D, $textureInfo[$offset]->{'texID_image'} );

	#glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL );
	#glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE, GL_BLEND, GL_REPLACE, GL_DECAL );
}


###############################################################################
###############################################################################
sub fadeChecker {
	foreach my $i ( 0..$#objects ) {
		next if ( ! defined $objects[$i]->{'timer'} );
		if ( $objects[$i]->{'timer'} > 0 ) {
			## Start fading the objects[$i]ect
			$objects[$i]->{'timer'} -= $fadeInterval;
			$objects[$i]->{'fade'} = 1 if ( $objects[$i]->{'timer'} <= 0 );
		}
		else {
			## Delete the objects[$i]ect if we are done fading
			if ( $objects[$i]->{'fadeValue'} <= 0 ) {
				delete $objects[$i];
			}
		}
	}	
}

sub myGlutInit {
	glutDisplayFunc( \&renderScene );
	glutIdleFunc( \&renderScene );
	glutReshapeFunc( \&reshapeWindow );
	glutKeyboardFunc( \&keyPressed );
	glutSpecialFunc( \&specialKeyPressed );
}

###############################################################################
##
## &init( );
##
##	Our own init function.  Set anything up we need before handing
##	control over to the main glutMainLoop function.
## 
###############################################################################
sub init {
	my ( $width, $height ) = @_;

##	$festival = Festival::Client->new( "localhost" );

	glutSetCursor(GLUT_CURSOR_NONE);

	glClearColor( 1.0, 1.0, 1.0, 0.0 );
	glClearDepth( 1.0 );
	glDepthFunc( GL_GREATER );

	glEnable( GL_TEXTURE_2D | GL_BLEND | GL_DECAL );#| GL_DEPTH_TEST );
#	glDisable( GL_LIGHTING );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE );
	glShadeModel( GL_SMOOTH );

	LoadTextures( );
#	glTexEnvf( GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE, GL_DECAL );

	&reshapeWindow( $width, $height );

	glLightfv_p( GL_LIGHT1, GL_AMBIENT, @LightAmbient );
	glLightfv_p( GL_LIGHT1, GL_DIFFUSE, @LightDiffuse );
	glLightfv_p( GL_LIGHT1, GL_POSITION, @LightPosition );
	glEnable( GL_LIGHT1 );


}

###############################################################################
###############################################################################
sub shutdown {
	my $sigName = shift;

	glutDestroyWindow( $windowID );
	print "$0: Shutdown (SIG$sigName) receieved.\n" if ( defined $sigName );

	exit;
}

###############################################################################
##
## fetchScreenRes
##
##  Attempts to determine the current screen resolution used for full screen
##  mode.  If all attemps fail it will fall back to 640x480
##
###############################################################################
sub fetchScreenRes {
	my $screenWidth  = 640;
	my $screenHeight = 480;

	my $results = `xrandr`;
	
	if ( $results =~ m/.*current\s(\d+)\sx\s(\d+),.*/ ) {
		$screenWidth = $1;
		$screenHeight = $2;
	}

	return ( $screenWidth, $screenHeight );
}

###############################################################################
###############################################################################
sub playSound {
	$mixer->play_channel( 1, $sounds[rand($#sounds)], 0 );
}

###############################################################################
###############################################################################
sub playSpeech {
	my $string = shift || return 0;

#	$festival->say( $string );
}

###############################################################################
###############################################################################
sub populateShapes {
	my @shapes = (
		{
			name	=> 'square',
			speak	=> 'square',
		},
		{
			name	=> 'rectangle',
			speak	=> 'rectable',
		},
		{
			name	=> 'star',
			speak	=> 'star',
		},
		{
			name	=> 'triangle',
			speak	=> 'triangle',
		},
		{
			name	=> 'circle',
			speak	=> 'circle',
		},
		{
			name	=> 'trapazoid',
			speak	=> 'trapazoid',
		},
	);
}

###############################################################################
###############################################################################
sub populateCharacters {
	my @chars = ( 
		{
			letter	=> 'a',
			code	=> [ 97, 65 ],
			speak	=> 'aye',
			offset  => 0,
		},
		{
			letter	=> 'b',
			code	=> [ 98, 66 ],
			speak	=> 'bee',
			offset  => 0,
		},
		{
			letter	=> 'c',
			code	=> [ 99, 67 ],
			speak	=> 'see',
			offset  => 0,
		},	
		{
			letter	=> 'd',
			code	=> [ 100, 68 ],
			speak	=> 'dee',
			offset  => 0,
		},
		{
			letter	=> 'e',
			code	=> [ 101, 69 ],
			speak	=> 'ee',
			offset  => 0,
		},
		{
			letter	=> 'f',
			code	=> [ 102, 70 ],
			speak	=> 'ef',
			offset  => 0,
		},
		{
			letter	=> 'g',
			code	=> [ 103, 71 ],
			speak	=> 'gee',
			offset  => 0,
		},
		{
			letter	=> 'h',
			code	=> [ 104, 72 ],
			speak	=> 'h',
			offset  => 0,
		},
		{
			letter	=> 'i',
			code	=> [ 105, 73 ],
			speak	=> 'eye',
			offset  => 0,
		},
		{
			letter	=> 'j',
			code	=> [ 106, 74 ],
			speak	=> 'jay',
			offset  => 0,
		},
		{
			letter	=> 'k',
			code	=> [ 107, 75 ],
			speak	=> 'kay',
			offset  => 0,
		},
		{
			letter	=> 'l',
			code	=> [ 108, 76 ],
			speak	=> 'el',
			offset  => 0,
		},
		{
			letter	=> 'm',
			code	=> [ 109, 77 ],
			speak	=> 'em',
			offset  => 0,
		},
		{
			letter	=> 'n',
			code	=> [ 110, 78 ],
			speak	=> 'en',
			offset  => 0,
		},
		{
			letter	=> 'o',
			code	=> [ 111, 79 ],
			speak	=> 'oh',
			offset  => 0,
		},
		{
			letter	=> 'p',
			code	=> [ 112, 80 ],
			speak	=> 'pee',
			offset  => 0,
		},
		{
			letter	=> 'q',
			code	=> [ 113, 81 ],
			speak	=> 'que',
			offset  => 0,
		},
		{
			letter	=> 'r',
			code	=> [ 114, 82 ],
			speak	=> 'are',
			offset  => 0,
		},
		{
			letter	=> 's',
			code	=> [ 115, 83 ],
			speak	=> 'es',
			offset  => 0,
		},
		{
			letter	=> 't',
			code	=> [ 116, 84 ],
			speak	=> 'tee',
			offset  => 0,
		},
		#### Second Texture begins here
		{
			letter	=> 'u',
			code	=> [ 117, 85 ],
			speak	=> 'you',
			offset  => 1,
		},
		{
			letter	=> 'v',
			code	=> [ 118, 86 ],
			speak	=> 'vee',
			offset  => 1,
		},
		{
			letter	=> 'w',
			code	=> [ 119, 87 ],
			speak	=> 'vee',
			offset  => 1,
		},
		{
			letter	=> 'x',
			code	=> [ 120, 88 ],
			speak	=> 'vee',
			offset  => 1,
		},
		{
			letter	=> 'y',
			code	=> [ 121, 89 ],
			speak	=> 'vee',
			offset  => 1,
		},
		{
			letter	=> 'z',
			code	=> [ 122, 90 ],
			speak	=> 'vee',
			offset  => 1,
		},
		## Numbers begin here, same texture file
		{
			letter	=> '1',
			code	=> [ 49, -1 ],
			speak	=> 'one',
			offset	=> 1,
		},
		{
			letter	=> '2',
			code	=> [ 50, -1 ],
			speak	=> 'two',
			offset	=> 1,
		},
		{
			letter	=> '3',
			code	=> [ 51, -1 ],
			speak	=> 'three',
			offset	=> 1,
		},
		{
			letter	=> '4',
			code	=> [ 52, -1 ],
			speak	=> 'four',
			offset	=> 1,
		},
		{
			letter	=> '5',
			code	=> [ 53, -1 ],
			speak	=> 'five',
			offset	=> 1,
		},
		{
			letter	=> '6',
			code	=> [ 54, -1 ],
			speak	=> 'six',
			offset	=> 1,
		},
		{
			letter	=> '7',
			code	=> [ 55, -1 ],
			speak	=> 'seven',
			offset	=> 1,
		},
		{
			letter	=> '8',
			code	=> [ 56, -1 ],
			speak	=> 'eight',
			offset	=> 1,
		},
		{
			letter	=> '9',
			code	=> [ 57, -1 ],
			speak	=> 'nine',
			offset	=> 1,
		},
		{
			letter	=> '0',
			code	=> [ 48, -1 ],
			speak	=> 'zero',
			offset	=> 1,
		},
	);

	return @chars;
}

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>