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