5.1 Polígonos regulares

Este projeto é um aprimoramento do projeto coloredtriangles da seção 4.4. No lugar de desenharmos triângulos (GL_TRIANGLES), desenharemos polígonos regulares 2D formados por leques de triângulos (GL_TRIANGLE_FAN). Para cada quadro de exibição, será renderizado um polígono regular colorido em uma posição aleatória do viewport. O número de lados de cada polígono também será escolhido aleatoriamente. A aplicação ficará como a seguir:

Configuração inicial

A configuração inicial é a mesma dos projetos anteriores. Apenas mude o nome do projeto para regularpolygons e inclua a linha add_subdirectory(regularpolygons) em abcg/examples/CMakeLists.txt.

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

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

Este projeto também terá os arquivos main.cpp, openglwindow.cpp e openglwindow.hpp.

main.cpp

O conteúdo de main.cpp é praticamente idêntico ao do projeto coloredtriangles:

#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, .title = "Regular Polygons"});

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

openglwindow.hpp

Aqui também há poucas mudanças em relação ao projeto anterior:

#ifndef OPENGLWINDOW_HPP_
#define OPENGLWINDOW_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_vboPositions{};
  GLuint m_vboColors{};
  GLuint m_program{};

  int m_viewportWidth{};
  int m_viewportHeight{};

  std::default_random_engine m_randomEngine;

  int m_delay{200};
  abcg::ElapsedTimer m_elapsedTimer;

  void setupModel(int sides);
};

#endif

Observe que há novamente dois VBOs: um para a posição e outro para a cor dos vértices (linhas 18 e 19).

Na linha 27, a variável m_delay é utilizada para especificar o intervalo de tempo, em milissegundos, entre a renderização dos polígonos.

Na linha 28, m_elapsedTimer, da classe abcg::ElapsedTimer, é um temporizador simples usando funções da biblioteca std::chrono. A contagem de tempo inicia quando o objeto é criado. Só há duas funções membro disponíveis:

  • double abcg::ElapsedTimer::elapsed() retorna o tempo, em segundos, desde a criação do objeto, ou desde a última chamada a abcg::ElapsedTimer::restart();
  • double abcg::ElapsedTimer::restart() reinicia a contagem de tempo.

Usaremos m_elapsedTimer junto com m_delay para definir a frequência de desenho dos polígonos.

openglwindow.cpp

Antes de qualquer coisa, vamos incluir os seguintes arquivos de cabeçalho:

#include "openglwindow.hpp"

#include <imgui.h>

#include <cppitertools/itertools.hpp>

#include "abcg.hpp"

A definição de OpenGLWindow::initializeGL é a mesma do projeto coloredtriangles. Apenas o conteúdo do vertex shader será modificado. A definição completa fica assim:

void OpenGLWindow::initializeGL() {
  const auto *vertexShader{R"gl(
    #version 410

    layout(location = 0) in vec2 inPosition;
    layout(location = 1) in vec4 inColor;

    uniform vec2 translation;
    uniform float scale;

    out vec4 fragColor;

    void main() {
      vec2 newPosition = inPosition * scale + translation;
      gl_Position = vec4(newPosition, 0, 1);
      fragColor = inColor;
    }
  )gl"};

  const auto *fragmentShader{R"gl(
    #version 410

    in vec4 fragColor;

    out vec4 outColor;

    void main() { outColor = fragColor; }
  )gl"};

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

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

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

Compare o código do vertex shader na string vertexShader com o vertex shader do projeto anterior. No projeto anterior (coloredtriangles), o vertex shader estava assim:

#version 410

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

Agora o vertex shader ficará assim:

#version 410

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec4 inColor;

uniform vec2 translation;
uniform float scale;

out vec4 fragColor;

void main() {
  vec2 newPosition = inPosition * scale + translation;
  gl_Position = vec4(newPosition, 0, 1);
  fragColor = inColor;
}

A principal mudança é o uso de duas variáveis uniformes, identificadas pela palavra-chave uniform. São elas:

  • translation: um fator de translação (deslocamento) da geometria;
  • scale: um fator de escala da geometria.

O conteúdo de translation e scale é definido em paintGL antes de cada renderização. Lembre-se que variáveis uniformes são variáveis globais do shader que não mudam de valor de um vértice para outro, ao contrário do que ocorre com as variáveis inPosition e inColor.

Observe o conteúdo da função main:

void main() {
  vec2 newPosition = inPosition * scale + translation;
  gl_Position = vec4(newPosition, 0, 1);
  fragColor = inColor;
}

A posição original do vértice (inPosition) é multiplicada por scale e somada com translation para gerar uma nova posição (newPosition), que é a posição final do vértice passada para gl_Position.

Na expressão inPosition * scale + translation, inPosition * scale resulta na aplicação do fator de escala nas coordenadas \(x\) e \(y\) do vértice. Como isso é feito para cada vértice da geometria, o resultado será a mudança do tamanho do objeto. Se o fator de escala for 1, não haverá mudança de escala. Se for 0.5, o tamanho do objeto será reduzido pela metade em \(x\) e em \(y\). Se for 2.0, o tamanho será dobrado em \(x\) e em \(y\).

O resultado de inPosition * scale é somado com translation. Isso significa que, após a mudança de escala, a geometria será deslocada pelas coordenadas \((x,y)\) da translação.

Ao aplicar a escala e a translação do vertex shader, podemos usar um mesmo VBO para renderizar o objeto em posições e escalas diferentes, como mostra a figura 5.1:

Resultado da transformação dos vértices de um triângulo usando diferentes fatores de escala e translação.

Figura 5.1: Resultado da transformação dos vértices de um triângulo usando diferentes fatores de escala e translação.

Observação

O uso de variáveis uniformes e transformações geométricas no vertex shader pode reduzir em muito o consumo de memória dos dados gráficos.

Suponha que queremos renderizar uma cena estilo Minecraft composta por 100.000 cubos. A estratégia mais ingênua para renderizar essa cena é criar um único VBO contendo os vértices dos 100.000 cubos. Se usarmos GL_TRIANGLES, cada lado do cubo terá de ser renderizado como 2 triângulos, isto é, precisaremos de 6 vértices. Como um cubo tem 6 lados, teremos então 36 vértices por cubo. Logo, nosso VBO de 100.000 cubos terá 3.600.000 vértices22.

Ao usar variáveis uniformes, podemos criar um VBO para apenas um cubo e renderizar esse cubo 100.000 vezes, cada um com um fator de escala e translação diferente. No fim, o número de vértices processados será igual, mas o uso de memória terá uma redução de 5 ordens de magnitude!

Vamos agora à definição de OpenGLWindow::paintGL():

void OpenGLWindow::paintGL() {
  // Check whether to render the next polygon
  if (m_elapsedTimer.elapsed() < m_delay / 1000.0) return;
  m_elapsedTimer.restart();

  // Create a regular polygon with a number of sides in the range [3,20]
  std::uniform_int_distribution<int> intDist(3, 20);
  const auto sides{intDist(m_randomEngine)};
  setupModel(sides);

  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  abcg::glUseProgram(m_program);

  // Choose a random xy position from (-1,-1) to (1,1)
  std::uniform_real_distribution<float> rd1(-1.0f, 1.0f);
  const glm::vec2 translation{rd1(m_randomEngine), rd1(m_randomEngine)};
  const GLint translationLocation{
      abcg::glGetUniformLocation(m_program, "translation")};
  abcg::glUniform2fv(translationLocation, 1, &translation.x);

  // Choose a random scale factor (1% to 25%)
  std::uniform_real_distribution<float> rd2(0.01f, 0.25f);
  const auto scale{rd2(m_randomEngine)};
  const GLint scaleLocation{abcg::glGetUniformLocation(m_program, "scale")};
  abcg::glUniform1f(scaleLocation, scale);

  // Render
  abcg::glBindVertexArray(m_vao);
  abcg::glDrawArrays(GL_TRIANGLE_FAN, 0, sides + 2);
  abcg::glBindVertexArray(0);

  abcg::glUseProgram(0);
}

Na linha 52, o tempo contado por m_elapsedTimer é comparado com m_delay. Se o tempo ainda não atingiu m_delay, a função retorna. Caso contrário, o temporizador é reiniciado na linha 53 e a execução continua nas linhas seguintes.

Na linha 58, setupModel(sides) é chamada para criar o VBO de um polígono regular de sides lados. O número de lados é escolhido aletoriamente do intervalo \([3,20]\).

Nas linhas 64 a 75 são definidos os valores das variáveis uniformes do shader:

  // Choose a random xy position from (-1,-1) to (1,1)
  std::uniform_real_distribution<float> rd1(-1.0f, 1.0f);
  const glm::vec2 translation{rd1(m_randomEngine), rd1(m_randomEngine)};
  const GLint translationLocation{
      abcg::glGetUniformLocation(m_program, "translation")};
  abcg::glUniform2fv(translationLocation, 1, &translation.x);

  // Choose a random scale factor (1% to 25%)
  std::uniform_real_distribution<float> rd2(0.01f, 0.25f);
  const auto scale{rd2(m_randomEngine)};
  const GLint scaleLocation{abcg::glGetUniformLocation(m_program, "scale")};
  abcg::glUniform1f(scaleLocation, scale);

Na linha 66, translation contém coordenadas 2D aleatórias no intervalo \([-1,1]\). Na linha 73, scale é um fator de escala aleatório no intervalo \([0.01, 0.25]\).

Nas linhas 67 e 74, translationLocation e scaleLocation contêm os identificadores de localização das variáveis uniformes do shader. Esse valores são obtidos com glGetUniformLocation passando o identificador do programa de shader como primeiro argumento (m_program) e uma string com o nome da variável uniforme como segundo argumento.

A atribuição dos valores das variáveis uniformes é feita nas linhas 69 e 75. As funções glUniform* têm como primeiro parâmetro a localização da variável uniforme que será modificada, seguida de uma lista de parâmetros que depende do sufixo no fim de glUniform:

  • Em glUniform2fv, 2fv significa que a variável uniforme é um arranjo de tuplas de dois valores float, isto é, um arranjo de vec2. Nesse caso, o segundo argumento é a quantidade de vec2 que serão copiados. O argumento é 1 porque translation não é apenas um vec2. O terceiro argumento é o endereço do primeiro elemento do conjunto de dados que serão copiados.
  • Em glUniform1f, 1f significa que a variável uniforme é apenas um valor float. Nesse caso, o segundo argumento é simplesmente o valor float que será copiado.
Observação

O formato geral de glUniform é glUniform{1|2|3|4}{f|i|ui}[v]:

  • {1|2|3|4} define o número de componentes do tipo de dado:
    • 1 para float, int, unsigned int e bool;
    • 2 para vec2, ivec2, uvec2, bvec2;
    • 3 para vec3, ivec3, uvec3, bvec3;
    • 4 para vec4, ivec4, uvec4, bvec4.
  • {f|i|ui} define o tipo de dado de cada componente:
    • f para float, vec2, vec3, vec4;
    • i para int, ivec2, ivec3, ivec4;
    • ui para unsigned int, uvec2, uvec3, uvec4.

Tanto f, i e ui podem ser usados para copiar dados para variáveis uniformes booleanas (bool, bvec2, bvec3, bvec4). Nesse caso, true é qualquer valor diferente de zero.

Se o v final não é especificado, então {1|2|3|4} é também o número de parâmetros após o identificador de localização. Por exemplo:

// Variável uniform é um float ou bool
glUniform1f(loc, 3.14f);        

// Variável uniform é um unsigned int ou bool
glUniform1ui(loc, 42);

// Variável uniform é um vec2 ou bvec2
glUniform2f(loc, 0.0f, 10.5f);

// Variável uniform é um ivec4 ou bvec4
glUniform4i(loc, -1, 2, 10, 3); 

Se o v é especificado, o segundo parâmetro é o número de elementos do arranjo, e o terceiro parâmetro é o ponteiro para os dados. Por exemplo:

// Variável uniform é um float ou bool
float pi{3.14f};
glUniform1fv(loc, 1, &pi);

// Variável uniform é um unsigned int ou bool
unsigned int answer{42};
glUniform1uiv(loc, 1, &answer);

// Variável uniform é um vec2 ou bvec2
glm::vec2 foo{0.0f, 10.5f};
glUniform2fv(loc, 1, &foo.x);

// Variável uniform é um ivec4[2] ou bvec4[2]
std::array bar{glm::ivec4{-1, 2, 10, 3},
               glm::ivec4{7, -5, 1, 90}};
glUniform4iv(loc, 2, &bar.at(0).x); 

Nas linhas 77 a 80 temos a chamada à função de renderização:

  // Render
  abcg::glBindVertexArray(m_vao);
  abcg::glDrawArrays(GL_TRIANGLE_FAN, 0, sides + 2);
  abcg::glBindVertexArray(0);

O VAO é vinculado na linha 78 e automaticamente ativa e configura a ligação dos VBOs com o programa de shader. O comando de renderização é chamado na linha 79. Observe o uso da constante GL_TRIANGLE_FAN. O número de vértices é sides + 2 porque vamos definir nossos polígonos de tal modo que o número de vértices será sempre o número de lados mais dois, como mostra a figura 5.2 para a definição de um pentágono:

Pentágono formado por um leque de sete vértices.

Figura 5.2: Pentágono formado por um leque de sete vértices.

No pentágono, o vértice de índice 6 tem a mesma posição do vértice de índice 1 para “fechar” o leque de triângulos. Na verdade, o leque poderia definir um pentágono com apenas cinco vértices, como mostra a figura 5.3:

Pentágono formado por um leque de cinco vértices.

Figura 5.3: Pentágono formado por um leque de cinco vértices.

A escolha de manter o vértice de índice 0 no centro é proposital pois permite simular um efeito de gradiente de cor parecido com um gradiente radial. Para isto, basta atribuir uma cor ao vértice 0, e outra cor aos demais vértices. Como os atributos dos vértices são interpolados linearmente pelo rasterizador para cada fragmento gerado, o resultado será um gradiente de cor. A figura 5.4 mostra um exemplo usando amarelo no vértice central e azul nos demais vértices:

Pentágono com gradiente de cor formado através da interpolação do atributo de cor dos vértices.

Figura 5.4: Pentágono com gradiente de cor formado através da interpolação do atributo de cor dos vértices.

Continuando com a definição das funções membro de OpenGLWindow, definiremos OpenGLWindow::paintUI() usando o código a seguir. Ele é bem parecido com o do projeto anterior. A diferença é que, no lugar de ImGui::ColorEdit3, criaremos um slider para controlar o valor de m_delay e criaremos um botão para limpar a janela:

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

  {
    const auto widgetSize{ImVec2(200, 72)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportWidth - widgetSize.x - 5,
                                   m_viewportHeight - widgetSize.y - 5));
    ImGui::SetNextWindowSize(widgetSize);
    const auto windowFlags{ImGuiWindowFlags_NoResize |
                           ImGuiWindowFlags_NoCollapse |
                           ImGuiWindowFlags_NoTitleBar};
    ImGui::Begin(" ", nullptr, windowFlags);

    ImGui::PushItemWidth(140);
    ImGui::SliderInt("Delay", &m_delay, 0, 200, "%d ms");
    ImGui::PopItemWidth();

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

    ImGui::End();
  }
}

A definição de OpenGLWindow::resizeGL e OpenGLWindow::terminateGL é idêntica à do projeto coloredtriangles.

Vamos agora à definição da função membro OpenGLWindow::setupModel. O código completo é mostrado abaixo, mas analisaremos cada trecho em seguida:

void OpenGLWindow::setupModel(int sides) {
  // Release previous resources, if any
  abcg::glDeleteBuffers(1, &m_vboPositions);
  abcg::glDeleteBuffers(1, &m_vboColors);
  abcg::glDeleteVertexArrays(1, &m_vao);

  // Select random colors for the radial gradient
  std::uniform_real_distribution<float> rd(0.0f, 1.0f);
  const glm::vec3 color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  const glm::vec3 color2{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};

  // Minimum number of sides is 3
  sides = std::max(3, sides);

  std::vector<glm::vec2> positions(0);
  std::vector<glm::vec3> colors(0);

  // Polygon center
  positions.emplace_back(0, 0);
  colors.push_back(color1);

  // Border vertices
  const auto step{M_PI * 2 / sides};
  for (const auto angle : iter::range(0.0, M_PI * 2, step)) {
    positions.emplace_back(std::cos(angle), std::sin(angle));
    colors.push_back(color2);
  }

  // Duplicate second vertex
  positions.push_back(positions.at(1));
  colors.push_back(color2);

  // Generate VBO of positions
  abcg::glGenBuffers(1, &m_vboPositions);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboPositions);
  abcg::glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(glm::vec2),
                     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, colors.size() * sizeof(glm::vec3),
                     colors.data(), GL_STATIC_DRAW);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // Get location of attributes in the program
  const auto positionAttribute{
      abcg::glGetAttribLocation(m_program, "inPosition")};
  const auto 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, 3, GL_FLOAT, GL_FALSE, 0,
                              nullptr);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

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

No início da função, os VBOs e o VAO são liberados caso tenham sido criados anteriormente:

// Release previous resources, if any
  abcg::glDeleteBuffers(1, &m_vboPositions);
  abcg::glDeleteBuffers(1, &m_vboColors);
  abcg::glDeleteVertexArrays(1, &m_vao);

Em seguida temos o código que cria os vértices do polígono regular (arranjos positions e colors):

  // Select random colors for the radial gradient
  std::uniform_real_distribution<float> rd(0.0f, 1.0f);
  const glm::vec3 color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  const glm::vec3 color2{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};

  // Minimum number of sides is 3
  sides = std::max(3, sides);

  std::vector<glm::vec2> positions(0);
  std::vector<glm::vec3> colors(0);

  // Polygon center
  positions.emplace_back(0, 0);
  colors.push_back(color1);

  // Border vertices
  const auto step{M_PI * 2 / sides};
  for (const auto angle : iter::range(0.0, M_PI * 2, step)) {
    positions.emplace_back(std::cos(angle), std::sin(angle));
    colors.push_back(color2);
  }

  // Duplicate second vertex
  positions.push_back(positions.at(1));
  colors.push_back(color2);

Duas cores RGB são sorteadas nas linhas 132 e 134. color1 é utilizada na definição do vértice do centro (linhas 144 e 145), e color2 é utilizada para os demais vértices.

Nas linhas 148 a 152, a posição dos vértices é calculada com a equação paramétrica de um círculo unitário:

\[ \begin{eqnarray} x&=&cos(t),\\ y&=&sin(t), \end{eqnarray} \]

onde \(t\) é o ângulo (angle) que varia de \(0\) a \(2\pi\) usando um tamanho do passo (step) igual à divisão de \(2\pi\) pelo número de lados do polígono.

A definição dos VBOs é semelhante à forma utilizada no projeto anterior. Nas linhas 183 a 193 é definido como os dados dos VBOs serão mapeados para a entrada do vertex shader. Vamos nos concentrar na definição do mapeamento de m_vboPositions (o mapeamento de m_vboColors é similar):

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

Na linha 183, glEnableVertexAttribArray habilita o atributo de posição do vértice (inPosition) para ser utilizado durante a renderização.

Em seguida, glBindBuffer vincula o VBO m_vboPositions, que contém os dados das posições dos vértices.

Na linha 185, glVertexAttribPointer define como os dados do VBO serão mapeados para o atributo. Lembre-se que o VBO é apenas um arranjo linear de bytes copiados pela função glBufferData. Com glVertexAttribPointer, informamos ao OpenGL como esses bytes devem ser mapeados para uma variável de atributo de entrada do vertex shader. A assinatura de glVertexAttribPointer é a seguinte:

void glVertexAttribPointer(GLuint index, 
                           GLint size,
                           GLenum type,
                           GLboolean normalized,
                           GLsizei stride,
                           const void * pointer);

Os parâmetros são descritos a seguir:

  1. index: índice do atributo que será modificado. No nosso caso (linha 180) é positionAttribute.
  2. size: número de componentes do atributo. No nosso caso é 2 pois inPosition é um vec2, isto é, um atributo de dois componentes.
  3. type: tipo de dado de cada valor do VBO. Usamos GL_FLOAT pois cada coordenada \(x\) e \(y\) do VBO de posições é um float.
  4. normalized: flag que indica se valores inteiros devem ser normalizados para \([-1,1]\) (para valores com sinal) ou \([0,1]\) (para valores sem sinal) quando forem enviados ao atributo. Usamos GL_FALSE porque nossas coordenadas são valores do tipo float;
  5. stride: é o número de bytes entre o início do atributo de um vértice e o início do atributo do próximo vértice. O argumento 0 indica que não há bytes extras entre uma posição \((x,y)\) e a posição \((x,y)\) do vértice seguinte.
  6. pointer: apesar do nome, não é um ponteiro, mas um deslocamento em bytes que informa qual é a posição do primeiro componente do atributo. Usamos nullptr, que corresponde a zero, pois não há bytes extras no início do VBO antes da primeira posição \((x,y)\).
Observação

Os parâmetros stride e pointer de glVertexAttribPointer podem ser utilizados para especificar o mapeamento de VBOs que contém dados intercalados (interleaved data).

Nosso m_vboPositions não usa dados intercalados. O arranjo contém apenas posições \((x,y)\) em sequência. Assim, para um triângulo (três vértices), o VBO é um arranjo no formato:

\[[x\; y\; x\; y\; x\; y],\]

onde cada grupo de \((x, y)\) é a posição de um vértice, e tanto \(x\) quanto \(y\) são do tipo float.

Da mesma forma, m_vboColors não usa dados intercalados. Para a definição das cores dos vértices de um triângulo, o arranjo tem o formato:

\[[r\; g\; b\; r\; g\; b\; r\; g\; b],\]

onde cada grupo de \((r,g,b)\) define a cor de um vértice, e \(r\), \(g\) e \(b\) também são do tipo float.

Quando os dados não são intercalados, podemos especificar 0 como argumento de stride, que é o que fizemos. Além disso, pointer também é 0.

Suponha agora que os dados tenham sido intercalados em um único VBO no seguinte formato:

\[[x\; y\; r\; g\; b\; x\; y\; r\; g\; b\; x\; y\; r\; g\; b].\]

Agora, o atributo de posição \((x,y)\) tem um stride que corresponde à quantidade de bytes contida em \((x,y,r,g,b)\). Esse valor é 20 se cada float tiver 4 bytes (5*4=20 bytes). pointer continua sendo 0, pois não há deslocamento no início do arranjo.

O atributo de cor \((r,g,b)\) também tem um stride de 20 bytes. Entretanto, pointer precisa ser 8, pois \(x\) e \(y\) formam 8 bytes antes do início do primeiro grupo de \((r,g,b)\).

Suponha agora um único VBO no formato a seguir:

\[[x\; y\; x\; y\; x\; y\; r\; g\; b\; r\; g\; b\; r\; g\; b].\]

O stride da posição pode ser 0, pois após um grupo de \((x,y)\) há imediatamente outro \((x,y)\)23. O stride da cor também pode ser 0 pelo mesmo raciocínio. Entretanto, o pointer para o atributo de cor precisa ser 24 (8*3=24 bytes), pois o primeiro grupo de \((r,g,b)\) ocorre apenas depois de três grupos de \((x,y)\).

Com todas essas opções de formatação de VBOs, não há uma forma mais certa ou mais recomendada de organizar os dados. É possível que algum driver use algum formato de forma mais eficiente, mas isso só pode ser determinado através de medição de tempo. Na prática, use o formato que melhor fizer sentido para o caso de uso.

Para simplificar, fizemos as contas supondo 4 bytes por float, mas lembre-se sempre de usar sizeof(float) pois o tamanho de um float pode variar dependendo da arquitetura.

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

#include "openglwindow.hpp"

#include <imgui.h>

#include <cppitertools/itertools.hpp>

#include "abcg.hpp"

void OpenGLWindow::initializeGL() {
  const auto *vertexShader{R"gl(
    #version 410

    layout(location = 0) in vec2 inPosition;
    layout(location = 1) in vec4 inColor;

    uniform vec2 translation;
    uniform float scale;

    out vec4 fragColor;

    void main() {
      vec2 newPosition = inPosition * scale + translation;
      gl_Position = vec4(newPosition, 0, 1);
      fragColor = inColor;
    }
  )gl"};

  const auto *fragmentShader{R"gl(
    #version 410

    in vec4 fragColor;

    out vec4 outColor;

    void main() { outColor = fragColor; }
  )gl"};

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

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

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

void OpenGLWindow::paintGL() {
  // Check whether to render the next polygon
  if (m_elapsedTimer.elapsed() < m_delay / 1000.0) return;
  m_elapsedTimer.restart();

  // Create a regular polygon with a number of sides in the range [3,20]
  std::uniform_int_distribution<int> intDist(3, 20);
  const auto sides{intDist(m_randomEngine)};
  setupModel(sides);

  abcg::glViewport(0, 0, m_viewportWidth, m_viewportHeight);

  abcg::glUseProgram(m_program);

  // Choose a random xy position from (-1,-1) to (1,1)
  std::uniform_real_distribution<float> rd1(-1.0f, 1.0f);
  const glm::vec2 translation{rd1(m_randomEngine), rd1(m_randomEngine)};
  const GLint translationLocation{
      abcg::glGetUniformLocation(m_program, "translation")};
  abcg::glUniform2fv(translationLocation, 1, &translation.x);

  // Choose a random scale factor (1% to 25%)
  std::uniform_real_distribution<float> rd2(0.01f, 0.25f);
  const auto scale{rd2(m_randomEngine)};
  const GLint scaleLocation{abcg::glGetUniformLocation(m_program, "scale")};
  abcg::glUniform1f(scaleLocation, scale);

  // Render
  abcg::glBindVertexArray(m_vao);
  abcg::glDrawArrays(GL_TRIANGLE_FAN, 0, sides + 2);
  abcg::glBindVertexArray(0);

  abcg::glUseProgram(0);
}

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

  {
    const auto widgetSize{ImVec2(200, 72)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportWidth - widgetSize.x - 5,
                                   m_viewportHeight - widgetSize.y - 5));
    ImGui::SetNextWindowSize(widgetSize);
    const auto windowFlags{ImGuiWindowFlags_NoResize |
                           ImGuiWindowFlags_NoCollapse |
                           ImGuiWindowFlags_NoTitleBar};
    ImGui::Begin(" ", nullptr, windowFlags);

    ImGui::PushItemWidth(140);
    ImGui::SliderInt("Delay", &m_delay, 0, 200, "%d ms");
    ImGui::PopItemWidth();

    if (ImGui::Button("Clear window", ImVec2(-1, 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() {
  abcg::glDeleteProgram(m_program);
  abcg::glDeleteBuffers(1, &m_vboPositions);
  abcg::glDeleteBuffers(1, &m_vboColors);
  abcg::glDeleteVertexArrays(1, &m_vao);
}

void OpenGLWindow::setupModel(int sides) {
  // Release previous resources, if any
  abcg::glDeleteBuffers(1, &m_vboPositions);
  abcg::glDeleteBuffers(1, &m_vboColors);
  abcg::glDeleteVertexArrays(1, &m_vao);

  // Select random colors for the radial gradient
  std::uniform_real_distribution<float> rd(0.0f, 1.0f);
  const glm::vec3 color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  const glm::vec3 color2{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};

  // Minimum number of sides is 3
  sides = std::max(3, sides);

  std::vector<glm::vec2> positions(0);
  std::vector<glm::vec3> colors(0);

  // Polygon center
  positions.emplace_back(0, 0);
  colors.push_back(color1);

  // Border vertices
  const auto step{M_PI * 2 / sides};
  for (const auto angle : iter::range(0.0, M_PI * 2, step)) {
    positions.emplace_back(std::cos(angle), std::sin(angle));
    colors.push_back(color2);
  }

  // Duplicate second vertex
  positions.push_back(positions.at(1));
  colors.push_back(color2);

  // Generate VBO of positions
  abcg::glGenBuffers(1, &m_vboPositions);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, m_vboPositions);
  abcg::glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(glm::vec2),
                     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, colors.size() * sizeof(glm::vec3),
                     colors.data(), GL_STATIC_DRAW);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

  // Get location of attributes in the program
  const auto positionAttribute{
      abcg::glGetAttribLocation(m_program, "inPosition")};
  const auto 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, 3, GL_FLOAT, GL_FALSE, 0,
                              nullptr);
  abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);

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

O código completo do projeto pode ser baixado deste link.

Agora que vimos como usar variáveis uniformes para fazer transformações geométricas no vertex shader e como organizar os dados de um VBO de diferentes maneiras, vamos ao jogo!


  1. Como veremos posteriomente, é possível reduzir esse número com o uso de geometria indexada, mas ainda assim o consumo de memória seria alto para este caso.↩︎

  2. Na verdade, o stride nesse caso é de 8 bytes, mas o argumento 0 serve para indicar que os atributos estão agrupados de forma “apertada.”↩︎