4.4 Triângulos coloridos
Na seção 3.5, renderizamos pontos (GL_POINTS
) para gerar o Triângulo de Sierpinski. Neste projeto, desenharemos triângulos (GL_TRIANGLES
). Para cada quadro de exibição, renderizaremos um triângulo colorido com coordenadas 2D aleatórias dentro do viewport que ocupa toda a janela da aplicação. O resultado ficará como a seguir:
Durante o desenvolvimento desta atividade veremos com mais detalhes os comandos do OpenGL utilizados para especificar os dados gráficos e configurar o pipeline.
Configuração inicial
Repita a configuração inicial dos projetos anteriores e mude o nome do projeto para coloredtriangles
.
O arquivo abcg/examples/CMakeLists.txt
ficará assim (com a compilação desabilitada para os projetos anteriores):
# add_subdirectory(helloworld)
# add_subdirectory(firstapp)
# add_subdirectory(tictactoe)
# add_subdirectory(sierpinski)
add_subdirectory(coloredtriangles)
O arquivo abcg/examples/coloredtriangles/CMakeLists.txt
ficará assim:
project(coloredtriangles)
add_executable(${PROJECT_NAME} main.cpp window.cpp)
enable_abcg(${PROJECT_NAME})
Como nos projetos anteriores, crie os arquivos main.cpp
, window.cpp
e window.hpp
em abcg/examples/coloredtriangles
. Vamos editá-los a seguir.
main.cpp
O conteúdo de main.cpp
é praticamente idêntico ao do projeto sierpinski
:
#include "window.hpp"
int main(int argc, char **argv) {
try {
abcg::Application app(argc, argv);
Window window;
window.setOpenGLSettings(
{.samples = 2, .doubleBuffering = false});
window.setWindowSettings(
{.width = 600, .height = 600, .title = "Colored Triangles"});
app.run(window);
} catch (std::exception const &exception) {
fmt::print(stderr, "{}\n", exception.what());
return -1;
}
return 0;
}
window.hpp
A definição da classe Window
também é parecida com a do projeto anterior:
#ifndef WINDOW_HPP_
#define WINDOW_HPP_
#include <random>
#include "abcgOpenGL.hpp"
class Window : public abcg::OpenGLWindow {
protected:
void onCreate() override;
void onPaint() override;
void onPaintUI() override;
void onResize(glm::ivec2 const &size) override;
void onDestroy() override;
private:
glm::ivec2 m_viewportSize{};
GLuint m_VAO{};
GLuint m_VBOPositions{};
GLuint m_VBOColors{};
GLuint m_program{};
std::default_random_engine m_randomEngine;
std::array<glm::vec4, 3> m_colors{{{0.36f, 0.83f, 1.00f, 1},
{0.63f, 0.00f, 0.61f, 1},
{1.00f, 0.69f, 0.30f, 1}}};
void setupModel();
};
#endif
No projeto sierpinski
utilizamos apenas um VBO (m_VBOVertices
). Dessa vez usaremos dois VBOs: um para a posição dos vértices (m_VBOPositions
) e outro para as cores (m_VOBColors
).
O arranjo m_vertexColors
contém as cores RGBA que serão copiadas para m_VBOColors
. São três cores, uma para cada vértice do triângulo.
window.cpp
Vamos primeiro incluir o cabeçalho window.hpp
e a definição de Window::onCreate
:
#include "window.hpp"
void Window::onCreate() {
auto const *vertexShader{R"gl(#version 300 es
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec4 inColor;
out vec4 fragColor;
void main() {
gl_Position = vec4(inPosition, 0, 1);
fragColor = inColor;
}
)gl"};
auto const *fragmentShader{R"gl(#version 300 es
precision mediump float;
in vec4 fragColor;
out vec4 outColor;
void main() { outColor = fragColor; }
)gl"};
m_program = abcg::createOpenGLProgram(
{{.source = vertexShader, .stage = abcg::ShaderStage::Vertex},
{.source = fragmentShader, .stage = abcg::ShaderStage::Fragment}});
abcg::glClearColor(0, 0, 0, 1);
abcg::glClear(GL_COLOR_BUFFER_BIT);
auto const seed{std::chrono::steady_clock::now().time_since_epoch().count()};
m_randomEngine.seed(seed);
}
O código é praticamente idêntico ao do projeto anterior.
Observe o conteúdo da string em vertexShader
. A string é inicializada com um raw string literal. O código do shader é o texto que está entre R"gl(
e )gl"
:
#version 300 es
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec4 inColor;
out vec4 fragColor;
void main() {
gl_Position = vec4(inPosition, 0, 1);
= inColor;
fragColor }
Este vertex shader define dois atributos de entrada: inPosition
, que recebe a posição 2D do vértice, e inColor
que recebe a cor do vértice em formato RGBA. A saída, fragColor
, é também uma cor RGBA. Na função main
, a posição \((x,y)\) do vértice é repassada sem modificações para gl_Position
. A conversão de \((x,y)\) em coordenadas cartesianas para \((x,y,0,1)\) em coordenadas homogêneas preserva a geometria do triângulo20. A cor do atributo de entrada também é repassada sem modificações para o atributo de saída.
Vejamos agora o fragment shader:
#version 300 es
precision mediump float;
in vec4 fragColor;
out vec4 outColor;
void main() { outColor = fragColor; }
O fragment shader é mais simples. O atributo de entrada (fragColor
) é copiado sem modificações para o atributo de saída (outColor
).
A compilação e ligação dos shaders é feita pela chamada a abcg::createOpenGLProgram
na linha 26. O resultado é m_program
, um número inteiro que identifica o programa de shader composto pelo par de vertex/fragment shader. Para ativar o programa no pipeline, devemos chamar glUseProgram(m_program)
. Para desativá-lo, podemos ativar outro programa (se existir) ou chamar glUseProgram(0)
.
A função Window::onPaint
é definida assim:
void Window::onPaint() {
setupModel();
abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);
abcg::glUseProgram(m_program);
abcg::glBindVertexArray(m_VAO);
abcg::glDrawArrays(GL_TRIANGLES, 0, 3);
abcg::glBindVertexArray(0);
abcg::glUseProgram(0);
}
Novamente, o código é similar ao utilizado no projeto sierpinski
. O comando de renderização, glDrawArrays
, dessa vez usa GL_TRIANGLES
e 3
vértices, sendo que o índice inicial dos vértices no arranjo é 0
. Isso significa que o pipeline desenhará apenas um triângulo.
Em Window::onPaintUI
, usaremos controles de UI da ImGui para criar uma pequena janela de edição das três cores dos vértices:
void Window::onPaintUI() {
abcg::OpenGLWindow::onPaintUI();
{
auto const widgetSize{ImVec2(250, 90)};
ImGui::SetNextWindowPos(ImVec2(m_viewportSize.x - widgetSize.x - 5,
m_viewportSize.y - widgetSize.y - 5));
ImGui::SetNextWindowSize(widgetSize);
auto windowFlags{ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar};
ImGui::Begin(" ", nullptr, windowFlags);
// Edit vertex colors
auto colorEditFlags{ImGuiColorEditFlags_NoTooltip |
ImGuiColorEditFlags_NoPicker};
ImGui::PushItemWidth(215);
ImGui::ColorEdit3("v0", &m_colors.at(0).x, colorEditFlags);
ImGui::ColorEdit3("v1", &m_colors.at(1).x, colorEditFlags);
ImGui::ColorEdit3("v2", &m_colors.at(2).x, colorEditFlags);
ImGui::PopItemWidth();
ImGui::End();
}
}
As funções ImGui::SetNextWindowPos
e ImGui::SetNextWindowSize
definem a posição e tamanho da janela da ImGui que está prestes a ser criada na linha 70. A janela é inicializada com alguns flags para que ela não possa ser redimensionada (ImGuiWindowFlags_NoResize
) e não tenha a barra de título (ImGuiWindowFlags_NoTitleBar
). Os controles ImGui::ColorEdit3
também são criados com flags para desabilitar o color picker (ImGuiColorEditFlags_NoPicker
) e os tooltips (ImGuiColorEditFlags_NoTooltip
), pois eles podem atrapalhar o desenho dos triângulos.
A definição de Window::onResize
é idêntica à do projeto sierpinski
. A definição de Window::onDestroy
também é bem semelhante e libera os recursos do pipeline:
void Window::onResize(glm::ivec2 const &size) {
m_viewportSize = size;
abcg::glClear(GL_COLOR_BUFFER_BIT);
}
void Window::onDestroy() {
abcg::glDeleteProgram(m_program);
abcg::glDeleteBuffers(1, &m_VBOPositions);
abcg::glDeleteBuffers(1, &m_VBOColors);
abcg::glDeleteVertexArrays(1, &m_VAO);
}
Vamos agora à função Window::setupModel
. A definição completa é dada a seguir. Em seguida faremos uma análise detalhada de cada trecho:
void Window::setupModel() {
abcg::glDeleteBuffers(1, &m_VBOPositions);
abcg::glDeleteBuffers(1, &m_VBOColors);
abcg::glDeleteVertexArrays(1, &m_VAO);
// Create array of random vertex positions
std::uniform_real_distribution rd(-1.5f, 1.5f);
std::array<glm::vec2, 3> const positions{
{{rd(m_randomEngine), rd(m_randomEngine)},
{rd(m_randomEngine), rd(m_randomEngine)},
{rd(m_randomEngine), rd(m_randomEngine)}}};
// Generate VBO of positions
abcg::glGenBuffers(1, &m_VBOPositions);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOPositions);
abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions.data(),
GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Generate VBO of colors
abcg::glGenBuffers(1, &m_VBOColors);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOColors);
abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(m_colors), m_colors.data(),
GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Get location of attributes in the program
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
auto const colorAttribute{abcg::glGetAttribLocation(m_program, "inColor")};
// 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_VBOPositions);
abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
abcg::glEnableVertexAttribArray(colorAttribute);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOColors);
abcg::glVertexAttribPointer(colorAttribute, 4, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// End of binding to current VAO
abcg::glBindVertexArray(0);
}
As linhas 96 a 98 liberam os VBOs e o VAO, caso tenham sido criados anteriormente:
abcg::glDeleteBuffers(1, &m_VBOPositions);
abcg::glDeleteBuffers(1, &m_VBOColors);
abcg::glDeleteVertexArrays(1, &m_VAO);
É importante fazer isso, pois a função Window::setupModel
é chamada continuamente em Window::onPaint
. Se não liberarmos os recursos, em algum momento eles consumirão toda a memória da GPU e CPU21.
As linhas 100 a 105 criam um arranjo com as posições dos vértices. Esse arranjo será copiado para o VBO m_VBOPositions
. Também precisamos copiar as cores para m_VBOColors
, mas nesse caso não precisamos criar um novo arranjo pois já temos m_colors
definido como membro de Window
:
// Create array of random vertex positions
std::uniform_real_distribution rd(-1.5f, 1.5f);
std::array<glm::vec2, 3> const positions{
{{rd(m_randomEngine), rd(m_randomEngine)},
{rd(m_randomEngine), rd(m_randomEngine)},
{rd(m_randomEngine), rd(m_randomEngine)}}};
As coordenadas das posições dos vértices são valores float
pseudoaleatórios no intervalo \([-1.5, 1.5)\). Vimos no projeto anterior que, para uma primitiva ser vista no viewport, ela precisa ser especificada entre \([-1, -1]\) e \([1, 1]\). Logo, nossos triângulos terão partes que ficarão para fora da janela. O pipeline se encarregará de recortar os triângulos e mostrar apenas os fragmentos que estão dentro do viewport.
Nas linhas 107 a 119 são criados os VBOs (um para as posições 2D, outro para as cores RGBA):
// Generate VBO of positions
abcg::glGenBuffers(1, &m_VBOPositions);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOPositions);
abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions.data(),
GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Generate VBO of colors
abcg::glGenBuffers(1, &m_VBOColors);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOColors);
abcg::glBufferData(GL_ARRAY_BUFFER, sizeof(m_colors), m_colors.data(),
GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenBuffers
cria o identificador de um objeto de buffer (buffer object). Um objeto de buffer é um arranjo de dados alocado pelo OpenGL, geralmente na memória da GPU.glBindBuffer
com o argumentoGL_ARRAY_BUFFER
vincula o objeto de buffer a um buffer de atributos de vértices. Isso define o objeto de buffer como um objeto de buffer de vértice (VBO). O objeto de buffer pode ser desvinculado comglBindBuffer(0)
, ou vinculando outro objeto de buffer.glBufferData
aloca a memória e inicializa o buffer com o conteúdo copiado de um ponteiro alocado na CPU. O primeiro parâmetro indica o tipo de objeto de buffer utilizado. O segundo parâmetro é o tamanho do buffer em bytes. O terceiro parâmetro é um ponteiro para o primeiro elemento do arranjo contendo os dados que serão copiados. O quarto parâmetro é uma “dica” fornecida ao driver de como o buffer será utilizado. Essas dicas podem ser:GL_STATIC_DRAW
significa que o buffer será modificado apenas uma vez, mas utilizado muitas vezes.GL_STREAM_DRAW
significa que o buffer será modificado apenas uma vez e utilizado algumas poucas vezes.GL_DYNAMIC_DRAW
significa que o buffer será modificado repetidamente, e utilizado também muitas vezes. As modificações do buffer podem ser feitas com comandos comoglBufferData
,glBufferSubData
eglMapBuffer
.
O sufixo
DRAW
nesses identificadores significa que os dados são escritos pela aplicação e utilizados em um comando de desenho. Além doDRAW
é possível usar o sufixoREAD
(os dados são escritos pela GPU e lidos pela aplicação) eCOPY
(os dados são escritos pela GPU e utilizados em um comando de desenho).
Após a cópia dos dados com o glBufferData
, o arranjo do qual os dados foram copiados não é mais necessário e pode ser destruído. No nosso código, positions
é um arranjo alocado na pilha e portanto é liberado no fim do escopo da função.
As linhas 121 a 124 usam glGetAttribLocation
para obter o número de localização de cada atributo de entrada do vertex shader de m_program
:
// Get location of attributes in the program
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
auto const colorAttribute{abcg::glGetAttribLocation(m_program, "inColor")};
O resultado de positionAttribute
será 0, pois o vertex shader define inPosition
com layout(location = 0)
. Da mesma forma, colorAttribute
será 1, pois o vertex shader define inColor
com layout(location = 1)
. Poderíamos omitir esse código e escrever os valores 0 e 1 diretamente no código como valores hard-coded, mas é recomendável fazer a consulta da localização com glGetAttribLocation
.
Agora que sabemos a localização dos atributos inPosition
e inColor
no vertex shader, podemos especificar como os dados de cada VBO serão mapeados para esses atributos. Isso é feito nas linhas 126 a 145 a seguir:
// 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_VBOPositions);
abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
abcg::glEnableVertexAttribArray(colorAttribute);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBOColors);
abcg::glVertexAttribPointer(colorAttribute, 4, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// End of binding to current VAO
abcg::glBindVertexArray(0);
Na linha 127, glGenVertexArray
cria um VAO que, como vimos no projeto sierpinski
, armazena o estado da especificação de vinculação dos VBOs com o vertex shader. Neste projeto, essa especificação é feita nas linhas 132 a 142.
Em Window::onPaint
, quando vinculamos esse VAO com glBindVertexArray
, o estado da configuração dos VBOs com o programa de shader é recuperado automaticamente (isto é, a configuração feita nas linhas 132 a 142). Na nossa aplicação isso não é incrivelmente útil: as linhas 132 a 142 já são executadas para todo quadro de exibição, pois chamamos Window::setupModel
logo antes de glDrawArrays
. Mas, em aplicações futuras, chamaremos Window::setupModel
apenas uma vez (por exemplo, em Window::onCreate
). Geralmente, o modelo geométrico é definido apenas uma vez e nunca mais é alterado (ou é raramente alterado). Nesse caso, o VAO é útil para que não tenhamos de configurar manualmente a ligação dos VBOs com os atributos do vertex shader para todo quadro de exibição.
Ajustando a taxa de atualização
Neste momento, se compilarmos e executarmos a aplicação, perceberemos que os triângulos coloridos estão sendo gerados muito rapidamente. Isso ocorre porque um novo triângulo é desenhado a cada chamada a Window::onPaint
, na maior taxa possível que a GPU consegue suportar. Geralmente isso é muito acima da taxa de atualização do monitor. Uma tentativa de solucionar isso é habilitar o modo Vsync (sincronização vertical) através da seguinte modificação em main.cpp
:
Com a sincronização vertical ativada, as chamadas de Window::onPaint
serão sincronizadas com a taxa de atualização do monitor, geralmente 60 Hz. O problema é que essa solução muito provavelmente não funcionará em nossa aplicação. Muitos drivers de vídeo só permitem habilitar o modo Vsync quando o double buffering está habilitado, o que não é nosso caso. Precisamos de outra estratégia para ajustar a velocidade da geração de novos triângulos. Uma solução é usar um temporizador e desenhar um novo triângulo apenas se um intervalo de tempo tiver decorrido desde o desenho do último triângulo. É isso que faremos a seguir.
Adicione abcg::Timer m_timer
como variável membro de Window
. abcg::Timer
é uma classe da ABCg que implementa um temporizador usando std::chrono
, da biblioteca padrão do C++. Quando o objeto m_timer
é criado, o horário de sua inicialização é armazenado internamente. Podemos chamar m_timer.elapsed()
sempre que quisermos saber quantos segundos se passaram desde o início do temporizador. Podemos também chamar m_timer.restart()
para reiniciar o temporizador.
Modifique Window::onPaint
com o seguinte código:
void Window::onPaint() {
// Check whether to render the next triangle
if (m_timer.elapsed() < 1.0 / 20)
return;
m_timer.restart();
setupModel();
abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);
abcg::glUseProgram(m_program);
abcg::glBindVertexArray(m_VAO);
abcg::glDrawArrays(GL_TRIANGLES, 0, 3);
abcg::glBindVertexArray(0);
abcg::glUseProgram(0);
}
Na linha 41, verificamos se o temporizador já passou de 0.05 segundos (1.0 / 20
). Se ainda não, precisamos aguardar mais algum tempo antes de desenhar um novo triângulo, e por isso retornamos na linha 42. Em algum momento, depois de algumas chamadas de Window::onPaint
, o temporizador terá passado 0.05 segundos. Neste caso, reiniciamos o temporizador na linha 43 e continuamos com a execução das instruções do restante do código para desenhar um novo triângulo. Desse modo, a taxa de geração de triângulos será fixada em 20 por segundo.
Execute o código novamente e veja o resultado.
O projeto completo pode ser baixado deste link.
Habilite o modo de mistura de cor usando o código mostrado na seção 4.3. Inclua o código a seguir em Window::onCreate
:
::glEnable(GL_BLEND);
abcg::glBlendEquation(GL_FUNC_ADD);
abcg::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); abcg
Além disso, mude a componente A (alpha) das cores RGBA de m_colors
. Por exemplo, com a definição a seguir, os triângulos ficarão 50% transparentes:
std::array<glm::vec4, 3> m_colors{{{0.36f, 0.83f, 1.00f, 0.5f},
{0.63f, 0.00f, 0.61f, 0.5f},
{1.00f, 0.69f, 0.30f, 0.5f}}};
Modifique o projeto coloredtriangles
para suportar novas funcionalidades:
- Geração de cores aleatórias nos vértices;
- Possibilidade de desenhar cada triângulo com uma cor sólida;
- Ajuste do intervalo de tempo entre a renderização de cada triângulo.
Um exemplo é dado a seguir:
O conceito de coordenadas homogêneas será abordado futuramente, quando trabalharmos com transformações geométricas 3D.↩︎
Em geral, destruir e criar os VBOs a cada quadro de exibição não é muito eficiente. É preferível criar o VBO apenas uma vez e, se necessário, modificá-lo com
glBufferData
a cada quadro. Em nossa aplicação, optamos por chamarsetupModel
a cadaWindow::onPaint
apenas para manter o código mais simples.↩︎