Building a 3D Engine in Perl, Part 3
by Geoff Broadwell
|
Pages: 1, 2, 3, 4, 5
Moving the Viewpoint
That view is more than a tad overplayed. The user can't even move the viewpoint to see the back or sides of the scene. It's time to change that. I started by defining some new key bindings:
bind => {
escape => 'quit',
f4 => 'screenshot',
a => '+move_left',
d => '+move_right',
w => '+move_forward',
s => '+move_back',
left => '+yaw_left',
right => '+yaw_right',
tab => '+look_behind',
}
I then updated the command_action lookup hash to handle these as
movement keys:
$self->{lookup}{command_action} = {
quit => \&action_quit,
screenshot => \&action_screenshot,
'+move_left' => \&action_move,
'+move_right' => \&action_move,
'+move_forward' => \&action_move,
'+move_back' => \&action_move,
'+yaw_left' => \&action_move,
'+yaw_right' => \&action_move,
'+look_behind' => \&action_move,
};
init_view needs to initialize two more velocity components and
matching deltas:
$self->{world}{view} = {
position => [6, 2, 10],
orientation => [0, 0, 1, 0],
d_yaw => 0,
v_yaw => 0,
v_forward => 0,
v_right => 0,
dv_yaw => 0,
dv_forward => 0,
dv_right => 0,
};
action_move needs a new movement speed to match the existing yaw
speed and some additions to %move_update:
my $speed_move = 5;
my %move_update = (
'+yaw_left' => [dv_yaw => $speed_yaw ],
'+yaw_right' => [dv_yaw => -$speed_yaw ],
'+move_right' => [dv_right => $speed_move],
'+move_left' => [dv_right => -$speed_move],
'+move_forward' => [dv_forward => $speed_move],
'+move_back' => [dv_forward => -$speed_move],
'+look_behind' => [d_yaw => 180 ],
);
So far, the changes are mostly hash updates instead of procedural code; that's a good sign that the existing code design has some more life left. When conceptually simple changes require significant code modification, especially special cases or repetitive blocks of code, it's time to look for a refactoring opportunity. Thankfully, these changes are in initialization and configuration rather than special cases.
One routine that requires a good bit of new code is
update_view. I added these lines to the end:
$view->{v_right} += $view->{dv_right};
$view->{dv_right} = 0;
$view->{v_forward} += $view->{dv_forward};
$view->{dv_forward} = 0;
my $vx = $view->{v_right};
my $vz = -$view->{v_forward};
$view->{position}[0] += $vx * $d_time;
$view->{position}[2] += $vz * $d_time;
That routine is beginning to look a bit repetitious and has several copies of very similar lines of code, so it goes on the list of places to refactor in the future. There are not yet enough cases to make the best solution obvious, so I'll hold off for a bit.
The new code starts by applying the new velocity deltas in the same
way that it updates v_yaw earlier in the routine. It converts the right and
forward velocities to velocities along the world axes by noting that
the view starts out with "forward" parallel to the negative Z axis and
"right" parallel to the positive X axis. It then multiplies the X and Z
velocities by the time delta to arrive at a position change, which it
adds into the current view position.
This version of the code works fine as long as the user doesn't rotate the view. When the view rotates, "forward" and "right" don't match the new view directions. They still point down the -Z and +X axes respectively, which can prove very disorienting for high rotations. The solution is a bit of trigonometry. The idea is treat the initial X and Z velocities as components of the total velocity vector, and rotate that vector through the same angle that the user rotated the view:
my $vx = $view->{v_right};
my $vz = -$view->{v_forward};
my $angle = $view->{orientation}[0];
($vx, $vz) = rotate_xz($angle, $vx, $vz);
$view->{position}[0] += $vx * $d_time;
$view->{position}[2] += $vz * $d_time;
The two middle lines are the new ones. They call rotate_xz to do
the vector rotation work and then set $vx and $vz to the returned
components of the rotated velocity vector. rotate_xz is:
sub rotate_xz
{
my ($angle, $x, $z) = @_;
my $radians = $angle * PI / 180;
my $cos = cos($radians);
my $sin = sin($radians);
my $rot_x = $cos * $x + $sin * $z;
my $rot_z = -$sin * $x + $cos * $z;
return ($rot_x, $rot_z);
}
After converting the angle from degrees to radians, the code calculates and saves the sine and cosine of the angle. It then calculates the rotated velocity components given the original unrotated components. Finally, it returns the rotated components to the caller.
I'll skip the derivation here (you're welcome), but if you're curious about how and why this calculation performs a rotation, there are numerous books that explain the wonders of vector mathematics in amazing detail. O'Reilly's Physics for Game Developers, by David M. Bourg, includes a high-level discussion of rotation. Charles River Media's Mathematics for 3D Game Programming & Computer Graphics, by Eric Lengyel, includes a deeper discussion though I, for one, have college math flashbacks every time I read it. Speaking of which, any college textbook on linear algebra should include as much detail as you desire.
This code requires a definition for PI, provided by the following
line near the top of the program, right after requesting warnings
from Perl:
use constant PI => 4 * atan2(1, 1);
The constant module evaluates possibly complex calculations during the
compile phase and then converts them into constants at runtime. The
above calculation takes advantage of a standard trig identity to derive a
value for PI accurate to as many digits as the system can deliver.
update_view now does the right thing, no matter what angle the view
is facing. It doesn't take long to find a more interesting view:


