3. Creating and animating a custom ClutterDeformEffect

3.1. Problem

You want to deform an actor's geometry: for example, to make it appear stretched, twisted or folded.

This recipe demonstrates how to do this with a simple page fold effect, which folds one half of the actor over its other half.

3.2. Solution

Subclass ClutterDeformEffect and implement a deform_vertex() function to modify the actor's vertices.

The signature for deform_vertex() is:

void
deform_vertex (ClutterDeformEffect *effect,
               gfloat               width,
               gfloat               height,
               CoglTextureVertex   *vertex);

The width and height are the width and height of the target material, stored in the offscreen buffer. Usually the target material's size will match the actor's transformed size; however, if the effect implements create_texture(), the target material's size may differ from the actor's transformed size.

The vertex contains the position and color of a vertex, to be deformed by your effect. Your deform_vertex() function should modify the member variables of this CoglTextureVertex in place. Usually, this will mean modifying the x, y and y member variables of the vertex, which describe its position in 3D space.

The example function below, taken from the full example, applies a transformation to vertices falling in the "right-hand" half of the actor (i.e. vertices with an x value greater than or equal to half the width of the actor).

static void
cb_page_fold_effect_deform_vertex (ClutterDeformEffect *effect,
                                   gfloat               width,
                                   gfloat               height,
                                   CoglTextureVertex   *vertex)
{
  CbPageFoldEffectPrivate *priv = CB_PAGE_FOLD_EFFECT (effect)->priv;

  /* the rotation angle is modified by the percentage progress of the fold,
   * as represented by the period variable
   */
  gfloat radians = (priv->angle * priv->period) / (180.0f / G_PI);

  /* rotate from the center of the actor on the y axis */
  gfloat adjusted_x = vertex->x - (width / 2);

  /* only rotate vertices to the right of the middle of the actor */
  if (adjusted_x >= 0.0)
    {
      vertex->x = (vertex->z * sin (radians))
                  + (adjusted_x * cos (radians))
                  + width / 2;

      /* NB add 1 to z to prevent "z fighting"; otherwise, when fully-folded
       * the image has "stripes" where vertices from the folded part
       * of the actor interfere with vertices from the unfolded part
       */
      vertex->z = (vertex->z * cos (radians))
                  + (adjusted_x * sin (radians))
                  + 1;
    }

  /* adjust depth of all vertices so they fit inside the actor while folding;
   * this has the effect of making the image smaller within the texture,
   * but does produce a cleaner fold animation
   */
  vertex->z -= width / 2;
}

Note that this effect has two properties set in its constructor or through setters:

  1. angle, representing the angle of the full fold; for the actor to fully fold in half, this would be set to 180.0

  2. period, representing the percentage of the fold to apply

As well as rotating the vertex, the deform_vertex() function also shifts the z coordinate "up" by 1 (towards the viewpoint) for vertices on the right-hand side of the actor. This is so that the "folded over" vertices are above vertices on the left-hand side. Without this small shift, the vertices interfere with each other, which can cause striping artefacts.

All vertices are also shifted "down", so that the the folding part of the actor remains within the texture. Otherwise the part which is folding may be clipped to the allocation of the actor.

This effect can now be applied to an actor, using the approach outlined in the introduction. The result looks like this when period is set to 0.25 and angle to 180.0 (i.e. the page is folded by 45 degrees):

Applying a custom ClutterDeformEffect to a texture loaded with an image

Because the effect is a GObject which exposes its properties, it can easily be animated, as described in the discussion section.

3.3. Discussion

A deform effect processes an actor as follows:

  • The actor is divided into a series of triangular tiles. The number of horizontal and vertical tiles is configurable; more tiles implies more vertices. See this section for more details about tiles.

  • The position of each vertex of each tile is then modified (or not) by the deform_vertex() function. In this function, you can change the vertex's position (x, y, z coordinates). You can also modify the color at the vertex if desired.

    The resulting deformed vertices are stored in an offscreen buffer.

  • Once the deformation has been applied to all vertices, the content of the offscreen buffer is painted at the onscreen position of the actor.

You may find it useful to visualise this process by imagining your actor's surface as a net, composed of triangles. (Something like a fishing net, not a mathematical one.) At each corner of each triangle is a marble; and between each pair of corners is an infinitely flexible length of elastic. Moving a marble doesn't change the position of its neighbours; it just stretches or relaxes the elastic.

In this analogy, the marbles are the vertices; and the surfaces between the marbles, bordered by triangles of elastic, are the tiles. More triangles (tiles) means more marbles (vertices).

When you create a ClutterDeformEffect, think of it as specifying movements of marbles in the net. Changing the position of a vertex corresponds to moving a marble up/down (-/+ y position), left/right (-/+ x position) or away/towards you (-/+ z position) (ignoring color for the moment).

Now imagine that you are asked to fold the whole net of marbles; but you can't just grab the edge of the net and pull it over: you can only move one marble at a time. However, once moved, each marble magically stays where you put it in 3D space.

To do this, you could project where each marble would be if you could fold the whole sheet in one go; then move the marbles one by one to their projected positions. Even though you'd be moving the marbles one at a time, it would eventually look as though you'd folded the whole net with a single movement.

When you write a ClutterDeformEffect, you have to accomplish a similar feat: change the shape of an actor by individually modifying the positions of points on its surface. In most cases, your deform_vertex() implementation can take advantage of an existing geometric transformation method to achieve this. (For example, the page fold in this recipe is based on equations from p.412 of Computer Graphics (C Version), 2nd Edition by Hearn and Baker, 1996.)

3.3.1. Customising the back material

When you set up a deform effect, you can optionally specify a material to use for the "back" of any actor it is applied to.

If you think of an actor as a sheet of paper with a picture on it, specifying a back is similar to turning the sheet of paper over (rotating it around the y axis) and drawing another picture on the other side. If you then folded or twisted the paper, you would be able to see parts of the pictures on both the front and back of the paper.

Similarly, during deformation of an actor, if any vertices of the actor are deformed such that the actor's surface is folded or twisted over itself, parts of its back become visible. If you set a back material, you will see parts of that where the surface is folded over. If you don't set a back material, you will instead see mirror images of parts of the actor's front: as if the actor was flexible stained glass, rather than paper. You can see this if you watch the animation in this section.

The back material should be an instance of CoglMaterial. You can either create this via the Cogl API directly; or indirectly through the Clutter API (for example, by getting the material from a ClutterTexture). The code below gives an example of how to do the latter:

/* create a texture */
ClutterActor *back = clutter_texture_new ();
clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (back), TRUE);
clutter_actor_set_width (back, 400);

/* load image into texture (ignoring errors for brevity) */
clutter_texture_set_from_file (CLUTTER_TEXTURE (back),
                               back_image_file,
                               NULL);

/* get a handle to the texture's Cogl material */
CoglHandle material = clutter_texture_get_cogl_material (CLUTTER_TEXTURE (back));

/* cast the effect to ClutterDeformEffect and set its back material
 * to the handle
 */
clutter_deform_effect_set_back_material (CLUTTER_DEFORM_EFFECT (effect),
                                         material);

See the ClutterDeformEffect API reference for more details about back materials.

Here's a screenshot of the example with the addition of a back material, folded at an angle of 60 degrees:

Applying a custom ClutterDeformEffect to a texture loaded with an image

3.3.2. Animating a custom deform effect

Clutter's animation API can animate any GObject which exposes its properties. In the case of the page fold effect, we can expose the period property using standard GObject property installation:

/* GObject class init */
static void
cb_page_fold_effect_class_init (CbPageFoldEffectClass *klass)
{
  GParamSpec *pspec;
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  /* ...other class setup code... */

  /* expose the period as a GObject property */
  pspec = g_param_spec_double ("period",
                               "Period",
                               "The period of the page fold",
                               0.0, 1.0,
                               0.0,
                               G_PARAM_READWRITE);
  obj_props[PROP_PERIOD] = pspec;
  g_object_class_install_property (gobject_class, PROP_PERIOD, pspec);

  /* ...install other properties... */
}

We also add a get_property() implementation, as well as a setter (see the full GObject implementation for details).

Then set up an animation for the property; in this case, using a ClutterState:

ClutterEffect *effect = cb_page_fold_effect_new (180.0, 0.0);

ClutterState *transitions = clutter_state_new ();
clutter_state_set_duration (transitions, NULL, NULL, 500);

clutter_state_set (transitions, NULL, "unfolded",
                   effect, "period", CLUTTER_LINEAR, 0.0,
                   NULL);

clutter_state_set (transitions, NULL, "folded",
                   effect, "period", CLUTTER_LINEAR, 1.0,
                   NULL);

To start the animation, warp the ClutterState into its "unfolded" state, then set it to "folded":

/* this changes state instantaneously */
clutter_state_warp_to_state (transitions, "unfolded");

/* this starts an animation to the state */
clutter_state_set_state (transitions, "folded");

Note that the full code sample is slightly more complex, as it triggers state changes when a mouse button is pressed on the texture. There is also a third "partially folded" state (used to create the screenshot for the previous section).

Here's what the resulting animation looks like:

3.3.3. Tiles

A ClutterDeformEffect divides the actor being deformed into a number of tiles: the larger the number of tiles, the larger the number of vertices to be manipulated by the effect. Increasing the number of tiles increases the number of vertex computations required, which can slow down animations; at the same time, finer-grained tiles can make an effect appear smoother, particularly when animated.

Most of the time, the default number of tiles in the x and y axes should suffice. You can get the current number of tiles associated with an effect with:

guint x_tiles;
guint y_tiles;

/* effect must be a subclass of ClutterDeformEffect */
clutter_deform_effect_get_n_tiles (CLUTTER_DEFORM_EFFECT (effect),
                                   &x_tiles,
                                   &y_tiles);

However, if an effect produces jerky or fragmented output, you want to tweak the number of tiles. Use the clutter_deform_effect_set_n_tiles() function to do this:

/* 64 tiles in both axes */
guint x_tiles = 64;
guint y_tiles = 64;

clutter_deform_effect_set_n_tiles (CLUTTER_DEFORM_EFFECT (effect),
                                   x_tiles,
                                   y_tiles);

3.4. Full example

This example consists of three files:

As Clutter effect subclasses are written using GObject, you might find this recipe (which goes into GObject in more detail) a useful introduction.

Example 9.7. cb-page-fold-effect.h (header file)

#ifndef __CB_PAGE_FOLD_EFFECT_H__
#define __CB_PAGE_FOLD_EFFECT_H__

#include <clutter/clutter.h>

GType cb_page_fold_effect_get_type (void);

#define CB_TYPE_PAGE_FOLD_EFFECT (cb_page_fold_effect_get_type ())
#define CB_PAGE_FOLD_EFFECT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
                                                                         CB_TYPE_PAGE_FOLD_EFFECT, \
                                                                         CbPageFoldEffect))
#define CB_IS_PAGE_FOLD_EFFECT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
                                                                         CB_TYPE_PAGE_FOLD_EFFECT))
#define CB_PAGE_FOLD_EFFECT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), \
                                                                      CB_TYPE_PAGE_FOLD_EFFECT, \
                                                                      CbPageFoldEffectClass))
#define CB_IS_PAGE_FOLD_EFFECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
                                                                      CB_TYPE_PAGE_FOLD_EFFECT))
#define CB_PAGE_FOLD_EFFECT_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
                                                                        CB_TYPE_PAGE_FOLD_EFFECT, \
                                                                        CbPageFoldEffectClass))

typedef struct _CbPageFoldEffectPrivate CbPageFoldEffectPrivate;
typedef struct _CbPageFoldEffect        CbPageFoldEffect;
typedef struct _CbPageFoldEffectClass   CbPageFoldEffectClass;

/* object */
struct _CbPageFoldEffect
{
  ClutterDeformEffect      parent_instance;
  CbPageFoldEffectPrivate *priv;
};

/* class */
struct _CbPageFoldEffectClass
{
  ClutterDeformEffectClass parent_class;
};

ClutterEffect *cb_page_fold_effect_new (gdouble angle,
                                        gdouble period);
void cb_page_fold_effect_set_angle (CbPageFoldEffect *effect,
                                    gdouble           angle);
void cb_page_fold_effect_set_period (CbPageFoldEffect *effect,
                                     gdouble           period);
gdouble cb_page_fold_effect_get_period (CbPageFoldEffect *effect);
gdouble cb_page_fold_effect_get_angle (CbPageFoldEffect *effect);

#endif /* __CB_PAGE_FOLD_EFFECT_H__ */


Example 9.8. cb-page-fold-effect.c (code file)

#include <math.h>
#include "cb-page-fold-effect.h"

G_DEFINE_TYPE (CbPageFoldEffect, cb_page_fold_effect, CLUTTER_TYPE_DEFORM_EFFECT);

#define CB_PAGE_FOLD_EFFECT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \
                                                                           CB_TYPE_PAGE_FOLD_EFFECT, \
                                                                           CbPageFoldEffectPrivate))

struct _CbPageFoldEffectPrivate
{
  gdouble angle;
  gdouble period;
};

enum {
  PROP_0,

  PROP_PERIOD,
  PROP_ANGLE,

  PROP_LAST
};

static GParamSpec *obj_props[PROP_LAST];

/* ClutterDeformEffect implementation */
static void
cb_page_fold_effect_deform_vertex (ClutterDeformEffect *effect,
                                   gfloat               width,
                                   gfloat               height,
                                   CoglTextureVertex   *vertex)
{
  CbPageFoldEffectPrivate *priv = CB_PAGE_FOLD_EFFECT (effect)->priv;

  gfloat radians = (priv->angle * priv->period) / (180.0f / G_PI);

  /* rotate from the center of the actor on the y axis */
  gfloat adjusted_x = vertex->x - (width / 2);

  /* only rotate vertices to the right of the middle of the actor */
  if (adjusted_x >= 0.0)
    {
      vertex->x = (vertex->z * sin (radians))
                  + (adjusted_x * cos (radians))
                  + width / 2;

      /* NB add 1 to z to prevent "z fighting"; otherwise, when fully-folded
       * the image has "stripes" where vertices from the folded part
       * of the actor interfere with vertices from the unfolded part
       */
      vertex->z = (vertex->z * cos (radians))
                  + (adjusted_x * sin (radians))
                  + 1;
    }

  /* adjust depth of all vertices so they fit inside the actor while folding;
   * this has the effect of making the image smaller within the texture,
   * but does produce a cleaner fold animation
   */
  vertex->z -= width / 2;
}

/* GObject implementation */
static void
cb_page_fold_effect_set_property (GObject      *gobject,
                                  guint         prop_id,
                                  const GValue *value,
                                  GParamSpec   *pspec)
{
  CbPageFoldEffect *effect = CB_PAGE_FOLD_EFFECT (gobject);

  switch (prop_id)
    {
    case PROP_PERIOD:
      cb_page_fold_effect_set_period (effect, g_value_get_double (value));
      break;

    case PROP_ANGLE:
      cb_page_fold_effect_set_angle (effect, g_value_get_double (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
cb_page_fold_effect_get_property (GObject    *gobject,
                                  guint       prop_id,
                                  GValue     *value,
                                  GParamSpec *pspec)
{
  CbPageFoldEffectPrivate *priv = CB_PAGE_FOLD_EFFECT (gobject)->priv;

  switch (prop_id)
    {
    case PROP_PERIOD:
      g_value_set_double (value, priv->period);
      break;

    case PROP_ANGLE:
      g_value_set_double (value, priv->angle);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

/* GObject class and instance init */
static void
cb_page_fold_effect_class_init (CbPageFoldEffectClass *klass)
{
  GParamSpec *pspec;
  ClutterDeformEffectClass *effect_class = CLUTTER_DEFORM_EFFECT_CLASS (klass);
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  effect_class->deform_vertex = cb_page_fold_effect_deform_vertex;

  gobject_class->set_property = cb_page_fold_effect_set_property;
  gobject_class->get_property = cb_page_fold_effect_get_property;

  g_type_class_add_private (klass, sizeof (CbPageFoldEffectPrivate));

  /**
   * CbPageFoldEffect:period:
   *
   * The period of the page fold, between 0.0 (no fold) and
   * 1.0 (fully folded)
   */
  pspec = g_param_spec_double ("period",
                               "Period",
                               "The period of the page fold",
                               0.0, 1.0,
                               0.0,
                               G_PARAM_READWRITE);
  obj_props[PROP_PERIOD] = pspec;
  g_object_class_install_property (gobject_class, PROP_PERIOD, pspec);

  /**
   * CbPageFoldEffect:angle:
   *
   * The angle of the page fold, in degrees, between 0.0 and 180.0
   */
  pspec = g_param_spec_double ("angle",
                               "Angle",
                               "The angle of the page fold, in degrees",
                               0.0, 180.0,
                               0.0,
                               G_PARAM_READWRITE);
  obj_props[PROP_ANGLE] = pspec;
  g_object_class_install_property (gobject_class, PROP_ANGLE, pspec);
}

static void
cb_page_fold_effect_init (CbPageFoldEffect *self)
{
  CbPageFoldEffectPrivate *priv;

  priv = self->priv = CB_PAGE_FOLD_EFFECT_GET_PRIVATE (self);

  priv->period = 0.0;
  priv->angle = 0.0;
}

/* public API */
ClutterEffect *
cb_page_fold_effect_new (gdouble angle,
                         gdouble period)
{
  return g_object_new (CB_TYPE_PAGE_FOLD_EFFECT,
                       "angle", angle,
                       "period", period,
                       NULL);
}

/**
 * cb_page_fold_effect_set_period:
 * @effect: a #CbPageFoldEffect
 * @period: the period of the page fold, between 0.0 and 1.0
 *
 * Sets the period of the page fold, between 0.0 (no fold)
 * and 1.0 (fully folded)
 */
void
cb_page_fold_effect_set_period (CbPageFoldEffect *effect,
                                gdouble           period)
{
  g_return_if_fail (CB_IS_PAGE_FOLD_EFFECT (effect));
  g_return_if_fail (period >= 0.0 && period <= 1.0);

  effect->priv->period = period;

  clutter_deform_effect_invalidate (CLUTTER_DEFORM_EFFECT (effect));
}

/**
 * cb_page_fold_effect_get_period:
 * @effect: a #CbPageFoldEffect
 *
 * Retrieves the value set using cb_page_fold_effect_get_period()
 *
 * Return value: the period of the page fold
 */
gdouble
cb_page_fold_effect_get_period (CbPageFoldEffect *effect)
{
  g_return_val_if_fail (CB_IS_PAGE_FOLD_EFFECT (effect), 0.0);

  return effect->priv->period;
}

/**
 * cb_page_fold_effect_set_angle:
 * @effect: #CbPageFoldEffect
 * @angle: the angle of the page fold, in degrees
 *
 * Sets the angle of the page fold, in degrees; must be a value between
 * 0.0 and 180.0
 */
void
cb_page_fold_effect_set_angle (CbPageFoldEffect *effect,
                               gdouble           angle)
{
  g_return_if_fail (CB_IS_PAGE_FOLD_EFFECT (effect));
  g_return_if_fail (angle >= 0.0 && angle <= 180.0);

  effect->priv->angle = angle;

  clutter_deform_effect_invalidate (CLUTTER_DEFORM_EFFECT (effect));
}

/**
 * cb_page_fold_effect_get_angle:
 * @effect: a #CbPageFoldEffect:
 *
 * Retrieves the angle of the page fold, in degrees
 *
 * Return value: the angle of the page fold
 */
gdouble
cb_page_fold_effect_get_angle (CbPageFoldEffect *effect)
{
  g_return_val_if_fail (CB_IS_PAGE_FOLD_EFFECT (effect), 0.0);

  return effect->priv->angle;
}


Example 9.9. Application which uses CbPageFoldEffect to do animated folding of a ClutterTexture

/* Example of using a custom CbPageFoldEffect to do
 * an animated fold of a texture containing an image
 *
 * Pass the full path to the image on the command line;
 * click on the texture to trigger the folding animation
 */
#include <stdlib.h>
#include <clutter/clutter.h>

#include "cb-page-fold-effect.h"

static const ClutterColor stage_color = { 0x33, 0x33, 0x55, 0xff };

static gboolean
button_pressed_cb (ClutterActor *actor,
                   ClutterEvent *event,
                   gpointer      user_data)
{
  ClutterState *transitions = CLUTTER_STATE (user_data);

  if (g_strcmp0 (clutter_state_get_state (transitions), "folded") == 0)
    clutter_state_set_state (transitions, "unfolded");
  else
    clutter_state_set_state (transitions, "folded");

  return TRUE;
}

int
main (int   argc,
      char *argv[])
{
  ClutterActor *stage;
  ClutterActor *texture;
  ClutterEffect *effect;
  ClutterState *transitions;
  GError *error = NULL;

  gchar *filename;

  if (argc < 2)
    {
      g_print ("Usage: %s <path to image file>\n", argv[0]);
      return EXIT_FAILURE;
    }

  filename = argv[1];

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, 400, 300);
  clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  texture = clutter_texture_new ();
  clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (texture), TRUE);
  clutter_actor_set_width (texture, 400);
  clutter_actor_set_reactive (texture, TRUE);
  clutter_texture_set_from_file (CLUTTER_TEXTURE (texture),
                                 filename,
                                 &error);

  if (error != NULL)
    {
      g_critical ("Error loading texture from file %s; error was:\n%s",
                  filename,
                  error->message);
      return EXIT_FAILURE;
    }

  /* create the page fold effect instance with destination fold angle
   * of 180 degrees and starting period of 0 (no folding)
   */
  effect = cb_page_fold_effect_new (180.0, 0.0);

  /* add the effect to the texture actor */
  clutter_actor_add_effect (texture, effect);

  clutter_container_add_actor (CLUTTER_CONTAINER (stage), texture);

  /* animation for the period property of the effect,
   * to animate its value between 0.0 and 1.0 and back
   */
  transitions = clutter_state_new ();
  clutter_state_set_duration (transitions, NULL, NULL, 500);

  clutter_state_set_duration (transitions,
                              "partially-folded",
                              "folded",
                              375);

  clutter_state_set (transitions, NULL, "folded",
                     effect, "period", CLUTTER_LINEAR, 1.0,
                     NULL);

  clutter_state_set (transitions, NULL, "partially-folded",
                     effect, "period", CLUTTER_LINEAR, 0.25,
                     NULL);

  clutter_state_set (transitions, NULL, "unfolded",
                     effect, "period", CLUTTER_LINEAR, 0.0,
                     NULL);

  clutter_state_warp_to_state (transitions, "partially-folded");

  g_signal_connect (texture,
                    "button-press-event",
                    G_CALLBACK (button_pressed_cb),
                    transitions);

  clutter_actor_show (stage);

  clutter_main ();

  g_object_unref (transitions);

  return EXIT_SUCCESS;
}