Version 17 of Particle System

Updated 2002-04-23 20:21:12

As a weekend project, I decided to play with a particle system. This particular system is not an original - it is a tcl adaptation of a system originally presented by Jeff Lander in an article for Game Developer Magazine in 1998. This article and others can be found here [L1 ]. As far as particle systems go, it's pretty basic, but still fun to play with. Advanced particle systems are commony used to create "natural" effects like smoke, explosions, fog, water, etc. Put bluntly, particle systems are cool!

The GUI needs much more work, since of the 26 variables that can be set to effect (affect? - I *always* confuse those two...) the system, only 3 are presented. All together, these variables include such things as total particles, yaw angle, pitch angle, number of particles emitted per frame, start and end colors, lifespan, speed, force factors, and a whole bunch of random inputs to impact all of the above. Check the "initVars" proc and adjust them as desired. Maybe someday I'll get around to adding more to the GUI. Currently, the GUI only exposes the maximum number of particles, wind, and gravity effects. Also, you can move the particle emitter by either clicking, or click-dragging in the canvas.

More particles tend to be more interesting, but my sorry old 300MHz PII can't handle too many in tcl. The code desparately needs to be sped up (and I'm sure it can be to a certain extent), but I haven't gotten there yet - I just got it working correctly.

I hope to do more work on it soon, but we all know how that goes. I thought I'd place it here in case I never get back to it...

Enjoy.

Jeff Godfrey


I took the liberty of updateing the GUI a bit to expose more of the elements, hope you don't mind ;^) BBH


Part of the reason I didn't expose more vars in the GUI is: 1) I was out of weekend; and more importantly 2) I didn't have a good idea for cleanly creating 26 sliders (without just plain old brute force). Anyway, it looks like you solved both. Nicely done - Thanks Jeff Godfrey


Fixed a divide by zero error caused by the "life" variable. Tom Krehbiel


 proc main {} {
     buildUI
     initVars
     animate
 }

 proc animate {} {
     # --- crank it as fast as we can...
     while {$::emitter(alive)} {
         nextFrame
         update
     }
     exit
 }

 proc defineVar {key val args} {
     set ::emitter($key) $val
     set ::gui($key) $val
     switch [llength $args] {
         0 {
             # no GUI needed
             return
         }
         2 - 3 - 4 {
             foreach {min max desc conv} $args {break}
         }
         1 - default {
             error "Invalid \# args"
         }
     }

     if {"$desc" == ""} { set desc $key }
     set num 0
     while { [winfo exists .f1.l$num] } {incr num}
     # the -label option of sliders puts the name above the slider
     # which ends up taking up a lot of room - so put our own label
     # to the left
     label .f1.l$num -text "$desc"
     scale .f1.s$num -from $min -to $max -label "" -length 100 \
            -showvalue 1 -orient horizontal -width 8 -sliderlength 15
     if {[string is int $val] && [string is int $min] && [string is int $max]} {
         .f1.s$num config -resolution 1
     } else {
         .f1.s$num config -resolution .1
     }
     grid .f1.l$num .f1.s$num -row $num -sticky w
     if {$conv != ""} {
         .f1.s$num config -variable ::gui($key) -command "guiMod $key $conv"
         set ::emitter($key) [eval $conv $val]
     } else {
         .f1.s$num config -variable ::emitter($key)
     }
 }

 proc guiMod {key conv val} {
     if {"$conv" == ""} {
         set ::emitter($key) $val
     } else {
         set ::emitter($key) [eval $conv $val]
     }
 }

 proc initVars {} {

     # --- Particle Emitter...
     defineVar alive             1                                           ; # still running?
     defineVar pos.x           300                                           ; # x position of emitter
     defineVar pos.y           370                                           ; # y position of emitter
     defineVar pos.z             0                                           ; # z position of emitter
     defineVar yaw               0    0   360 "Initial Yaw"     degreeToRad  ; # initial yaw angle
     defineVar yawVar          360    0   360 "Yaw Variation"   degreeToRad  ; # random variation range on yaw
     defineVar pitch           -90  -180  180 "Initial Pitch"   degreeToRad  ; # initial pitch (up
     defineVar pitchVar         40    0   360 "Pitch Variation" degreeToRad  ; # random variation range
     defineVar speed            12    5   50  "Initial Velocity"             ; # particle speed
     defineVar speedVar          2    1   10  "Velocity Variation"           ; # random variation range
     defineVar totalParticles   50    1   500 "Max Particles"                ; # total particles in system
     defineVar particleCount     0                                           ; # current particle count
     defineVar emitsPerFrame     5    1   10  "Emission Rate"                ; # number of particles/frame
     defineVar emitVar           2                                           ; # random variation range
     defineVar life             60   10   250 "Lifespan"                     ; # particle life (frames)
     defineVar lifeVar          15                                           ; # random variation
     defineVar startColor.r    150    0   255 "Start Color (red)"            ; # start color (red component)
     defineVar startColor.g    150    0   255 "Start Color (green)"          ; # start color (green component)
     defineVar startColor.b    200    0   255 "Start Color (blue)"           ; # start color (blue component)
     defineVar startColorVar.r  25                                           ; # random variation - red
     defineVar startColorVar.g  25                                           ; # random variation - green
     defineVar startColorVar.b  25                                           ; # random variation - blue
     defineVar endColor.r        0    0   255 "End Color (red)"              ; # end color (red component
     defineVar endColor.g        0    0   255 "End Color (green)"            ; # end color (green component
     defineVar endColor.b      200    0   255 "End Color (blue)"             ; # end color (blue component
     defineVar endColorVar.r    25                                           ; # random variation - red
     defineVar endColorVar.g    25                                           ; # random variation - green
     defineVar endColorVar.b    50                                           ; # random variation - blue
     defineVar force.x         0.0  -5.0  5.0 "Wind"                         ; # x force factor (wind)
     defineVar force.y         0.3  -5.0  5.0 "Gravity"                      ; # y force factor (gravity)
     defineVar 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])}]
         if { $life <= 0 } { set life 10 }
         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 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
     button .f1.btnExit -text "Exit" -width 10 -command {set ::emitter(alive) 0}
     grid .f1.btnExit -  -row 999 -sticky s -padx 10
     grid rowconfig .f1 999 -weight 1
     bind .c1 <B1-Motion> {updateEmitterLoc %x %y}
     bind .c1 <ButtonPress-1> {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 specific 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