LibGDX: dynamic textures with Pixmap

While poking around with some shader techniques, I got curious about how I could generate a texture at run-time based on some arbitrary (a.k.a. user) input and use that to do fun things in the shader. As it turns out, LibGDX has a class called Pixmap which is a good fit for the task.

Pixmap provides an in-memory, modifiable image. It exposes methods to draw shapes and images onto the Pixmap, so you can think of it as a blank canvas of pixels. Pixmap is, under the covers, a flat array of color data for each pixel. It lets you define the format of that array.

Pixmap.Format.RGBA8888, for example, means each pixel has four color components (red, green, blue, and alpha; in that order) and each color is represented as a single byte. Pixmap.Format.Alpha on the other hand uses just a single byte per pixel to represent its transparency value. If the various format options are confusing to you, you can probably stick with one of the two I've listed. Or if you're loading a Pixmap from an image, it deals with the format for you.

The other thing to keep in mind is that you will have to manually dispose of the Pixmap when you're done with it, since the memory it uses is not garbage collected (it's off-heap).

With this in mind I created a demo game in which an image is initially hidden (transparent) and is revealed by "scribbling" over the window with the left mouse button.

DrawingGame in all its glory!

The basic concept is to use a Pixmap to create a mask—anywhere the Pixmap is transparent, the rendered image will be transparent; anywhere it's opaque, the rendered image will be opaque. I 'draw' on the Pixmap using mouse input. A custom fragment shader will receive two textures: the standard texture from a regular old SpriteBatch and our mask texture (which I update when the Pixmap changes). The fragment shader simply multiplies the fragment's usual alpha with our mask alpha. Good to go!

So this really demonstrates two concepts: 1) a dynamically created texture using Pixmap, and 2) passing multiple textures to a shader to do funky things. One ancillary detail is that the mouse input needs to be linearly interpolated ("lerped") when dragging the mouse around or else you just get a spinkling of dots instead of a nice paint brush effect. Here's the code listing (or view it on gist):

package com.javadocmd.drawing;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Disposable;

/**
 * A very simple game where an image is initially hidden (transparent). The
 * image is revealed by 'scribbling' over the window with the mouse.
 */
public class DrawingGame extends ApplicationAdapter {

  private SpriteBatch batch;
  private ShaderProgram shader;
  private Texture outline, color;
  private FileHandle vertexShader, fragmentShader;

  private DrawablePixmap drawable;

  @Override
  public void create() {
    /* Some regular textures to draw on the scene. */
    outline = new Texture("smiley-outline.png");
    color = new Texture("smiley-color.png");

    /* I like to keep my shader programs as text files in the assets
     * directory rather than dealing with horrid Java string formatting. */
    vertexShader = Gdx.files.internal("vertex.glsl");
    fragmentShader = Gdx.files.internal("fragment.glsl");

    /* Bonus: you can set `pedantic = false` while tinkering with your
     * shaders. This will stop it from crashing if you have unused variables
     * and so on. */
    // ShaderProgram.pedantic = false;

    /* Construct our shader program. Spit out a log and quit if the shaders
     * fail to compile. */
    shader = new ShaderProgram(vertexShader, fragmentShader);
    if (!shader.isCompiled()) {
      Gdx.app.log("Shader", shader.getLog());
      Gdx.app.exit();
    }

    /* Construct a simple SpriteBatch using our shader program. */
    batch = new SpriteBatch();
    batch.setShader(shader);

    /* Tell our shader that u_texture will be in the TEXTURE0 spot and
     * u_mask will be in the TEXTURE1 spot. We can set these now since
     * they'll never change; we don't have to send them every render frame. */
    shader.begin();
    shader.setUniformi("u_texture", 0);
    shader.setUniformi("u_mask", 1);
    shader.end();

    /* Pixmap blending can result result in some funky looking lines when
     * drawing. You may need to disable it. */
    Pixmap.setBlending(Blending.None);

    /* Construct our DrawablePixmap (custom class, defined below) with a
     * Pixmap that is the dimensions of our screen. Alpha format is chosen
     * because we are just using it as a mask and don't care about RGB color
     * information. This will require less memory. */
    drawable = new DrawablePixmap(new Pixmap(Gdx.graphics.getWidth(),
        Gdx.graphics.getHeight(), Format.Alpha), 1);

    Gdx.input.setInputProcessor(new DrawingInput());
  }

  @Override
  public void render() {
    Gdx.gl.glClearColor(1, 1, 1, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    /* Update the mask texture (only if necessary). */
    drawable.update();

    /* Color and outline are drawn separately here, but only to demonstrate
     * this technique supports multiple images in a batch. */
    batch.begin();
    batch.draw(color, 0, 0);
    batch.draw(outline, 0, 0);
    batch.end();
  }

  @Override
  public void dispose() {
    drawable.dispose();
    outline.dispose();
    color.dispose();
    batch.dispose();
  }

  /**
   * Nested (static) class to provide a nice abstraction over Pixmap, exposing
   * only the draw calls we want and handling some of the logic for smoothed
   * (linear interpolated, aka 'lerped') drawing. This will become the 'owner'
   * of the underlying pixmap, so it will need to be disposed.
   */
  private static class DrawablePixmap implements Disposable {

    private final int brushSize = 10;
    private final Color clearColor = new Color(0, 0, 0, 0);
    private final Color drawColor = new Color(1, 1, 1, 1);

    private Pixmap pixmap;
    private Texture texture;
    private boolean dirty;

    public DrawablePixmap(Pixmap pixmap, int textureBinding) {
      this.pixmap = pixmap;
      pixmap.setColor(drawColor);

      /* Create a texture which we'll update from the pixmap. */
      this.texture = new Texture(pixmap);
      this.dirty = false;

      /* Bind the mask texture to TEXTURE<N> (TEXTURE1 for our purposes),
       * which also sets the currently active texture unit. */
      this.texture.bind(textureBinding);

      /* However SpriteBatch will auto-bind to the current active texture,
       * so we must now reset it to TEXTURE0 or else our mask will be
       * overwritten. */
      Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0);
    }

    /** Write the pixmap onto the texture if the pixmap has changed. */
    public void update() {
      if (dirty) {
        texture.draw(pixmap, 0, 0);
        dirty = false;
      }
    }

    public void clear() {
      pixmap.setColor(clearColor);
      pixmap.fill();
      pixmap.setColor(drawColor);
      dirty = true;
    }

    private void drawDot(Vector2 spot) {
      pixmap.fillCircle((int) spot.x, (int) spot.y, brushSize);
    }

    public void draw(Vector2 spot) {
      drawDot(spot);
      dirty = true;
    }

    public void drawLerped(Vector2 from, Vector2 to) {
      float dist = to.dst(from);
      /* Calc an alpha step to put one dot roughly every 1/8 of the brush
       * radius. 1/8 is arbitrary, but the results are fairly nice. */
      float alphaStep = brushSize / (8f * dist);

      for (float a = 0; a < 1f; a += alphaStep) {
        Vector2 lerped = from.lerp(to, a);
        drawDot(lerped);
      }

      drawDot(to);
      dirty = true;
    }

    @Override
    public void dispose() {
      texture.dispose();
      pixmap.dispose();
    }
  }

  /**
   * Inner (non-static) class to handle mouse and keyboard events. Mostly we
   * want to pass on appropriate draw calls to our DrawablePixmap and this
   * means keeping track of some state (last coordinates drawn and whether or
   * not the left mouse button is pressed) to handle smooth drawing while
   * dragging the mouse.
   */
  private class DrawingInput extends InputAdapter {

    private Vector2 last = null;
    private boolean leftDown = false;

    @Override
    public boolean touchDown(int screenX, int screenY, int pointer,
        int button) {
      if (button == Input.Buttons.LEFT) {
        Vector2 curr = new Vector2(screenX, screenY);
        drawable.draw(curr);
        last = curr;
        leftDown = true;
        return true;
      } else {
        return false;
      }
    }

    @Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
      if (leftDown) {
        Vector2 curr = new Vector2(screenX, screenY);
        drawable.drawLerped(last, curr);
        last = curr;
        return true;
      } else {
        return false;
      }
    }

    @Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
      if (button == Input.Buttons.LEFT) {
        drawable.draw(new Vector2(screenX, screenY));
        last = null;
        leftDown = false;
        return true;
      } else {
        return false;
      }
    }

    @Override
    public boolean keyDown(int keycode) {
      switch (keycode) {
        case Input.Keys.ESCAPE:
        case Input.Keys.F5:
          return true;
        default:
          return false;
      }
    }

    @Override
    public boolean keyUp(int keycode) {
      switch (keycode) {
        case Input.Keys.ESCAPE:
          Gdx.app.exit();
          return true;
        case Input.Keys.F5:
          drawable.clear();
          return true;
        default:
          return false;
      }
    }
  }
}

Next, this is just the default vertex shader. Nothing to change here.

uniform mat4 u_projTrans;

attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;

varying vec4 v_color;
varying vec2 v_texCoord0;

void main()
{
    v_color = a_color;
    v_texCoord0 = a_texCoord0;
    gl_Position = u_projTrans * a_position;
}

And the fragment shader where the mask texture is applied.

#ifdef GL_ES
    precision mediump float;
#endif

uniform sampler2D u_texture;
uniform sampler2D u_mask;

varying vec4 v_color;
varying vec2 v_texCoord0;

void main()
{
    vec4 texColor = texture2D(u_texture, v_texCoord0);
    vec4 mask = texture2D(u_mask, v_texCoord0);
    texColor.a *= mask.a;
    gl_FragColor = v_color * texColor;
}

And our two images: smiley-outline.png and smiley-color.png.

Smiley outline
Smiley color