Introduction to VCL library
Preparing your system
- The lab classes all take place in C++ using OpenGL.
- > Follow these instructions corresponding to your system to make sure that you can compile and execute C++ programs with the needed library.
- Notes
-
- - The compiler should be compatible with C++14.
- - Glad is used to load function for OpenGL 3.3.
- - GLFW is used to create a window.
- - GUI is handled through imgui library.
VCL library
The exercises will be using a set of helper function provided as a library named VCL.Download and compile
- > Download the library and exercises hosted on github: https://github.com/drohmer/inf585_vcl
- Each exercises is described as a main program defined in the scenes/ directory.
-
- Note that a CMakeLists.txt is associated to each program.
- The first introductory scene is described in scenes/inf585/00_introduction/
- > Compile and execute the code in redoing the installation instructions in this directory (cmake + compilation + execution).
-
- Note: you need to adapt the path of the instruction to the directory of the CMakeLists.txt
- Once executed, you should observe a basic scene as seen below with rotating cylinder, cube, and curve (some details may vary).
-
- Note: The source code of the scene is fully described in the single src/main.cpp file (the rest are the files of the VCL library and external dependencies).
Information on the library
VCL 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.
- - Easy and secured storage for buffers of elements.
- - 3D scene manipulation: Mouse controlled camera
- - 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.
General structure of the code
Role of the different high level directories- scenes/ Contains the code associated to each 3D scene.
-
- - One exercise is an independant main file (+ possible other files) and associated CMakeLists.txt for its compilation.
- - Changing exercise consists in compiling the code from another directory.
- - All your code will take place in these directories (unless you want to modify the library).
- library/vcl/ Contains the actual source code of the VCL library: set of structures and functions to ease generating your 3D scene.
- library/third_party External library used by VCL such as glad (OpenGL loader), imgui (GUI), lodepng (loader for png images).
Code editor
- The library contains multiples files. Make sure you use a sufficiently advanced (or well parameterized) IDE to have
-
- C++ code completion (in particular complete function names, display expected arguments and types, objects arguments, etc.)
- efficiently switch between files and jump to the signature and code of any function and object.
-
- Note: Visual Studio Code and other lightweights text editor usually doesn't provide bu default correct C++ code completion.
- If you are not already familiar with C++ code editor (in Linux/Mac), you may try QtCreator, a C++ IDE able to load complete project from the CMakeLists.txt, and providing usefull tool such as code completion and structure navigation (in Windows, Visual Studio is the default IDE for C++).
- Finally, note that the code should be executed from the root of the code directory (the directory containing the CMakeLists.txt of the corresponding exercise) in order to load external files at runtime. You may need to parameterize your run directory in your IDE (ex. QtCreator) to start the executable within it.
Use of the code library and program structure
- > 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 initialize_data and display_scene are called.
- > Change the rotation (axis and angle) from some of the object within the scene in the function display_scene.
-
- 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 between vec3 and mat3 using mathematical operators (+,-,*,/).
- Note that there is two type of mesh structures in use
-
- - mesh storing buffer on data (per vertex: position, normal, uv, color, and triangle connectivity) on CPU
-
- This structure allows to conveniently access to all the data defining a mesh from the C++ code. However these data are not on the GPU, so a mesh cannot be directly displayed.
-
- - mesh_drawable storing VBOs associated to these buffer once sent on the GPU memory (in the sub-structure mesh_drawable_gpu_data) as well as its VAO. The structure also stores uniform parameters that are sent to the shader at every draw call. A default shader and texture id are also be stored with the structure.
-
- This structure only stores the index corresponding to elements on GPU. You cannot modify individual per-vertex elements easily from this structure.
-
- Remark: a mesh_drawable can be automatically generated from a mesh structure in calling the construction mesh_drawable(meshName). However, you cannot create a mesh from a mesh_drawable.
Adding a sphere
In this first part we display a new sphere to the scene.- > Add the global variable to the scene
mesh_drawable sphere;
- > Initialize this variable in the initialize_data function
mesh const sphere_mesh = mesh_primitive_sphere(); sphere = mesh_drawable(sphere_mesh);
- (or variant: simply in one line without explicitely storing the mesh variable)
sphere = mesh_drawable(mesh_primitive_sphere());
- > Display this sphere in the display_scene function with the following code
draw(sphere, scene);
- > Observe that the sphere can be seen in the 3D scene.
- Note: You can apply geometric transformation using the uniform properties of the mesh_drawable structure (reminder: uniform are parameters passed to the shaders).
-
- sphere.transform.translate/rotate/scale = ... (see example on the other displayed shapes)
- Note also that unless specified explicitely, the default shader associated to the mesh_drawable structure is used.
- > Add the following line before calling draw on the sphere and observe that the color (as well as any other uniform parameter) can be changed through time in modifying its value at every frame.
sphere.shading.color = vec3(1+std::cos(time), 1+std::sin(time), 2.0)/2.0f;
Checkbox interface
We will now add a GUI checkbox (button that can be checked) to activate/deactivate the wireframe display of the sphere.- > Add the following line in the definition of the gui_parameters structure to store a boolean state indicating when the wireframe should be displayed or not
bool is_wireframe = false;
- > In the function display_interface add a Checkbox (handled by ImGui library) and link it (through its adress) to the variable is_wireframe in adding this line of code
-
- Note: the variable can be access as user.gui.is_wireframe.
ImGui::Checkbox("Wireframe", &user.gui.is_wireframe);
- In running the code, the checkbox should appear. Every time you select/unselect it, the value of the variable is_wireframe change from true to false.
- > Add the following code in the display_scene function
if(user.gui.is_wireframe) draw_wireframe(sphere, scene, {1,1,0});
- Check that you can now interactively display the wireframe representation of the sphere.
Deforming vertices of a surface
The following example show a case where mesh position are modified in the C++ code and need to be updated at each frame.- > Add the following elements as global variables
mesh shape; buffer<vec3> initial_position; mesh_drawable shape_visual;
-
- shape is use to store (on CPU memory) the current state of the deformed mesh
- initial_position is use to store (on CPU memory) the initial position of each vertex of the shape
- shape_visual is use to display the deformed shape.
- > Initialise these variables to a uniformly sampled grid shape in the initialize_data function with the following code
size_t const N = 100; shape = mesh_primitive_grid({0,0,0},{1,0,0},{1,1,0},{0,1,0},N,N); initial_position = shape.position; shape_visual = mesh_drawable(shape); shape_visual.shading.color = {0.6f, 0.6f, 0.9f};
- > Call the drawing of this surface at the end of the display_scene function
draw(shape_visual, scene); if(user.gui.is_wireframe) draw_wireframe(shape_visual, scene, {0,0,0});
- > Create a function computing the deformation of the surface and call it in the display_scene function.
void evolve_shape(float time) { size_t const N = initial_position.size(); for(size_t k=0; k<N; ++k) { vec3 const& p0 = initial_position[k]; vec3& p = shape.position[k]; p.z = p0.z + 0.1f*std::cos(10*p.x+4*time); } }
- While the structure mesh is updated by the function, its visual representation (and the associated VBO) are not. An explicit update of the data stored on the GPU is necessary.
- > To this end add the following code after your call to evolve_shape
shape_visual.update_position(shape.position);
- This function send again to the GPU the position from the buffer. Note that the update doesn't reallocate any VBO (it is therefore faster that generating a new object) and assume that the size of the buffer remains constant.
- Observe that the surface is now deformed, but its color remains uniform despite the undulation. Indeed, the shader still use the initial normals of the planar grid, and doesn't take into account the change of geometry in the shading.
- > Normals of the surface can be recomputed and updated to the GPU using the following code
// Recompute normals on the CPU (given the position and the connectivity currently in the mesh structure) shape.compute_normal(); // Send updated normals on the GPU shape_visual.update_normal(shape.normal);
- Observe that the surface is now correctly updated through its deformation.
- > Change in the evolve_shape function the deformation to the following one using Perlin noise, make sure you understand the result.
float const dz = 0.3f*noise_perlin({p0.x+0.2f*time, p0.y, 0}, 2) + 0.015f*noise_perlin({4*p0.x, 4*p0.y, time}, 2); p = p0 + vec3(0, 0, dz);
Texture
Textures images can also be associated to surfaceNote that the per-vertex uv coordinates need to be defined correctly to get the mapping of the image on the surface.
- > Update the initialization of you shape variable with the following two lines code and observe that your surface should now be textured
// Reset the color of the shape to white (only the texture image will be seen) shape_visual.shading.color = {1,1,1}; // Load the image and associate the texture id to the structure shape_visual.texture = opengl_texture_to_gpu(image_load_png("assets/squirrel.png"));
-
- Note: If your program crash, check if the error log indicates that the file assets/squirrel.png cannot be accessed. In this case, it means that your executable was not run from the root directory (directory where the CMakeLists.txt is located). Follow carrefully the procedure corresponding to your IDE/method to correct it (parameterize QtCreator/Visual Studio, or change the directory from where you run your executable in command line).