Q Solutions

Purpose

This document describes generating 3D scenes from within Tcl using the openGL graphics API.

Overview

Audience

People need to have an in depth inderstranding of Tcl programming as well as using extensions for Tcl. An understanding of the openGL graphics API is needed to appreciate how the scene is generated.

Getting Started

The full code is not listed here. Only the code snipits needed to demonstrate the

Define Togl Window

	togl .fr.toglwin -width 400 -height 400 -double true \
				-alpha true -depth true  -rgba true -privatecmap false \
                 -createproc tclCreateFunc \
                 -reshapeproc tclReshapeFunc \
                 -displayproc tclDisplayFunc 
We create a 3D widget and pass -alpha - Need an alpha buffer to enable smoothing. -depth Need a depth buffer for correct z perception -rgba Run in True colour mode. The next three options are the ones that interface to our TCL code. -createproc Called Once when the GL window is first created. -reshapeproc Called Once everytime the widget is resized. -displayproc Called Once whenever the window needs to be refreshed.

The Create Function

The purpose of this function is to establish the correct GL environment for rendering our drawing.

Tclogl

Given that Togl can now call a Tcl procedure to generate the 3D display, we now need some mechanism to call the GL api from Tcl. This is done via that tclogl extension. This maps each GL API Function into a Tcl command.

Create Function

proc tclCreateFunc {toglwin} {
	variable Scene
    eval glClearColor $Scene(BACKGROUND) 1.0
    glMaterialf GL_FRONT_AND_BACK  GL_SHININESS 20.0
    glLightfv GL_LIGHT0 GL_POSITION  {0.7 0.7 1.25 0.5}
    glEnable GL_LIGHT0
    glEnable GL_CULL_FACE
    glEnable GL_DEPTH_TEST
    glEnable GL_NORMALIZE
	glEnable GL_LIGHTING
	glShadeModel GL_FLAT
    glEnable GL_COLOR_MATERIAL
	glShadeModel GL_SMOOTH

	MakeScene
}


We see from the above that there are no drawing commands here. We do however create the model to be displayed in the MakeScene proc as follows:

Reshape function

Prior to the scene being rendered for the first time, the -reshapeCommand function will be called. This function should set the gl viewport, viewing transformation and other display oriented parameters.

proc tclReshapeFunc { toglwin width height } {

    glViewport 0 0 $width $height
    glMatrixMode GL_PROJECTION
    glLoadIdentity
    if { $width > $height } {
        set w [expr double ($width) / double ($height)]
        glFrustum [expr -1.0*$w] $w -1.0 1.0 5.0 70.0
    } else {
        set h [expr double ($height) / double ($width)]
        glFrustum -1.0 1.0 [expr -1.0*$h] $h 5.0 70.0
    }

    glMatrixMode GL_MODELVIEW
    glLoadIdentity
    glTranslatef 0.0 0.0 -40.0
    glClear [expr $::GL_COLOR_BUFFER_BIT | $::GL_DEPTH_BUFFER_BIT]
}

Make A Scene

During the -createProc function we should set any static display parameters as well as create any Textures and other state data that is necessary for display.

    
    
  1. Using the generated scene data reserve space for the display lists

    	set nlists [llength $Scene(Gears)]
    	incr nlists [expr {[llength $Scene(Axles)] * 2} ]
    	incr nlists [llength $Scene(Belts)]
    	incr nlists
    	set dlist [glGenLists $nlists]
    	set idx 1
    
    
  2. Make the display lists

    We first generate the axles used to suspend the gears in space.

    	
    foreach axle $Scene(Axles) {
    		set Scene(DList,$axle) [expr {$idx + $dlist}]
    		glNewList $Scene(DList,$axle) GL_COMPILE
    		incr idx
    		glPushMatrix
    		foreach {x y z} $Scene($axle,position) {break}
    		glTranslatef $x $y $z
    		foreach {x y z} {0 0 0} {break}
    		if {$Scene($axle,axis) == 0} {
               set y 1.0
        		} elseif {$Scene($axle,axis) == 1} {
               set x  1.0
        		} else {
               set z 1.0
    		}
    		if {$z != 1.0} {
      	    	glRotatef 90.0 $x $y $z
    		}
    	    glMaterialfv GL_FRONT GL_SPECULAR $Scene($axle,color)
    		glColor4fv $Scene($axle,color)
    		axle $Scene($axle,radius) $Scene($axle,length)
    		glPopMatrix
    		glEndList
    	}
    
    
  3. Make the Gear lists

    We note here that we use two display lists for each gear

    	
    	foreach gear $Scene(Gears) {	
    		set Scene(DList,$gear,pre) [expr {$idx + $dlist}]
    		glNewList $Scene(DList,$gear,pre) GL_COMPILE
    		incr idx
    		glPushMatrix
    		foreach {x y z} [gPos $gear] {break}
    		glTranslatef $x $y $z
    		set axle $Scene($gear,axle)
    		foreach {x y z} {0 0 0} {break}
    		if {$Scene($axle,axis) == 0} {
               set y 1.0
        		} elseif {$Scene($axle,axis) == 1} {
               set x  1.0
        		} else {
               set z 1.0
    		}
    		if {$z != 1.0} {
               glRotatef 90.0 $x $y $z
    		}
    		glEndList
    

    We now generate the rotated view of the gear (Spinning on it's axis). This fact is lost but is placed here for completeness. We must insert this command during a display update between the two display lists as it is generated here.

    		glRotatef [expr {$Scene($gear,direction) * $Scene($gear,angle)}] 0.0 0.0 1.0
    

    Now render the gear into the second display list.

    		set Scene(DList,$gear,post) [expr {$idx + $dlist}]
    		glNewList $Scene(DList,$gear,post) GL_COMPILE
    		incr idx
    	    glMaterialfv GL_FRONT GL_SPECULAR $Scene($gear,color)
    		glColor4fv $Scene($gear,color)
    		gear $gear $Scene($gear,type) $Scene($gear,radius) \
    				$Scene($gear,width) $Scene($gear,teeth) $Scene($gear,toothdepth)
    		glPopMatrix
    		glEndList
    	}
    
    
  4. Make the Belts driving two gears.

    	foreach belt $Scene(Belts) {	
    		set Scene(DList,$belt) [expr {$idx + $dlist}]
    		glNewList $Scene(DList,$belt) GL_COMPILE
    		incr idx
    		glPushMatrix
    		glDisable GL_CULL_FACE
    		foreach {x y z} [gPos $Scene($belt,gear1name)] {break}
    		glTranslatef $x $y $z
    		set axle $Scene($Scene($belt,gear1name),axle)
    		foreach {x y z} {0 0 0} {break}
    		if {$Scene($axle,axis) == 0} {
               set y 1.0
        		} elseif {$Scene($axle,axis) == 1} {
               set x  1.0
        		} else {
               set z 1.0
    		}
    		if {$z != 1.0} {
               glRotatef 90.0 $x $y $z
    		}
    		belt $Scene($belt,gear1name) $Scene($belt,gear2name)
    		glEnable GL_CULL_FACE
    		glPopMatrix
    		glEndList
    	}
    
  5. Make the display list of display lists for static items

    Here we will make a display list that calls the other display lists.
    This may not be as efficient as it looks as some GL drivers will merge the display lists into one contigious list which takes a lot of space and may slow down the graphics card.

    	set Scene(DList,allaxles) [expr {$idx + $dlist}]
    	glNewList $Scene(DList,allaxles) GL_COMPILE
    	foreach axle $Scene(Axles) {
    		glCallList $Scene(DList,$axle)
    	}
    	foreach belt $Scene(Belts) {
    		glCallList $Scene(DList,$belt)
    	}
    	glEndList
    }
    
    
Here we place all the drawing commands into display lists. We do this so that there are few Tcl level calls during the tclDisplayProc function to maximise the number of frames we can render per second.

What is important in the above is the use of display lists. We see at the end of the routine we create a display list that just calls the other display lists. We do this as there could be hundreds of objects and Tcl does not do loops quickly.

It should be noted that for the Gear Objects, that we cannot put all commands in one display list. This is because the Position of the gear in the Z axis is updated in Real time to give the impression of the gear rotating. For This we have split the display list into two lists, which allow us to quickly set the matrix for positioning the object which is fixed at creation time via the first list, one call to then rotate the matrix in the Z axis prior to calling the second display list to generate the Gear faces. This means that each gear only takes three Tcl -> Gl function calls.
This allows us to render a larger number of gears that we could if we did not use display lists.

One issue of the above approach is that the display lists can get very large which can impact on the performance of the hardware rendering engine. What ever speed degredation we experience from this is, it will still be orders of magnitude less than the degredation of rendering the scene using discrete Tcl -> GL calls in the tclDisplayProc function.

Since all the axles and belts to not animate, we can put these items into one display list and call it before rendering any gears (or other dynamic items). This means that half the scene can be rendered with just one Tcl->GL function call.

Display Procedure

By placing most of the GL calls in the create function we now have a lean -displayProc function.

proc tclDisplayFunc {toglwin} {
	variable Scene
	set sc $Scene(Scale)

    glClear [expr $::GL_COLOR_BUFFER_BIT | $::GL_DEPTH_BUFFER_BIT]
    glPushMatrix
    glRotatef $Scene(Rotx) 1.0 0.0 0.0
    glRotatef $Scene(Roty) 0.0 1.0 0.0
    glRotatef $Scene(Rotz) 0.0 0.0 1.0

	glScalef  $sc $sc $sc
    glRotatef $Scene(Angle) 0.0 0.0 1.0


	# Draw all axles and belts (Static items)
	glCallList $Scene(DList,allaxles)

	foreach gear $Scene(Gears) {
		glCallList $Scene(DList,$gear,pre)
Here we insert the call to spin the gear on its axis,
		glRotatef [expr {$Scene($gear,direction) * $Scene($gear,angle)}] 0.0 0.0 1.0
And now we can call the second list to draw the gear.
		glCallList $Scene(DList,$gear,post)
	}

	glPopMatrix

 
  	$toglwin swapbuffers
}

In the display update function we:

  1. Set the Scene view transformations.
  2. Call the static display list to draw axles and belts.
  3. Loop over each gear rotating the gear in the z axis by its correct amount before calling it's display list.

Note that we need to call the display update function anytime the scene has changed. The function automatically gets called if we get window exposure events.
We can force the window to be updated using the widgets postredisplay subcommand.

Performance

Using the above techniques we should realise 25-50 fps for moderately complex scenes.
Particle systems would not be practical since we need to recompute each particles parameters each frame and so cannot make use of display lists to speed this process up.

References

Togl Tclogl Mesa OpenGL Tcl/Tk