CGPdocs

Change the color of a shape

Goal: change the color of a shape, as a whole or vertex by vertex, and change it while the program runs with a color picker.

Starting point: the default project template (src/scene.hpp, src/scene.cpp, see Your first scene). It already declares the shapes sphere and cylinder, a timer, and the members camera_control, environment and gui: 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.

Interactive CGP demo (change-the-color). Open in a new tab.

Where the color of a pixel comes from

The default shader (shaders/mesh/mesh.frag.glsl) multiplies three colors, then applies the Phong lighting:

color_object = vertex color  x  material.color  x  texture color        (per channel, RGB in [0,1])
pixel        = (ambient + diffuse * cos) * color_object  +  specular * highlight (white)
alpha        = material.alpha  x  texture alpha
Source Set with Default
One color for the whole shape material.color of the mesh_drawable white {1,1,1}
One color per vertex color of the mesh, before the upload white (filled by the primitives)
A texture image texture of the mesh_drawable a 1x1 white image

Since the defaults are white, setting one of them is enough: the two others leave it unchanged. Colors are vec3 with components between 0 and 1 (not 0 to 255).

1. One color for the whole shape: material.color

The template already does it for its two shapes:

// scene.cpp - already in the template, in initialize()
sphere.material.color = {1.0f, 0.0f, 0.0f};
cylinder.material.color = {0.0f, 1.0f, 0.0f};

For a new shape, set the color after initialize_data_on_gpu: this function resets the material (and the model transform) to their default values.

// scene.hpp - add in scene_structure
mesh_drawable ball;
// scene.cpp - add at the end of initialize()
ball.initialize_data_on_gpu(mesh_primitive_sphere(0.25f));
ball.model.translation = {0.5f, 0.75f, 0.0f};
ball.material.color = {0.2f, 0.4f, 1.0f};   // blue: R, G, B in [0,1]
// scene.cpp - add in display_frame(), before timer.update()
draw(ball, environment);

material.color is sent to the shader (uniform material.color) at every draw call: it costs nothing to change it, even at every frame (see section 3).

2. One color per vertex: mesh.color

To vary the color over the surface, fill the per-vertex array color of the mesh before the upload. Here, a gradient from blue at the bottom to red at the top:

// scene.hpp - add in scene_structure
mesh_drawable colored_ball;
// scene.cpp - add at the end of initialize()
mesh colored_ball_mesh = mesh_primitive_sphere(0.25f);
for (size_t k = 0; k < colored_ball_mesh.position.size(); ++k) {
    float const t = (colored_ball_mesh.position[k].z + 0.25f) / 0.5f;   // 0 at the bottom, 1 at the top
    colored_ball_mesh.color[k] = {t, 0.2f, 1.0f - t};
}
colored_ball.initialize_data_on_gpu(colored_ball_mesh);
colored_ball.model.translation = {-0.5f, -0.75f, 0.0f};
// scene.cpp - add in display_frame(), before timer.update()
draw(colored_ball, environment);

The mesh_primitive_* functions fill color with white, so it already has one entry per vertex. For a mesh built by hand, give color the size of position (or call fill_empty_field() first, see Build a mesh by hand). The colors are interpolated between the vertices of each triangle.

The material color still multiplies the vertex colors: keep material.color white to see them as they are. A non-white material color filters them: with {1,1,0} (yellow), the blue part of the gradient becomes black.

To change the vertex colors after the upload, modify the mesh and send the new array to the GPU: colored_ball.vbo_color.update(colored_ball_mesh.color) (keep the mesh as a member of the scene for this; see Update a mesh at every frame).

3. Change the color while the program runs, or from the GUI

Since material.color is read at each draw, assign it in display_frame(). With a color picker in the GUI:

// scene.hpp - add in gui_parameters
vec3 ball_color = {0.2f, 0.4f, 1.0f};
// scene.cpp - add in display_gui()
ImGui::ColorEdit3("Ball color", &gui.ball_color[0]);   // edits 3 consecutive floats

The whole display_frame() becomes:

// scene.cpp - replace display_frame()
void scene_structure::display_frame()
{
    environment.camera_view = camera_control.camera().matrix_view();
    environment.light = camera_control.camera_model.position();

    if (gui.display_frame)
        draw(global_frame, environment);

    draw(sphere, environment);
    cylinder.model.rotation = rotation_transform::from_axis_angle({1,0,0}, timer.t);
    draw(cylinder, environment);

    ball.material.color = gui.ball_color;   // the color chosen in the GUI
    draw(ball, environment);

    // the color can also depend on time: here the green channel oscillates
    colored_ball.material.color = {1.0f, 0.5f + 0.5f * std::cos(2 * timer.t), 1.0f};
    draw(colored_ball, environment);

    timer.update();
}

Changing material.color never requires a new initialize_data_on_gpu.

4. Tint a texture

With a texture (see Apply a texture image), the color of the image is multiplied by material.color as well: leave it white to see the image as it is, or set it to tint the image ({1.0f, 0.8f, 0.6f} warms it up). To drop the texture and get back to a plain color, set material.texture_settings.active = false.

5. Transparency: material.alpha

material.alpha (1 by default) is the opacity of the shape: ball.material.alpha = 0.5f; makes it half transparent. It is only visible when blending is enabled (glEnable(GL_BLEND)), and transparent shapes must be drawn after the opaque ones: see Draw transparent objects.

6. Shading: brighter, matte, or flat color

The lighting coefficients are in material.phong (type phong_parameters):

Field Default Effect
phong.ambient 0.3 color even where the light does not reach
phong.diffuse 0.6 color on the side facing the light
phong.specular 0.3 white highlight (0: matte surface)
phong.specular_exponent 64 size of the highlight (larger: smaller and sharper)

With the defaults, a surface facing the light shows 90% of its color (0.3 + 0.6) plus the highlight. To display the exact color, without shading (a flat color, as in a diagram), use only the ambient term: ball.material.phong = {1, 0, 0, 1};.

Common pitfalls

  • The color is ignored, the shape stays white: the color was set before initialize_data_on_gpu, which resets the material. Set it after.
  • The shape is black: one factor of the product is zero: material.color of {0,0,0}, vertex colors left at zero (a color array resized without being filled), or a vertex color and a material color with no common channel (pure red times pure blue). See also Troubleshooting.
  • The color is saturated, not the one chosen: the components were given between 0 and 255 ({255, 128, 0}); divide them by 255.
  • Colors look darker than the value set: the shading multiplies them by at most 0.9 on the lit side and 0.3 on the dark side; use the ambient-only material above for exact colors.
  • Per-vertex colors do not change after editing mesh.color: the GPU has its own copy; call vbo_color.update(...).
  • With your own shader, the colors have no effect: the shader must read the vertex color (location 2) and the uniform material.color, as mesh.frag.glsl does.

Variations

See also

CGP - Computer Graphics Programming.