Assuming that you have read Drawing 3d text

Here is a WebGL example showing what’s taking place in Sketch.h and Sketch.cpp from the DrawingLotsOf2dText project’s source folder.

If you run the example, you can see a 3d plane with the lyrics of 8 songs by the Smiths. There is about 9000 glyphs, and the text engine is capable of rendering even more at 60fps.

Drag in order to move on the plane.

Sketch.h

We can see 2 new methods: addTouch() and updateTouch(). They are similar to mousePressed() and mouseDragged(), except that they also work on mobile (iOS and Android) and provide multi-touch capabilities.

Sketch.cpp

Inside setup()

vector<vector<u16string>> songs;
for (auto &name : {"song1.txt", "song2.txt", "song3.txt", "song4.txt", "song5.txt", "song6.txt", "song7.txt", "song8.txt"})
{
  songs.push_back(utils::readLines<u16string>(InputSource::resource(name)));
}

We load our 8 songs. Each of them is stored in a vector of u16string.

Then we load our font, and we create a FontSequence. Note that this is done only once, i.e. it is a static sequence.

font->beginSequence(sequence);

float x = -150;
float y= -70;
float maxHeight = 0;
vector<float> xx;
for (const auto &song : songs)
{
  auto size = drawLines(*font, song, x, y);
  x += size.x + GUTTER;
  xx.push_back(x);
  x += GUTTER;

  if (size.y > maxHeight) maxHeight = size.y;
}

font->endSequence();

For each song, we call drawLines() which returns the size taken by the column of text. Using that information, we can define the x position of the next song. In adition: we store the x position of each song for later use. This loop is also defining the maximum height of the songs.

Then, we enter another loop:

float yy = y - font->getAscent();
for (int i = 0; i < xx.size() -1; i++)
{
  lineBatch.addVertices(glm::vec2(xx[i], yy), glm::vec2(xx[i], yy + maxHeight));
}

Here, we draw the lines separating the songs.

Now, let’s come back to our drawLines() function:

glm::vec2 Sketch::drawLines(XFont &font, const vector<u16string> &lines, float x, float y)
{
  float lineHeight = font.getHeight() * 1.2f;
  float maxWidth = 0;
  for (const auto &line : lines)
  {
    float width = font.getStringAdvance(line);
    if (width > maxWidth) maxWidth = width;

    drawText(font, line, x, y);
    y += lineHeight;
  }

  return glm::vec2(maxWidth, lineHeight * lines.size());
}

It draws each line of text by calling drawText() (which we already covered in Drawing 2d text) and returns the size of the text column.

The input functions

void Sketch::addTouch(int index, float x, float y)
{
  dragOrigin = convert(glm::vec2(x, y)) - pan;
}
void Sketch::updateTouch(int index, float x, float y)
{
  pan = convert(glm::vec2(x, y)) - dragOrigin;
}

It’s a classical dragging system, with a particularity: it is mapped to our 3d plane, thanks to the convert() function:

glm::vec2 Sketch::convert(const glm::vec2 &position)
{
  auto ray = camera.getRay(position);
  auto result = ray.planeIntersection(glm::vec3(0), glm::vec3(0, 0, 1));
  if (result.first)
  {
    return glm::vec2(ray.origin + result.second * ray.direction); // Simplification to 2d is possible because plane lies on ground (0)
  }

  return glm::vec2(0);
}

We obtain a ray – passing by the given window position – from the camera. Then we check if this ray is interesecting our 3d plane. If it’s the case, we compute and return the 2d position on the plane.

Inside draw()

Matrix panMatrix;
panMatrix.translate(pan);

State()
  .setShaderMatrix<MVP>(panMatrix * camera.getViewProjectionMatrix())
  .apply();

In order for the drag operations to work, we must multiply the camera’s view-projection matrix by a matrix translated with pan.