Skip to content
Bruno Levy edited this page Dec 29, 2022 · 5 revisions

GLUP: eye candy made easy !

Introduction

To display things, one needs to talk to the graphic board (GPU) through an API (OpenGL, Vulkan, DirectX, Metal). In the early days of consumers graphics (1990's, 2000's), these APIs could only do basic things (draw points, segments, triangles, with optional color interpolation and texture mapping), whereas modern GPUs can be fully programmed using their own language (shaders). A drawback of this situation is that doing a simple thing (such as drawing a couple of triangles) has became very complicated as compared to what it was in the early days. This evolution occured between OpenGL 2.x (that has the so-called "fixed functionality pipeline) and OpenGL 3.x (that declared it deprecated, and finally removed it). For this reason, Geogram comprises GLUP, an API very similar to OpenGL 2.x, implemented under the hood with OpenGL 3.x (with shaders and buffer objects). It makes it possible to draw points, segments and triangles with easy-to-use calls, like in the early days. In addition, it has volumetric primitives that did not exist in OpenGL 2.x (raytraced spheres, tetrahedra, hexahedra, pyramids, prisms) and advanced clipping modes that let you display the interior of the volumetric primitives with optional interpolated colors and texture mapping. It has other features that did not exist in the early days, such as automatic normals in flat shading, automatic mesh outline, normal mapping and indirect texture mapping.

The GLUP API

GLUP is part of the geogram_gfx library, that contains both GLUP and the GUI (based on Dear ImGUI). All the API functions of GLUP are declared in geogram_gfx/GLUP/GLUP.h. Somebody familiar with OpenGL 2.x will immediately feel "at home" there.

GLUP primitives in Immediate mode

Immediate mode is the easiest way of drawing things with GLUP (but not the most efficient). It works as follows:

glupBegin(PRIMITIVE);
    glupVertex...(...);
    glupVertex...(...);
    ...
    glupVertex...(...);    
glupEnd();

where PRIMITIVE is one of GLUP_POINTS, GLUP_LINES, GLUP_TRIANGLES, GLUP_QUADS (that work like their OpenGL 2.x counterparts) and the new volumetric primitives GLUP_TETRAHEDRA, GLUP_HEXAHEDRA, GLUP_PRISMS, GLUP_PYRAMIDS, GLUP_CONNECTORS and GLUP_SPHERES.

Vertices will be grouped to form the primitives (2 vertices for each segment, 3 vertices for each triangle...).

Like in OpenGL 2.x, there are several variants of the glupVertex[suffix]() function, where [suffix] indicates the number of specified coordinates (1,2,3 or 4), the type of the coordinates (f for float, d for double), and whether coordinates are passed as individual parameters (default) or passed through a pointer (v). In addition, there are overloads for vec2,vec3 and vec4 (no suffix).

The demo_GLUP program demonstrates the different primitives. One may also refer to the Delaunay2d and Delaunay3d tutorials for simple examples. Using GLUP, it is reasonably easy to port old programs, as we did for this sphere eversion demo.

GLUP vertex attributes

Vertices can be "decorated" with attributes (colors, texture coordinates, normals). The attributes to be used can be specified through:

    glupEnable(GLUPtoggle toggle);
    glupDisable(GLUPtoggle toggle);

where toggle can be one of GLUP_VERTEX_COLORS, GLUP_TEXTURING, GLUP_VERTEX_NORMALS (there are other toggles that we will see later).

Vertex attributes can be specified through glupColor...(), glupTexCoord...() and glupNormal...() (with the same variants as glupVertex...() depending on the number and type of the components). They need to be specified before calling glupVertex...() (that keeps the latest specified attributes).

When no vertex color is specified, GLUP uses the default color, specified by one of the glupSetColor...() variants. Different colors can be specified for front-facing (GLUP_FRONT_COLOR) and back-facing (GLUP_BACK_COLOR) polygons. The function glupSetColor...() can also specify the color used to draw mesh outlines (GLUP_MESH_COLOR), when GLUP_DRAW_MESH is enabled.

GLUP toggles

Toggle Description
GLUP_LIGHTING Lighting (one directional light only)
GLUP_VERTEX_COLORS Per vertex colors, interpolated (Gouraud)
GLUP_TEXTURING Per vertex texture coordinates
GLUP_DRAW_MESH Automatic mesh outline
GLUP_CLIPPING Clipping (one clipping plane only)
GLUP_INDIRECT_TEXTURING Use texel value as index in a 1D texture
GLUP_VERTEX_NORMALS Per vertex normals, interpolated (Phong)
GLUP_PICKING Generate primitive IDs instead of colors
GLUP_ALPHA_DISCARD Discard fragment if alpha != 1.0
GLUP_NORMAL_MAPPING Use texel value as normals

Lighting

In GLUP, there is a single vector light source, that can be manipulated through the following function:

  void GLUP_API glupLightVector3f(GLUPfloat x, GLUPfloat y, GLUPfloat z);

In flat shading mode (that is, GLUP_VERTEX_NORMALS not enabled), there is no need of specifying vertex normals, GLUP automatically computes facet normals and uses them for shading. Under the hood, it is done by the shaders.

Clipping

The clipping plane equation can be specified using the following function:

    void GLUP_API glupClipPlane(const GLUPdouble* eqn);

where eqn is an array with the four coefficients a,b,c,d of the clipping plane ax+by+cz+d=0.

Advanced glupClipPlane behaves like its OpenGL 2.x counterpart (that is, the specified plane equation is pre-multiplied by the inverse of the current modelview matrix before being stored in the state).

For volumetric primitives, one can specify different clipping modes using:

    void GLUP_API glupClipMode(GLUPclipMode mode);

The effect of the different modes in the table below is shown in the figure above.

clip mode Description
GLUP_CLIP_STANDARD (standard OpenGL) just clip the facets
GLUP_CLIP_WHOLE_CELLS display all cells straddling and below clip plane
GLUP_CLIP_STRADDLING_CELLS display only cells that straddle the clip plane
GLUP_CLIP_CELLS display intersection between cells and clip plane

Texturing

GLUP texturing uses the standard OpenGL calls. It uses three texture units, for 1D, 2D and 3D texture (GLUP_TEXTURE_1D_UNIT, GLUP_TEXTURE_2D_UNIT and GLUP_TEXTURE_3D_UNIT). One needs to call glActiveTexture(GL_TEXTURE0 + unit before binding the xture. For instance, for 2D texturing, one calls `glActiveTexture(GL_TEXTURE0 + GLUP_TEXTURE_2D_UINT).

GLUP supports three texture modes: GLUP_TEXTURE_REPLACE, GLUP_TEXTURE_MODULATE and GLUP_TEXTURE_ADD, that specify how the texel value is combined with the fragment color.

Fast drawing primitives

Clearly, sending the vertices and attributes one by one using glupVertex() is not the fastest way of drawing things with GLUP. One can use instead the following functions, that work just like their OpenGL 2.x and 3.x counterparts, with the exception that GLUP volumetric primitives are supported !

    void glupDrawArrays(
        GLUPprimitive primitive, GLUPint first, GLUPsizei count
    );
    
    void glupDrawElements(
        GLUPprimitive primitive, GLUPsizei count,
        GLUPenum type, const GLUPvoid* indices
    );

These functions work with the currently bound vertex arrays object (VAO). To manipulate VAOs, GLUP has its own functions:

    void glupGenVertexArrays(GLUPsizei n, GLUPuint* arrays);
    void glupDeleteVertexArrays(GLUPsizei n, const GLUPuint *arrays);
    void glupBindVertexArray(GLUPuint array);
    GLUPuint glupGetVertexArrayBinding(void);

they work exactly like their OpenGL 3.x counterparts. Under the hood, they directly call OpenGL if VAO are supported, else they emulate them by querying the VBO state.

Using these functions, one can draw a clipping plane in a tetrahedral mesh with interpolated attributes using a single call to OpenGL, that will be directly executed by the GPU, without any communication with the CPU. Internally, everything is done with buffer objects and shaders.

However, not all GPUs support this mode for all primitives. One can check whether array mode works with a given primitive by calling the following function (and then fallback to a slower mode if the function returns GL_FALSE):

   GLUPboolean glupPrimitiveSupportsArrayMode(GLUPprimitive prim);

Under the hood, GLUP uses different profile, depending on the detected GPU capabilities:

  • The most basic profile (for OpenGL ES and WebGL) does not support array mode. It is so because we need geometry shaders for that.
  • If GLSL 1.5 shaders (OpenGL 3.3) are supported, all primitives except GLUP_HEXES and GLUP_PYRAMIDS support array mode.
  • For GLSL >= 4.4 and OpenGL >= 4.4, all primitives support array mode.

Geogram meshes

For displaying Geogram Mesh objects, geogram_gfx has a MeshGfx class, that uses the most efficient available GLUP primitive to draw the mesh (array mode if available for the GPU, or slower fallback). It can also display mesh attributes.

Primitives filtering (advanced)

It is sometimes interesting to deactivate rendering of some primitives based on an attribute. To do that, GLUP has a GLUP_PRIMITIVE_FILTERING toggle. It uses a 8bpp buffer texture indexed by primitive Id to decide whether the primitive should be displayed. This texture is bound to GLUP_TEXTURE_PRIMITIVE_FILTERING_UNIT. Here is a simple example that uses this mechanism to display all primitives with an odd index. Note that MeshGfx has an easy-to-use API to do that, what follows explains how it works internally to illustrate GLUP primitive filtering.

Step 1: create the filter that selects the primitives to be displayed

   vector<Numeric::uint8> filter(mesh_grob()->facets.nb());
   for(index_t f: mesh_grob()->facets) {
     filter[f] = f&1;
   }

Step 2: create a buffer object and send the filter to it

   GLuint buffer=0;
   glGenBuffers(1,&buffer);
   glBindBuffer(GL_ARRAY_BUFFER,buffer);
   glBufferData(
      GL_ARRAY_BUFFER, mesh_grob()->facets.nb(), 
      filter.data(), GL_STATIC_DRAW
   );
   glBindBuffer(GL_ARRAY_BUFFER,0);

Step 3: create a texture and attach the buffer object to it (texture buffer).

   GLuint texture=0;
   glGenTextures(1,&texture);
   glActiveTexture(
       GL_TEXTURE0 + GLUP_TEXTURE_PRIMITIVE_FILTERING_UNIT
   );
   glBindTexture(GL_TEXTURE_BUFFER, texture);
   glTexBuffer(GL_TEXTURE_BUFFER, GL_R8, buffer);

Step 4: Display the surface with primitive filtering

   glupEnable(GLUP_PRIMITIVE_FILTERING);
   gfx_.draw_surface();
   glupDisable(GLUP_PRIMITIVE_FILTERING);

Step 5: Cleanup

   glDeleteTextures(1,&texture);                
   glDeleteBuffers(1,&buffer);