This document discusses the NeHe7 tutorial by Jeff Molofee. It introduces mip-mapped textures, and Contex support for lighting and keyboard-event callbacks.
See NeHe6 for discussion of the Context
setup procedure here...
from OpenGLContext import testingcontext
BaseContext, MainFunction = testingcontext.getInteractive()
from OpenGL.GL import *
The tutorial uses the GLU function gluBuild2DMipmaps, so we make the
GLU functions available and continue with our normal setup routines.
from OpenGL.GLU import *
import time
from Image import open
class TestContext( BaseContext ):
usage ="""Demonstrates filter functions:
press 'f' to toggle filter functions
press 'l' to toggle lighting
press '<pageup>' to speed up rotation
press '<pagedown>' to slow down rotation
"""
initialPosition = (0,0,0) # set initial camera position, tutorial does the re-positioning
def OnInit( self ):
"""Load the image on initial load of the application"""
As required by the tutorial, we load a single image as 3 different textures, see the loadImages method below for the new code. The OnInit handler loads the textures and stores their textureIDs as a list of integers. It then does some basic instance setup bookkeeping.
self.imageIDs = self.loadImages()
self.currentFilter = 0 # index into imageIDs
self.lightsOn = 1 # boolean
self.currentZOffset = -6
self.rotationCycle = 8.0
Now we register the keyboard callback functions. We register
for "keypress" events, which are the "character" events from the
keyboard, which allows repetition-by-holding in most Context
environments (PyGame being the notable exception). See below for
the definitions of the various callback methods.
## Adds event handlers for the given keys
# Note that these are different bindings from the tutorial,
# as you can wander around with the arrow keys already in
# OpenGLContext
self.addEventHandler(
'keypress', name = 'f', function = self.OnFilter
)
self.addEventHandler(
'keypress', name = 'l', function = self.OnLightToggle
)
self.addEventHandler(
'keypress', name = '<pageup>', function = self.OnSpeedUp
)
self.addEventHandler(
'keypress', name = '<pagedown>', function = self.OnSlowDown
)
print self.usage
As per the tutorial, we set up a global light-source which can be
enabled/disabled by the 'l' callback.
glLightfv( GL_LIGHT1, GL_AMBIENT, (0.2, .2, .2, 1.0) );
glLightfv(GL_LIGHT1, GL_DIFFUSE, (.8,.8,.8));
glLightfv(GL_LIGHT1, GL_POSITION, (-2,0,3,1) );
As in the previous tutorial, we use PIL's open function to get the image data. Because the image is paletted, we convert it to RGB before storing it. The rest of the loadImages method is taken directly from the tutorial.
def loadImages( self, imageName = "nehe_crate.bmp" ):
"""Load an image from a file using PIL,
produces 3 textures to demo filter types.
Converts the paletted image to RGB format.
"""
im = open(imageName)
try:
## Note the conversion to RGB the crate bitmap is paletted!
im = im.convert( 'RGB')
ix, iy, image = im.size[0], im.size[1], im.tostring("raw", "RGBA", 0, -1)
except SystemError:
ix, iy, image = im.size[0], im.size[1], im.tostring("raw", "RGBX", 0, -1)
IDs = []
# a Nearest-filtered texture...
ID = glGenTextures(1)
IDs.append( ID )
glBindTexture(GL_TEXTURE_2D, ID)
glPixelStorei(GL_UNPACK_ALIGNMENT,1)
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexImage2D(GL_TEXTURE_2D, 0, 3, ix, iy, 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
# linear-filtered
ID = glGenTextures(1)
IDs.append( ID )
glBindTexture(GL_TEXTURE_2D, ID)
glPixelStorei(GL_UNPACK_ALIGNMENT,1)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, 3, ix, iy, 0, GL_RGBA, GL_UNSIGNED_BYTE, image)
# linear + mip-mapping
ID = glGenTextures(1)
IDs.append( ID )
glBindTexture(GL_TEXTURE_2D, ID)
glPixelStorei(GL_UNPACK_ALIGNMENT,1)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST)
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, ix, iy, GL_RGBA, GL_UNSIGNED_BYTE, image)
return IDs
The Lights method of the Context is called if there is no scenegraph. It is called before the Render method. In this code, it merely checks to see if the global light is to be enabled or disabled, then goes about doing the appropriate thing.
def Lights (self, mode = 0):
''' Setup the global lights for your scene, called by context before rendering'''
if self.lightsOn:
glEnable( GL_LIGHTING )
glEnable(GL_LIGHT1);
glDisable(GL_LIGHT0);
else:
glDisable( GL_LIGHTING )
glDisable(GL_LIGHT1);
glDisable(GL_LIGHT0);
We've seen just about everything in the Render and OnIdle methods before. The only difference is that we now use the texture ID corresponding to the currently-bound texture (as chosen by pressing the 'f' key).
def Render( self, mode = 0):
BaseContext.Render( self, mode )
glTranslatef(1.5,0.0,self.currentZOffset);
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.imageIDs[self.currentFilter])
glRotated( time.time()%(self.rotationCycle)/self.rotationCycle * -360, 1,0,0)
self.drawCube()
### Callback-handlers
def OnIdle( self, ):
self.triggerRedraw(1)
return 1
The various keyboard-callback methods. Each of these simply updates state in the Context which was initialised by OnInit so that the next rendering pass will reflect the updated values.
def OnFilter( self, event):
"""Handles the key event telling us to change the filter"""
self.currentFilter = (self.currentFilter + 1 ) % 3
print 'Drawing filter now %s'% (
["Nearest","Linear","Linear Mip-Mapped"][ self.currentFilter]
)
def OnLightToggle( self, event ):
"""Handles the key event telling us to toggle the lighting"""
self.lightsOn = not self.lightsOn
print "Lights now %s"% (["off", "on"][self.lightsOn])
def OnSpeedUp( self, event):
"""Handles key event to speed up"""
self.rotationCycle = self.rotationCycle /2.0
def OnSlowDown( self, event ):
"""Handles key event to slowdown"""
self.rotationCycle = self.rotationCycle * 2.0
The drawCube method has only one minor change, the addition of Normals, which allow the faces to interact with the newly-defined lighting.
def drawCube( self ):
"Draw a cube with both normals and texture coordinates"
glBegin(GL_QUADS);
glNormal3f( 0.0, 0.0, 1.0)
glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, 1.0);
glTexCoord2f(1.0, 0.0); glVertex3f( 1.0, -1.0, 1.0);
glTexCoord2f(1.0, 1.0); glVertex3f( 1.0, 1.0, 1.0);
glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, 1.0);
glNormal3f( 0.0, 0.0,-1.0);
glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, -1.0);
glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, 1.0, -1.0);
glTexCoord2f(0.0, 1.0); glVertex3f( 1.0, 1.0, -1.0);
glTexCoord2f(0.0, 0.0); glVertex3f( 1.0, -1.0, -1.0);
glNormal3f( 0.0, 1.0, 0.0)
glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, -1.0);
glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, 1.0, 1.0);
glTexCoord2f(1.0, 0.0); glVertex3f( 1.0, 1.0, 1.0);
glTexCoord2f(1.0, 1.0); glVertex3f( 1.0, 1.0, -1.0);
glNormal3f( 0.0,-1.0, 0.0)
glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, -1.0, -1.0);
glTexCoord2f(0.0, 1.0); glVertex3f( 1.0, -1.0, -1.0);
glTexCoord2f(0.0, 0.0); glVertex3f( 1.0, -1.0, 1.0);
glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, 1.0);
glNormal3f( 1.0, 0.0, 0.0)
glTexCoord2f(1.0, 0.0); glVertex3f( 1.0, -1.0, -1.0);
glTexCoord2f(1.0, 1.0); glVertex3f( 1.0, 1.0, -1.0);
glTexCoord2f(0.0, 1.0); glVertex3f( 1.0, 1.0, 1.0);
glTexCoord2f(0.0, 0.0); glVertex3f( 1.0, -1.0, 1.0);
glNormal3f(-1.0, 0.0, 0.0)
glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, -1.0);
glTexCoord2f(1.0, 0.0); glVertex3f(-1.0, -1.0, 1.0);
glTexCoord2f(1.0, 1.0); glVertex3f(-1.0, 1.0, 1.0);
glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, -1.0);
glEnd()
And finally, our old friend to make the module executable as a script.
if __name__ == "__main__":
## We only want to run the main function if we
## are actually being executed as a script
MainFunction ( TestContext)
You can find the complete code for this sample in
OpenGLContext/tests/nehe7.py