Version 0 of Particle System

Updated 2002-04-14 23:57:23

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 <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 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