Q Solutions |
This document describes generating 3D scenes from within Tcl using the openGL graphics API.
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.
The full code is not listed here. Only the code snipits needed to demonstrate the
togl .fr.toglwin -width 400 -height 400 -double true \ -alpha true -depth true -rgba true -privatecmap false \ -createproc tclCreateFunc \ -reshapeproc tclReshapeFunc \ -displayproc tclDisplayFuncWe 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 purpose of this function is to establish the correct GL environment for rendering our drawing.
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.
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:
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] }
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.
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
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 }
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 }
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 }
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 }
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.
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:
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.
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.