proc main {} { initVars buildUI animate } proc animate {} { # --- crank it as fast as we can... while {$::emitter(alive)} { nextFrame update } exit } proc initVars {} { # --- Particle Emitter... set ::emitter(alive) 1 ; # still running? set ::emitter(pos.x) 300 ; # x position of emitter set ::emitter(pos.y) 370 ; # y position of emitter set ::emitter(pos.z) 0 ; # z position of emitter set ::emitter(yaw) [degreeToRad 0] ; # initial yaw angle set ::emitter(yawVar) [degreeToRad 360] ; # random variation range on yaw set ::emitter(pitch) [degreeToRad -90] ; # initial pitch (up) set ::emitter(pitchVar) [degreeToRad 40] ; # random variation range set ::emitter(speed) 12 ; # particle speed set ::emitter(speedVar) 2 ; # random variation range set ::emitter(totalParticles) 50 ; # total particles in system set ::emitter(particleCount) 0 ; # current particle count set ::emitter(emitsPerFrame) 5 ; # number of particles/frame set ::emitter(emitVar) 2 ; # random variation range set ::emitter(life) 60 ; # particle life (frames) set ::emitter(lifeVar) 15 ; # random variation set ::emitter(startColor.r) 150 ; # start color (red component) set ::emitter(startColor.g) 150 ; # start color (green component) set ::emitter(startColor.b) 200 ; # start color (blue component) set ::emitter(startColorVar.r) 25 ; # random variation - red set ::emitter(startColorVar.g) 25 ; # random variation - green set ::emitter(startColorVar.b) 25 ; # random variation - blue set ::emitter(endColor.r) 0 ; # end color (red component) set ::emitter(endColor.g) 0 ; # end color (green component) set ::emitter(endColor.b) 200 ; # end color (blue component) set ::emitter(endColorVar.r) 25 ; # random variation - red set ::emitter(endColorVar.g) 25 ; # random variation - green set ::emitter(endColorVar.b) 50 ; # random variation - blue set ::emitter(force.x) 0.0 ; # x force factor (wind) set ::emitter(force.y) 0.3 ; # y force factor (gravity) set ::emitter(force.z) 0.0 ; # z force factor (?) } proc nextFrame {} { # --- update all living particles foreach me [.c1 find withtag "alive"] { updateParticle $me } # --- Add up to "emitsPerFrame" more particles to the scene without # exceeding "totalParticles" for {set i 1} {$i <= $::emitter(emitsPerFrame)} {incr i} { if {![addNewParticle]} { break } } } proc addNewParticle {} { # --- if we've reached our population cap, just return if {$::emitter(particleCount) >= $::emitter(totalParticles)} { return 0 } else { # --- throw another particle on the pile incr ::emitter(particleCount) # --- see if we can recycle any dead particles set me [lindex [.c1 find withtag "dead"] 0] if {[string length $me]} { .c1 itemconfigure $me -tag "alive" } else { set me [.c1 create line -tag "alive"] } # --- starting particle position (delta from the emitter) set ::particle($me,pos.x) 0 set ::particle($me,pos.y) 0 set ::particle($me,pos.z) 0 set ::particle($me,prevPos.x) 0 set ::particle($me,prevPos.y) 0 set ::particle($me,prevPos.z) 0 # --- calculate the starting direction vector set yaw [expr {$::emitter(yaw) + ($::emitter(yawVar) * [randomNum])}] set pitch [expr {$::emitter(pitch) + ($::emitter(pitchVar) * [randomNum])}] # --- determine vector information set vectorInfo [rotationToDirection $pitch $yaw] set x [lindex $vectorInfo 0] set y [lindex $vectorInfo 1] set z [lindex $vectorInfo 2] # --- account for the speed factor set speed [expr {$::emitter(speed) + ($::emitter(speedVar) * [randomNum])}] set x [expr {$x * $speed}] set y [expr {$y * $speed}] set z [expr {$z * $speed}] # --- we are done with these, so store them with the particle set ::particle($me,dir.x) $x set ::particle($me,dir.y) $y set ::particle($me,dir.z) $z # --- calculate the colors for this particle set start_r [expr {$::emitter(startColor.r) + \ ($::emitter(startColorVar.r) * [randomNum])}] set start_g [expr {$::emitter(startColor.g) + \ ($::emitter(startColorVar.g) * [randomNum])}] set start_b [expr {$::emitter(startColor.b) + \ ($::emitter(startColorVar.b) * [randomNum])}] set end_r [expr {$::emitter(endColor.r) + \ ($::emitter(endColorVar.r) * [randomNum])}] set end_g [expr {$::emitter(endColor.g) + \ ($::emitter(endColorVar.g) * [randomNum])}] set end_b [expr {$::emitter(endColor.b) + \ ($::emitter(endColorVar.b) * [randomNum])}] set ::particle($me,color.r) $start_r set ::particle($me,color.g) $start_g set ::particle($me,color.b) $start_b # --- calculate the lifespan of this particle # we know *exactly* how long it will live, even before it's born... set life [expr {$::emitter(life) + int($::emitter(lifeVar) * [randomNum])}] set ::particle($me,life) $life # --- calculate the color delta using the lifespan of this particle set ::particle($me,deltaColor.r) [expr {($end_r - $start_r) / $life}] set ::particle($me,deltaColor.g) [expr {($end_g - $start_g) / $life}] set ::particle($me,deltaColor.b) [expr {($end_b - $start_b) / $life}] # --- A new particle is born - it's a beautiful thing... return 1 } } proc updateParticle {me} { # --- if this particle has died, prepare for it for resurrection... if {$::particle($me,life) <= 0} { incr ::emitter(particleCount) -1 .c1 itemconfigure $me -tag "dead" .c1 coords $me -10 -10 -10 -10 return 0 } else { # --- save it's old position as the next start coord set ::particle($me,prevPos.x) $::particle($me,pos.x) set ::particle($me,prevPos.y) $::particle($me,pos.y) set ::particle($me,prevPos.z) $::particle($me,pos.z) # --- update the new end coordinates by the particles motion vectors set ::particle($me,pos.x) [expr {$::particle($me,pos.x) + \ $::particle($me,dir.x)}] set ::particle($me,pos.y) [expr {$::particle($me,pos.y) + \ $::particle($me,dir.y)}] set ::particle($me,pos.z) [expr {$::particle($me,pos.z) + \ $::particle($me,dir.z)}] # --- apply global forces to the particle set ::particle($me,dir.x) [expr {$::particle($me,dir.x) + \ $::emitter(force.x)}] set ::particle($me,dir.y) [expr {$::particle($me,dir.y) + \ $::emitter(force.y)}] set ::particle($me,dir.z) [expr {$::particle($me,dir.z) + \ $::emitter(force.z)}] # --- update the particle color set ::particle($me,color.r) [expr {$::particle($me,color.r) + \ $::particle($me,deltaColor.r)}] set ::particle($me,color.g) [expr {$::particle($me,color.g) + \ $::particle($me,deltaColor.g)}] set ::particle($me,color.b) [expr {$::particle($me,color.b) + \ $::particle($me,deltaColor.b)}] # --- Age the particle... # In the immortal words of Pink Floyd... # "The sun is the same in a relative way, but you're older" # "Shorter of breath and one day closer to death" incr ::particle($me,life) -1 set x_org $::emitter(pos.x) set y_org $::emitter(pos.y) set xStart [expr {$x_org + $::particle($me,prevPos.x)}] set yStart [expr {$y_org + $::particle($me,prevPos.y)}] set xEnd [expr {$x_org + $::particle($me,pos.x)}] set yEnd [expr {$y_org + $::particle($me,pos.y)}] .c1 coords $me $xStart $yStart $xEnd $yEnd .c1 itemconfigure $me -fill [createColor $::particle($me,color.r) \ $::particle($me,color.g) $::particle($me,color.b)] return 1 } } proc createColor {r g b} { # --- convert all passed vals to ints set r [expr {int($r)}] set g [expr {int($g)}] set b [expr {int($b)}] # --- push colors within valid range if {$r > 255} {set r 255} if {$g > 255} {set g 255} if {$b > 255} {set b 255} if {$r < 0} {set r 0} if {$g < 0} {set g 0} if {$b < 0} {set b 0} # --- return a TK acceptable color string return [format "#%02x%02x%02x" $r $g $b] } # --- this lacks *a lot*. It should allow GUI access to a total of # 26 emitter variables, not just 3 - maybe someday... proc buildUI {} { canvas .c1 -bg black -width 600 -height 400 -highlightthickness 0 -borderwidth 0 frame .f1 pack .c1 -side left -fill both -expand 1 pack .f1 -side left -fill y -expand 1 scale .f1.s1 -from 1 -to 500 -label "Max Particles" -length 100 -showvalue 1 \ -orient horizontal -width 8 -sliderlength 15 -variable ::emitter(totalParticles) scale .f1.s2 -from -5 -to 5 -label "Wind" -length 100 -showvalue 1 \ -orient horizontal -width 8 -sliderlength 15 -variable ::emitter(force.x) -resolution .1 scale .f1.s3 -from -5 -to 5 -label "Gravity" -length 100 -showvalue 1 \ -orient horizontal -width 8 -sliderlength 15 -variable ::emitter(force.y) -resolution .1 button .f1.btnExit -text "Exit" -width 10 -command {set ::emitter(alive) 0} pack .f1.s1 .f1.s2 .f1.s3 pack .f1.btnExit -side bottom bind .c1 {updateEmitterLoc %x %y} bind .c1 {updateEmitterLoc %x %y} wm title . "Particle System Editor" } # --- generate a random in the range of "-1 to < 1" proc randomNum {} { return [expr {(-.5 + rand()) * 2.0}] } proc degreeToRad {degrees} { return [expr {$degrees / 57.2957795786}] } # --- move the emitter to the specifiec location proc updateEmitterLoc {x y} { set ::emitter(pos.x) $x set ::emitter(pos.y) $y } proc rotationToDirection {pitch yaw} { set x [expr {-sin($yaw) * cos($pitch)}] set y [expr {sin($pitch)}] set z [expr {cos($pitch) * cos($yaw)}] return [list $x $y $z] } main