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:
- Mova \(P\) para o ponto médio entre \(P\) e um dos pontos \(A\), \(B\), \(C\) escolhido de forma aleatória;
- 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).
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 deabcg::OpenGLWindow
que recebe uma estruturaabcg::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.
- O atributo
- 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 teclaF11
.
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âmetroswidth
eheight
. Na nossa aplicação, vamos armazenar esses valores nas variáveism_viewportWidth
(linha 23) em_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 deinitializeGL
, usada para liberar os recursos do OpenGL que foram alocados noinitializeGL
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
em_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
em_viewportHeight
servem para armazenar o tamanho da janela da aplicação informado peloresizeGL
.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 estruturaglm::vec2
. O namespaceglm
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 usarglm::vec2
, incluímos o arquivo de cabeçalhoglm/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:
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);m_P.x, m_P.y);
glVertex2f( 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:
- 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. - 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 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.
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:
No nosso caso o arranjo de vértices contém apenas um vértice e equivale ao ponto \(P\) que queremos desenhar.↩︎
A numeração das linhas é a mesma do código completo de
openglwindow.cpp
mostrado no final do capítulo.↩︎Experimente outras distribuições e observe a mudança no comportamento do fractal.↩︎
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.↩︎