3.4 Triângulo de Sierpinski

O triângulo de Sierpinski é um fractal que pode ser gerado por um tipo de sistema dinâmico chamado de sistema de função iterativa (iterated function system, ou IFS). Esse processo pode ser implementado através de um algoritmo conhecido como jogo do caos.

Para jogar o jogo do caos, começamos definindo três pontos \(A\), \(B\) e \(C\) não colineares. Por exemplo, \(A=(0, 1)\), \(B=(-1, -1)\) e \(C=(1, -1)\):

Além dos pontos \(A\), \(B\) e \(C\), definimos mais um ponto \(P\) em uma posição aleatória do plano. Com \(A\), \(B\), \(C\) e \(P\) definidos, o jogo do caos consiste nos seguintes passos:

  1. Mova \(P\) para o ponto médio entre \(P\) e um dos pontos \(A\), \(B\), \(C\) escolhido de forma aleatória;
  2. Volte ao passo 1.

Para gerar o triângulo de Sierpinski, basta desenhar \(P\) a cada iteração. O jogo não tem fim, mas quanto maior o número de iterações, mais pontos serão desenhados e mais detalhes terá o fractal (figura 3.30).

Triângulo de Sierpinski desenhado com 1.000, 10.000 e 100.000 iterações em uma área de 210x210 pixels.

Figura 3.30: Triângulo de Sierpinski desenhado com 1.000, 10.000 e 100.000 iterações em uma área de 210x210 pixels.

Vamos implementar o jogo do caos com a ABCg, usando a estrutura da aplicação que fizemos no projeto firstapp (seção 2.3). O procedimento será simples: para cada chamada de paintGL, faremos uma iteração do jogo e desenharemos um ponto na posição \(P\) usando um comando de renderização do OpenGL. Os pontos desenhados serão acumulados no framebuffer e visualizaremos o fractal.

Configuração inicial

Repita a configuração inicial do projeto firstapp, mas mudando o nome do projeto para sierpinski.

O arquivo abcg/examples/CMakeLists.txt ficará assim:

add_subdirectory(helloworld)
add_subdirectory(firstapp)
add_subdirectory(sierpinski)

Para a construção não ficar muito lenta, podemos comentar as linhas de add_subdirectory dos projetos anteriores para que eles não sejam compilados. Por exemplo:

#add_subdirectory(helloworld)
#add_subdirectory(firstapp)
add_subdirectory(sierpinski)

O arquivo abcg/examples/sierpinski/CMakeLists.txt ficará assim:

project(sierpinski)
add_executable(${PROJECT_NAME} main.cpp openglwindow.cpp)
enable_abcg(${PROJECT_NAME})

Crie também os arquivos main.cpp, openglwindow.cpp e openglwindow.hpp em abcg/examples/sierpinski. Vamos editá-los a seguir.

main.cpp

O conteúdo de main.cpp ficará como a seguir:

#include <fmt/core.h>

#include "abcg.hpp"
#include "openglwindow.hpp"

int main(int argc, char **argv) {
  try {
    // Create application instance
    abcg::Application app(argc, argv);

    // Create OpenGL window
    auto window{std::make_unique<OpenGLWindow>()};
    window->setOpenGLSettings(
        {.samples = 2, .preserveWebGLDrawingBuffer = true});
    window->setWindowSettings({.width = 600,
                               .height = 600,
                               .showFullscreenButton = false,
                               .title = "Sierpinski Triangle"});

    // Run application
    app.run(std::move(window));
  } catch (const abcg::Exception &exception) {
    fmt::print(stderr, "{}\n", exception.what());
    return -1;
  }
  return 0;
}

Esse código é bem parecido com o main.cpp do projeto firstapp. As únicas diferenças estão nas linhas 13 a 18:

    window->setOpenGLSettings(
        {.samples = 2, .preserveWebGLDrawingBuffer = true});
    window->setWindowSettings({.width = 600,
                               .height = 600,
                               .showFullscreenButton = false,
                               .title = "Sierpinski Triangle"});
  • setOpenGLSettings é uma função membro de abcg::OpenGLWindow que recebe uma estrutura abcg::OpenGLSettings com as configurações de inicialização do OpenGL. Essas configurações são usadas pela SDL no momento da criação de um “contexto do OpenGL” que representa o framebuffer vinculado à janela:
    • O atributo samples = 2 faz com que o framebuffer suporte suavização de serrilhado (antialiasing) das primitivas do OpenGL;
    • O atributo preserveWebGLDrawingBuffer = true é utilizado apenas no binário em WebAssembly. No WebGL, preserveDrawingBuffer é uma configuração de criação do contexto do OpenGL que faz com que o framebuffer vinculado ao canvas da página Web não seja apagado entre os quadros de exibição.
  • Em setWindowSettings, utilizamos alguns atributos novos de definição de propriedades da janela. Definimos a largura (width) e altura (height) inicial da janela, e desligamos a exibição do botão de tela cheia (showFullscreenButton = false) para que o botão não obstrua o desenho do triângulo. Mesmo sem o botão, o modo janela pode ser alternado com o modo de tela cheia pela tecla F11.

openglwindow.hpp

Na definição da classe OpenGLWindow, vamos substituir novas funções virtuais de abcg::OpenGLWindow e vamos definir variáveis que serão utilizados para atualizar o jogo do caos e para desenhar o ponto na tela:

#ifndef OPENGLWINDOW_HPP_
#define OPENGLWINDOW_HPP_

#include <array>
#include <glm/vec2.hpp>
#include <random>

#include "abcg.hpp"

class OpenGLWindow : public abcg::OpenGLWindow {
 protected:
  void initializeGL() override;
  void paintGL() override;
  void paintUI() override;
  void resizeGL(int width, int height) override;
  void terminateGL() override;

 private:
  GLuint m_vao{};
  GLuint m_vboVertices{};
  GLuint m_program{};

  int m_viewportWidth{};
  int m_viewportHeight{};

  std::default_random_engine m_randomEngine;

  const std::array<glm::vec2, 3> m_points{glm::vec2( 0,  1), 
                                          glm::vec2(-1, -1),
                                          glm::vec2( 1, -1)};
  glm::vec2 m_P{};

  void setupModel();
};
#endif

Observe que, além de usarmos as funções initializeGL, paintGL e paintUI, estamos agora substituindo mais duas funções virtuais de abcg::OpenGLWindow:

  • resizeGL é chamada pela ABCg sempre que o tamanho da janela é alterado. O novo tamanho é recebido pelos parâmetros width e height. Na nossa aplicação, vamos armazenar esses valores nas variáveis m_viewportWidth (linha 23) e m_viewportHeight (linha 24). Precisamos disso para fazer com que a janela de exibição (viewport) do OpenGL tenha o mesmo tamanho da janela da aplicação. O conceito de viewport será detalhado mais adiante.

  • terminateGL é chamada pela ABCg quando a janela é destruída, no fim da aplicação. Essa é a função complementar de initializeGL, usada para liberar os recursos do OpenGL que foram alocados no initializeGL ou durante a aplicação.

Da linha 19 a 31 temos a definição das variáveis da classe:

  GLuint m_vao{};
  GLuint m_vboVertices{};
  GLuint m_program{};

  int m_viewportWidth{};
  int m_viewportHeight{};

  std::default_random_engine m_randomEngine;

  const std::array<glm::vec2, 3> m_points{glm::vec2( 0,  1), 
                                          glm::vec2(-1, -1),
                                          glm::vec2( 1, -1)};
  glm::vec2 m_P{};
  • m_vao, m_vboVertices e m_program são identificadores de recursos alocados pelo OpenGL (recursos geralmente armazenados na memória da GPU). Esses recursos correspondem ao arranjo ordenado de vértices utilizado para montar as primitivas geométricas no pipeline de renderização12 e os shaders que definem o comportamento da renderização.

  • m_viewportWidth e m_viewportHeight servem para armazenar o tamanho da janela da aplicação informado pelo resizeGL.

  • m_randomEngine é um objeto do gerador de números pseudoaleatórios do C++ (observe o uso do #include <random> na linha 6). Esse objeto é utilizado para sortear a posição inicial de \(P\) e para sortear qual ponto (\(A\), \(B\) ou \(C\)) será utilizado em cada iteração do jogo do caos.

  • m_points é um arranjo que contém a posição dos pontos \(A\), \(B\) e \(C\). As coordenadas dos pontos são descritas por uma estrutura glm::vec2. O namespace glm contém definições da biblioteca OpenGL Mathematics (GLM) que fornece estruturas e funções de operações matemáticas compatíveis com a especificação da linguagem de shaders do OpenGL. Observe que, para usar glm::vec2, incluímos o arquivo de cabeçalho glm/vec2.hpp.

  • m_P é a posição do ponto \(P\).

Além da definição das variáveis, na linha 33 é definida a função membro OpenGLWindow::setupModel que cria os recursos identificados por m_vao e m_vboVertices. A função é chamada sempre que um novo ponto \(P\) precisa ser desenhado.

openglwindow.cpp

Vamos implementar primeiro a lógica do jogo do caos, sem desenhar nada na tela. Em seguida incluiremos o código que usa o OpenGL para desenhar os pontos.

Vamos começar incluindo os seguintes arquivos de cabeçalho:

#include "openglwindow.hpp"

#include <fmt/core.h>
#include <imgui.h>

#include <chrono>

Em OpenGLWindow::initializeGL, iniciaremos o gerador de números pseudoaleatórios e sortearemos as coordenadas iniciais de \(P\) (que no código é m_P):

void OpenGLWindow::initializeGL() {
  // Start pseudo-random number generator
  auto seed{std::chrono::steady_clock::now().time_since_epoch().count()};
  m_randomEngine.seed(seed);

  // Randomly choose a pair of coordinates in the interval [-1; 1]
  std::uniform_real_distribution<float> realDistribution(-1.0f, 1.0f);
  m_P.x = realDistribution(m_randomEngine);
  m_P.y = realDistribution(m_randomEngine);
}

O gerador m_randomEngine é iniciado usando como semente o tempo do sistema (para isso é preciso incluir o cabeçalho <chrono>).

As coordenadas de m_P são iniciadas como valores sorteados de um intervalo de -1 a 1. O intervalo poderia ser qualquer outro, mas fazendo assim garantimos que o ponto inicial será visto na tela. Na configuração padrão do OpenGL, só conseguimos visualizar as primitivas gráficas que estão situadas entre as coordenadas \((-1, -1)\) e \((1, 1)\). A coordenada \((-1, -1)\) geralmente é mapeada ao canto inferior esquerdo da janela, e a coordenada \((1, 1)\) é mapeada ao canto superior direito (esse mapeamento será configurado posteriormente com a função glViewport).

Vamos agora implementar o passo iterativo do jogo. Faremos isso no paintGL, de modo que cada quadro de exibição corresponderá a uma iteração13:

void OpenGLWindow::paintGL() {
  // Randomly choose a triangle vertex index
  std::uniform_int_distribution<int> intDistribution(0, m_points.size() - 1);
  int index{intDistribution(m_randomEngine)};

  // The new position is the midpoint between the current position and the
  // chosen vertex
  m_P = (m_P + m_points.at(index)) / 2.0f;

  // Print coordinates to the console
  // fmt::print("({:+.2f}, {:+.2f})\n", m_P.x, m_P.y);
}

Neste trecho de código, index é um índice do arranjo m_points. Assim, m_points.at(index) é um dos pontos \(A\), \(B\) ou \(C\) que definem os vértices do triângulo. Observe que utilizamos uma distribuição uniforme para sortear o índice. Isso é importante para que o fractal seja desenhado como esperado14.

A nova posição de m_P é calculada como o ponto médio entre m_P e o ponto de m_points.

O código comentado pode ser utilizado para imprimir no terminal as novas coordenadas de m_P.

Basicamente isso conclui a lógica do jogo do caos. Todo o resto do código será para desenhar m_P como um ponto na tela. No OpenGL anterior à versão 3.1, isso seria tão simples quanto acrescentar o seguinte código em paintGL:

glBegin(GL_POINTS);
  glVertex2f(m_P.x, m_P.y);
glEnd();

Entretanto, como vimos na seção 3.1, esse código é obsoleto e não é mais suportado em muitos drivers e plataformas. Para desenhar um simples ponto na tela, precisaremos seguir os seguintes passos:

  1. Criar um “buffer de vértices” como recurso do OpenGL. Esse recurso é chamado VBO (Vertex Buffer Object) e corresponde ao arranjo ordenado de vértices utilizado pelo pipeline de renderização para montar as primitivas que serão renderizadas. No nosso caso, o buffer de vértices só precisa ter um vértice, que é a coordenada do ponto que queremos desenhar. A variável m_vboVertices é um inteiro que identifica esse recurso.
  2. Programar o comportamento do pipeline de renderização. Isso é feito compilando e ligando um par de shaders que fica armazenado na GPU como um único “programa de shader,” identificado pela variável m_program. No OpenGL, os shaders são escritos na linguagem GLSL (OpenGL Shading Language), que é parecida com a linguagem C, mas possui novos tipos de dados e operações.
  3. Especificar como o buffer de vértices será lido pelo programa de shader. No nosso código, o estado dessa configuração é armazenado como um objeto do OpenGL chamado VAO (Vertex Array Object), identificado pela variável m_vao.

Somente após alocar e ativar esses recursos é que podemos iniciar o pipeline de renderização, chamando uma função de desenho no paintGL. Não se preocupe se tudo isso está parecendo muito complexo nesse momento. Nos próximos capítulos revisitaremos cada etapa várias vezes até nos familiarizarmos com todo o processo. Por enquanto, utilizaremos o código já pronto.

Primeiro, defina a função setupModel como a seguir:

void OpenGLWindow::setupModel() {
  // Release previous VBO and VAO
  abcg::glDeleteBuffers(1, &m_vboVertices);
  abcg::glDeleteVertexArrays(1, &m_vao);

  // Generate a new VBO and get the associated ID
  abcg::glGenBuffers(1, &m_vboVertices);
  // Bind VBO in order to use it
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboVertices);
  // Upload data to VBO
  abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(m_P), &m_P, GL_STATIC_DRAW);
  // Unbinding the VBO is allowed (data can be released now)
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // Get location of attributes in the program
  const GLint positionAttribute{
      abcg::glGetAttribLocation(m_program, "inPosition")};

  // Create VAO
  abcg::glGenVertexArrays(1, &m_vao);

  // Bind vertex attributes to current VAO
  abcg::glBindVertexArray(m_vao);

  abcg::glEnableVertexAttribArray(positionAttribute);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboVertices);
  abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
                              nullptr);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // End of binding to current VAO
  abcg::glBindVertexArray(0);
}

Esse código cria o VBO (m_vboVertices) e VAO (m_VAO) usando a posição atual de m_P.

Agora, modifique initializeGL para o seguinte código final:

void OpenGLWindow::initializeGL() {
  const auto *vertexShader{R"gl(
    #version 410
    layout(location = 0) in vec2 inPosition;
    void main() { 
      gl_PointSize = 2.0;
      gl_Position = vec4(inPosition, 0, 1); 
    }
  )gl"};

  const auto *fragmentShader{R"gl(
    #version 410
    out vec4 outColor;
    void main() { outColor = vec4(1); }
  )gl"};

  // Create shader program
  m_program = createProgramFromString(vertexShader, fragmentShader);

  // Clear window
  abcg::glClearColor(0, 0, 0, 1);
  abcg::glClear(GL_COLOR_BUFFER_BIT);

  std::array<GLfloat, 2> sizes{};
#if !defined(__EMSCRIPTEN__)
  abcg::glEnable(GL_PROGRAM_POINT_SIZE);
  abcg::glGetFloatv(GL_POINT_SIZE_RANGE, sizes.data());
#else
  abcg::glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, sizes.data());
#endif
  fmt::print("Point size: {:.2f} (min), {:.2f} (max)\n", sizes[0], sizes[1]);

  // Start pseudo-random number generator
  m_randomEngine.seed(
      std::chrono::steady_clock::now().time_since_epoch().count());

  // Randomly choose a pair of coordinates in the interval [-1; 1]
  std::uniform_real_distribution<float> realDistribution(-1.0f, 1.0f);
  m_P.x = realDistribution(m_randomEngine);
  m_P.y = realDistribution(m_randomEngine);
}

Nesta função, vertexShader e fragmentShader são strings que contêm o código-fonte dos shaders. vertexShader é o código do chamado vertex shader, que programa o processamento de vértices na GPU. fragmentShader é o código do fragment shader que programa o processamento de pixels na GPU (ou, mais precisamente, o processamento de fragmentos, que são conjuntos de atributos que representam uma amostra de geometria rasterizada).

A compilação e ligação dos shaders é feita pela função createProgramFromString que faz parte de abcg::OpenGLWindow. Se acontecer algum erro de compilação, a mensagem de erro será exibida no console e uma exceção será lançada.

Note que limpamos o buffer de cor com a cor preta, usando glClearColor e glClear (linhas 28 e 29).

Observe o trecho de código entre as diretivas de pré-processamento:

#if !defined(__EMSCRIPTEN__)
  glEnable(GL_PROGRAM_POINT_SIZE);
  abcg::glGetFloatv(GL_POINT_SIZE_RANGE, sizes.data());
#else

Esse código só será compilado quando não usarmos o Emscripten, isto é, quando o binário for compilado para desktop. No OpenGL para desktop, o comando da linha 33 é necessário para que o tamanho do ponto que será desenhado possa ser definido no vertex shader. Quando o código é compilado com o Emscripten, a definição do tamanho do ponto no vertex shader já é suportada por padrão, pois o OpenGL utilizado é o OpenGL ES (o WebGL usa um subconjunto de funções do OpenGL ES).

Observe, no código do vertex shader, que o tamanho do ponto é definido com gl_PointSize = 2.0 (dois pixels). Os tamanhos válidos dependem do que é suportado pelo hardware. Para imprimir no console o tamanho mínimo e máximo, usamos glGetFloatv neste trecho de código:

  std::array<GLfloat, 2> sizes{};
#if !defined(__EMSCRIPTEN__)
  abcg::glEnable(GL_PROGRAM_POINT_SIZE);
  abcg::glGetFloatv(GL_POINT_SIZE_RANGE, sizes.data());
#else
  abcg::glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, sizes.data());
#endif
  fmt::print("Point size: {:.2f} (min), {:.2f} (max)\n", sizes[0], sizes[1]);

A função glGetFloatv com o identificador GL_POINT_SIZE_RANGE (para OpenGL desktop) e GL_ALIASED_POINT_SIZE_RANGE (para OpenGL ES) preenche o arranjo sizes com os tamanhos mínimo e máximo suportados. Em seguida, fmt::print mostra os valores no console.

Voltando agora à implementação de OpenGLWindow::paintGL, o código final ficará assim:

void OpenGLWindow::paintGL() {
  // Create OpenGL buffers for the single point at m_P
  setupModel();

  // Set the viewport
  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  // Start using the shader program
  abcg::glUseProgram(m_program);
  // Start using VAO
  abcg::glBindVertexArray(m_vao);

  // Draw a single point
  abcg::glDrawArrays(GL_POINTS, 0, 1);

  // End using VAO
  abcg::glBindVertexArray(0);
  // End using the shader program
  abcg::glUseProgram(0);

  // Randomly choose a triangle vertex index
  std::uniform_int_distribution<int> intDistribution(0, m_points.size() - 1);
  const int index{intDistribution(m_randomEngine)};

  // The new position is the midpoint between the current position and the
  // chosen vertex
  m_P = (m_P + m_points.at(index)) / 2.0f;

  // Print coordinates to the console
  // fmt::print("({:+.2f}, {:+.2f})\n", m_P.x, m_P.y);
}

Na linha 52, setupModel cria os recursos do OpenGL necessários para desenhar um ponto na posição atual de m_P.

Na linha 55, glViewport configura o mapeamento entre o sistema de coordenadas no qual nossos pontos foram definidos (coordenadas normalizadas do dispositivo, ou NDC, de normalized device coordinates), e o sistema de coordenadas da janela (window coordinates), em pixels, com origem no canto inferior esquerdo da janela da aplicação.

A figura 3.31 ilustra como fica configurado o mapeamento entre coordenadas em NDC para coordenadas da janela, supondo uma chamada a glViewport(x, y, w, h), onde x, y, w e h são inteiros dados em pixels da tela. Na figura, o chamado viewport do OpenGL é a janela formada pelo retângulo entre os pontos \((x,y)\) e \((x+w,y+h)\).

No nosso código com glViewport(0, 0, m_viewportWidth, m_viewportHeight), o ponto \((-1,-1)\) em NDC é mapeado para o pixel \((0, 0)\) da janela (canto inferior esquerdo), e o ponto \((1,1)\) em NDC é mapeado para o pixel \((0,0)\) + (m_viewportWidth, m_viewportHeight). Isso faz com que o viewport ocupe toda a janela da aplicação.

Mapeamento das coordenadas normalizadas no dispositivo (NDC) para coordenadas da janela usando `glViewport(x, y, w, h)`.

Figura 3.31: Mapeamento das coordenadas normalizadas no dispositivo (NDC) para coordenadas da janela usando glViewport(x, y, w, h).

Com o viewport devidamente configurado, iniciamos o pipeline de renderização neste trecho:

  // Start using the shader program
  abcg::glUseProgram(m_program);
  // Start using VAO
  abcg::glBindVertexArray(m_vao);

  // Draw a single point
  abcg::glDrawArrays(GL_POINTS, 0, 1);

  // End using VAO
  abcg::glBindVertexArray(0);
  // End using the shader program
  abcg::glUseProgram(0);

Na linha 58, glUseProgram ativa os shaders compilados no programa m_program.

Na linha 60, glBindVertexArray ativa o VAO (m_VAO), que contém as especificações de como o arranjo de vértices (VBO) será lido no vertex shader atualmente ativo. Ao ativar o VAO, também é ativado automaticamente o VBO identificado por m_VBO.

Finalmente, na linha 63, glDrawArrays inicia o pipeline de renderização usando os shaders e o VBO ativo. O primeiro argumento (GL_POINTS) indica que os vértices do arranjo de vértices devem ser tratados como pontos. O segundo argumento (0) é o índice inicial dos vértices no VBO, e o terceiro argumento (1) informa quantos vértices devem ser processados.

O processamento no pipeline de renderização é realizado de forma paralela e assíncrona com a CPU. Isto é, glDrawArrays retorna imediatamente, enquanto a GPU trabalha em paralelo renderizando a geometria no framebuffer15.

Após o comando de renderização, as linhas 66 e 68 desativam o VAO e os shaders. Essa desativação é opcional pois, de qualquer forma, o mesmo VAO e os mesmos shaders serão utilizados na próxima chamada de paintGL. Ainda assim, é uma boa prática de programação desativá-los logo após o uso.

Vamos agora definir a função membro OpenGLWindow::resizeGL, assim:

void OpenGLWindow::resizeGL(int width, int height) {
  m_viewportWidth = width;
  m_viewportHeight = height;

  abcg::glClear(GL_COLOR_BUFFER_BIT);
}

Como vimos, resizeGL é chamada sempre que a janela da aplicação muda de tamanho. Observe que simplesmente armazenamos o tamanho da janela em m_viewportWidth e m_viewportHeight. Como essas variáveis são usadas em glViewport, garantimos que o viewport sempre ocupará toda a janela da aplicação.

Observe que também chamamos glClear para apagar o buffer de cor. Dessa forma, o triângulo de Sierpinski no novo tamanho não é desenhado sobre o triângulo do tamanho anterior, o que estragaria o fractal.

A função membro OpenGLWindow::terminateGL é definida da seguinte maneira:

void OpenGLWindow::terminateGL() {
  // Release shader program, VBO and VAO
  abcg::glDeleteProgram(m_program);
  abcg::glDeleteBuffers(1, &m_vboVertices);
  abcg::glDeleteVertexArrays(1, &m_vao);
}

Os comandos glDelete* liberam os recursos alocado em setupModel.

Para finalizar, vamos definir paintUI usando o seguinte código:

void OpenGLWindow::paintUI() {
  abcg::OpenGLWindow::paintUI();

  {
    ImGui::SetNextWindowPos(ImVec2(5, 81));
    ImGui::Begin(" ", nullptr, ImGuiWindowFlags_NoDecoration);

    if (ImGui::Button("Clear window", ImVec2(150, 30))) {
      abcg::glClear(GL_COLOR_BUFFER_BIT);
    }

    ImGui::End();
  }
}

Na linha 83 chamamos o paintUI da classe base, responsável por mostrar o contador de FPS (lembre-se que desabilitamos a exibição do botão de tela cheia).

O código nas linhas 85 a 94 cria um botão “Clear window” que chama glClear sempre que pressionado.

Isso é tudo! O código completo de openglwindow.cpp é mostrado a seguir:

#include "openglwindow.hpp"

#include <fmt/core.h>
#include <imgui.h>

#include <chrono>

void OpenGLWindow::initializeGL() {
  const auto *vertexShader{R"gl(
    #version 410
    layout(location = 0) in vec2 inPosition;
    void main() { 
      gl_PointSize = 2.0;
      gl_Position = vec4(inPosition, 0, 1); 
    }
  )gl"};

  const auto *fragmentShader{R"gl(
    #version 410
    out vec4 outColor;
    void main() { outColor = vec4(1); }
  )gl"};

  // Create shader program
  m_program = createProgramFromString(vertexShader, fragmentShader);

  // Clear window
  abcg::glClearColor(0, 0, 0, 1);
  abcg::glClear(GL_COLOR_BUFFER_BIT);

  std::array<GLfloat, 2> sizes{};
#if !defined(__EMSCRIPTEN__)
  abcg::glEnable(GL_PROGRAM_POINT_SIZE);
  abcg::glGetFloatv(GL_POINT_SIZE_RANGE, sizes.data());
#else
  abcg::glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, sizes.data());
#endif
  fmt::print("Point size: {:.2f} (min), {:.2f} (max)\n", sizes[0], sizes[1]);

  // Start pseudo-random number generator
  m_randomEngine.seed(
      std::chrono::steady_clock::now().time_since_epoch().count());

  // Randomly choose a pair of coordinates in the interval [-1; 1]
  std::uniform_real_distribution<float> realDistribution(-1.0f, 1.0f);
  m_P.x = realDistribution(m_randomEngine);
  m_P.y = realDistribution(m_randomEngine);
}

void OpenGLWindow::paintGL() {
  // Create OpenGL buffers for the single point at m_P
  setupModel();

  // Set the viewport
  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  // Start using the shader program
  abcg::glUseProgram(m_program);
  // Start using VAO
  abcg::glBindVertexArray(m_vao);

  // Draw a single point
  abcg::glDrawArrays(GL_POINTS, 0, 1);

  // End using VAO
  abcg::glBindVertexArray(0);
  // End using the shader program
  abcg::glUseProgram(0);

  // Randomly choose a triangle vertex index
  std::uniform_int_distribution<int> intDistribution(0, m_points.size() - 1);
  const int index{intDistribution(m_randomEngine)};

  // The new position is the midpoint between the current position and the
  // chosen vertex
  m_P = (m_P + m_points.at(index)) / 2.0f;

  // Print coordinates to the console
  // fmt::print("({:+.2f}, {:+.2f})\n", m_P.x, m_P.y);
}

void OpenGLWindow::paintUI() {
  abcg::OpenGLWindow::paintUI();

  {
    ImGui::SetNextWindowPos(ImVec2(5, 5 + 50 + 16 + 5));
    ImGui::Begin(" ", nullptr, ImGuiWindowFlags_NoDecoration);

    if (ImGui::Button("Clear window", ImVec2(150, 30))) {
      abcg::glClear(GL_COLOR_BUFFER_BIT);
    }

    ImGui::End();
  }
}

void OpenGLWindow::resizeGL(int width, int height) {
  m_viewportWidth = width;
  m_viewportHeight = height;

  abcg::glClear(GL_COLOR_BUFFER_BIT);
}

void OpenGLWindow::terminateGL() {
  // Release shader program, VBO and VAO
  abcg::glDeleteProgram(m_program);
  abcg::glDeleteBuffers(1, &m_vboVertices);
  abcg::glDeleteVertexArrays(1, &m_vao);
}

void OpenGLWindow::setupModel() {
  // Release previous VBO and VAO
  abcg::glDeleteBuffers(1, &m_vboVertices);
  abcg::glDeleteVertexArrays(1, &m_vao);

  // Generate a new VBO and get the associated ID
  abcg::glGenBuffers(1, &m_vboVertices);
  // Bind VBO in order to use it
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboVertices);
  // Upload data to VBO
  abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(m_P), &m_P, GL_STATIC_DRAW);
  // Unbinding the VBO is allowed (data can be released now)
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // Get location of attributes in the program
  const GLint positionAttribute{
      abcg::glGetAttribLocation(m_program, "inPosition")};

  // Create VAO
  abcg::glGenVertexArrays(1, &m_vao);

  // Bind vertex attributes to current VAO
  abcg::glBindVertexArray(m_vao);

  abcg::glEnableVertexAttribArray(positionAttribute);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboVertices);
  abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
                              nullptr);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // End of binding to current VAO
  abcg::glBindVertexArray(0);
}

Construa a aplicação para ver o resultado:


  1. No nosso caso o arranjo de vértices contém apenas um vértice e equivale ao ponto \(P\) que queremos desenhar.↩︎

  2. A numeração das linhas é a mesma do código completo de openglwindow.cpp mostrado no final do capítulo.↩︎

  3. Experimente outras distribuições e observe a mudança no comportamento do fractal.↩︎

  4. A ABCg habilita a técnica de backbuffering vista na seção 3.3. Desse modo, a GPU renderiza primeiro a geometria no backbuffer. Quando a renderização é concluída, o conteúdo é enviado automaticamente para o frontbuffer, que atualiza o dispositivo de exibição.↩︎