[SPOILER] Solution to Perl 'Expert' Quiz of the Week #26

John Heitmann <[email protected]> Mon, 25 Oct 2004 01:27:52 -0400
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
I didn't intend to do this week's quiz when I wrote a few days ago, but 
I got the urge the more I thought about it. This entry uses POV-Ray (a 
free/free raytracer) instead of Tk to render the animation. The output 
of this script is a file 'coaster.pov' that contains the necessary info 
to render the track plus an array of ball locations that can be 
iterated over with the 'clock' variable using POV-Ray's built-in 
animation system. You'll have to look at the ball array size (at the 
very bottom of the file) to figure out how the clock should range. 
Pre-rendered demos of each of the 3 examples:

http://homepage.mac.com/third_man/.Movies/hurricane.mov
http://homepage.mac.com/third_man/.Movies/loop.mov
http://homepage.mac.com/third_man/.Movies/quad.mov

Problems: The core code itself isn't great. The physics are too complex 
for the task; I like Pr. Zentara's solution there much better. I ran 
out of time to make a nice smart camera, but maybe I'll have some spare 
time in the next few days to clean that in. It leaks energy. I'm not 
sure why yet.

I have a question for the list... I wanted to put in quick and dirty 
input file error handling with something like:

eval {
     use warnings FATAL => "all";
     snarf_input();
     subdivide_track();
};
if ($@) { die "Input error etc." }

The idea being that those 2 subs stressed most of the features of the 
input stream that I care about, and if they had problems it was a 
problem with the input. The warnings remained benign, however, so that 
did not work out. Can anyone see why that isn't working?

----------------------------------------------------------------

#!/usr/bin/perl
use strict;
use warnings;

my @coords;

# Global vars to track the ball
my $current_segment = 0;
my $current_segment_position = 0;
my $current_time = 0;
my $current_velocity = 0; # velocity is parallel to the track at any 
point
                           # on the track. Positive is forwards.

my $g = 9.8;
my $m = 10; # Mass isn't important as-implemented, but may be in the 
future.

my $track_subdivisions = 4;

my @ball_positions;

snarf_input();
subdivide_track() for (0..$track_subdivisions);
compute_z();
animate(total_seconds => 20, fps => 30);

exit;

sub snarf_input {
     while (<>) {
         # Convert to meters (input is in decimeters).
         push @coords, [map { $_ / 10 } split " ", $_];
     }
}

sub subdivide_track {
     my @new_coords = ($coords[0]);
     for (my $i = 1; $i < @coords; $i++) {
         push @new_coords, [
             map { ($coords[$i]->[$_] + $coords[$i-1]->[$_])/2 } (0..1)
         ],
         $coords[$i];
     }
     @coords = @new_coords;
}

# Calculate the z value of each point on the track as a linear function 
of
# the distance traveled on the track.
sub compute_z {
     my $z_ratio = .25; # For every 1 (x,y) meter traveled on the track, 
how
                        # far back we bump the z.

     my $initial_z = 0;
     $coords[0][2] = $initial_z;
     $coords[0][3] = 0; # 4th coord holds distance marker of this point
                        # along the track.

     for (my $i = 1; $i < @coords; $i++) {
         my $distance = (
             ($coords[$i][0] - $coords[$i-1][0])**2 +
             ($coords[$i][1] - $coords[$i-1][1])**2
         ) ** .5;

         $coords[$i][3] = $coords[$i-1][3] + $distance;
         $coords[$i][2] = $coords[$i][3] * $z_ratio + $initial_z;
     }
}

sub animate {
     my %args = @_;

     my $total_frames = $args{total_seconds} * $args{fps};

     for (my $frame = 0; $frame < $total_frames; $frame++) {
         animate_ball(
             time => (1.0 * $frame) / $args{fps},
         );
     }

     my $fd;
     open($fd, ">coaster.pov")
         or die "Either this program or your computer is screwed up: $!";
     output_static_scene(fd => $fd);
     output_camera(fd => $fd);
     output_ball(fd => $fd)
}

sub animate_ball {
     my %args = @_;
     my $fd = $args{fd};
     my $time = $args{time};

     my $delta_t = $time - $current_time;
     $current_time = $time;

     # Roll across track segments until $delta_t goes to 0
     while($delta_t > 0) {
         my $kinetic_energy = $m * ($current_velocity ** 2) / 2;

         # Stop if we're about to fly off the end.
         return if $current_segment == $#coords;

         # Points of interest
         my $p_next = $coords[$current_segment+1];
         my $p_prev = $coords[$current_segment];
         my $scale = ($current_segment_position / ($p_next->[3] 
-$p_prev->[3]));
         my $p_cur = [ map {
             $p_prev->[$_] + $scale * ($p_next->[$_] - $p_prev->[$_])
         } (0..2) ];

         my $is_upwards_slope =
             $p_prev->[1] < $p_next->[1];

         my $going_forwards = $current_velocity > 0 ||
             ($current_velocity==0 && !$is_upwards_slope);

         my $delta_y = $going_forwards ?
             $p_next->[1] - $p_cur->[1] : $p_prev->[1] - $p_cur->[1];

         # Get what the kinetic energy would be added if the ball rolled 
all
         # the way to the end of this track segment.
         my $energy_change = -($m * $g * $delta_y);
         my $end_ke = $energy_change + $kinetic_energy;

         # If the ball will reverse before it reaches the endpoint
         # in the direction it's currently going...
         if ($end_ke < 0) {
             # Fudge and set velocity to 0 rather than find the apex.
             $current_velocity = 0;
             next;
         }

         my $end_velocity = ($end_ke * 2/$m) ** .5;
         $end_velocity *= -1 if !$going_forwards;
         my $avg_velocity = ($current_velocity + $end_velocity) / 2;

         my $distance_left = $going_forwards ?
             $p_next->[3] - $p_prev->[3] - $current_segment_position :
             $current_segment_position;

         # If we will reach the endpoint with time to spare...
         if (abs($delta_t * $avg_velocity) >= $distance_left)  {
             # Watch out for the case where we fudge the apex and 
avg_velocity
             # is zero.
             if ($distance_left > 0) {
                 my $actual_time = $distance_left / abs($avg_velocity);
                 $delta_t -= $actual_time;
             }
             $current_velocity = $end_velocity;

             if ($going_forwards) {
                 $current_segment++;
                 $current_segment_position = 0;
             }
             else {
                 $current_segment_position =
                     $p_prev->[3] - $coords[$current_segment-1]->[3];
                 $current_segment--;
             }
             next;
         }
         # Otherwise, where do we end up on this segment?
         else {
             # Fudge and pretend avg_velocity is valid even if we fall 
short
             # or long.
             $current_segment_position += $avg_velocity * $delta_t;

             # ... but get the new velocity right so that it's just a
             # temporal problem and not an energy violation.
             my $old_y = $p_cur->[1];
             my $scale = (
                 $current_segment_position / ($p_next->[3] -$p_prev->[3])
             );
             my $new_y = $p_prev->[1] + $scale * ($p_next->[1] - 
$p_prev->[1]);

             my $changed_energy = abs($m * $g * ($old_y - $new_y));
             my $new_v = ($changed_energy * 2/$m) ** .5;
             $new_v *= -1 if $old_y < $new_y;
             $new_v *= -1 if !$going_forwards;
             $current_velocity += $new_v;
             $delta_t = 0;
             next;
         }
     }

     my $p_next = $coords[$current_segment+1];
     my $p_prev = $coords[$current_segment];
     my $scale = ($current_segment_position / ($p_next->[3] 
-$p_prev->[3]));
     my $p_cur = [
         map { $p_prev->[$_] + $scale * ($p_next->[$_] - $p_prev->[$_]) 
} (0..2)
     ];

     push @ball_positions, $p_cur;
}

sub output_camera {
     my %args = @_;
     my $fd = $args{fd};

     print $fd <<CAMERA;
camera{
     location  <20, 30, -40>
     direction z
     look_at <20, 30, 20>
}
CAMERA
}

sub output_ball {
     my %args = @_;
     my $fd = $args{fd};
     my $size = @ball_positions;
     print $fd "#declare ball_array = array[$size][3] {\n";
     print $fd join(", ", map { "{" . join(",", @{$_}) . "}" } 
@ball_positions);
     print $fd "\n}\n";
     print $fd <<BALL;
sphere {
     < ball_array[clock][0], ball_array[clock][1], ball_array[clock][2] >
     .8
     texture { txtMetal }
}
BALL
}

sub output_static_scene {
     my %args = @_;
     my $fd = $args{fd};

     print $fd <<BOILERPLATE;
// Generated by coaster.pl

#include "stdinc.inc"
#include "metals.inc"

#declare txtMetal=T_Chrome_2C

light_source { <-15, 30, -25> color <1, 1, 1> }
light_source { < 15, 30, -25> color <1, 1, 1> }

plane {
    y, 0
    texture {
        pigment {checker color <0.1, 0.3, 0.4>, color <0.2, 0.5, 0.7>}
        finish {diffuse 0.7 reflection 0.2}
        scale <4, 1, 4>
    }
}
BOILERPLATE

     for (my $i = 1; $i < @coords; $i+= 2^$track_subdivisions) {
         print $fd <<TRACK;
union {
     sphere {
         <$coords[$i-1][0], $coords[$i-1][1], $coords[$i-1][2] - .3>
         .2
     }
     cylinder {
         <$coords[$i-1][0], $coords[$i-1][1], $coords[$i-1][2] - .3>
         <$coords[$i][0], $coords[$i][1], $coords[$i][2] - .3>
         .2
     }

     sphere {
         <$coords[$i-1][0], $coords[$i-1][1], $coords[$i-1][2] + .3>
         .2
     }
     cylinder {
         <$coords[$i-1][0], $coords[$i-1][1], $coords[$i-1][2] + .3>
         <$coords[$i][0], $coords[$i][1], $coords[$i][2] + .3>
         .2
     }
     texture {
         txtMetal
     }
}
TRACK
     }
}

sub min {
     return $_[0] < $_[1] ? $_[0] : $_[1];
}

sub max {
     return $_[0] > $_[1] ? $_[0] : $_[1];
}