AGL
A graphics library
Setting up scenes

The main classes implemented in this library are

  • agl::Window: Manages user input, window properties, and the main application loop
  • agl::Renderer: Manages drawing and shaders.
  • agl::Image: Helper class for textures.
  • agl::Mesh: Implements primitives composed of large numbers of vertices, such as spheres and models.

Setting up scenes

The camera and projection determine the size of the scene that will be drawn (called the view volume). In other words, only objects that are in front of the camera and inside the projection's bounds will be rendered. Let's start with a simple example which uses the default camera and projection.

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"

class MyWindow : public agl::Window {
  void draw() {
    renderer.sphere();
  }
};

int main() {
  MyWindow window;
  window.run();
}

By default, the camera is located at position (0, 0, 2) and looks forward along the negative Z direction at position (0, 0, 0). The view volume is a box that extends from -1 to 1 in the X and Y directions, and -10 and 10 in the z direction. The default projection is an orthographic projection, meaning that all objects appear the same depth and there is no forshortening that makes further objects look smaller. The sphere has a default diameter of 1 and is centered at (0, 0, 0), so it appears in view.

Note
The coordinate system is a right-handed coordinate system where +Y points up and +Z points out of the screen.

You have two choices for the projection.

The easiest way to change the scene size is to use the above functions. These functions will set both the camera position, look at point, and projection to fit the given parameters. Alternatively, you can set the camera and projection properties yourself using

Using the default camera

By default, agl::Window defines a default camera, agl::Camera. This camera has orbit and pan controls mapped to mouse input. The disable camera movement, call agl::Window::setCameraEnabled.

Responding to events

Override the mouse and keyboard methods in agl::Window to respond to user input. For example, the following example modifies a texture on agl::Window::keyUp

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"
#include "agl/image.h"

using glm::vec2;
using glm::vec3;

class MyWindow : public agl::Window {
  void setup() {
    renderer.setUniform("Material.specular", vec3(0.0));
    renderer.setUniform("MainTexture.enabled", true);
    renderer.setUniform("MainTexture.tile", vec2(10.0));

    _texture.load("../textures/bricks.png");
    renderer.loadTexture("bricks", _texture, 0);
    renderer.texture("MainTexture.texture", "bricks");

    setupPerspectiveScene(vec3(0.0), vec3(10.0));
    background(vec3(0.9f));
  }

  void draw() {
    renderer.scale(vec3(20.0f));
    renderer.plane();
  }

  void keyUp(int key, int mods) {
    if (key == GLFW_KEY_UP) {
      swirl();
      renderer.loadTexture("bricks", _texture, 0);  // replace old image
    }
  }

  void swirl() {
    for (int i = 0; i < _texture.height(); i++) {
      for (int j = 0; j < _texture.width(); j++) {
        agl::Pixel color = _texture.get(i, j);

        // swirl colors
        unsigned char red = color.r;
        color.r = color.g;
        color.g = color.b;
        color.b = red;

        _texture.set(i, j, color);
      }
    }
  }

 private:
  agl::Image _texture;
};

int main() {
  MyWindow window;
  window.run();
}

Drawing shapes

Use the primitive calls in agl::Renderer to draw shapes. By default, they will have unit size (e.g. fit into a 1x1x1 box) and be centered at te origin.

Use the transform functions the modify the positions of these shapes.

Below is an example.

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"

using glm::vec2;
using glm::vec3;
using glm::vec4;

class MyWindow : public agl::Window {
  void setup() {
    renderer.setUniform("Fog.enabled", true);
    renderer.setUniform("Fog.color", vec3(0.9f));
    renderer.setUniform("Fog.minDist", 5.0f);
    renderer.setUniform("Fog.maxDist", 9.0f);
    renderer.setUniform("Material.specular", vec3(0.0));
    renderer.setUniform("MainTexture.enabled", true);
    renderer.setUniform("MainTexture.tile", vec2(20.0));

    int halfw = 32;
    int halfh = 32;
    agl::Image checker(halfw*2, halfh*2);
    for (int i = 0; i < checker.height(); i++) {
      for (int j = 0; j < checker.width(); j++) {
        if ((i < halfh && j < halfw) || (i > halfh && j > halfw)) {
          checker.setVec4(i, j, vec4(0.7, 0.7, 0.7, 1));
        } else {
          checker.setVec4(i, j, vec4(1, 1, 1, 1));
        }
      }
    }
    renderer.loadTexture("checker", checker, 0);

    setupPerspectiveScene(vec3(0.0), vec3(5.0));
    background(vec3(0.9f));
  }

  void draw() {
    renderer.setUniform("MainTexture.enabled", true);
    renderer.setUniform("Material.specular", vec3(0.0));
    renderer.setUniform("Material.diffuse", vec3(1));
    renderer.texture("MainTexture.texture", "checker");
    renderer.identity();
    renderer.translate(vec3(0));
    renderer.scale(vec3(20.0f));
    renderer.plane();

    renderer.setUniform("MainTexture.enabled", false);
    renderer.setUniform("Material.specular", vec3(1));
    renderer.setUniform("Material.diffuse", vec3(1, 0, 0));
    renderer.identity();
    renderer.translate(vec3(0, 0.5, 0));
    renderer.scale(vec3(0.5));
    renderer.rotate(elapsedTime(), vec3(0.5, 1, 0));
    renderer.capsule();

    renderer.setUniform("Material.diffuse", vec3(0.5, 1, 0));
    renderer.identity();
    renderer.translate(vec3(-1, 0.5, 1));
    renderer.scale(vec3(0.5));
    renderer.rotate(elapsedTime(), vec3(0.5, 0.5, 0));
    renderer.cone();

    renderer.setUniform("Material.diffuse", vec3(1, 0, 1));
    renderer.identity();
    renderer.translate(vec3(0, 0.5, 1));
    renderer.scale(vec3(0.5));
    renderer.sphere();

    renderer.setUniform("Material.diffuse", vec3(1, 1, 0.3));
    renderer.identity();
    renderer.translate(vec3(1, 0.5, 1));
    renderer.scale(vec3(0.5));
    renderer.rotate(elapsedTime(), vec3(-0.5, 0.5, 0));
    renderer.cube();

    renderer.setUniform("Material.diffuse", vec3(1, 0.8, 0.1));
    renderer.identity();
    renderer.translate(vec3(-1, 0.5, 0));
    renderer.scale(vec3(0.5));
    renderer.rotate(elapsedTime(), vec3(1, 0.5, 0));
    renderer.cylinder();

    renderer.setUniform("Material.diffuse", vec3(1, 0, 0.5));
    renderer.identity();
    renderer.translate(vec3(1, 0.5, 0));
    renderer.scale(vec3(0.75));
    renderer.rotate(elapsedTime(), vec3(0.5, 0.5, 0));
    renderer.teapot();
  }
};

int main() {
  MyWindow window;
  window.run();
}

You can also define your own meshs by extending the base classes

For example,

// Copyright 2020, Savvy Sine, Aline Normoyle
// Visualizes strange attractors
// See: Julien C. Sprott, 2000, Strange Attractors: Creating patterns in Chaos

#include "agl/window.h"
#include "agl/mesh/point_mesh.h"

using glm::vec2;
using glm::vec4;
using glm::vec3;

class Attractor : public agl::PointMesh {
 public:
  Attractor() : PointMesh() {
    init();  // initialize the mesh rather than wait for first frame
  }

  void setParameters(const std::string& name, std::vector<float>* params) {
    float startc = 32;
    float startv = -4.5;
    params->clear();
    for (int i = 0; i < name.size(); i++) {
      char c = name[i];
      float v = (c - startc)*0.1 + startv;
      std::cout << i << ") " << name[i] << " = " << v << "\n";
      params->push_back(v);
    }
  }

  void init() override {
    std::vector<float> a;
    // setParameters("MTISVBKHOIJFWSYEKEGYLWJKEOGVLM", &a);
    setParameters("JKRADSXGDBHIJTQJJDICEJKYSTXFNU", &a);

    int iterationsMax = 400000;
    float minValue = -1000000;
    float maxValue =  1000000;
    vec3 maxBound = vec3(-10.1);  // for window bounds
    vec3 minBound = vec3(10.1);   // for window bounds
    bool done = false;
    vec3 p = vec3(0.05);

    std::vector<GLfloat> points;
    for (int i = 0; i < iterationsMax && !done; i++) {
      float x = p.x;
      float y = p.y;
      float z = p.z;

      float xnew = a[0] + a[1]*x + a[2]*x*x + a[3]*x*y + a[4]*x*z +
                   a[5]*y + a[6]*y*y + a[7]*y*z + a[8]*z + a[9]*z*z;

      float ynew = a[10] + a[11]*x + a[12]*x*x + a[13]*x*y + a[14]*x*z +
                   a[15]*y + a[16]*y*y + a[17]*y*z + a[18]*z + a[19]*z*z;

      float znew = a[20] + a[21]*x + a[22]*x*x + a[23]*x*y + a[24]*x*z +
                   a[25]*y + a[26]*y*y + a[27]*y*z + a[28]*z + a[29]*z*z;

      p = glm::clamp(vec3(xnew, ynew, znew), vec3(minValue), vec3(maxValue));
      if (std::abs(p.x) > maxValue-1) {
        done = true;

      } else {
        points.push_back(p.x);
        points.push_back(p.y);
        points.push_back(p.z);
      }

      maxBound = glm::max(p, maxBound);
      minBound = glm::min(p, minBound);
    }
    bounds = maxBound - minBound;
    center = minBound + 0.5f * (maxBound - minBound);
    initBuffers(&points, 0, 0, 0);
  }

 public:
  vec3 bounds = vec3(0);
  vec3 center = vec3(0.0);
};

class MyWindow : public agl::Window {
 public:
  void setup() {
    background(vec3(1.0));

    float fov = glm::radians(45.0);
    vec3 bounds = _mesh.bounds;
    float minD = std::min(std::min(bounds.x, bounds.y), bounds.z);
    float maxD = std::max(std::max(bounds.x, bounds.y), bounds.z);
    float near = minD * 0.05f / tan(fov);
    float far = maxD * 10.0f;

    perspective(fov, width()/height(), near, far);
    lookAt(vec3(0, 0, -2), vec3(0));
  }

  void draw() {
    renderer.beginShader("unlit");
    renderer.setUniform("Material.color", vec4(0.25, 0.25, 0.25, 1));
    renderer.translate(-_mesh.center);
    renderer.mesh(_mesh);
    renderer.endShader();
  }

  Attractor _mesh;
};

int main() {
  MyWindow window;
  window.run();
}

You can also access the built-in primitives to customize their properties. The following example creates a custom torus.

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"
#include "agl/mesh/torus.h"

using agl::Torus;
using glm::vec3;

class MyWindow : public agl::Window {
 public:
  void draw() {
    renderer.rotate(elapsedTime(), vec3(1, 1, 0));
    renderer.mesh(_fatTorus);
  }

  Torus _fatTorus = Torus(0.45, 0.5, 20, 20);
};

int main() {
  MyWindow window;
  window.run();
}

By default, mesh vertex properties are static (e.g. they don't change values after the mesh os created). You can also define dynamic meshes by calling agl::Mesh::setIsDynamic(). For example,

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"
#include "agl/mesh/plane.h"

using glm::vec2;
using glm::vec4;
using glm::vec3;

class UndulateMesh : public agl::Plane {
 public:
  UndulateMesh(int xsize, int ysize) : Plane(1, 1, xsize, ysize) {
    setIsDynamic(true);
    init();  // initialize the mesh rather than wait for first frame
  }

  void update(float elapsedTime) {
    for (int i = 0; i < numVertices(); i++) {
      vec3 p = vec3(vertexData(POSITION, i));
      setVertexData(POSITION, i, vec4(position(p, elapsedTime), 0));
      setVertexData(NORMAL, i, vec4(normal(p, elapsedTime), 0));
    }
  }

  vec3 position(const vec3& p, float t) {
    float angle = t;
    float frequency = 7.0;
    float amplitude = 0.05;

    float heightFn = (angle + frequency * p[0] * frequency * p[2]);
    float y = amplitude * sin(heightFn);
    return vec3(p.x, y, p.z);
  }

  vec3 normal(const vec3& p, float t) {
    float eps = 0.001;
    vec3 x = position(p+vec3(eps, 0, 0), t) - position(p-vec3(eps, 0, 0), t);
    vec3 z = position(p+vec3(0, 0, eps), t) - position(p-vec3(0, 0, eps), t);
    vec3 y = glm::cross(z, x);
    return normalize(y);
  }
};

class MyWindow : public agl::Window {
 public:
  void setup() {
    perspective(glm::radians(30.0), 1, 0.1, 100);
    renderer.setUniform("Material.specular", vec3(1.0, 0.2, 0.8));
    renderer.setUniform("Material.ambient", vec3(0.3, 0.0, 0.2));
  }

  void draw() {
    _mesh.update(elapsedTime());
    renderer.rotate(kPI * 0.2, vec3(1, 0, 0));
    renderer.mesh(_mesh);
  }

  UndulateMesh _mesh = UndulateMesh(100, 100);
};

int main() {
  MyWindow window;
  window.run();
}

Using shaders

The active shader determines how shapes are drawn. By default, the active shader is "phong" (defined in phong.vs) and will color objects with a shiny white, "plastic" material. You can change the properties of the phong shader (such as colors, light direction, textures, and fog) by changing the values of the uniform variables defined in the shader. Some useful properties for phong are

Light.position Light.color Material.diffuse Material.ambient Material.specular Material.shininess Fog.enabled Fog.color Fog.minDist Fog.maxDist MainTexture.texture

Shader properties can be changed by called agl::Renderer::setUniform.

The built-in shaders

In addition to phong, agl also includes shaders for billboards (useful for sprites) and lines.

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"

using glm::vec3;

class MyWindow : public agl::Window {
  void setup() {
    renderer.loadTexture("cloud", "../textures/cloud.png", 0);
    renderer.loadTexture("particle", "../textures/particle.png", 0);
    renderer.blendMode(agl::ADD);
  }

  void draw() {
    renderer.beginShader("sprite");
    renderer.texture("image", "cloud");
    renderer.sprite(vec3(-0.5f, 0.0f, 0.0f), red, 0.25f);
    renderer.sprite(vec3(0.5f, 0.0f, 0.0f), green, 0.25f);

    renderer.texture("image", "particle");
    renderer.sprite(vec3(0.0f, 0.25f, 0.0f), blue, 0.25f);
    renderer.endShader();
  }

  const glm::vec4 red = glm::vec4(1, 0, 0, 1);
  const glm::vec4 green = glm::vec4(0, 1, 0, 1);
  const glm::vec4 blue = glm::vec4(0, 0, 1, 1);
};

int main() {
  MyWindow window;
  window.run();
}

Custom shaders

You can also define your own shaders. Call agl::Renderer::loadShader to load it. When you load a shader, you also provide a string key that you use to active the shader when drawing using agl::Renderer::beginShader.

For compatibility with agl::Renderer your shader should support the following assumptions:

  • Vertex positions are in layout location 0
  • Vertex normals (if present) are in layout location 1
  • Vertex texture coordinate (if present) are in layout location 2
  • Vertex tangents (if present) are in layout location 3

Some properties may not be defined for all meshes. Your shader should also define the following uniform parameters in the vertex shader.

  • uniform mat3 NormalMatrix;
  • uniform mat4 ModelViewMatrix;
  • uniform mat4 MVP;

The above matrices will be set when primitives are drawn.

Textures

Textures can be loaded with agl::Renderer::loadTexture or using the agl::Image class. Textures are associated with a texture slot. If multiple textures should be passed to a shader, each should have its own unique slot. To pass a texture to a shader, use the method agl::Renderer::texture.

// Copyright 2020, Savvy Sine, Aline Normoyle

#include "agl/window.h"

using glm::vec2;
using glm::vec3;
using glm::vec4;

class MyWindow : public agl::Window {
  void setup() {
    renderer.setUniform("Fog.enabled", true);
    renderer.setUniform("Fog.color", vec3(0.9f));
    renderer.setUniform("Fog.minDist", 5.0f);
    renderer.setUniform("Fog.maxDist", 9.0f);
    renderer.setUniform("Material.specular", vec3(0.0));
    renderer.setUniform("MainTexture.enabled", true);
    renderer.setUniform("MainTexture.tile", vec2(10.0));

    int halfw = 32;
    int halfh = 32;
    agl::Image checker(halfw*2, halfh*2);
    for (int i = 0; i < checker.height(); i++) {
      for (int j = 0; j < checker.width(); j++) {
        if ((i < halfh && j < halfw) || (i > halfh && j > halfw)) {
          checker.setVec4(i, j, vec4(0, 0, 0, 1));
        } else {
          checker.setVec4(i, j, vec4(1, 1, 1, 1));
        }
      }
    }
    renderer.loadTexture("checker", checker, 0);
    renderer.texture("MainTexture.texture", "checker");

    setupPerspectiveScene(vec3(0.0), vec3(10.0));
    background(vec3(0.9f));
  }

  void draw() {
    renderer.scale(vec3(20.0f));
    renderer.plane();
  }
};

int main() {
  MyWindow window;
  window.run();
}

Cubemaps are special textures composed of 6 images intended to map to the sides of a cube. Cubemaps also need unique slots. Use agl::Renderer::loadCubemap and agl::renderer::cubemap to use cubemaps in your programs.

// Copyright (c) 2020, Savvy Sine, Aline Normoyle
// Visualize a cubemap texture using the skybox primitive

#include "agl/window.h"

class MyWindow : public agl::Window {
  void setup() {
    renderer.loadCubemap("cubemap", "../textures/sea", 0);
    perspective(glm::radians<float>(60.0f), 1.0f, 0.1f, 100.0f);
  }

  void draw() {
    renderer.beginShader("cubemap");
    renderer.skybox(10);
    renderer.endShader();
  }
};

int main() {
  MyWindow window;
  window.run();
}

Common problems

I have a black screen. What's wrong?

  • Check your projection and camera properties. Make sure that any objects you define are located inside the view volume.
  • Try drawing a simple shape
  • Try drawing with the unlit shader
  • Make sure the the object color and background colors are different
  • If your colors have transparency, make sure that alpha is not 0.0

Known issues

  • Only RGBA images are currently supported