#!/usr/bin/perl -w use strict; use OpenGL qw/ :all /; use Math::Trig; eval 'use Time::HiRes qw( gettimeofday )'; my $hasHires = !$@; $|++; # ---------------------- # Based on a cube demo by # Chris Halsall (chalsall@chalsall.com) for the # O'Reilly Network on Linux.com (oreilly.linux.com). # May 2000. # # Translated from C to Perl by J-L Morel # ( http://www.bribes.org/perl/wopengl.html ) # # Updated for FBO, VBO and Vertex/Fragment Program extensions # by Bob Free # ( http://graphcomp.com/opengl ) # use constant PROGRAM_TITLE => "OpenGL Test App"; use constant DO_TESTS => 0; # Some global variables. my $useMipMap = 1; my $hasFBO = 0; my $hasVBO = 0; my $hasFragProg = 0; my $er; # Window and texture IDs, window width and height. my $Window_ID; my $Window_Width = 300; my $Window_Height = 300; my $Inset_Width = 40; my $Inset_Height = 40; # Our display mode settings. my $Light_On = 0; my $Blend_On = 0; my $Texture_On = 1; my $Alpha_Add = 1; my $FBO_On = 0; my $Inset_On = 1; my $Curr_TexMode = 0; my @TexModesStr = qw/ GL_DECAL GL_MODULATE GL_BLEND GL_REPLACE /; my @TexModes = ( GL_DECAL, GL_MODULATE, GL_BLEND, GL_REPLACE ); my($TextureID_image,$TextureID_FBO); my $FrameBufferID; my $RenderBufferID; my $VertexProgID; my $FragProgID; my $FBO_rendered = 0; # Object and scene global variables. my $Teapot_Rot = 0.0; # Cube position and rotation speed variables. my $X_Rot = 0.9; my $Y_Rot = 0.0; my $X_Speed = 0.0; my $Y_Speed = 0.5; my $Z_Off =-5.0; # Settings for our light. Try playing with these (or add more lights). my @Light_Ambient = ( 0.1, 0.1, 0.1, 1.0 ); my @Light_Diffuse = ( 1.2, 1.2, 1.2, 1.0 ); my @Light_Position = ( 2.0, 2.0, 0.0, 1.0 ); # Vertex Buffer Object data my($VertexObjID,$NormalObjID,$ColorObjID,$TexCoordObjID,$IndexObjID); my @verts = ( -1.0, -1.3, -1.0, 1.0, -1.3, -1.0, 1.0, -1.3, 1.0, -1.0, -1.3, 1.0, -1.0, 1.3, -1.0, -1.0, 1.3, 1.0, 1.0, 1.3, 1.0, 1.0, 1.3, -1.0, -1.0, -1.0, -1.3, -1.0, 1.0, -1.3, 1.0, 1.0, -1.3, 1.0, -1.0, -1.3, 1.3, -1.0, -1.0, 1.3, 1.0, -1.0, 1.3, 1.0, 1.0, 1.3, -1.0, 1.0, -1.0, -1.0, 1.3, 1.0, -1.0, 1.3, 1.0, 1.0, 1.3, -1.0, 1.0, 1.3, -1.3, -1.0, -1.0, -1.3, -1.0, 1.0, -1.3, 1.0, 1.0, -1.3, 1.0, -1.0 ); my $verts = OpenGL::Array->new_list(GL_FLOAT,@verts); # Could calc norms on the fly my @norms = ( 0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,-1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0 ); my $norms = OpenGL::Array->new_list(GL_FLOAT,@norms); my @colors = ( 0.9,0.2,0.2,.75, 0.9,0.2,0.2,.75, 0.9,0.2,0.2,.75, 0.9,0.2,0.2,.75, 0.5,0.5,0.5,.5, 0.5,0.5,0.5,.5, 0.5,0.5,0.5,.5, 0.5,0.5,0.5,.5, 0.2,0.9,0.2,.5, 0.2,0.9,0.2,.5, 0.2,0.9,0.2,.5, 0.2,0.9,0.2,.5, 0.2,0.2,0.9,.25, 0.2,0.2,0.9,.25, 0.2,0.2,0.9,.25, 0.2,0.2,0.9,.25, 0.9, 0.2, 0.2, 0.5, 0.2, 0.9, 0.2, 0.5, 0.2, 0.2, 0.9, 0.5, 0.1, 0.1, 0.1, 0.5, 0.9,0.9,0.2,0.0, 0.9,0.9,0.2,0.66, 0.9,0.9,0.2,1.0, 0.9,0.9,0.2,0.33 ); my $colors = OpenGL::Array->new_list(GL_FLOAT,@colors); my @rainbow = ( 0.9, 0.2, 0.2, 0.5, 0.2, 0.9, 0.2, 0.5, 0.2, 0.2, 0.9, 0.5, 0.1, 0.1, 0.1, 0.5 ); my $rainbow = OpenGL::Array->new_list(GL_FLOAT,@rainbow); my $rainbow_offset = 64; my @rainbow_inc; my @texcoords = ( 0.800, 0.800, 0.200, 0.800, 0.200, 0.200, 0.800, 0.200, 0.005, 1.995, 0.005, 0.005, 1.995, 0.005, 1.995, 1.995, 0.995, 0.005, 2.995, 2.995, 0.005, 0.995, -1.995, -1.995, 0.995, 0.005, 0.995, 0.995, 0.005, 0.995, 0.005, 0.005, -0.5, -0.5, 1.5, -0.5, 1.5, 1.5, -0.5, 1.5, 0.005, 0.005, 0.995, 0.005, 0.995, 0.995, 0.005, 0.995 ); my $texcoords = OpenGL::Array->new_list(GL_FLOAT,@texcoords); my @indices = (0..23); my $indices = OpenGL::Array->new_list(GL_UNSIGNED_INT,@indices); # ------ # Frames per second (FPS) statistic variables and routine. use constant CLOCKS_PER_SEC => $hasHires ? 1000 : 1; use constant FRAME_RATE_SAMPLES => 50; my $FrameCount = 0; my $FrameRate = 0; my $last=0; sub ourDoFPS { if (++$FrameCount >= FRAME_RATE_SAMPLES) { my $now = $hasHires ? gettimeofday() : time(); # clock(); my $delta= ($now - $last); $last = $now; $FrameRate = FRAME_RATE_SAMPLES / ($delta || 1); $FrameCount = 0; } } # ------ # String rendering routine; leverages on GLUT routine. sub ourPrintString { my ($font, $str) = @_; my @c = split '', $str; for(@c) { glutBitmapCharacter($font, ord $_); } } # ------ # Does everything needed before losing control to the main # OpenGL event loop. sub ourInit { my ($Width, $Height) = @_; # Set initial colors for rainbow face for (my $i=0; $i<16; $i++) { $rainbow[$i] = rand(1.0); $rainbow_inc[$i] = 0.01 - rand(0.02); } # Initialize VBOs if supported if ($hasVBO) { ($VertexObjID,$NormalObjID,$ColorObjID,$TexCoordObjID,$IndexObjID) = glGenBuffersARB_p(5); glBindBufferARB(GL_ARRAY_BUFFER_ARB, $VertexObjID); glBufferDataARB_p(GL_ARRAY_BUFFER_ARB, $verts, GL_STATIC_DRAW_ARB); glVertexPointer_c(3, GL_FLOAT, 0, 0); if (DO_TESTS) { print "\nTests:\n"; my $size = glGetBufferParameterivARB_p(GL_ARRAY_BUFFER_ARB, GL_BUFFER_SIZE_ARB); print " Vertex Buffer Size (bytes): $size\n"; my $count = $verts->elements(); print " Vertex Buffer Size (elements): $count\n"; my $test = glGetBufferSubDataARB_p(GL_ARRAY_BUFFER_ARB,12,3,GL_FLOAT); my @test = $test->retrieve(0,3); my $ords = join('/',@test); print " glGetBufferSubDataARB_p: $ords\n"; } glBindBufferARB(GL_ARRAY_BUFFER_ARB, $NormalObjID); glBufferDataARB_p(GL_ARRAY_BUFFER_ARB, $norms, GL_STATIC_DRAW_ARB); glNormalPointer_c(GL_FLOAT, 0, 0); glBindBufferARB(GL_ARRAY_BUFFER_ARB, $ColorObjID); glBufferDataARB_p(GL_ARRAY_BUFFER_ARB, $colors, GL_DYNAMIC_DRAW_ARB); $rainbow->assign(0,@rainbow); glBufferSubDataARB_p(GL_ARRAY_BUFFER_ARB, $rainbow_offset, $rainbow); glColorPointer_c(4, GL_FLOAT, 0, 0); glBindBufferARB(GL_ARRAY_BUFFER_ARB, $TexCoordObjID); glBufferDataARB_p(GL_ARRAY_BUFFER_ARB, $texcoords, GL_STATIC_DRAW_ARB); glTexCoordPointer_c(2, GL_FLOAT, 0, 0); glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, $IndexObjID); glBufferDataARB_p(GL_ELEMENT_ARRAY_BUFFER_ARB, $indices, GL_STATIC_DRAW_ARB); } else { glVertexPointer_p(3, $verts); glNormalPointer_p($norms); $colors->assign($rainbow_offset,@rainbow); glColorPointer_p(4, $colors); glTexCoordPointer_p(2, $texcoords); } # Build texture. ($TextureID_image,$TextureID_FBO) = glGenTextures_p(2); ourBuildTextures(); glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL); # Initialize rendering parameters glEnable(GL_TEXTURE_2D); glDisable(GL_LIGHTING); glBlendFunc(GL_SRC_ALPHA,GL_ONE); #glEnable(GL_BLEND); # Color to clear color buffer to. glClearColor(0.1, 0.1, 0.1, 0.0); # Depth to clear depth buffer to; type of test. glClearDepth(1.0); glDepthFunc(GL_LESS); # Enables Smooth Color Shading; try GL_FLAT for (lack of) fun. glShadeModel(GL_SMOOTH); # Load up the correct perspective matrix; using a callback directly. cbResizeScene($Width, $Height); # Set up a light, turn it on. glLightfv_p(GL_LIGHT1, GL_POSITION, @Light_Position); glLightfv_p(GL_LIGHT1, GL_AMBIENT, @Light_Ambient); glLightfv_p(GL_LIGHT1, GL_DIFFUSE, @Light_Diffuse); glEnable(GL_LIGHT1); # A handy trick -- have surface material mirror the color. glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE); glEnable(GL_COLOR_MATERIAL); } # ------ # Function to build a simple full-color texture with alpha channel, # and then create mipmaps. # Also sets up FBO texture and Vertex/Fragment programs. sub ourBuildTextures { # Build Image Texture ($TextureID_image,$TextureID_FBO) = glGenTextures_p(2); my $tex; my $gluerr; my $hole_size = 3300; # ~ == 57.45 ^ 2. # Iterate across the texture array. for(my $y=0; $y<128; $y++) { for(my $x=0; $x<128; $x++) { # A simple repeating squares pattern. # Dark blue on white. if ( ( ($x+4)%32 < 8 ) && ( ($y+4)%32 < 8)) { $tex .= pack "C3", 0,0,120; # Dark blue } else { $tex .= pack "C3", 240, 240, 240; # White } # Make a round dot in the texture's alpha-channel. # Calculate distance to center (squared). my $t = ($x-64)*($x-64) + ($y-64)*($y-64); if ( $t < $hole_size) { $tex .= pack "C", 255; # The dot itself is opaque. } elsif ($t < $hole_size + 100) { $tex .= pack "C", 128; # Give our dot an anti-aliased edge. } else { $tex .= pack "C", 0; # Outside of the dot, it's transparent. } } } glBindTexture(GL_TEXTURE_2D, $TextureID_image); # Use MipMap if ($useMipMap) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); # The GLU library helps us build MipMaps for our texture. if (($gluerr = gluBuild2DMipmaps_s(GL_TEXTURE_2D, GL_RGBA8, 128, 128, GL_RGBA, GL_UNSIGNED_BYTE, $tex))) { printf STDERR "GLULib%s\n", gluErrorString($gluerr); exit(-1); } } # Use normal texture - Note: dimensions must be power of 2 else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D_s(GL_TEXTURE_2D, 0, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, $tex); } # Build FBO texture if ($hasFBO) { ($FrameBufferID) = glGenFramebuffersEXT_p(1); ($RenderBufferID) = glGenRenderbuffersEXT_p(1); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, $FrameBufferID); glBindTexture(GL_TEXTURE_2D, $TextureID_FBO); # Initiate texture glTexImage2D_c(GL_TEXTURE_2D, 0, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); # Bind texture/frame/render buffers glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, $TextureID_FBO, 0); glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, $RenderBufferID); glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT24, 128, 128); glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, $RenderBufferID); # Test status if (DO_TESTS) { my $stat = glCheckFramebufferStatusEXT(GL_RENDERBUFFER_EXT); printf("FBO Status: %04X\n",$stat); } } # Setup Vertex/Fragment Programs to render FBO texture if ($hasFragProg) { ($VertexProgID,$FragProgID) = glGenProgramsARB_p(2); # NOP Vertex shader my $VertexProg = qq {!!ARBvp1.0 TEMP vertexClip; DP4 vertexClip.x, state.matrix.mvp.row[0], vertex.position; DP4 vertexClip.y, state.matrix.mvp.row[1], vertex.position; DP4 vertexClip.z, state.matrix.mvp.row[2], vertex.position; DP4 vertexClip.w, state.matrix.mvp.row[3], vertex.position; MOV result.position, vertexClip; MOV result.color, vertex.color; MOV result.texcoord[0], vertex.texcoord; END }; glBindProgramARB(GL_VERTEX_PROGRAM_ARB, $VertexProgID); glProgramStringARB_p(GL_VERTEX_PROGRAM_ARB, $VertexProg); if (DO_TESTS) { my $format = glGetProgramivARB_p(GL_VERTEX_PROGRAM_ARB,GL_PROGRAM_FORMAT_ARB); printf("glGetProgramivARB_p format: '#%04X'\n",$format); my @params = glGetProgramEnvParameterdvARB_p(GL_VERTEX_PROGRAM_ARB,0); my $params = join(', ',@params); print "glGetProgramEnvParameterdvARB_p: $params\n"; @params = glGetProgramEnvParameterfvARB_p(GL_VERTEX_PROGRAM_ARB,0); $params = join(', ',@params); print "glGetProgramEnvParameterfvARB_p: $params\n"; my $vprog = glGetProgramStringARB_p(GL_VERTEX_PROGRAM_ARB); print "Vertex Prog: $vprog\n"; } # Lazy Metalic Fragment shader my $FragProg = qq {!!ARBfp1.0 TEMP color; MUL color, fragment.texcoord[0].y, 2; ADD color, 1, -color; ABS color, color; ADD result.color, 1.01, -color; MOV result.color.a, 1; END }; glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, $FragProgID); glProgramStringARB_p(GL_FRAGMENT_PROGRAM_ARB, $FragProg); if (DO_TESTS) { my $fprog = glGetProgramStringARB_p(GL_FRAGMENT_PROGRAM_ARB); print "Fragment Prog: $fprog\n"; } } # Select active texture ourSelectTexture(); glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL); } sub ourSelectTexture { glBindTexture(GL_TEXTURE_2D, $FBO_On ? $TextureID_FBO : $TextureID_image); } # ------ # Routine which actually does the drawing sub cbRenderScene { my $buf; # For our strings. # Animated Texture Rendering if ($FBO_On && ($FBO_On == 2 || !$FBO_rendered)) { $FBO_rendered = 1; glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, $FrameBufferID); glPushMatrix(); glTranslatef(-0.35, -0.48, -1.5); glRotatef($Teapot_Rot--, 0.0, 1.0, 0.0); glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushAttrib(GL_ENABLE_BIT); glEnable(GL_DEPTH_TEST); # Run Fragment program for texture if ($hasFragProg) { glEnable(GL_VERTEX_PROGRAM_ARB); glEnable(GL_FRAGMENT_PROGRAM_ARB); } glColor3f(1.0, 1.0, 1.0); #glutSolidTeapot(0.125); glutWireTeapot(0.125); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); if ($hasFragProg) { glDisable(GL_FRAGMENT_PROGRAM_ARB); glDisable(GL_VERTEX_PROGRAM_ARB); } glPopAttrib(); glPopMatrix(); } ourSelectTexture(); # Enables, disables or otherwise adjusts as # appropriate for our current settings. if ($Texture_On) { glEnable(GL_TEXTURE_2D); } else { glDisable(GL_TEXTURE_2D); } if ($Light_On) { glEnable(GL_LIGHTING); } else { glDisable(GL_LIGHTING); } if ($Alpha_Add) { glBlendFunc(GL_SRC_ALPHA,GL_ONE); } else { glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); } # If we're blending, we don'$t want z-buffering. if ($Blend_On) { glDisable(GL_DEPTH_TEST); } else { glEnable(GL_DEPTH_TEST); } # Need to manipulate the ModelView matrix to move our model around. glMatrixMode(GL_MODELVIEW); # Reset to 0,0,0; no rotation, no scaling. glLoadIdentity(); # Move the object back from the screen. glTranslatef(0.0,0.0,$Z_Off); # Rotate the calculated amount. glRotatef($X_Rot,1.0,0.0,0.0); glRotatef($Y_Rot,0.0,1.0,0.0); # Clear the color and depth buffers. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); # Update Rainbow Cube Face for (my $i=0; $i 1) { $rainbow[$i] = 1.0; } else { next; } $rainbow_inc[$i] = -$rainbow_inc[$i]; } if ($hasVBO) { glBindBufferARB(GL_ARRAY_BUFFER_ARB, $ColorObjID); my $color_map = glMapBufferARB_p(GL_ARRAY_BUFFER_ARB,GL_WRITE_ONLY_ARB,GL_FLOAT); #my $buffer = glGetBufferPointervARB_p(GL_ARRAY_BUFFER_ARB,GL_BUFFER_MAP_POINTER_ARB,GL_FLOAT); $color_map->assign($rainbow_offset,@rainbow); glUnmapBufferARB(GL_ARRAY_BUFFER_ARB); } else { $colors->assign($rainbow_offset,@rainbow); glColorPointer_p(4, $colors); } # Render cube glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_NORMAL_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); for (my $i=0; $i 3 ) { $Curr_TexMode=0; } glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,$TexModes[$Curr_TexMode]); } elsif ($c eq 'T') { $Texture_On = !$Texture_On; } elsif ($c eq 'A') { $Alpha_Add = !$Alpha_Add; } elsif ($c eq 'F' && $hasFBO) { $FBO_On = ($FBO_On+1) % 3; ourSelectTexture(); } elsif ($c eq 'I') { $Inset_On = !$Inset_On; } elsif ($c eq 'S' or $key == 32) { $X_Speed=$Y_Speed=0; } elsif ($c eq 'R') { $X_Speed = -$X_Speed; $Y_Speed = -$Y_Speed; } else { printf "KP: No action for %d.\n", $key; } } # ------ # Callback Function called when a special $key is pressed. sub cbSpecialKeyPressed { my $key = shift; if ($key == GLUT_KEY_PAGE_UP) { $Z_Off -= 0.05; } elsif ($key == GLUT_KEY_PAGE_DOWN) { $Z_Off += 0.05; } elsif ($key == GLUT_KEY_UP) { $X_Speed -= 0.01; } elsif ($key == GLUT_KEY_DOWN) { $X_Speed += 0.01; } elsif ($key == GLUT_KEY_LEFT) { $Y_Speed -= 0.01; } elsif ($key == GLUT_KEY_RIGHT) { $Y_Speed += 0.01; } else { printf "SKP: No action for %d.\n", $key; } } # ------ # Callback routine executed whenever our window is resized. Lets us # request the newly appropriate perspective projection matrix for # our needs. Try removing the gluPerspective() call to see what happens. sub cbResizeScene { my ($Width, $Height) = @_; # Let's not core dump, no matter what. $Height = 1 if ($Height == 0); glViewport(0, 0, $Width, $Height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0,$Width/$Height,0.1,100.0); glMatrixMode(GL_MODELVIEW); $Window_Width = $Width; $Window_Height = $Height; } # ------ # The main() function. Inits OpenGL. Calls our own init function, # then passes control onto OpenGL. eval {glutInit(); 1} or die "This test requires GLUT:\n$@\n"; # To see OpenGL drawing, take out the GLUT_DOUBLE request. glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH | GLUT_ALPHA); glutInitWindowSize($Window_Width, $Window_Height); # Open a window $Window_ID = glutCreateWindow( PROGRAM_TITLE ); # Get OpenGL Info print "\n"; print PROGRAM_TITLE; print ' (using hires timer)' if ($hasHires); print "\n\n"; my $version = glGetString(GL_VERSION); my $vendor = glGetString(GL_VENDOR); my $renderer = glGetString(GL_RENDERER); print "OpenGL installation: $version\n$vendor\n$renderer\n\n"; print "Installed extensions (* implemented in the module):\n"; my $extensions = glGetString(GL_EXTENSIONS); my @extensions = split(' ',$extensions); foreach my $ext (sort @extensions) { my $stat = OpenGL::glpCheckExtension($ext); printf("%s $ext\n",$stat?' ':'*'); print(" $stat\n") if ($stat && $stat !~ m|^$ext |); } if (!OpenGL::glpCheckExtension('GL_ARB_vertex_buffer_object')) { $hasVBO = 1; } if (!OpenGL::glpCheckExtension('GL_EXT_framebuffer_object')) { $hasFBO = 1; $FBO_On = 1; if (!OpenGL::glpCheckExtension('GL_ARB_fragment_program')) { $hasFragProg = 1; $FBO_On++; } } # Register the callback function to do the drawing. glutDisplayFunc(\&cbRenderScene); # If there's nothing to do, draw. glutIdleFunc(\&cbRenderScene); # It's a good idea to know when our window's resized. glutReshapeFunc(\&cbResizeScene); # And let's get some keyboard input. glutKeyboardFunc(\&cbKeyPressed); glutSpecialFunc(\&cbSpecialKeyPressed); # OK, OpenGL's ready to go. Let's call our own init function. ourInit($Window_Width, $Window_Height); # Print out a bit of help dialog. print qq { Hold down arrow keys to rotate, 'R' to reverse, 'S' to stop. Page up/down will move cube away from/towards camera. Use first letter of shown display mode settings to alter. Q or [Esc] to quit; OpenGL window must have focus for input. }; # Pass off control to OpenGL. # Above functions are called as appropriate. glutMainLoop(); __END__