Send a custom uniform to a shader
Goal: make a flat grid wave in the vertex shader, driven by a time value and an amplitude slider sent from the C++ code.
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.
Where your uniforms come from
draw already sends the uniforms of the drawable (model, material) and of the
environment (view, projection, light); the full list and their order are in
Shaders and uniforms.
Your own values go in environment.uniform_generic, a uniform_generic_structure with one
map per GLSL type (uniform_float, uniform_vec3...): each entry name -> value is sent to
the shader of every drawable at each draw call, where it is declared as
uniform <type> name;.
1. Write the shader
Copy shaders/mesh/mesh.vert.glsl to shaders/mesh_custom/mesh_custom.vert.glsl, declare
the new uniforms and use them:
// shaders/mesh_custom/mesh_custom.vert.glsl (only the changed lines) uniform float time; // seconds, sent from C++ uniform float amplitude; // set by a GUI slider void main() { vec3 p = vertex_position; p.z += amplitude * sin(6.0 * p.x + 3.0 * time); vec4 position = model * vec4(p, 1.0); // ... rest of the default shader unchanged }
2. Load it and assign it to a drawable (once)
The time comes from the timer of the template: do not declare a second one.
// scene.hpp - add in scene_structure mesh_drawable water; float amplitude = 0.1f;
// scene.cpp - add at the end of initialize() water.initialize_data_on_gpu(mesh_primitive_grid({-1,-1,0}, {1,-1,0}, {1,1,0}, {-1,1,0}, 100, 100)); water.model.translation = {0, 0, -0.5f}; // under the sphere and the cylinder of the template water.material.color = {0.5f, 0.6f, 0.8f}; opengl_shader_structure shader_custom; shader_custom.load( project::path + "shaders/mesh_custom/mesh_custom.vert.glsl", project::path + "shaders/mesh/mesh.frag.glsl"); // keep the default fragment shader water.shader = shader_custom;
load compiles and links the two files; the program stops with the GLSL compiler
message if the shader has an error. Assigning the shader to a drawable only copies its
GPU id: several drawables can share it.
3. Set the values (every frame)
// scene.cpp - add in display_frame(), before timer.update() environment.uniform_generic.uniform_float["time"] = timer.t; environment.uniform_generic.uniform_float["amplitude"] = amplitude; draw(water, environment);
The template already calls timer.update() once, at the end of display_frame().
// scene.cpp - add in display_gui() ImGui::SliderFloat("Amplitude", &litude, 0.0f, 0.3f);
A vec3 is sent the same way: environment.uniform_generic.uniform_vec3["light_color"] = {1,1,1};
matches uniform vec3 light_color; in GLSL.
Common pitfalls
- Warning "Try to send uniform variable [time] to a shader that doesn't use it": the
uniform_genericvalues are sent to every drawn shape, including those using the default shader, which does not declare time. It also happens when the uniform is declared but unused: the GLSL compiler removes it. The warning is harmless; silence it withcgp_warning::max_warning = 0;ininitialize(). - The value never changes: the name in the map and in GLSL differ (case included), or
the map does not match the GLSL type (a
uniform intis filled fromuniform_int, auniform floatfromuniform_float). - The shape keeps the default look: the shader was assigned to another drawable, or
the file you edited is not the one loaded (compare with the path given to
load). - Normals and lighting do not follow the waves: the vertex shader moved the positions but not the normals. Compute the normal in the shader from the derivative of the displacement if the lighting matters.
Variations
- Load the shader directly into the drawable, for a shader used by one shape only:
water.shader.load(vertex_path, fragment_path);(the other shapes keep the default shader, see Shaders and uniforms). - Uniform for a single drawable only: pass a
uniform_generic_structureas theadditional_uniformsargument ofdraw. - Custom fragment shader as well (Phong coefficients, fog...): copy
shaders/mesh/mesh.frag.glslthe same way and declareuniform float coeff_ambient;etc. - Release a shader explicitly:
shader_custom.clear()(new in cgp-2027), only once no drawable uses it any more (the drawables hold copies of the same GPU id).
Source: course labs of CSC_43043 (public version, without the solutions): 05_shaders/b_shading_effect (scene.cpp and mesh_custom.frag.glsl: Phong
coefficients and light_color from the GUI) and 07_texture/d_animation (liquid surface
animated in the vertex shader).
See also
- How-to: Add GUI controls, Update a mesh at every frame
- Understand: Shaders and uniforms
- Reference:
opengl_shader_structure,environment_generic_structure,mesh_drawable