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 pelo jogo do caos.

Para jogar o jogo do caos, precisamos primeiro definir três pontos \(A\), \(B\) e \(C\) não colineares. Esses pontos formarão os vértices do triângulo fractal. Por exemplo, \(A=(0, 1)\), \(B=(-1, -1)\) e \(C=(1, -1)\):

Além dos pontos \(A\), \(B\) e \(C\), definiremos 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(window);
  } catch (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 é um método 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 novos métodos 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 os métodos initializeGL, paintGL e paintUI, estamos agora substituindo mais dois métodos virtuais de abcg::OpenGLWindow:

  • resizeGL é chamado 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 é chamado 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 na 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 de 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 é definido o método setupModel, que cria os recursos de GPU identificados por m_vao e m_vboVertices. O método é chamado sempre que um novo ponto \(P\) precisa ser desenhado.

openglwindow.cpp

Vamos primeiro implementar a lógica do jogo do caos sem desenhar nada na tela. 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 \([-1, 1]\). O intervalo poderia ser qualquer outro, mas fazendo assim garantimos que o ponto inicial será visto na tela. No OpenGL, só conseguimos renderizar as primitivas que estão situadas entre \((-1, -1)\) e \((1, 1)\). O ponto \((-1, -1)\) geralmente é mapeado ao canto inferior esquerdo da janela, e o ponto \((1, 1)\) é mapeado 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ção:

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);
}

Nesse 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 Sierpinski. Observe que utilizamos uma distribuição uniforme para sortear o índice. Isso é importante para que o fractal seja desenhado como esperado13.

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 é tudo para 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 já 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:

  • Criar na GPU um recurso chamado de “buffer de vértices”. Esse recurso 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 identifica esse recurso que é chamado de VBO (Vertex Buffer Object).
  • 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.
  • Especificar como o buffer de vértices será lido pelo programa de shaders. No nosso código, o estado dessa configuração é armazenado como um objeto do OpenGL chamado de 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 com os detalhes disso e com os conceitos novos. Vamos detalhar esse processo no próximo capítulo. Por enquanto, utilizaremos o código já pronto.

Primeiro, defina o método setupModel como a seguir14:

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

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

  // Get location of attributes in the program
  GLint positionAttribute = glGetAttribLocation(m_program, "inPosition");

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

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

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

  // End of binding to current VAO
  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
  glClearColor(0, 0, 0, 1);
  glClear(GL_COLOR_BUFFER_BIT);

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

  // 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);
}

Nesse método, 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 de representam uma amostra de geometria rasterizada).

A compilação e ligação dos shaders é feita pelo método createProgramFromString que faz parte da 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 30 e 31).

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

#if !defined(__EMSCRIPTEN__)
  glEnable(GL_PROGRAM_POINT_SIZE);
#endif

Esse código só será compilado quando o Emscripten não for utilizado. No OpenGL para desktop, o código é 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). Definimos o tamanho como dois, mas poderíamos ter escolhido outro valor. A faixa de tamanhos depende do que é suportado pelo hardware. Para sabermos o tamanho mínimo e máximo, usamos esse trecho de código:

  std::array<GLfloat, 2> sizes{};
  glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, sizes.data());
  fmt::print("Point size: {:.2f} (min), {:.2f} (max)\n", sizes[0], sizes[1]);

A função glFloatv com o identificador GL_ALIASED_POINT_SIZE_RANGE, preenche o arranjo sizes com os tamanhos mínimo e máximo suportados. Em seguida, fmt::print mostra os valores no console.

O código final de paintGL ficará assim:

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

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

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

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

  // End using VAO
  glBindVertexArray(0);
  // End using the shader program
  glUseProgram(0);
  
  // 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);
}

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

Na linha 55, glViewport informa ao OpenGL como será feito 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 será mapeado para o pixel \((0, 0)\) da janela (canto inferior esquerdo), e o ponto \((1,1)\) em NDC será mapeado para o pixel \((0,0)\) + (m_viewportWidth, m_viewportHeight). Isso fará 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
  glUseProgram(m_program);
  // Start using VAO
  glBindVertexArray(m_vao);

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

  // End using VAO
  glBindVertexArray(0);
  // End using the shader program
  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 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 (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 o método resizeGL, assim:

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

  glClear(GL_COLOR_BUFFER_BIT);
}

Como vimos, resizeGL é chamado 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 ocupe 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.

Vamos definir o método terminateGL da seguinte maneira:

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

Os comandos glDelete* liberam os recursos alocado em setupModel.

Para finalizar, definiremos 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))) {
      glClear(GL_COLOR_BUFFER_BIT);
    }

    ImGui::End();
  }
}

A linha 83 chama o paintUI da classe base, que mostra o contador de FPS (lembre-se que desabilitamos o 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>

#include "abcg.hpp"

void OpenGLWindow::initializeGL() {
  const auto *vertexShader{R"gl(
    #version 410
    layout(location = 0) in vec2 inPosition;
    void main() { 
      gl_PointSize = 1.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
  glClearColor(0, 0, 0, 1);
  glClear(GL_COLOR_BUFFER_BIT);

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

  // 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);
}

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

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

  // Start using VAO
  glUseProgram(m_program);
  // Start using buffers created in createBuffers()
  glBindVertexArray(m_vao);

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

  // End using VAO
  glBindVertexArray(0);
  // End using the shader program
  glUseProgram(0);
  
  // 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);
}

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

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

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

    ImGui::End();
  }
}

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

  glClear(GL_COLOR_BUFFER_BIT);
}

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

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

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

  // Get location of attributes in the program
  GLint positionAttribute = glGetAttribLocation(m_program, "inPosition");

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

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

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

  // End of binding to current VAO
  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. Caso tenha curiosidade, experimente outras distribuições e observe a mudança no comportamento do fractal.↩︎

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

  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.↩︎