————————package
SDL::Tutorial::3DWorld::Camera;
=pod
=head1 NAME
SDL::Tutorial::3DWorld::Camera - A movable viewpoint in the game world
=head1 SYNOPSIS
# Start the camera 1.5 metres above the ground, 10 metres back from
# the world origin, looking north and slightly downwards.
my $camera = SDL::Tutorial::3DWorld::Camera->new(
X => 0,
Y => 1.5,
Z => 10,
angle => 0,
elevation => -5,
};
=head1 DESCRIPTION
The B<SDL::Tutorial::3DWorld::Camera> represents the viewpoint that the
user controls to move through the 3D world.
In this initial skeleton code, the camera is fixed and cannot be moved.
=head1 METHODS
=cut
use
strict;
use
warnings;
use
OpenGL;
use
SDL::Mouse;
use
SDL::Constants ();
our
$VERSION
=
'0.23'
;
=pod
=head2 new
# Start the camera at the origin, facing north and looking at the horizon
my $camera = SDL::Tutorial::3DWorld::Camera->new(
X => 0,
Y => 0,
Z => 0,
angle => 0,
elevation => 0,
};
The C<new> constructor creates a camera that serves as the primary
abstraction for the viewpoint as it moves through the world.
=cut
sub
new {
my
$class
=
shift
;
my
$self
=
bless
{
@_
},
$class
;
# The default position of the camera is at 0,0,0 facing north
$self
->{X} ||= 0;
$self
->{Y} ||= 0;
$self
->{Z} ||= 0;
$self
->{angle} ||= 0;
$self
->{elevation} ||= 0;
# Ignore the mouse origin position.
# We'll set this to the real values during init.
$self
->{mouse_origin} = [ 0, 0 ];
# The speed of the camera in metres per second when we are moving
$self
->{speed} ||= 0.2;
# Key tracking
$self
->{down} = {
# Move camera forwards and backwards
SDL::Constants::
SDLK_w
=> 0,
SDL::Constants::
SDLK_s
=> 0,
# Strafe camera left and right
SDL::Constants::
SDLK_a
=> 0,
SDL::Constants::
SDLK_d
=> 0,
# Shift makes us run
SDL::Constants::
SDLK_LSHIFT
=> 0,
};
return
$self
;
}
=pod
=head2 X
The C<X> accessor provides the location of the camera in metres on the east
to west dimension within the 3D world. The positive direction is east.
=cut
sub
X {
$_
[0]->{X};
}
=pod
=head2 Y
The C<Y> accessor is location of the camera in metres on the vertical
dimension within the 3D world. The positive direction is up.
=cut
sub
Y {
$_
[0]->{Y};
}
=pod
=head2 Z
The C<Z> accessor provides the location of the camera in metres on the north
to south dimension within the 3D world. The positive direction is north.
=cut
sub
Z {
$_
[0]->{Z};
}
=pod
=head2 angle
The C<angle> accessor provides the direction the camera is facing on the
horizontal plane within the 3D world. Positive indicates clockwise degrees
from north. Thus C<0> is north, C<90> is east, C<180> is south and C<270>
is west.
The C<angle> is more correctly known as the "azimuth" but we prefer the
simpler common term for a gaming API. For more details see
=cut
sub
angle {
$_
[0]->{angle};
}
=pod
=head2 elevation
The C<elevation> accessor provides the direction the camera is facing on
the vertical plane. Positive indicates degrees above the horizon. Thus
C<0> is looking at the horizon, C<90> is facing straight up, and
C<-90> is facing straight down.
The C<elevation> is more correctly known as the "altitude" but we prefer the
simpler common term for a gaming API. For more details see
see L<http://en.wikipedia.org/w/index.php?title=Altitude_(astronomy)>.
=cut
sub
elevation {
$_
[0]->{elevation};
}
######################################################################
# Engine Interface
# Note that this doesn't position the camera, just sets it up
sub
init {
my
$self
=
shift
;
$self
->{width} =
shift
;
$self
->{height} =
shift
;
# Calculate the mouse origin position
$self
->{mouse_origin} = [
int
(
$self
->{width} / 2 ),
int
(
$self
->{height} / 2 ),
];
# As a mouselook game, we don't want users to see the cursor.
# We also position the cursor at the exact centre of the window.
# (This will result in another spurious mouse event)
SDL::Mouse::show_cursor( SDL::Constants::SDL_DISABLE );
SDL::Mouse::warp_mouse( @{
$self
->{mouse_origin}} );
# Select and reset the projection, flushing any old state
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
# Set the perspective we will look through.
# We'll use a standard 60 degree perspective, removing any
# shapes closer than one metre or further than one kilometre.
gluPerspective( 45.0,
$self
->{width} /
$self
->{height}, 0.1, -1000 );
# Work super hard to make perspective calculations not suck
glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
return
;
}
sub
display {
my
$self
=
shift
;
# Transform the location of the entire freaking world in the opposite
# direction and angle to where the camera is and which way it is
# pointing. This makes it LOOK as if the camera is moving but it isn't.
glRotatef(
$self
->{elevation}, -1, 0, 0 );
glRotatef(
$self
->{angle}, 0, 1, 0 );
glTranslatef( -
$self
->X, -
$self
->Y, -
$self
->Z );
return
;
}
sub
move {
my
$self
=
shift
;
my
$step
=
shift
;
my
$speed
=
$self
->{speed} *
$step
;
my
$down
=
$self
->{down};
# Find the camera-wards and sideways components of our velocity
my
$move
=
$speed
* (
$down
->{SDL::Constants::SDLK_s} -
$down
->{SDL::Constants::SDLK_w}
);
my
$strafe
=
$speed
* (
$down
->{SDL::Constants::SDLK_d} -
$down
->{SDL::Constants::SDLK_a}
);
# Apply this movement in the direction of the camera
my
$angle
=
$self
->{angle} * D2R;
$self
->{X} += (
cos
(
$angle
) *
$strafe
) - (
sin
(
$angle
) *
$move
);
$self
->{Z} += (
sin
(
$angle
) *
$strafe
) + (
cos
(
$angle
) *
$move
);
return
;
}
sub
event {
my
$self
=
shift
;
my
$event
=
shift
;
my
$type
=
$event
->type;
if
(
$type
== SDL::Constants::SDL_MOUSEMOTION ) {
# Ignore mouse motion events at the mouse origin
$event
->motion_x ==
$self
->{mouse_origin}->[0] and
$event
->motion_y ==
$self
->{mouse_origin}->[1] and
return
1;
# Convert the mouse motion to mouselook behaviour
my
$x
=
$event
->motion_xrel;
my
$y
=
$event
->motion_yrel;
$x
=
$x
- 65536
if
$x
> 32000;
$y
=
$y
- 65536
if
$y
> 32000;
$self
->{angle} = (
$self
->{angle} +
$x
/ 5) % 360;
$self
->{elevation} -=
$y
/ 10;
$self
->{elevation} = 90
if
$self
->{elevation} > 90;
$self
->{elevation} = -90
if
$self
->{elevation} < -90;
# Move the mouse back to the centre of the window so that
# it can never escape the game window.
SDL::Mouse::warp_mouse( @{
$self
->{mouse_origin}} );
return
1;
}
if
(
$type
== SDL::Constants::SDL_KEYDOWN ) {
my
$key
=
$event
->key_sym;
if
(
exists
$self
->{down}->{
$key
} ) {
$self
->{down}->{
$key
} = 1;
return
1;
}
}
if
(
$type
== SDL::Constants::SDL_KEYUP ) {
my
$key
=
$event
->key_sym;
if
(
exists
$self
->{down}->{
$key
} ) {
$self
->{down}->{
$key
} = 0;
return
1;
}
}
# We don't care about this event
return
0;
}
######################################################################
# Support Methods
# Is a bounding box visible to the camera
# Test each corner as a point, shortcutting if one is visible
sub
visible_box {
my
$self
=
shift
;
# Find the vector for camera-relative forwards
# For angle = 0, elevation = 0 this should be 0, 0, 1
my
$angle
=
$self
->{angle} * D2R;
my
$elevation
=
$self
->{elevation} * D2R;
my
$XB
=
sin
(
$angle
) *
cos
(
$elevation
);
my
$YB
=
sin
(
$elevation
);
my
$ZB
= -
cos
(
$angle
) *
cos
(
$elevation
);
# Offset the bounding box by the camera position and
# multiple by vector component for each axis.
# This gives the contribution to how "forward" each
# face makes a point for that axis.
my
$X1
=
$XB
* (
$_
[0] -
$self
->{X});
my
$Y1
=
$YB
* (
$_
[1] -
$self
->{Y});
my
$Z1
=
$ZB
* (
$_
[2] -
$self
->{Z});
my
$X2
=
$XB
* (
$_
[3] -
$self
->{X});
my
$Y2
=
$YB
* (
$_
[4] -
$self
->{Y});
my
$Z2
=
$ZB
* (
$_
[5] -
$self
->{Z});
# If the sum of the most positive value on each axis
# results in a negative value then the most forward
# corner of the bounding box is behind us, and there is
# no possible way any part of the actor needs to be
# drawn on screen. Thus, the actor is not visible.
my
$sum
= (
$X1
>
$X2
?
$X1
:
$X2
)
+ (
$Y1
>
$Y2
?
$Y1
:
$Y2
)
+ (
$Z1
>
$Z2
?
$Z2
:
$Z2
);
return
1
if
$sum
> 0;
return
0;
}
# Is a point visible to the camera
sub
visible_point {
my
$self
=
shift
;
# Find the vector for camera-relative forwards
# For angle = 0, elevation = 0 this is 0, 0, -1
my
$angle
=
$self
->{angle} * D2R;
my
$elevation
=
$self
->{elevation} * D2R;
my
$XB
=
sin
(
$angle
) *
cos
(
$elevation
);
my
$YB
=
sin
(
$elevation
);
my
$ZB
= -
cos
(
$angle
) *
cos
(
$elevation
);
# Multiply the camera-relative position by the vector.
# A positive total means the point is behind us.
my
$behind
=
$XB
* (
$_
[0] -
$self
->{X})
+
$YB
* (
$_
[1] -
$self
->{Y})
+
$ZB
* (
$_
[2] -
$self
->{Z});
return
0
if
$behind
> 0;
return
1;
}
# Sort a series of vectors by distance from the camera, returning
# the result as a list of index positions.
sub
distance_isort {
my
$self
=
shift
;
my
$X
=
$self
->{X};
my
$Y
=
$self
->{Y};
my
$Z
=
$self
->{Z};
# Calculate the distances
my
@distance
=
map
{
(
$X
-
$_
->[0]) ** 2 + (
$Y
-
$_
->[1]) ** 2 + (
$Z
-
$_
->[2]) ** 2
}
@_
;
# Sort index by distance
my
@order
=
sort
{
$distance
[
$b
] <=>
$distance
[
$a
]
} ( 0 ..
$#_
);
return
@order
;
}
1;
=pod
=head1 SUPPORT
Bugs should be reported via the CPAN bug tracker at
=head1 AUTHOR
Adam Kennedy E<lt>adamk@cpan.orgE<gt>
=head1 SEE ALSO
L<SDL>, L<OpenGL>
=head1 COPYRIGHT
Copyright 2010 Adam Kennedy.
This program is free software; you can redistribute
it and/or modify it under the same terms as Perl itself.
The full text of the license can be found in the
LICENSE file included with this module.
=cut