1. Code library
This part introduce a library dedicated to your class that will be used for the rest of the exercises. This library provides a set of functionalities to ease 3D graphics programming such as
-
Structure for 3D vectors and matrices (and 2D, 4D).
-
Object structure ready to be displayed with OpenGL (Meshes, lines, etc) and their associated shaders.
-
3D scene manipulation: Mouse controlled camera
The library tries to be as lightweight as possible. It is written to ease 3D graphics programming, but not to hide underlying working state. Although this is not required, you should be able to understand every part of the code. It keeps in particular the following principles
-
Minimalistic code written to be simple to understand (not necessarily the most generic nor efficient).
-
Objects are written to be lightweight and with minimal hidden states. - You can directly interact with object attributes.
-
The code and functionalities remain close to basic C++/OpenGL. - You can use direct OpenGL calls with the library. You should also keep in mind the way that OpenGL works to avoid unexpected display.
Download and run the library
-
Download the following code [ src ]
-
Make sure you can compile and execute the code.
-
Use the provided CMakeLists.txt (a README file is provided with instructions)
-
Once executed, you should observe a basic scene with rotating cylinder, cube, and curve.
-
-
A model of camera is already implemented, it allows
-
Rotation - left click + mouse displacement (trackball model)
-
Zoom - right click + up/down mouse displacement
-
Panning (/panning) - CTRL + left click + mouse displacement
-
Moving forward/backward (orthogonal to the camera plane) - CTRL + right click + up/down mouse displacement.
-
General structure of the code
Role of the different high level directories
-
src/ Contains source code of the VCL library as well as the code related to exercises
-
shaders/ Contains GLSL shaders code
-
external/ Contains external code library used by VCL such as glad (OpenGL loader), imgui (GUI), lodepng (loader for png images).
The src/ directory contains itself subdirectories and files
-
src/exercises/ Contains the code related to the exercises. You will mostly modify and add your own code in these files. Each file will be related to a different scene and exercise.
-
src/vcl/ Contains the actual source code of the VCL library
-
src/main.cpp Describes the main call of the program. It calls the initialization of the different library, setup data, and run the animation loop.
-
src/exercise_current.hpp Indicate which exercise is currently compiled. Initially, the exercise is set of EXERCISE_INTRODUCTION.
VCL Libray
The VCL source code is organized as follows
-
vcl/core/ contains the stand-alone code of the library. It is itself split between
-
vcl/core/math/ defining helper mathematical functions and structures such as 2/3/4D vectors and matrices.
-
vcl/core/opengl/ contains objects and functions to ease the use of OpenGL.
-
vcl/core/shape/ handles 3D objects such as meshes. These elements are stored on CPU memory.
-
vcl/core/drawable/ handles OpenGL drawable object stored on GPU memory.
-
vcl/core/scene/ contains non-geometric objects and functions used within 3D scene such as camera, as well as timer elements.
-
vcl/core/helper/ useful functions to handle string and i/o files.
-
-
vcl/external_lib/ contains wrapper structures and functions using external library (glfw, imgui, lodepng).
Note on your code editor
-
The library contains multiples files. Make sure you are able to use your text editor, or IDE, to efficiently switch between files and jump toward the signature and code of any function and object.
-
If you are not already familiar with C++ code editor, you may try QtCreator, a C++ IDE able to load complete project from the CMakeLists.txt, and providing usefull tool such as code completion.
-
Finally, note that the code should be executed from the root of the code directory. You may need to parameterize your the run directory from your IDE to start the executable within it.
Using the library
Vector and matrix
-
Observe the file main.cpp. You should recognize the general organization of the program, in particular the setup stage, and the animation loop stage. Note where the functions setup_data and frame_draw are called.
-
Observe the files in exercises/00_introduction/. Each exercise file has the following organization:
-
An object scene_exercise is defined and implement, at least, the functions setup_data and frame_draw. scene_exercise derives from base_scene_exercise which provides the signature (and empty body) of several functions expected by the main structure. In addition, each scene_exercise object can handle all internal data you need for the current exercise.
-
Application
-
Change the rotation (axis and angle) from some of the object within the scene in the function frame_draw.
-
Note that the structure vec3 implements a model of 3D vector with (x,y,z) coordinates.
-
The rotation is stored as a 3x3 matrix using the mat3 structure.
-
You can apply most of the basic operations on vec3 and mat3 using mathematical operators.
-
Implementation details in C++
The objects vec2, vec3, and vec4 share lots of common operations as they differ only by their size (similarly with mat2, mat3, mat4). To avoid duplicate large portion of the code in all of these files, a generic vector (resp. matrix) class has been implemented in vcl/core/math/vec/vec/vec.hpp (resp. vcl/core/math/mat/mat/mat.hpp). These generic classes are parametezied by their respective size as template parameters, and provide common functions implementations such as +/-/*, etc.
The specific implementation of vec2/3/4 (resp. mat2/3/4), are then implemented as template specialization from the generic class in order to only define specific structure and functions related to their size, while being compatible with the generically defined functions.
Meshes
-
Let us consider the case where we want to add a quadrangle in the scene.
-
First, we need to generate the structure containing the per-vertex information such as position coordinates, normals, etc. as well as the triangle connectivity. This structure is stored in an object of type mesh.
-
Observe the mesh object structure and note which kinds of vertex-attributes are natively handled within the object.
-
The attributes of mesh can be filled manually as vectors of values, or you can use the helper functions building meshes for a set of basic primitives (vcl/core/shape/mesh/mesh_primitive/).
-
Observe the code of the function mesh_primitive_quad()
-
Create a quadrangle with default parameter in the function setup_data with the syntax
const mesh mesh_quad = mesh_primitive_quad();
The structure mesh only defines values stored on CPU memory, it can be conveniently manipulated from the C++ code, but cannot be directly displayed by OpenGL.
The structure mesh_gpu (vcl/core/opengl/mesh_gpu) handle the conversion from a mesh structure stored on CPU memory onto OpenGL compatible VBOs.
-
Observe the code of mesh_gpu, note that its constructor can take as input a mesh.
mesh_gpu stores directly low level VBOs, and can display the corresponding surface. However, it doesn’t handle its environment such as the shader used to display it and uniform attributes to be sent. These elements can be set before the draw call manually, but to ease such setting a higher level structure called mesh_drawable (vcl/core/drawable/mesh_drawable) is proposed.
A mesh_drawable can be seen as an aggregation between a mesh_gpu and a set of possible uniform attributes used at each draw call. Moreover, the shader is explicitly passed as a parameter at each draw call. A mesh_drawable can be built from a mesh_gpu or directly from a mesh.
-
Add a mesh_drawable quad object within the data structure of the scene_exercise, and initialize it with the quadrangle you created before (in setup_data function).
quad = mesh_drawable(mesh_quad);
Note that you also need to define the variable quad in the header structure of the class scene_exercise
vcl::mesh_drawable quad;
-
Display the quad using the mesh shader in the frame_draw function. If necessary, you can also display mesh objects with the wireframe shader.
quad.draw(shaders["mesh"], scene.camera);
-
Note that the quad is displayed at its default centered position. Change the uniform parameters from the quad object to translate it and change its color.
// Example
quad.uniform_parameter.translation = {0,-0.5,-1};
quad.uniform_parameter.color = {1,0,0};
Texture
Meshes can be displayed with 2D image texture. The quadrangle you defined has defaults (u,v)-texture coordinates and is displayed with a default white texture.
Follow these steps to apply a texture image to the quadrangle.
-
Download and save a .png image of your choice (consider a square image for better result) in data/texture.png
-
In the function setup_data, add the following code to load the .png image and send the data onto GPU memory (add the variable texture_id of type GLuint in your class attributes).
// Load a png and store it on CPU
const image texture_image = image_load_png("data/texture.png");
// Send image data onto GPU and store its ID
texture_id = texture_gpu(texture_image);
-
In frame_draw, before displaying the quadrangle, set the texture to texture_id.
// Set the current texture
glBindTexture(GL_TEXTURE_2D, texture_id);
// Display quadrangle
quad.draw(shaders["mesh"],scene.camera);
// To avoid unwanted reuse of the texture in future drawing, it is safe to set the current texture to a white image (pre-set by the library of code)
glBindTexture(GL_TEXTURE_2D, scene.texture_white);
-
Compile and run the code, the image should be displayed on the quadrangle.
Visualizing trajectory
Another display-ready structure is proposed by curve_dynamic_drawable. This structure can display a curve with limited number of points in a FIFO (First In First Out) way. When the maximal size is reached, every time a new point is added, the oldest one is retrieved. This can be typically be used to draw local trajectories of points through time.
-
Add a curve_dynamic_drawable in your data structure
-
Display it using, for instance, the following code that plots the trajectory of one of the point of the cylinder (adapt the code if you changed the translation of the cylinder).
curve_dynamic.uniform_parameter.color = {0,1,1};
const vec3 p = rotation*vec3(1.5f,1.0f,-0.2f);
curve_dynamic.add_point(p);
curve_dynamic.draw(shaders["curve"],scene.camera);
Generating a scene with several elements
Add the following pieces of code
(in the class header)
std::vector<vcl::vec3> positions;
vcl::mesh_drawable cone;
(in the function setup_data)
cone = mesh_primitive_cone(0.1f,{0,0,0},{0,0.2f,0});
const int N_cone = 40;
positions.resize(N_cone);
for(int k=0; k<N_cone; ++k)
{
float x = vcl::rand_interval(-2,2);
float z = vcl::rand_interval(-2,2);
positions[k] = {x,-1,z};
}
(in the function frame_draw)
const int N = positions.size();
for(int k=0; k<N; ++k)
{
float u = k/(N-1);
cone.uniform_parameter.translation = positions[k];
cone.draw(shaders["mesh"],scene.camera);
}
-
Display the scene and observe the result (you should see a set of cones on the floor)
-
Note that
-
Only one cone is created and stored in memory
-
This single cone is displayed several times at different pre-stored positions
-
What would have happend if the loop initializing the variable positions was placed in the frame_draw function instead of setup_data ?
-
Exercise
-
Adapt the code to model the following scene
-
Note that trees should not intersect each others
-