Update a mesh at every frame
Goal: animate a terrain or a wave surface by moving its vertices, without rebuilding
the mesh_drawable.
Starting point: the default project template (
src/scene.hpp,src/scene.cpp, see Your first scene). It already declares the shapessphereandcylinder, atimer, and the memberscamera_control,environmentandgui: reuse them, do not declare them again. The first comment of each code block says in which file and where its lines go: add means insert them, replace means replace the whole function by the one shown.
Why an explicit update is needed
initialize_data_on_gpu copies the arrays of the mesh into GPU buffers (VBO). After
that, the mesh and the mesh_drawable are independent: modifying mesh.position has
no visible effect until the new values are sent to the GPU. The mesh_drawable keeps one
buffer per field (vbo_position, vbo_normal, vbo_color, vbo_uv) that can be
overwritten with update, as long as the number of vertices does not change.
1. Keep the mesh next to the drawable
The time comes from the timer of the template: do not declare a second one.
// scene.hpp - add in scene_structure mesh surface_mesh; // CPU copy, modified at each frame mesh_drawable surface; // GPU buffers
2. Create both (once)
// scene.cpp - add at the end of initialize() int const N = 100; surface_mesh = mesh_primitive_grid({-1,-1,0}, {1,-1,0}, {1,1,0}, {-1,1,0}, N, N); surface.initialize_data_on_gpu(surface_mesh); surface.model.translation = {0, 0, -0.5f}; // under the sphere and the cylinder of the template surface.material.color = {0.5f, 0.6f, 0.8f};
3. Modify, recompute normals, update the buffers (every frame)
// scene.cpp - add in display_frame(), before timer.update() float const t = timer.t; // CPU: move the vertices for (vec3& p : surface_mesh.position) p.z = 0.1f * std::sin(6 * p.x + 3 * t) * std::cos(4 * p.y); // CPU: the normals depend on the positions surface_mesh.normal_update(); // GPU: overwrite the existing buffers (same size, no reallocation) surface.vbo_position.update(surface_mesh.position); surface.vbo_normal.update(surface_mesh.normal); draw(surface, environment);
The template already calls timer.update() once, at the end of display_frame().
Update only the buffers whose content changed: here the colors and uv are untouched, so
vbo_color and vbo_uv are left as they are.
Common pitfalls
- Nothing moves: the
meshwas modified butvbo_position.updatewas not called (or was called on another drawable). - The shape moves but the lighting stays frozen: the normals were not recomputed
(
normal_update()) orvbo_normalwas not updated. - Error "Cannot update VBO with more data than was allocated":
updatecannot enlarge a buffer. If the number of vertices or triangles changes, callsurface.clear()theninitialize_data_on_gpuagain (costly: avoid doing it at every frame). - Low frame rate: do not call
initialize_data_on_gpuormesh_primitive_gridinsidedisplay_frame(); reuse the same arrays and only rewrite their values.
Variations
- Rigid motion only (move, rotate, scale the whole shape): no buffer update is needed,
change
surface.modelinstead (see Move, rotate and scale a shape). - Recompute only when a parameter changes (GUI slider) instead of at every frame: see Add GUI controls.
- Per-vertex colors that change: modify
surface_mesh.color, thensurface.vbo_color.update(surface_mesh.color). - Purely visual waves can be computed on the GPU in a custom vertex shader, with no CPU update at all (see Send a custom uniform to a shader).
Source: course lab of CSC_43043 (public version, without the solutions): 07_texture/c_perlin_noise (terrain.cpp, function update_terrain).
See also
- How-to: Build a mesh by hand, Animate a shape with time
- Understand: From CPU to GPU
- Reference:
mesh,mesh_drawable