Introduction to CGP library
The next exercises will be using a set of helper function provided as a library named CGP.Files of the scene
- > Download the library and exercises here code.tar.gz (Linux), or code.zip (Windows)
- 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/inf630/01_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.
-
- Note: The source code of the scene is fully described in the files src/main.cpp and src/scene.h/cpp. The main.cpp file contains mostly common elements through all the exercises (general set up of the scene, animation loop, basic handling of the keyboard/mouse input). The specific content is defined in the files scene.cpp, and its header scene.hpp.
Information on the library
CGP 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/cgp/ 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 method scene.initialize() and scene.display() are called, and their implementation in the file scene.cpp.
- > Change the position from some of the object within the scene in the function initialize.
-
- Note that the structure vec3 implements a model of 3D vector with (x,y,z) coordinates.
- The translation is set using the method object.transform.rotation = {x,y,z}
- 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. You can optionally add a name to a mesh_drawable to ease debugging.
-
- Remark: a mesh_drawable can be automatically generated from a mesh structure in calling the initialization method [mesh_drawable_name].initialize([mesh_name]). 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 class variable to scene_structure class (in the file scene.hpp)
cgp::mesh_drawable sphere;
- > Initialize this variable in the initialize() function (in the file scene.cpp) as a sphere of radius 0.5
mesh sphere_mesh = mesh_primitive_sphere(0.5f); sphere.initialize(sphere_mesh, "new Sphere");
- (or variant: simply in one line without explicitely storing the mesh variable)
sphere.initialize(mesh_primitive_sphere(0.5f), "new Sphere");
- > Display this sphere in the display function with the following code
draw(sphere, environment);
- > Compile, run, and observe the sphere in the 3D scene.
- Note: You can set geometric transformation using the uniform properties of the mesh_drawable structure (reminder: uniform are parameters passed to the shaders).
-
- sphere.transform.translation/rotation/scaling = ... (see example on the other displayed shapes)
- Note also that unless specified explicitely, the default shader associated to the mesh_drawable structure is used.
- The three trasformations: translation, rotation, and scaling are simply variables that you can write on. By default, their values are respectively, (0,0,0), the identity, and 1.
- > 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 variable in the definition of the structure struct gui_parameters (in the file scene.hpp) to store a boolean state indicating when the wireframe should be displayed or not
bool display_wireframe = false;
- > In the function display_gui() (in scene.cpp) add a Checkbox (handled by ImGui library) and link it (through its adress) to the variable display_wireframe in adding this line of code
ImGui::Checkbox("Wireframe", &gui.display_wireframe);
- In running the code, the checkbox should appear. Every time you select/unselect it, the value of the variable display_wireframe change from true to false but doesn't change yet anything in the 3D display.
- > Add the following code in the display function
if (gui.display_wireframe) draw_wireframe(sphere, environment, { 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 variables of the object scene_structure
cgp::mesh shape; cgp::buffer<cgp::vec3> initial_position; cgp::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 function with the following code
int 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.initialize(shape, "Deforming shape"); shape_visual.shading.color = { 0.6f, 0.6f, 0.9f };
- > Call the drawing of this surface at the end of the display() function
draw(shape_visual, environment); if (gui.display_wireframe) draw_wireframe(shape_visual, environment, { 0,0,0 });
- > Add a new method to the scene class computing the deformation of the surface in
-
- Adding the following content in the file scene.cpp
- Adding the signature of this new method (void evolve_shape()) in the file scene.hpp
- Call this method in the display() function.
void scene_structure::evolve_shape() { 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 * timer.t); } }
- While the structure mesh is updated by the function, its visual representation (and the associated VBO) are not therefore no change is visible when the code is run. 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 * timer.t, p0.y, 0 }, 2) + 0.015f * noise_perlin({ 4 * p0.x, 4 * p0.y, timer.t }, 2);
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 your 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.jpg"));