Triangle Strips for OpenGL

I recently had a need to generate triangle strips for OpenGL triangle meshes and found Pierre Terdiman's nice C++ code at http://www.codercorner.com/Strips.htm . I was in the middle of converting it to plain C when it occurred to me that this would be a nice little project to implement in Tcl, not only to validate my specfic conversion, but also to share with the community.

The basic idea is to turn a list of triangles (triples of vertex indices) into a list of tristrips (arbitrary length lists of vertex indices) while preserving the orientation of the original input triangles.

Here's some Tcl to do this :

 package provide tristrip

 namespace eval tristrip {

 # public
        variable oneside 1 ;# generate one-sided triangle strips
        variable cnctall 0 ;# connect all strips
        variable SGIalgo 0 ;# use SGI algorithm for trilist traversal
        variable oppoext 1 ;# do opposite direction strip extension

        #Generate Triangle Strips from Triangle List
        #
        proc genTriStrips {trilst {one 1s} {cnc noconnect} {sgi nosgi}} {
                variable edgmap; catch { array unset edgmap }
                variable usegbl; catch { array unset usegbl }

                # set options (this needs reworking (suggestions welcome)...)
                #
                variable oneside; variable cnctall; variable SGIalgo
                if {[string equal $one "1s"]} {
                        set oneside 1 } else { set oneside 0 }
                if {[string equal $cnc "connect"]} {
                        set cnctall 1 } else { set cnctall 0 }
                if {[string equal $sgi "sgi"]} {
                        set SGIalgo 1 } else { set SGIalgo 0; }

                # create edge -> triangles mapping
                #
                newEdgMap $trilst ;

                # unimplemented (exercise for the reader :)
                #
                variable SGIalgo
                if {$SGIalgo} {
                        # sort trilst ascending based on the number of neighbors
                        # per tri. i.e., visit most-isolated tris first.
                }

                # create strips
                #
                set stripLst [list]
                foreach tri $trilst {
                        # starting from unused triangles ...
                        if {![info exists usegbl($tri)]} {
                                # generate a strip, save it, mark its tri's used
                                foreach {s u} [genBestStrip $tri] { break }
                                lappend stripLst $s
                                foreach t $u { set usegbl($t) 1 }
                        }
                }

                catch { array unset edgmap; array unset usegbl }

                if {$cnctall} {
                        set stripLst [list [connectAllStrips $stripLst]]
                }

                return $stripLst
        }

 # private
        variable edgmap    ;# triangles sharing an edge
        variable usegbl    ;# triangles in use globally

        proc genBestStrip {tri0} {
                # best strip so far for input tri
                set bestVtxLst {}; set bestTriLst {}

                # generate strips in all three directions
                #
                foreach \
                        fwdDir {{old mid dum} {mid dum old} {dum old mid}} \
                        bakDir {{mid old dum} {old dum mid} {dum mid old}} \
                {
                        # initialize for this strip
                        catch {array unset uselcl};
                        foreach $fwdDir [set tri $tri0] { break }
                        set vLst [list $old $mid]; set tLst [list]

                        # extend strip
                        foreach {vLst tLst} \
                                [extendStrip $vLst $tLst $tri $old $mid uselcl] { break }

                        # if opposite-direction strip extension is configured
                        variable oppoext
                        if {$oppoext} {
                                # look backwards from original tri
                                foreach $bakDir [set tri $tri0] { break }

                                # for an adjacent unused tri
                                set tri [unusedTri [otherTri $tri0 $old $mid] uselcl]
                                if {$tri != ""} {
                                        # found one so reverse strip
                                        reverseLst vLst; reverseLst tLst

                                        # extend again
                                        foreach {vLst tLst} \
                                                [extendStrip $vLst $tLst $tri $old $mid uselcl] \
                                                        { break }

                                        # for one-sided strips,
                                        # reverse strip and check/correct original windings
                                        #
                                        variable oneside
                                        if {$oneside} {
                                                reverseLst vLst; reverseLst tLst;
                                                set idxtri0 0;
                                                foreach t $tLst {
                                                        if {$t == $tri0} { break };
                                                        incr idxtri0
                                                }
                                                if {[expr {$idxtri0%2}] == 1} {
                                                        set vLst [linsert $vLst 0 [lindex $vLst 0]]
                                                }
                                        } 
                                }
                        }

                        # save strip if longer than current best strip
                        #
                        set tLen [llength $tLst]
                        set bLen [llength $bestTriLst]
                        if {$tLen > $bLen} {
                                set bestVtxLst $vLst; set bestTriLst $tLst
                        }
                }

                return [list $bestVtxLst $bestTriLst]
        }

        # extend input strip (and it's trilst) in the old/mid direction
        proc extendStrip {vLst tLst tri old mid uselclnam} {
                upvar 1 $uselclnam uselcl

                while {$tri != ""} {
                        lappend vLst [set new [otherVtx $tri $old $mid]]
                        lappend tLst $tri; set uselcl($tri) 1
                        set tri [unusedTri [otherTri $tri $mid $new] uselcl]
                        set old $mid; set mid $new
                }

                return [list $vLst $tLst]
        }

        # flatten all strip lists to one strip if configured
        proc connectAllStrips {stripLst} {
                variable oneside

                set vLst [list]; set vLen 0
                foreach s $stripLst {
                        if {$vLst != ""} {
                                set vEnd [lindex $vLst end]
                                set sBeg [lindex $s 0]

                                lappend vLst $vEnd $sBeg; incr vLen 2

                                # check/correct for one sided strip winding flip
                                if {$oneside && [expr {$vLen%2}] == 1} {
                                        foreach {v1 v2 rest} $s { break }
                                        if {$v1 != $v2} {
                                                lappend vLst $v1; incr vLen
                                        }
                                }
                        }

                        # append the existing strip
                        foreach v $s { lappend vLst $v; incr vLen }
                }

                return $vLst
        }

        # Create an edge-to-triangles map.  Keys are ordered pairs of
        # vertex indices and values are a list of triangles sharing the edge
        #
        proc newEdgMap {trilst} {
                variable edgmap; array unset edgmap
                foreach tri $trilst { foreach {v1 v2 v3} $tri { break }
                        addEdgTri $v1 $v2 $tri
                        addEdgTri $v2 $v3 $tri
                        addEdgTri $v3 $v1 $tri
                }
                # can't handle non-manifold meshes
                foreach edg [array names edgmap] {
                        if {[llength $edgmap($edg)] > 2} {
                                return -code error \
                                        "Non-manifold input : edge $edg $edgmap($edg)"
                        }
                }
        }

        # add a triangle to the list of triangles sharing edge ab
        proc addEdgTri {a b tri} {
                variable edgmap;
                if {$a < $b} { set lo $a; set hi $b
                } else       { set lo $b; set hi $a }
                lappend edgmap($lo,$hi) $tri
        }

        # get the list of triangles sharing edge ab
        proc getEdgTriLst {a b} {
                variable edgmap
                if {$a < $b} { set lo $a; set hi $b 
                } else       { set lo $b; set hi $a }
                return $edgmap($lo,$hi)
        }

        # reverse list variable in the caller's scope
        proc reverseLst {lstvarnam} {
                upvar 1 $lstvarnam lst
                set revlst [list]; set n [llength $lst] 
                while {[incr n -1] >= 0} { lappend revlst [lindex $lst $n] }
                set lst $revlst
        }

        # find the other vertex of a triangle when given two
        proc otherVtx {tri a b} {
                foreach {v1 v2 v3} $tri { break }
                if {$v1 == $a && $v2 == $b || $v1 == $b && $v2 == $a} { return $v3 }
                if {$v2 == $a && $v3 == $b || $v2 == $b && $v3 == $a} { return $v1 }
                if {$v3 == $a && $v1 == $b || $v3 == $b && $v1 == $a} { return $v2 }
        }

        # find the other triangle sharing the edge ab
        proc otherTri {tri a b} {
                variable edgmap
                foreach {t1 t2} [getEdgTriLst $a $b] { break }
                if {$tri == $t1} { set oth $t2 } else { set oth $t1 }
                return $oth
        }

        # return input triangle if unused both globally and in the given map
        proc unusedTri {tri lclusenam} {
                variable usegbl; upvar 1 $lclusenam uselcl
                set unused $tri
                if {[info exists usegbl($tri)]} { set unused "" } ;# in use globally
                if {[info exists uselcl($tri)]} { set unused "" } ;# in use locally
                return $unused
        }
 }

I suppose we need some code to test this too. Yes, this is probably more complex than needed (feel free to add the simple test cases) but I needed to check performance numbers on large regular closed meshes.

 proc genShape {typ} {
        set vclst [list]

        if {[string equal $typ "o"]} {
                # octahedron vertices on unit sphere
                set p [expr {double(1)}];
                set m [expr {double(-1)}];
                set z [expr {double(0)}];
                set xp [list $p $z $z] ; set xm [list $m $z $z]
                set yp [list $z $p $z] ; set ym [list $z $m $z]
                set zp [list $z $z $p] ; set zm [list $z $z $m]
                # octohedron (all tris ccw)
                lappend vclst \
                        [list $xp $yp $zp] [list $xp $zm $yp] [list $xp $zp $ym] \
                        [list $xp $ym $zm] [list $xm $yp $zm] [list $xm $zm $ym] \
                        [list $xm $zp $yp] [list $xm $ym $zp]

        } elseif {[string equal $typ "d"]} {
                # triangular dipyramid (a convex deltahedron)
                # vertices on unit sphere
                set Pi    [expr {3.14159265358979323846}]
                set cos60 [expr {cos($Pi*30/180.0)}]
                set sin60 [expr {sin($Pi*30/180.0)}]
                set p     [expr {double(1)}]
                set m     [expr {double(-1)}]
                set z     [expr {double(0)}]
                set top [list $z $p $z]; set bot [list $z $m $z]
                set bak [list $z $z $m]
                set lft [list -$cos60 $z $sin60]; set rit [list $cos60 $z $sin60]
                # triangular dipyramid
                lappend vclst \
                        [list $rit $top $lft] [list $lft $bot $rit] \
                        [list $rit $bot $bak] [list $bak $top $rit] \
                        [list $top $bak $lft] [list $lft $bot $bak]

        } elseif {[string equal $typ "t"]} {
                # tetrahedron vertices on unit sphere
                set sqrt3p [expr {0.5773502692}]
                set sqrt3m [expr {-0.5773502692}]
                set PPP [list $sqrt3p $sqrt3p $sqrt3p] ;# +X, +Y, +Z
                set MMP [list $sqrt3m $sqrt3m $sqrt3p] ;# -X, -Y, +Z
                set MPM [list $sqrt3m $sqrt3p $sqrt3m] ;# -X, +Y, -Z
                set PMM [list $sqrt3p $sqrt3m $sqrt3m] ;# +X, -Y, -Z
                # tetrahedron (all tris ccw)
                lappend vclst \
                        [list $PPP $MPM $MMP] [list $PPP $MMP $PMM] \
                        [list $MPM $PMM $MMP] [list $MPM $PPP $PMM]

        } else {
                return -code error "unknown shape type $typ"
        }

        return $vclst
 }

 # repeatedly subdivide a list of triangles to the given depth
 # normalizes all generated vertices to lie on the unit sphere
 # returns new list of triangle vertices
 #
 proc sphdivtrilst {tclst {depth 3}} {
    proc K {a b} {set a}; # local K combiner

        if {$depth < 0}         { set depth 0
        } elseif {$depth > 5}   { set depth 5 }

        set curlst $tclst
        set nxtlst [list]

        while {[incr depth -1] >= 0} {
                foreach t [K $curlst [set curlst [list]]] {
                        # get triangle vertex coordiates
                        foreach {v1 v2 v3} $t break
                        foreach {x1 y1 z1} $v1 {x2 y2 z2} $v2 {x3 y3 z3} $v3 \
                                break

                        set x [expr {($x1+$x2)}]
                        set y [expr {($y1+$y2)}]
                        set z [expr {($z1+$z2)}]
                        set l [expr {sqrt($x*$x + $y*$y + $z*$z)}]
                        set v12 [list [expr {$x/$l}] [expr {$y/$l}] [expr {$z/$l}]]

                        set x [expr {($x2+$x3)}]
                        set y [expr {($y2+$y3)}]
                        set z [expr {($z2+$z3)}]
                        set l [expr {sqrt($x*$x + $y*$y + $z*$z)}]
                        set v23 [list [expr {$x/$l}] [expr {$y/$l}] [expr {$z/$l}]]

                        set x [expr {($x3+$x1)}]
                        set y [expr {($y3+$y1)}]
                        set z [expr {($z3+$z1)}]
                        set l [expr {sqrt($x*$x + $y*$y + $z*$z)}]
                        set v31 [list [expr {$x/$l}] [expr {$y/$l}] [expr {$z/$l}]]

                        lappend nxtlst \
                                [list $v1 $v12 $v31] \
                                [list $v2 $v23 $v12] \
                                [list $v3 $v31 $v23] \
                                [list $v12 $v23 $v31] \
                }

                set curlst $nxtlst
                set nxtlst [list]
        }

        return $curlst
 }

 proc genSphere {ndv {typ o} {regen 0}} {

        if {!$regen} {
                global sphereCache; if {[info exists sphereCache($typ,$ndv)]} {
                        return $sphereCache($typ,$ndv)
                } else {
                        array unset sphereCache ;# only cache 1 typ,ndv pair
                }
        }

        set trilst [sphdivtrilst [genShape $typ] $ndv]

        # create vertex list from sphere's triangles
        set unqvtxlst {}
        foreach t $trilst {
                foreach {v1 v2 v3} $t { lappend unqvtxlst $v1 $v2 $v3 } }
        set unqvtxlst [lsort -unique $unqvtxlst]

        # create unique vertex map
        set idx 0
        foreach v $unqvtxlst {
                if {![info exists vtxidxmap($v)]} {
                        set vtxidxmap($v) $idx
                        incr idx
                }
        }

        # create triangle list using vertex indices
        set i 0
        set trivtxidxlst {}
        foreach t $trilst {
                foreach {v1 v2 v3} $t {
                        lappend trivtxidxlst \
                                [list $vtxidxmap($v1) $vtxidxmap($v2) $vtxidxmap($v3)]
                }
        }
        #puts "[llength $trivtxidxlst] tris [llength $unqvtxlst] unqvtx"

        return [set sphereCache($typ,$ndv) [list $unqvtxlst $trivtxidxlst]]
 }

 proc statStripList {vL tL sL} {
        set vLen [llength $vL]
        set tLen [llength $tL]
        set sLen [llength $sL]
        puts "\t$tLen input triangles $vLen vertices $sLen strips"
        set vr 0; set lL [list]
        foreach s $sL { incr vr [set l [llength $s]]; lappend lL $l }
        puts -nonewline "\ttotal vtx refs : $vr : #/strip :"
        foreach l $lL { puts -nonewline " $l" }; puts ""
        puts "\ttri/tristrip vtx ref ratio : [expr {$tLen*3.0/$vr}]"
        puts "\t#tristrip refs/~#min refs ratio : [expr {$vr/2.0/$vLen}]"
 }

 proc runtstTriStrip {n {typ o} {onesid 1s} {cnc connect}} {
        set et0 [time {
                foreach {vL tL} [genSphere $n $typ 1] { break }
        }]
        set et1 [time {
                set sL [tristrip::genTriStrips $tL $onesid $cnc]
        }]
        puts "genTriStrips \$tl $onesid $cnc : $et1" 
        puts "\tgenSphere $n $typ 1: $et0" 
        statStripList $vL $tL $sL
 }

 proc tstTriStrip {{nlst {0 1}} {tlst {t d}} {slst {2s}} {clst {connect}}} {
        foreach n $nlst {
        foreach t $tlst {
        foreach s $slst {
        foreach c $clst {
                puts "runtstTriStrip $n $t $s $c"
                runtstTriStrip $n $t $s $c
                puts ""
        } } } }
 }

 #tstTriStrip {0 1 2 3 4 5} {t d o} {2s 1s} {connect !connect} ;# test everything

I don't currently have a way to easily visualize this directly since I haven't yet found a Tcl/Tk OpenGL widget (I must admit I haven't looked thoroughly) that handles indirect vertex references and tristrips at the scripting level.

Mark K. Greene