#!/usr/bin/perl ####################################################################### # # rayperl.pl - A Perl script / program that does ray tracing. # Ain't I cool! # # Author: Aravind Krishnaswamy # Birthdate: December 26, 2001 # License: Public Domain, do as you please # ####################################################################### # Vectors and points are 3-Tuples (arrays of size 3) # Matrices are arrays of size 16 (4x4 only dude) # Don't deal with much else ################################################### # Constants ################################################### $PI = 3.1415926535897932386; $PI_OV_TWO = $PI * 0.5; $DEG_TO_RAD = $PI / 180.0; $TWO_PI = $PI * 2.0; $INFINITY = 999_999_999_999; $NEARZERO = 0.00001; ################################################### # Utility Functions ################################################### ################################################### # 3-Tuple operations ################################################### # Write the contents of a 3-Tuple (vector or point) to standard output sub Print3Tuple { print @_; my $v = shift; printf " = (%.4g, %.4g, %.4g)\n", $v->[0], $v->[1], $v->[2]; } # Adds two 3-Tuples together sub vvAdd3 { my ($ax, $ay, $az) = (shift,shift,shift); my ($bx, $by, $bz) = (shift,shift,shift); return ($ax + $bx, $ay + $by, $az + $bz); } # Subtracts one 3-Tuple from another sub vvSub3 { my ($ax, $ay, $az) = (shift,shift,shift); my ($bx, $by, $bz) = (shift,shift,shift); return ($ax - $bx, $ay - $by, $az - $bz); } # Compute the dot product of two vectors (3D) sub vvDot3 { my ($ax, $ay, $az) = (shift,shift,shift); my ($bx, $by, $bz) = (shift,shift,shift); return $ax * $bx + $ay * $by + $az * $bz; } # Compute the cross product of two vectors (3D) sub vvCross3 { my ($ax, $ay, $az) = (shift,shift,shift); my ($bx, $by, $bz) = (shift,shift,shift); return ($ay*$bz - $az*$by, $az*$bx - $ax*$bz, $ax*$by - $ay*$bx) } # Negates a 3-Tuple (vector or point) sub negate3Tuple { my ($vx, $vy, $vz) = (shift,shift,shift); return (-$vx, -$vy, -$vz) ; } # Multiples a scalar by a 3-Tuple sub vsMul3 { my ($vx, $vy, $vz) = (shift,shift,shift); my $s = shift; return ($vx * $s, $vy * $s, $vz * $s); } # Returns the magitude of a 3D vector sub vMagnitude3 { my ($vx, $vy, $vz) = (shift,shift,shift); return sqrt( $vx * $vx + $vy * $vy + $vz * $vz ); } # Returns the normalized vector of the given input vector (3D) sub vNormalize3 { my ($vx, $vy, $vz) = (shift,shift,shift); my $mag = vMagnitude3( $vx, $vy, $vz ); my $OVmag = 1.0 / $mag; return vsMul3( $vx, $vy, $vz, $OVmag ); } ################################################### # Matrix operations # Note: All matrix operations except for matrix # multiply return 2 matrices, the matrix for # the operation and the matrix for the inverse # operation. This is because writing a matrix # inversion function is something of a bitch... ################################################### my $mxIdentity4x4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; # Prints a 4x4 matrix to standard out sub PrintMatrix4x4 { my $m = shift; printf "\n| %.4g %.4g %.4g %.4g |\n", $m->[0], $m->[1], $m->[2], $m->[3]; printf "| %.4g %.4g %.4g %.4g |\n", $m->[4], $m->[5], $m->[6], $m->[7]; printf "| %.4g %.4g %.4g %.4g |\n", $m->[8], $m->[9], $m->[10], $m->[11]; printf "| %.4g %.4g %.4g %.4g |\n", $m->[12], $m->[13], $m->[14], $m->[15]; } # Multiples two 4x4 matrices together sub mmMul4x4 { my ($m1, $m2) = @_; my $s = [(0) x 16]; for (my $i=0; $i<4; $i++) { for (my $j=0; $j<16; $j += 4) { $s->[$i + $j] = $m1->[$j] * $m2->[$i] + $m1->[$j + 1] * $m2->[$i+4] + $m1->[$j+2] * $m2->[$i+8] + $m1->[$j+3] * $m2->[$i+12] ; } } return $s; } # Given three scale values for each axis, constructs a scale matrix sub mxScale4x4 { my $x = shift; my $y = shift; my $z = shift; my $m = [(0) x 16]; my $invm = [(0) x 16]; $m = [$x, 0, 0, 0, 0, $y, 0, 0, 0, 0, $z, 0, 0, 0, 0, 1]; $invm = [1/$x, 0, 0, 0, 0, 1/$y, 0, 0, 0, 0, 1/$z, 0, 0, 0, 0, 1]; return ($m, $invm); } # Given a translation vector, constructs a translation matrix sub mxTranslate4x4 { my $x = shift; my $y = shift; my $z = shift; my $m = [(0) x 16]; my $invm = [(0) x 16]; $m = [1, 0, 0, $x, 0, 1, 0, $y, 0, 0, 1, $z, 0, 0, 0, 1]; $invm = [1, 0, 0, -$x, 0, 1, 0, -$y, 0, 0, 1, -$z, 0, 0, 0, 1]; return ($m, $invm); } # Given a 4x4 matrix and a 3-tuple, returns a 3-tuple that is # transformed by the matrix sub mvMul { my ($x, $y, $z) = (shift, shift, shift); my $m = shift; my @r = ($m->[0]*$x + $m->[4]*$y + $m->[8]*$z + $m->[3], $m->[1]*$x + $m->[5]*$y + $m->[9]*$z + $m->[7], $m->[2]*$x + $m->[6]*$y + $m->[10]*$z + $m->[11]); return @r; } sub PointOnRay { my @raybegin = (shift,shift,shift); my @raydir = (shift,shift,shift); my $dist = shift; my @ret = vsMul3( @raydir, $dist ); @ret = vvAdd3( @ret, @raybegin ); return @ret; } ################################################### # Geometry intersections ################################################### # Given a ray and a sphere's radius and center # determines if the two intersect! sub RaySphereIntersection { my @raybegin = (shift,shift,shift); my @raydir = (shift,shift,shift); my @center = (shift,shift,shift); my $radius = shift; @raydir = vNormalize3( @raydir ); my $range = $INFINITY; my @normal = (0,0,0); my $sqrRadius = $radius * $radius; my @vOriginToCenter = vvSub3( @center, @raybegin ); my $OCSquared = vvDot3( @vOriginToCenter, @vOriginToCenter ); my $ClosestApproach = vvDot3( @vOriginToCenter, @raydir ); if (($OCSquared >= $sqrRadius) && ($ClosestApproach < $NEARZERO)) { return $range; } my $HalfChordSquared = $sqrRadius - $OCSquared + ($ClosestApproach*$ClosestApproach); if ($HalfChordSquared > $NEARZERO) { my $HalfChord = sqrt( $HalfChordSquared ); my $Distance1 = $ClosestApproach + $HalfChord; my $Distance2 = $ClosestApproach - $HalfChord; if (($Distance1 > $NEARZERO) || ($Distance2 > $NEARZERO)) { if ($Distance1 > $NEARZERO) { $range = $Distance1; } if (($Distance2 < $range) && ($Distance2 > $NEARZERO)) { $range = $Distance2; } } } if ($range < $INFINITY) { # Compute the normal! my @PointOnSurface = PointOnRay( @raybegin, @raydir, $range ); @normal = vvSub3( @PointOnSurface, @center ); @normal = vNormalize3( @normal ); } return ($range, @normal); } ################################################### # Camera / Viewer code ################################################### # Given x and y points on the screen, constructs the ray direction # for the point on the virtual screen, with the camera at the # specified position sub ConstructWorldRayDirFromCamera { my $screenx = shift; my $screeny = shift; my @p = ($screenx, $screeny, 0.0); my @ptrans = mvMul( @p, $mxCamera ); my @v = vvSub3( @CAMERA_POS, @ptrans ); @v = (-$v[0], -$v[1], $v[2]); return @v; } # uses sin and cos to compute tan sub tan { my $a = shift; return sin($a)/cos($a); } # This prepares the camera's transformation matrix # We expect field of view and the camera's position, # and orientation to be world set! sub PrepareCameraData { my $h = 2.0 * tan( $FOV * 0.5 ); my ($m1, $invm1) = mxTranslate4x4( -0.5 * $RES_X, -0.5 * $RES_Y, -1.0 ); my ($m2, $invm2) = mxScale4x4( $h/$RES_X, -$h/$RES_Y * $ASPECT_RATIO, 1 ); my ($m3, $invm3) = mxTranslate4x4( @CAMERA_POS ); $mxCamera = mmMul4x4( $m3, $m2 ); $mxCamera = mmMul4x4( $mxCamera, $m1 ); } ################################################### # Main program ################################################### # Given a row and column, evaluates if this a good place to place an N-Rooks sample sub IsGoodPlaceForSample { my $iRow = shift; my $iCol = shift; foreach my $Sample (@SAMPLES_TEMP) { if (($Sample->[0] == $iRow) || ($Sample->[1] == $iCol)) { return 0; } } return 1; } # This function creates n subsamples using the N-Rooks algorithm sub CreateSubSamples { my $numSamples = shift; @SAMPLES_TEMP = (); my $segmentWidth = 1.0 / $numSamples; my $segmentHeight = 1.0 / $numSamples; for ($i=0; $i<$numSamples; $i++) { # For each row try to randomly place a sample until we are successful my $iCol = 0; my $exit = 0; do { $iCol = rand() * $numSamples; $exit = IsGoodPlaceForSample( $i, $iCol ); } while (!$exit); # The current iCol should be good so we can place the sample here # Determine the bounds for this strata my $StrataStartX = $i*$segmentWidth; my $StrataStartY = $i*$segmentHeight; my $StrataCenterX = $StrataStartX + ($segmentWidth*0.5); my $StrataCenterY = $StrataStartY + ($segmentHeight*0.5); my $randX = rand() * 2.0 - 1.0; my $randY = rand() * 2.0 - 1.0; push @SUBSAMPLES, [$randX*$segmentWidth*0.5+$StrataCenterX, $randY*$segmentHeight*0.5+$StrataCenterY]; push @SAMPLES_TEMP, [$i, $iCol]; } } # Given the output image, it dumps it to a PPM file sub WriteImagePPM { my $fileHandle = shift; my $imageHeader = shift; my $imageBytes = shift; print $fileHandle "P6\n$imageHeader->[0] $imageHeader->[1]\n255\n"; print $fileHandle @$imageBytes; } # Given an incoming ray, and the normal for a surface, gives the outgoing reflected ray sub ReflectedRay { my @incoming = (shift,shift,shift); my @normal = (shift,shift,shift); my $c1 = -vvDot3( @normal, @incoming ); my @ret = vsMul3( @normal, $c1*2.0 ); @ret = vvAdd3( @incoming, @ret ); return @ret; } sub RayIntersectObjects { my @raybegin = (shift,shift,shift); my @raydir = (shift,shift,shift); my @RetColor = (); my $ClosestSoFar = $INFINITY; my @ClosestNormal = (); my $ClosestReflectivity = 0; # Iterate through the list of objects and intersect each one foreach my $Obj (@OBJECTS) { # First thing we read is the type my $ObjType = $Obj->[0]; if ($ObjType != 1) { die; } # Read the rest of the data my @SphereCenter = ($Obj->[1],$Obj->[2],$Obj->[3]); my $SphereRadius = $Obj->[4]; my @SphereColor = ($Obj->[5],$Obj->[6],$Obj->[7]); my $SphereReflectivity = $Obj->[8]; # Do the intersection test my ($range, @normal) = RaySphereIntersection( @raybegin, @raydir, @SphereCenter, $SphereRadius ); if (($range < $INFINITY) && ($range < $ClosestSoFar)) { $ClosestSoFar = $range; @RetColor = @SphereColor; @ClosestNormal = @normal; $ClosestReflectivity = $SphereReflectivity; } } if (($ClosestSoFar < $INFINITY) && ($ClosestReflectivity > 0)) { # Cast a reflected ray my @reflectedBegin = PointOnRay( @raybegin, @raydir, $ClosestSoFar-0.1 ); my @reflectedDir = ReflectedRay( @raydir, @ClosestNormal ); my @refColor = RayTrace( @reflectedBegin, @reflectedDir ); if (@refColor != (0.119608, 0.137255, 0.556863)) { # Then this reflection is useful! @refColor = vsMul3( @refColor, $ClosestReflectivity ); @RetColor = vvAdd3( @RetColor, @refColor ); } else { print "Reject!\n"; } } return ($ClosestSoFar, @RetColor, @ClosestNormal); } # This function given a ray, traces it into # the scene and returns a color sub RayTrace { my @raybegin = (shift,shift,shift); my @raydir = (shift,shift,shift); $RECURSION_COUNT++; if ($RECURSION_COUNT > $MAX_RECURSIONS) { $RECURSION_COUNT--; return (0.119608, 0.137255, 0.556863); # "Background" pixel color! DarkSlateBlue } # Intersect with all the objects my ($range, $cr, $cg, $cb, @normal) = RayIntersectObjects( @raybegin, @raydir ); if ($range < $INFINITY) { my @AccruedDiffuse = @AMBIENT_LIGHT; #(0, 0, 0); # my @AccruedSpecular = (0, 0, 0); # not yet used! # Do lighting calculations for each light foreach my $Light (@LIGHTS) { my $LightType = $Light->[0]; if ($LightType != 1) { die; } # Read the rest of the data my $LightPower = $Light->[1]; my @LightColor = ($Light->[2], $Light->[3], $Light->[4]); my @LightPosition = ($Light->[5], $Light->[6], $Light->[7]); # For diffuse, compute the dot product between the surface normal # and the vector the light's position from the surface my @Intersection = PointOnRay( @raybegin, @raydir, $range ); my @vToLight = vvSub3( @LightPosition, @Intersection ); my $DistanceToLight = vMagnitude3( @vToLight ); @vToLight = vNormalize3( @vToLight ); my $Dot = vvDot3( @normal, @vToLight ); if ($Dot > 0.0) { # No backlighting here! # Shadow checks! my ($shadowRange, $garbage, $garbage2, $garbage3, @garbage4) = RayIntersectObjects( @LightPosition, negate3Tuple( @vToLight ) ); if ($shadowRange > $DistanceToLight-0.1) { # No Shadowing!, we can safely accrue this light's contribution my @ThisLight = vsMul3( @LightColor, $LightPower ); @ThisLight = vsMul3( @ThisLight, $Dot ); @AccruedDiffuse = vvAdd3( @AccruedDiffuse, @ThisLight ); } } } $RECURSION_COUNT--; return ($cr*$AccruedDiffuse[0], $cg*$AccruedDiffuse[1], $cb*$AccruedDiffuse[2]); } $RECURSION_COUNT--; return (0.119608, 0.137255, 0.556863); # "Background" pixel color! DarkSlateBlue } # Renders the image, and outputs it to a PPM file sub RenderImage { # Create the canvas: { my @c = ("\x80" x (3*$RES_X)) x $RES_Y; $canvas = \@c; } for (my $y=0; $y<$RES_Y; $y++) { print STDERR "Line: ", $y, " of ", $RES_Y, ": ", $y/$RES_Y*100.0, "%\n"; for (my $x=0; $x<$RES_X; $x++) { if ($NUMSUBSAMPLES > 1) { # Do sub sampling! ($r, $g, $b) = (0, 0, 0); foreach my $Sample (@SUBSAMPLES) { my @rayDir = ConstructWorldRayDirFromCamera( $Sample->[0]+$x, $Sample->[1]+$y ); my ($tr, $tg, $tb) = RayTrace( @CAMERA_POS, @rayDir ); $tr = 1.0 if ($tr > 1.0); $tg = 1.0 if ($tg > 1.0); $tb = 1.0 if ($tb > 1.0); $r += $tr; $g += $tg; $b += $tb; } $r = $r / $NUMSUBSAMPLES * 255.0; $g = $g / $NUMSUBSAMPLES * 255.0; $b = $b / $NUMSUBSAMPLES * 255.0; } else { # Construct a ray and trace it my @rayDir = ConstructWorldRayDirFromCamera( $x, $y ); ($r, $g, $b) = RayTrace( @CAMERA_POS, @rayDir ); $r = $r * 255.0; $g = $g * 255.0; $b = $b * 255.0; $r = 255 if ($r > 255); $g = 255 if ($g > 255); $b = 255 if ($b > 255); } my $color; $color .= chr( $r ); $color .= chr( $g ); $color .= chr( $b ); substr($canvas->[$y], 3*$x, 3) = $color; } if ($GENERATE_INTERIM_OUTPUT > 0) { open( $INTERIM_OUT, "> rayperl_interim.ppm" ); WriteImagePPM( $INTERIM_OUT, [$RES_X, $RES_Y], $canvas ); close( $INTERIM_OUT ); } } print STDERR "\n"; open( $OUT, "> rayperl.ppm" ); WriteImagePPM( $OUT, [$RES_X, $RES_Y], $canvas ); close( $OUT ); } # Predicts the time it will take to render this scene sub PredictRenderTime { my $TotalRaysToFire = $RES_X * $RES_Y * $NUMSUBSAMPLES; print STDERR "\nPredicting Render Time:\n["; # This is based on the amount of time it takes to render 64 rays randomly my $StartTime = (times)[0]; for (my $i=0; $i<64; $i++) { my @rayDir = ConstructWorldRayDirFromCamera( rand($RES_X), rand($RES_Y) ); RayTrace( @CAMERA_POS, @rayDir ); print STDERR '.'; } print STDERR ']'; my $RunTime = (times)[0] - $StartTime; return $RunTime / 64.0 * $TotalRaysToFire; } ################################################### # Scenes ################################################### # This is the test scene with the random arrangement of spheres. # WARNING: This one takes a gross amount of time to render! With 512 x 512 # and 16 samples / pixel, it was predicting 2 weeks on a P3-550!!!! # sub RandomSpheresScene { my $SPHERES_PER_RING = 20; my $RINGS = 50; for (my $j=0; $j<$RINGS; $j++) { my $ringposX = rand(1.2) - 0.6; my $ringposY = rand(1.2) - 0.6; my $ringposZ = rand()*0.2 - 0.1; my $ringSize = rand(0.2) + 0.1; my $ringSphereSize = rand(0.05) + 0.04; my @ringColor = (rand()*0.5+0.5, rand()*0.5+0.5, rand()*0.5+0.5); for (my $i=0; $i<$SPHERES_PER_RING; $i++ ) { my $OnAxis = rand(3); my $x, $y, $z; if ($OnAxis < 1) { $x = sin($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposX; $y = cos($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposY; $z = $ringposZ; } elsif ($OnAxis < 2) { $x = $ringposX; $y = cos($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposY; $z = sin($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposZ; } else { $x = sin($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposX; $y = $ringposY; $z = sin($i/$SPHERES_PER_RING*$PI*2.0) * $ringSize + $ringposZ; } push @OBJECTS, [1, $x, $y, $z, $ringSphereSize, @ringColor, 0 ]; } } push @LIGHTS, [1, 0.6, 1.0, 1.0, 1.0, 0.0, 0.0, -3.5 ]; push @LIGHTS, [1, 0.6, 1.0, 1.0, 1.0, 0.0, 5.0, 0.0 ]; } # This is the really simple test scene. The ring with the sphere at the bottom sub SimpleScene { push @OBJECTS, [1, 0, -1.5, 0, 1.25, 0, 1.0, 0]; for (my $i=0; $i<10; $i++ ) { my $x = sin($i/10*$PI) * 0.2; my $y = cos($i/10*$PI) * 0.2; push @OBJECTS, [1, $x, $y, 0, 0.05, 1.0, 0, 0, 0]; push @OBJECTS, [1, -$x, $y, 0, 0.05, 1.0, 0, 0, 0]; } push @LIGHTS, [1, 0.50, 1.0, 1.0, 1.0, 0.0, 0.0, -1.0 ]; push @LIGHTS, [1, 0.75, 1.0, 1.0, 1.0, 0.0, 5.0, 0.0 ]; } # This is the scene with the helix like structure sub HelixScene { for (my $i=0; $i<20; $i++ ) { my $x = sin(($i/20+0.25)*$PI*2.0) * 0.2; my $y = 0.5 - $i/20*1.0; my $z = cos(($i/20_0.25)*$PI*2.0) * 0.075; push @OBJECTS, [1, $x, $y, $z, 0.05, rand()*0.5+0.5, rand()*0.5+0.5, 0.15, 0 ]; } for (my $i=0; $i<20; $i++ ) { my $x = sin(($i/20+0.75)*$PI*2.0) * 0.2; my $y = 0.5 - $i/20*1.0; my $z = cos(($i/20+0.75)*$PI*2.0) * 0.075; push @OBJECTS, [1, $x, $y, $z, 0.05, rand()*0.5+0.5, rand()*0.5+0.5, 0.25, 0 ]; } push @LIGHTS, [1, 1.00, 1.0, 1.0, 1.0, 0.0, 0.0, -1.0 ]; } # This is the reflection test scene. With the one ring through the other and the large # sphere at the bottom. All the spheres are reflective. sub ReflectionScene { push @OBJECTS, [1, 0, -1.5, 0, 1.25, 0, 1.0, 0, 0.75]; for (my $i=0; $i<20; $i++ ) { my $x = sin($i/20*$PI*2.0) * 0.2; my $y = cos($i/20*$PI*2.0) * 0.2; push @OBJECTS, [1, $x, $y, 0, 0.05, 1.0, 0, 0, 0.75]; } for (my $i=0; $i<20; $i++ ) { my $y = sin($i/20*$PI*2.0) * 0.2 + 0.2; my $z = cos($i/20*$PI*2.0) * 0.2; push @OBJECTS, [1, 0, $y, $z, 0.05, 0.65, 0.27, 0.60, 0.75]; } push @LIGHTS, [1, 0.75, 1.0, 1.0, 1.0, 0.0, 0.0, -1.0 ]; push @LIGHTS, [1, 0.50, 1.0, 1.0, 1.0, 0.0, 5.0, 0.0 ]; } # This is the colored lights test scene sub ColoredLightsScene { for (my $i=0; $i<10; $i++ ) { for (my $j=0; $j<10; $j++) { my $x = $i/10 * 0.5 - 0.25; my $y = $j/10 * 0.5 - 0.25; push @OBJECTS, [1, $x, $y, 0, 0.06, 1.0, 1.0, 1.0, 0.0]; } } push @LIGHTS, [1, 0.75, 1.0, 0.0, 0.0, 0.3, 0.3, -0.2 ]; push @LIGHTS, [1, 0.75, 0.0, 1.0, 0.0, -0.3, 0.3, -0.2 ]; push @LIGHTS, [1, 0.75, 0.0, 0.0, 1.0, 0.0, -0.3, -0.2 ]; } $GENERATE_INTERIM_OUTPUT = 0; $NUMSUBSAMPLES = 1; $RES_X = 32; $RES_Y = 32; $FOV = 30.0 * $DEG_TO_RAD; $MAX_RECURSIONS = 5; $RECURSION_COUNT = 0; $ASPECT_RATIO = $RES_Y / $RES_X; @CAMERA_POS = (0.0, 0.0, -1.5); @AMBIENT_LIGHT = (0.05, 0.05, 0.05); @OBJECTS = (); @LIGHTS = (); @SUBSAMPLES = (); CreateSubSamples( $NUMSUBSAMPLES ); PrepareCameraData( ); # Each object contains the following data: # First is the object type: # 1 = sphere # Then custom object data: # For sphere its, first the center, then the radius # Then the object's color: 3 values... (doubles) # Then the object's reflectivity # More to come # Each light contains the following data: # First the light type: # 1 = point # 2 = directional (not supported) # 3 = spot (not supported) # Light Power [0 - 1] # Light color (RGB) # Then custom data: # For point lights, its position # Create the scene SimpleScene(); #ColoredLightsScene(); #HelixScene(); #ReflectionScene(); #RandomSpheresScene(); my $predictedTime = PredictRenderTime(); printf "\nPredicted Render Time: %d minutes and %d seconds.\n", ($predictedTime-($predictedTime%60))/60, $predictedTime%60; my $RenderTime = (times)[0]; RenderImage( ); $RenderTime = (times)[0] - $RenderTime; printf "\nPredicted Render Time: %d minutes and %d seconds.\n", ($predictedTime-($predictedTime%60))/60, $predictedTime%60; printf "Actual Render Time: %d minutes and %d seconds.\n", ($RenderTime-($RenderTime%60))/60, $RenderTime%60;