Repository navigation
SimpleGL Tutorial
OpenGL, the most prevalent graphics specification, is used to render 3D scenes, and can be leveraged to make animations like movies and visualizations of physics simulations and interactive video games. However, current OpenGL implementation libraries are hard to learn and use due to the high level of customizability and functionality offered, even though much of this customizability isn't needed for the majority of use cases.
SimpleGL is a beginner-friendly, easy to use wrapper around GLFW, an open source implementation of OpenGL. By abstracting away and defaulting parts of OpenGL, SimpleGL significantly decreases the ramp up time for learning how to write a graphics program compared to barebones GLFW and dramatically decreases the lines of code needed to write a graphics program for those use cases that don't require extreme customizability.
This tutorial will walk through the writing of a simple graphics program from start to finish. It assumes the reader has worked with C++ before. For the full source code see test/src/hellosimplegl.cpp in the SimpleGL repository
- C++17
- clang: macOS version
Apple LLVM version 10.0.1 (clang-1001.0.46.3) - ar (command line tool)
- glfw3: Run
brew install glfw3if using Mac OS.
Make sure you have all of the above installed. Then, download the source code in this repository and run make in the top level. This will generate a libSimplegl.a file in the generated build/ directory.
(You can run make clean if you ever need to remove all the created files.)
Create a file named hellosimplegl.cpp. Include "simplegl.hpp" in the program and set up a main function. All of SimpleGL is inside the sgl namespace, so we can use using namespace sgl to make it easier to use SimpleGL functions.
So far, the file should look like this:
#include "simplegl.hpp"
using namespace sgl;
int main(int argc, char** argv){
}To start using SimpleGL create a Scene object (let's use the default values--for more options see the API):
Scene s; // default screen size is 800 x 600Everything you want to see at the end on the screen, needs to get added to the Scene object, which is also what ultimately handles rendering all the objects it contains.
Objects in SimpleGL are called identified and operated on by their ObjId. These objects can be one of the types provided by SimpleGL (sphere, truncated cone, cylinder, cone, pyramid, torus, box, composite). Composite is a special type of object that is made of two or more objects. You can also add a custom object which can be initialized from a .obj file.
The default objects, with the exception of composite, can be created with default parameters.
To add any object, you must use the add_obj() function which acts on a Scene object and returns an ObjId which as mentioned above, allows you to identify the object for transformations later. For a full explanation of all the options with respect to this function please see the API, otherwise, here we will demonstrate how to use this function by example.
The only required parameter for add_obj() is the type of object you are creating. For example, let's add a few basic default objects:
// add a default sphere and default box
ObjId my_sphere = s.add_obj(Shape::sphere);
ObjId my_box = s.add_obj(Shape::box);For all objects, you can also specify a non-default color using the color type which is a typedef of type glm::vec3, a 3-element vector that specifies the RGB values of a color.
// add a default box with a color
color myColor = glm::vec3(0.6, 0.6, 0.6);
ObjId my_box2 = s.add_obj(Shape::box, myColor);Some shapes can have additional non-default parameters. Like the pyramid, which defaults to 3 sides. We can change this value, by using the obj_params type. This type encapsulates all the possible parameters for any kind of object, including custom objects from .obj files and composite objects. Thus it's a bit complicated.
Here's a diagram showing the structure of obj_params, where the dotted line means the parent node can be either of the children nodes, and the solid lines mean the child is a field of the parent node:
graph TD;
A(obj_params) --> B(comp)
A --> C(glp_params)
C -.-> D(params)
C -.-> E(string)
D --> F(sides)
D --> G(accuracy)
That is to say, the obj_params contains two fields, comp which is a vector of ObjIds and glp_params, where glp_params is a variant of params and string and where params has two integer fields, sides and accuracy.
Here's an example where we create a pyramid object that has a non-default number of sides:
// add pyramid with 7 sides
obj_params oparams;
params p = {.sides = 7};
oparams.glp_params = p;
ObjId my_pyramid = s.add_obj(Shape::pyramid, myColor, oparams); // myColor is defined aboveAs you can see we set the sides field here which requires creating an obj_params and a params and then setting the sides field in params and then assigning our params variable to our obj_params's glp_params field.
In order to pass in a custom obj_params, we do need to specify a color. However, this color value can easily be changed later for each object so for now, we can use the color object we defined already as a placeholder.
We can use the correct fields of obj_params to create composite objects. Let's make a composite of the default box and sphere we created above.
// we created my_sphere and my_box above
obj_params opComposite;
opComposite.comp = {my_sphere.duplicate(), my_box.duplicate()};
ObjId c1 = s.add_obj(Shape::composite, myColor, opComposite);Creating a composite object is as simple as creating the objects you want to compose first and then setting the obj_params's comp field to be a vector of the objects you want to compose and then calling add_obj with the parameters and the type Shape::composite. Here we call duplicate on the objects we want to compose because we also want to use my_sphere and my_box separately from their composition later. And this way they won't influence each other.
We can also load objects from .obj files using the right settings of obj_params and add_obj(). If you don't have your own .obj file you want to load, the SimpleGL repository has several .obj files to choose from in test/obj_files/. Pick one and get the filepath of that file. Here we chose to use kitten.obj
obj_params oparamKitten;
oparamKitten.glp_params = "/path/to/test/obj_files/kitten.obj"; // replace this with your filepath
ObjId my_kitten = s.add_obj("kitten", myColor, oparamKitten);So far we have only added objects to the scene. Now let's move them to a different area of the screen, scale, rotate them etc. These transformations are accomplished via transformation functions that operate on an ObjId type. They are documented in detail in the API. Here we will explore a few transformations by example.
Let's translate and scale the pyramid we just added. Let's also set a new color for the pyramid. These translations, scale values, and color values are all arbitrary. You can change them at will.
my_pyramid.translate(glm::vec3(-.2,1.2,-0.2));
my_pyramid.scale(0.2);
my_pyramid.set_color({1.0, 0.5, 0.31}); For the sphere let's similarly move it, scale it, and re-color it.
my_sphere.translate(glm::vec3(2,.8,-0.5));
my_sphere.scale(0.4);
my_sphere.set_color({.4, 0.2, 0.31}); And also with the box we made earlier. Let's also rotate it 30 degrees around the point (1,1,0).
my_box.translate(glm::vec3(.1,1,-0.8));
my_box.scale(0.2);
my_box.set_color({.2, 0.8, 0.9});
my_box.rotate(30, glm::vec3(1, 1, 0));For the second box, let's make that a floor like object so that we can see the shadows casted from the other objects:
my_box2.translate(glm::vec3( -35, -4, -35));
my_box2.scale({70, 0.01, 70}); // make this box REALLY bigLet's transform the composite object also.
c1.translate(glm::vec3(1,.1,-0.2));
c1.scale(0.3);
c1.set_color({.1, 0.8, 0.6}); And lastly, for the kitten object:
my_kitten.scale(0.5);
my_kitten.set_color({0.8, 0.6, 0.8});Now all the objects are different colors and are at different locations, so they are all easily viewable. Their sizes are all adjusted so that different scale values can be demonstrated.
At this point we are satisfied with the transformations and are ready to render the scene. This is a simple one line call:
s.render();This call will open up a window with the scene rendered when you run the program. The window will be open until you close it (you can do so by hitting the esc key). You can use your mouse or the keyboard controls stated in the README to move around the scene and manipulate the view and objects.
Here is the compilation command for our program. It results in an executable called hellosimplegl, where /path_to_simplegl should be replaced by the path to the SimpleGL source code repository.
$ g++ -o hellosimplegl hellosimplegl.cpp -I/path_to_simplegl/src -I/path_to_simplegl/include -L/path_to_simplegl/build -L/usr/local/Cellar/glfw/3.2.1/lib -lglfw -lSimplegl -std=c++17
The location of glfw is based on the Mac brew install resultant location.
Then to run the program simply in the correct directory execute the following command:
$ ./hellosimplegl
A window should then pop up that looks like this:
You can move the camera around the scene either using your cursor or using the w (up), a (left), s (down), and d (right) keyboard keys. Move the view downward so that you can see the shadows the objects cast onto the "floor." Move the view upward to see the pyramid and move to the right to see the rest of the purple sphere.
For the full source code resulting from this tutorial please see test/src/hellosimplegl.cpp. Remember to replace the kitten.obj filepath with the correct filepath before compiling and executing the file.
Written with StackEdit.
Spring 2019 Design Using C++ (Lahav Lipson, Yuxuan Mei, Crystal Ren)