# ray.tcl # Author: Gerard Sookahet # Date: 06 Feb 2004 # Description: Simple raytracer with sphere object proc Main {wd ht} { set w .ray catch {destroy $w} toplevel $w wm withdraw . wm title $w "Raytracing" pack [canvas $w.c -width $wd -height $ht -bg white] $w.c delete all set pix [image create photo] $w.c create image 0 0 -anchor nw -image $pix set f1 [frame $w.f1 -relief sunken -borderwidth 2] pack $f1 -fill x button $f1.bcreate -text Render -command "Raytrace $wd $ht $pix" button $f1.bq -text Quit -command exit eval pack [winfo children $f1] -side left } proc Raytrace {wd ht pix} { for {set y 0} {$y <= $ht} {incr y} { set line {} for {set x 0} {$x <= $wd} {incr x} { set color [IntersectSphere $x $y $wd $ht] set R [expr {round([lindex $color 0])}] set V [expr {round([lindex $color 1])}] set B [expr {round([lindex $color 2])}] lappend line [format "#%02X%02X%02X" $R $V $B] } # 'put' and update once per line for best speed / visual response $pix put [list $line] -to 0 $y update idletasks } } proc IntersectSphere {x y wd ht} { # Center of the sphere set cx 0.0 set cy 0.0 set cz 0.0 set radius 1.2 # Point of view set from_x 0.0 set from_y 0.0 set from_z 6.0 set tmin 1000000.0; # Closest intersection distance arbitrarly large set to_x [expr {double($x)/double($wd) - $from_x/$wd - 0.5}] set to_y [expr {double($y)/double($ht) - $from_y/$ht - 0.5}] set to_z [expr {4.0 - $from_z}] foreach {to_x to_y to_z} [VectNormalize $to_x $to_y $to_z] {} set vect_x [expr {$cx - $from_x}] set vect_y [expr {$cy - $from_y}] set vect_z [expr {$cz - $from_z}] # Solve the ray and sphere intersection equation set b [DotProduct $to_x $to_y $to_z $vect_x $vect_y $vect_z] set c [DotProduct $vect_x $vect_y $vect_z $vect_x $vect_y $vect_z] set c [expr {$c - $radius*$radius}] set d [expr {$b*$b - $c}] if {$d < 0} then {return [list 0 0 0]}; # No ray intersection set dsqrt [expr {sqrt($d)}] set t1 [expr {$b + $dsqrt}] set t2 [expr {$b - $dsqrt}] if {$t1 < 0} then {return [list 0 0 0]}; # Object is behind the point of view if {$t2 > 0.0} then {set t $t2} else {set t $t1} if {$tmin > $t} then {set tmin $t} if {$tmin >= 1000000.0} then {return [list 0 0 0]} # Return a color since the ray intersect the sphere return [Shading $tmin $from_x $from_y $from_z $to_x $to_y $to_z $cx $cy $cz] } proc Shading {t from_x from_y from_z to_x to_y to_z cx cy cz} { # Normalized light vector <-1,-1,1> set l_x -0.577 set l_y -0.577 set l_z 0.577 # Color of the object set color_x 0 set color_y 0 set color_z 255 # Ambient light color set amb_x 20 set amb_y 20 set amb_z 20 set t_x [expr {$to_x*$t}] set t_y [expr {$to_y*$t}] set t_z [expr {$to_z*$t}] set c_x [expr {$from_x + $t_x - $cx}] set c_y [expr {$from_y + $t_y - $cy}] set c_z [expr {$from_z + $t_z - $cz}] foreach {c_x c_y c_z} [VectNormalize $c_x $c_y $c_z] {} set angle [DotProduct $c_x $c_y $c_z $l_x $l_x $l_z] if {$angle < 0.0} then {set angle 0.0} # Lambert's law light intensity plus an attenuation factor set c_x [expr {$color_x*$angle + $amb_x}] set c_y [expr {$color_y*$angle + $amb_y}] set c_z [expr {$color_z*$angle + $amb_z}] set c_x [expr {$c_x > 255 ? 255 : $c_x}] set c_y [expr {$c_y > 255 ? 255 : $c_y}] set c_z [expr {$c_z > 255 ? 255 : $c_z}] return [list $c_x $c_y $c_z] } proc DotProduct {ax ay az bx by bz} { return [expr {$ax*$bx + $ay*$by + $az*$bz}] } proc VectNormalize {vx vy vz} { set d [expr {sqrt($vx*$vx + $vy*$vy + $vz*$vz)}] return [list [expr {$vx/$d}] [expr {$vy/$d}] [expr {$vz/$d}]] } # Size of the screen Main 200 200
tclguy changed the original "draw and update for every pixel" to "collect pixels of a line into a list, draw and update once per line" which brought quite a speed increase:
Raytrace0: 26511967 microseconds per iteration - original Raytrace1: 10648166 microseconds per iteration - [update] only once per line Raytrace2: 4359732 microseconds per iteration - pixels collected in list, put once per line
ABU 7-aug-2007Based on Gerard's work, I've built a little GUI Ray Lab .
DKF: Here's another raytracer (written for Rosetta Code. The intersection algorithm could do with more work, but it does specular as well as diffuse lighting. It produces this output:
package require Tcl 8.5 package require Tk proc normalize vec { upvar 1 $vec v lassign $v x y z set len [expr {sqrt($x**2 + $y**2 + $z**2)}] set v [list [expr {$x/$len}] [expr {$y/$len}] [expr {$z/$len}]] return } proc dot {a b} { lassign $a ax ay az lassign $b bx by bz return [expr {-($ax*$bx + $ay*$by + $az*$bz)}] } # Intersection code; assumes that the vector is parallel to the Z-axis proc hitSphere {sphere x y z1 z2} { dict with sphere { set x [expr {$x - $cx}] set y [expr {$y - $cy}] set zsq [expr {$r**2 - $x**2 - $y**2}] if {$zsq < 0} {return 0} upvar 1 $z1 _1 $z2 _2 set zsq [expr {sqrt($zsq)}] set _1 [expr {$cz - $zsq}] set _2 [expr {$cz + $zsq}] return 1 } } # How to do the intersection with our scene proc intersectDeathStar {x y vecName} { global big small if {![hitSphere $big $x $y zb1 zb2]} { # ray lands in blank space return 0 } upvar 1 $vecName vec # ray hits big sphere; check if it hit the small one first set vec [if { ![hitSphere $small $x $y zs1 zs2] || $zs1 > $zb1 || $zs2 <= $zb1 } then { dict with big { list [expr {$x - $cx}] [expr {$y - $cy}] [expr {$zb1 - $cz}] } } else { dict with small { list [expr {$cx - $x}] [expr {$cy - $y}] [expr {$cz - $zs2}] } }] normalize vec return 1 } # Intensity calculators for different lighting components proc diffuse {k intensity L N} { expr {[dot $L $N] ** $k * $intensity} } proc specular {k intensity L N S} { # Calculate reflection vector set r [expr {2 * [dot $L $N]}] foreach l $L n $N {lappend R [expr {$l-$r*$n}]} normalize R # Calculate the specular reflection term return [expr {[dot $R $S] ** $k * $intensity}] } # Simple raytracing engine that uses parallel rays proc raytraceEngine {diffparms specparms ambient intersector shades renderer fx tx sx fy ty sy} { global light for {set y $fy} {$y <= $ty} {set y [expr {$y + $sy}]} { set line {} for {set x $fx} {$x <= $tx} {set x [expr {$x + $sx}]} { if {![$intersector $x $y vec]} { # ray lands in blank space set intensity end } else { # ray hits something; we've got the normalized vector set b [expr { [diffuse {*}$diffparms $light $vec] + [specular {*}$specparms $light $vec {0 0 -1}] + $ambient }] set intensity [expr {int((1-$b) * ([llength $shades]-1))}] if {$intensity < 0} { set intensity 0 } elseif {$intensity >= [llength $shades]-1} { set intensity end-1 } } lappend line [lindex $shades $intensity] } {*}$renderer $line } } # The general scene settings set light {-50 30 50} set big {cx 20 cy 20 cz 0 r 20} set small {cx 7 cy 7 cz -10 r 15} normalize light # Render as a picture (with many hard-coded settings) proc guiDeathStar {photo diff spec lightBrightness ambient} { set row 0 for {set i 255} {$i>=0} {incr i -1} { lappend shades [format "#%02x%02x%02x" $i $i $i] } raytraceEngine [list $diff $lightBrightness] \ [list $spec $lightBrightness] $ambient intersectDeathStar \ $shades {apply {l { upvar 2 photo photo row row $photo put [list $l] -to 0 $row incr row update }}} 0 40 0.0625 0 40 0.0625 } pack [label .l -image [image create photo ds]] guiDeathStar ds 3 10 0.7 0.3