Please send suggestions or corrections for this page to the author at sbuss@ucsd.edu.
This page describes some of the features of Modern OpenGL used in the SolarModern program. This is the third of a series of programs illustrating features of Modern OpenGL. The first two programs, SimpleDrawModern and SimpleAnimModern, already introduced many of the basic features of any OpenGL program, and these were described on their web pages. This webpage only discusses aspects of the software which are new to SolarModern.
The code fragments listed below are taken from the program SolarModern.cpp. It is recommended to refer to the source code as you read this web page to see how the code fits together. In some cases, the code examples given below are slightly modified to make this web page more readable.
The SolarModern program renders a large yellow sun, being orbited by a
blue earth, which in turn is orbited by a smaller white moon.
The GlGeomSphere
class is used to render spheres.
The LinearMapR3
and LinearMapR4
classes are used to handle matrices.
The SolarModern program uses the GlGeomSphere
class to triangulate and render spheres. These are drawn in
wireframe so as to make their spherical structure evident.
These classes encapsulate all the work of allocating and loading
a VAO and VBO with data triangulating a unit sphere. They also
take care of issuing the glDrawElements
to
render the sphere.
The three spheres for the sun, the earth, and the moon are allocated with constructors:
GlGeomSphere Moon1(6, 6); // A sphere with 6 slices and 6 stacks
GlGeomSphere Earth(8, 12); // A sphere with 8 slices and 12 stacks
GlGeomSphere Sun(10, 10); // A sphere with 10 slices and 10 stacks
The GlGeomSphere
constructors are called with two parameters: the numbers
slices and the number of stacks. The number of slices means
number of radial slices in the sphere. The number of stacks means the
the sphere is split into that many horizontal pieces. The slices go parallel
to the y-axis; the stacks are perpindicular to the y
axis. The GlGeomSphere
's
are always rendered as spheres of radius 1 centered at the original, but
the vertex shader uses a Modelview matrix to resize and reposition it as
needed.
The following code is then called to set up the VAO, VBO, and EBO
for the GlGeomSphere
's.
The value vertPos_loc
is equal to 0: it is the
vertex shader's location for the position of a vertex.
// These routines take care of loading info into their VAO's, VBO's and EBO's.
Sun.InitializeAttribLocations(vertPos_loc);
Earth.InitializeAttribLocations(vertPos_loc);
Moon1.InitializeAttribLocations(vertPos_loc);
When rendering the scene, myRenderScene
invokes
the following functions to render the sun, then later the earth,
and finally the moon:
Sun.Render();
…
Earth.Render();
…
Moon1.Render();
These routines bind the VAO associated with the GlGeomSphere
,
and render the sphere using glDrawElements
.
For more information on the GlGeomSphere
class,
see the GlGeomShapes webpages.
The sun, earth and moon are rendered using Modelview matrices
to place them at the correct location. These Modelview matrices
are updated every time the scene is rendered so as to
animate the solar system. There are two variables
HourOfDay
and DayOfYear
that control the position of the earth as it
revolves around the sun, how the earth is rotated
on its axis, and the position of the moon as it revolves
around the earth.
The view matrix is a LinearMapR4
object;
namely a 4x4 matrix. It computed by:
viewMatrix.Set_glTranslate(0.0, 0.0, -CameraDistance); // Translate to be in front of the camera
viewMatrix.Mult_glRotate(viewAzimuth, 1.0, 0.0, 0.0); // Rotate to view from slightly above
The first line sets viewMatrix
to a translation matrix
that puts the camera CameraDistance
out the z-axis.
The second line multiplies by a matrix that
rotates the scene around the x-axis by
viewAzimuth
radians: the direction of the rotation is given by
the righthand rule.
CameraDistance
is equal 30.0; viewAzimuth
is initially equal to 0.25 radians.
These place the solar system
in front of the camera, viewed slightly from above.
The sun is rendered with the following code:
float matEntries[16]; // Holds 16 floats (since cannot load doubles into a shader that uses floats)
…
LinearMapR4 SunPosMatrix = viewMatrix; // Place Sun at center of the scene
SunPosMatrix.DumpByColumns(matEntries);
glUniformMatrix4fv(modelviewMatLocation, 1, false, matEntries);
glVertexAttrib3f(vertColor_loc, 1.0f, 1.0f, 0.0f); // Make the sun yellow
Sun.Render();
The call to SunPosMatrix.DumpByColumns(matEntries)
puts into the matEntries
array,
the 16 entries of the 4x4 matrix SunPosMatrix
as float
's in column order. The call to
glUniformMatrix4fv
loads these 16 values into the
Modelview matrix used by the shader program. The Modelview
matrix is a uniform variable for the shader program: this means
that both the vertex and fragments shaders have access to this
variable. The value modelviewMatLocation
is the
shader program's location number for the Modelview matrix.
The call to Sun.Render()
renders the Sun as a unit
sphere, colored yellow.
The earth is then rendered with the following code:
// EarthPosMatrix - specifies position of the earth
// EarthMatrix - specifies the size of the earch and its rotation on its axis (on its axis).
LinearMapR4 EarthPosMatrix = SunPosMatrix;
double revolveAngle = (DayOfYear / 365.0)*PI2;
EarthPosMatrix.Mult_glRotate(revolveAngle, 0.0, 1.0, 0.0); // Revolve the earth around the sun
EarthPosMatrix.Mult_glTranslate(0.0, 0.0, 5.0); // Place the earth five units away from the sun
LinearMapR4 EarthMatrix = EarthPosMatrix;
double earthRotationAngle = (HourOfDay / 24.0)*PI2;
EarthMatrix.Mult_glRotate(earthRotationAngle, 0.0, 1.0, 0.0); // Rotate earth on y-axis
EarthMatrix.Mult_glScale(0.5); // Make radius 0.5.
EarthMatrix.DumpByColumns(matEntries);
glUniformMatrix4fv(modelviewMatLocation, 1, false, matEntries);
glVertexAttrib3f(vertColor_loc, 0.2f, 0.4f, 1.0f); // Make the earth bright cyan-blue
Earth.Render();
The earth is orbiting at distance 5.0 units from the sun.
The EarthPosMatrix
is initialized to equal the SunPosMatrix
, so it
inherits the positioning of the sun.
The first call to Mult_glRotate
multiplies the EarthPosMatrix
on the right with the rotation matrix generating a rotation of revolveAngle
radians around the y-axis; the direction of rotation is given by the righthand rule.
The next line calls the method Mult_glTranslate(0.0, 0.0, 5.0)
.
This multiplies on the right by the matrix that translates distance 5.0 along the
positive z. Together these two lines control the position of the earth as it revolves
around the sun.
The EarthPosMatrix
is then copied to the EarthMatrix
.
The next lines control the orientation of the earth it rotates
on its axis. Finally, the call to the method Mult_glScale(0.5)
multiplies
by the uniform scaling matrix to shrink the size of the earth by the factor 0.5.
The moon is rendered by the following code. It works similarly to the way the earth was
rendered. Note that the moon's matrix is based on the EarthPosMatrix
matrix, not the EarthMatrix
.
This is because the matrix EarthMatrix
incorporates
the earth's rotation on its axis and the scaling of the earth by 0.5;
whereas the EarthPosMatrix
does not. Thus the earth's rotation
and scaling do not affect the rendering of the moon.
LinearMapR4 MoonMatrix = EarthPosMatrix; // Base the moon's matrix off the earth's *POS* matrix (EarthPosMatrix)
double moonRotationAngle = (DayOfYear*12.0 / 365.0)*PI2;
MoonMatrix.Mult_glRotate(moonRotationAngle, 0.0, 1.0, 0.0); // Revolving around the earth twelve times per year
MoonMatrix.Mult_glTranslate(0.0, 0.0, 1.0); // Place the Moon one unit away from the earth
MoonMatrix.Mult_glScale(0.2); // Moon has radius 0.2
MoonMatrix.DumpByColumns(matEntries);
glUniformMatrix4fv(modelviewMatLocation, 1, false, matEntries);
glVertexAttrib3f(vertColor_loc, 0.9f, 0.9f, 0.9f); // Make the moon bright gray
Moon1.Render();
SolarModern tries to animate smoothly at 60 frames per second. For the main program loop uses
glfwWaitEventsTimeout(1.0/60.0); // Use this to animate at 60 frames/sec (timing is NOT reliable)
to request a callback for rendering 60 times per second. In "fixed-timestep" mode, the animation increments the motion of the earth and moon by 1/60th of a second. This timing is not always reliable; for instance, if there is mouse movement, there can be a callback for every pixel worth of mouse movement, and this can be much more frequent than 60 times per second. The net result can be faster animation speed when the mouse is moving!
To avoid this problem, "real-timestep" mode can be used. (Use the "t"
command to toggle fixed- and real-timestep modes.)
The routine glfwGetTime()
is used in real-timestep mode to get the current
time. The previous time and the current time are compared, and the elapsed
time since the previous rendering is used in place of (1/60)-th to update
the animation variables DayOfYear
and HourOfDay
.
Here is the main part of the code:
// Calculate the time step, either as AnimateIncrement or based on actual elapsed time
double thisAnimateIncrement = AnimateIncrement;
if (UseRealTime && !singleStep) {
double curTime = glfwGetTime();
thisAnimateIncrement *= (curTime - PreviousTime)*60.0;
PreviousTime = curTime;
}
// Update the animation state
HourOfDay += thisAnimateIncrement;
DayOfYear += thisAnimateIncrement / 24.0;
HourOfDay = HourOfDay - ((int)(HourOfDay / 24)) * 24; // Wrap back to be in range [0,24)
DayOfYear = DayOfYear - ((int)(DayOfYear / 365)) * 365; // Wrap back to be in range [0,365)
Note how the variables DayOfYear
and HourOfDay
are kept in the range 0 to 365 and 0 to 24. This doesn't make an
immediate functionality change, but can avoid loss
of precision if the animation is run for a long time.
Two Boolean variables spinMode
and singleStep
are used to the control the "single step" mode. You should look at
the code to see how these work.
Rotational aliasing: The earth is simulated as rotating on its axis, but when initially run, it will appear to not be rotating. This is due to the fact that the earth is modeled with eight slices; and if it rotates a multiple (1/8)-th of a full rotation, the slices will alias with the earth appearing to be in identical orientation from frame to frame. This mostly happens when using a fixed animation increment instead of using an animation increment based on the actual elapsed time.
SolarModern uses a perspective transformation for viewing the solar system. This will make the earth (and moon) larger on the screen when they are closer to the viewer, and smaller when they are farther away. It has the following features:
Xmax
and Ymax
mean that the entire
solar system should have
x-coordinates between -Xmax
and Xmax
,
and should have
y-coordinates between -Ymax
and Ymax
.
windowXmax
and windowYmax
are computed for what should be visible in the render window.
Zmin
and Zmax
mean the entire solar
system should fit within these z-coordinates.
zNearMin
means that the near clipping plane should not be
less than zNearMin
. This value is 1.0, and must be
positive.
CameraDistance
,
the near and far clipping values
zNear
and zFar
are computed.
Here is the main code for the perspective transformation represented by the Projection matrix:
double w = (width == 0) ? 1.0 : (double)width;
double h = (height == 0) ? 1.0 : (double)height;
double windowXmax, windowYmax;
double aspectFactor = w * Ymax / (h * Xmax); // == (w/h)/(Xmax/Ymax), ratio of aspect ratios
if (aspectFactor>1) {
windowXmax = Xmax * aspectFactor;
windowYmax = Ymax;
}
else {
windowYmax = Ymax / aspectFactor;
windowXmax = Xmax;
}
// Using the max & min values for x & y & z that should be visible in the window,
// we set up the perspective projection.
double zFar = -Zmin + CameraDistance;
double zNear = Max(CameraDistance - Zmax, ZnearMin);
theProjectionMatrix.Set_glFrustum(-windowXmax, windowXmax, -windowYmax, windowYmax, zNear, zFar);
if (glIsProgram(shaderProgram1)) {
glUseProgram(shaderProgram1);
theProjectionMatrix.DumpByColumns(matEntries);
glUniformMatrix4fv(projMatLocation, 1, false, matEntries);
}
The call to Set_glFrustum
sets theProjectionMatrix
to be the 4x4 matrix of the legacy OpenGL glFrustum
function.
This maps a frustum into the 2x2 cube with x,y,z values all between -1 and 1.
The first two parameters to Set_glFrustum
set the front of
the frustum to have x values ranging from
-windowXmax
to windowXmax
. The next two parameters
similarly bound the y extent of the front of the frustum.
The last two parameters set the distances to the near and far clipping
planes. The value zNear
must be less than zFar
.
Objects at distance less than zNear
or more than zFar
will be clipped or culled, and thus not be rendered at all.
Note how the last two lines of code load the 16 entries of the projection matrix into the
projection matrix used by the shader program. The Projection matrix
is a uniform variable for the shader program; projMatLocation
is the
shader program's location for the Projection matrix.