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
-
The code library is hosted at this address: https://github.com/drohmer/inf630_vcl
-
Make sure you can compile and execute the code.
-
When the program is launched, a camera model that can be manipulated with the mouse/keyboard is already implemented. It allows the following transformations
-
Rotation - left click + mouse displacement (trackball model)
-
Zoom - right click + up/down mouse displacement
-
Panning (/translation in the camera plane) - 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
-
scenes/ Contains the codes associated to the 3D scenes setup.
-
The exercises correspond to the code described in the scenes/3D_graphics/ directory. You will edit the code mainly in these directories.
-
The default scene can be found in the scenes/3D_graphics/00_default/ directory.
-
The file scenes/current_scene.hpp defines which scene (or exercise) should be compiled. Each scene is associated with a keyword. To compile another scene, just modify this keyword.
-
-
vcl/ Contains the source code of the VCL library: set of structures and functions to facilitate the implementation of 3D scenes.
-
third_party/** External libraries used by VCL such as glad (OpenGL loader), imgui (GUI), lodepng (PNG image loader).
-
main/ Describes the main calls of the program, the basic structure of a scene, initializes the external libraries, loads the data and launches the animation loop.
VCL Library
The source code of the VCL library is organized as follows
-
vcl/base/ basic library features such as assertions and error handling.
-
vcl/containers/ defines basic containers with features to facilitate the use of data buffers: e.g. buffer = extended version of std::vector, and buffer_stack = extended version of std::array.
-
vcl/math/ defines mathematical help functions and 2/3/4D vector and matrix structures.
-
vcl/opengl/** contains objects and functions that facilitate OpenGL calls.
-
vcl/shape/ corresponds to 3D objects such as meshes and curves. 3D shapes can be stored in main memory (RAM) (e.g. position buffer storage), or in GPU memory in their display version (_drawable) (stores only VBO/VAO indices and uniform parameters).
-
vcl/interaction/ contains the non-geometric objects and functions associated with the interaction with the 3D scene such as the camera or the timers.
-
vcl/wrapper/ structures and "wrapper" functions for the use of external libraries (glfw, imgui, lodepng).
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 scenes/sources/default/animation/. Each exercise file has the following organization:
-
An object scene_model is defined and implement, at least, the functions setup_data and frame_draw. scene_model derives from scene_base which provides the signature (and empty body) of several functions expected by the main structure. In addition, each scene_model 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 between vec3 and mat3 using mathematical operators (+,-,*,/).
-
-
Note that there is two type of mesh structure handled
-
mesh storing buffer on data (per vertex: position, normal, uv, color, and triangle connectivity) on CPU
-
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 can also be stored with the structure. Note that a mesh_drawable can be automatically generated from a mesh structure.
-
Meshes
Adding a sphere
In this first part we display a new sphere to the scene.
-
Add the variable
vcl::mesh_drawable sphere;
as a class attribute (in the file default_animation.hpp)
-
Initialize this variable in the setup_data function
sphere = mesh_primitive_sphere(); // Create a default sphere model
sphere.shader = shaders["mesh"]; // Associate its default shader
-
Display this sphere in the frame_draw function with the following code
sphere.uniform.transform.scaling = 0.2f;
sphere.uniform.transform.translation = {-1,1,-2};
draw(sphere, scene.camera);
-
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).
-
Note also that the draw function will use by default the shader set attached to the structure.
-
-
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.uniform.color = vec3(1+std::cos(time), 1+std::sin(time), 2.0)/2.0f;
Adding a checkbox interface
We will now add a checkbox (button that can be checked) to activate/deactivate the wireframe display of the sphere.
-
Add the following line in the header of the class used to store a boolean state indicating when the wireframe should be displayed or not
bool is_wireframe = false;
-
In the function set_gui add a Checkbox (handled by ImGui library) and link it (through its adress) to the variable is_wireframe in adding this code
ImGui::Checkbox("Wireframe", &is_wireframe);
-
In running the code, the checkbox should appear. Every time you select/unselect it, the state of the variable is_wireframe change from true to false.
-
The last step consists in adapting the behavior of the display. Add the following code in the frame_draw function
if(is_wireframe)
draw(sphere, scene.camera, shaders["wireframe"]);
Note: In this case, the draw is called with an explicit shader parameter and it instead of the one stored with its structure.
-
Check that you can now interactively display the wireframe.
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 variables in the class header
vcl::mesh shape;
vcl::buffer<vcl::vec3> initial_position;
vcl::mesh_drawable shape_visual;
-
shape used to store (on CPU memory) the current state of the deformed mesh
-
initial_position used to store (on CPU memory) the initial position of each vertex of the shape
-
shape_visual used display the deformed shape.
-
Initialise these variables to a uniformly sampled grid shape in the setup_data function
const size_t N = 100;
shape = mesh_primitive_grid(N,N);
initial_position = shape.position;
shape_visual = shape;
shape_visual.uniform.color = {0.6f, 0.6f, 0.9f};
shape_visual.shader = shaders["mesh"];
-
Call the drawing of this (initially static) surface at the end of the frame_draw function
draw(shape_visual, scene.camera);
if(is_wireframe)
draw(shape_visual, scene.camera, shaders["wireframe"]);
-
Create a free function computing the deformation of the surface and call it in the frame_draw function.
void evolve_surface(mesh& shape, buffer<vec3> const& initial_position, float const time)
{
const size_t 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 its geometry is necessary. To this end add the following code after your call to evolve_surface
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 and assume that the size of the buffer remains constant.
-
Observe that the surface is now deformed, but its color remains uniform. 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 a set of position and a connectivity)
normal(shape.position, shape.connectivity, shape.normal);
// Update 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_surface function the deformation to the following one using Perlin noise, make sure you understand the result.
float const dz = 0.3f*perlin(p0.x+0.2f*time, p0.y, p0.z, 2) + 0.015f*perlin(4*p0.x, 4*p0.y, 4*p0.z+time, 2);
p = p0 + vec3(0, 0, dz);
Texture
-
Texture can also be associated to surface (need per-vertex uv coordinates).
-
Update the initialization of you shape variable with the following two lines code and observe the result
// Reset the color of the shape to white (only the texture image will be seen)
shape_visual.uniform.color = {1,1,1};
// Load the image and associate the texture id to the structure
shape_visual.texture_id = texture_gpu(image_load_png("scenes/sources/default/animation/assets/squirrel.png"));
-
Note: A texture id which is not 0 is automatically bind where displaying a mesh.
-
As a consequence: All other objects displayed after this one will also use the same texture if they don’t define a texture_id. To avoid such side effect, you can add the following line after the draw call
glBindTexture(GL_TEXTURE_2D, scene.texture_white);