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 window.cpp)
enable_abcg(${PROJECT_NAME})

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

main.cpp

O conteúdo aqui é praticamente idêntico ao do projeto coloredtriangles. Continuaremos com o double buffering desabilitado de modo a desenhar cada novo polígono sobre o conteúdo anterior do framebuffer.

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

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

window.hpp

Aqui também há poucas mudanças em relação ao 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;

  abcg::Timer m_timer;
  int m_delay{200};

  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 20 e 21).

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

window.cpp

A definição de Window::onCreate é a mesma do projeto anterior, mas com o conteúdo do vertex shader modificado:

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

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

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

  m_randomEngine.seed(
      std::chrono::steady_clock::now().time_since_epoch().count());
}

No projeto coloredtriangles, o vertex shader estava assim:

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

Neste projeto o código está assim:

#version 300 es

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 onPaint antes da chamada do comando de 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 de 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\), e assim por diante.

O resultado de inPosition * scale é somado com translation. Isso significa que, após a mudança de escala, a geometria será deslocada no plano 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 significativamente o consumo de memória dos dados gráficos. Para citar um exemplo, suponha que queremos renderizar uma cena no estilo do jogo Minecraft composta por 100 mil cubos. A estratégia mais ingênua para renderizar essa cena é criar um único VBO contendo os vértices dos 100 mil cubos. Se usarmos GL_TRIANGLES, cada lado do cubo terá de ser renderizado como 2 triângulos, isto é, precisaremos de 6 vértices (3 vértices por triângulo). Como um cubo tem 6 lados, teremos então 36 vértices por cubo. Logo, nosso VBO de 100 mil cubos terá 3600000 vértices23.

Ao usar variáveis uniformes, podemos criar um VBO para apenas um cubo e renderizar esse cubo 100 mil 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 Window::onPaintGL:

void Window::onPaint() {
  if (m_timer.elapsed() < m_delay / 1000.0)
    return;
  m_timer.restart();

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

  abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);

  abcg::glUseProgram(m_program);

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

  // Pick a random scale factor (1% to 25%)
  std::uniform_real_distribution rd2(0.01f, 0.25f);
  auto const scale{rd2(m_randomEngine)};
  auto const 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 44, o tempo contado por m_timer é 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 46 e a execução continua nas linhas seguintes.

Na linha 51, 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 57 a 68 são definidos os valores das variáveis uniformes do shader:

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

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

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

Nas linhas 60 e 67, 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 o nome da variável uniforme como segundo argumento.

A atribuição dos valores das variáveis uniformes é feita nas linhas 62 e 68. 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 do tipo float, isto é, um arranjo de vec2. Nesse caso, o segundo parâmetro é a quantidade de vec2 que deverá ser copiada para a variável uniforme do shader. O argumento é 1 porque translation não é apenas um vec2. O terceiro parâmetro é o endereço do primeiro elemento do conjunto de dados que será copiado.
  • Em glUniform1f, 1f significa que a variável uniforme é apenas um valor do tipo float. Nesse caso, o segundo parâmetro é simplesmente o valor float.
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 71 a 73 temos a chamada ao comando de renderização:

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

O VAO é vinculado na linha 71. O resultado é a ativação e configuração da ligação dos VBOs com o programa de shader. O comando de renderização é chamado na linha 72. Observe o uso da constante GL_TRIANGLE_FAN. O número de vértices é sides + 2 porque definiremos 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 degradê 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 da classe Window, definiremos Window::onPaintUI usando o código a seguir. O código é 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 Window::onPaintUI() {
  abcg::OpenGLWindow::onPaintUI();

  {
    auto const widgetSize{ImVec2(200, 72)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportSize.x - widgetSize.x - 5,
                                   m_viewportSize.y - widgetSize.y - 5));
    ImGui::SetNextWindowSize(widgetSize);
    auto const 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 Window::onResize e Window::onDestroy é idêntica à do projeto anterior:

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 à definição da função Window::setupModel. O código completo é mostrado abaixo, mas analisaremos cada trecho em seguida:

void Window::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 rd(0.0f, 1.0f);
  glm::vec3 const color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  glm::vec3 const 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;
  std::vector<glm::vec3> colors;

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

  // Border vertices
  auto const step{M_PI * 2 / sides};
  for (auto const 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
  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, 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 rd(0.0f, 1.0f);
  glm::vec3 const color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  glm::vec3 const 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;
  std::vector<glm::vec3> colors;

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

  // Border vertices
  auto const step{M_PI * 2 / sides};
  for (auto const 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 124 e 126. color1 é utilizada na definição do vértice do centro (linhas 137), e color2 é utilizada para os demais vértices (linha 143).

Nas linhas 140 a 144, a posição dos vértices é calculada usando a equação paramétrica de um círculo unitário na origem:

\[ \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 175 a 185 é 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 175, 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 177, 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:

  1. index: índice do atributo que será modificado. No nosso caso (linha 175) é 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 no arranjo linear. O argumento 0 que utilizamos 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 a partir do início do arranjo linear, e que corresponde à 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 representa um arranjo no formato

\[[\underline{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 float24.

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:

\[[\underline{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:

\[[\underline{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 \(\times\) 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 (deslocamento a partir do início) precisa ser 8, pois \(x\) e \(y\) ocupam 8 bytes antes do início do primeiro grupo de \((r,g,b)\).

Suponha agora um único VBO no formato a seguir:

\[[\underline{x,\; y},\; \; x,\; y,\; \; x,\; y,\; \; \underline{r,\; g,\; b},\; \; r,\; g,\; b,\; \; r,\; g,\; b].\]

Nesse VBO, o stride da posição pode ser 0, pois após um grupo de \((x,y)\) há imediatamente outro \((x,y)\)25. O stride da cor também pode ser 0 pelo mesmo raciocínio. Entretanto, o pointer para o atributo de cor precisa ser 24 (3 vértices \(\times\) 8 bytes por vértices = 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 window.cpp é mostrado a seguir:

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

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

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

  m_randomEngine.seed(
      std::chrono::steady_clock::now().time_since_epoch().count());
}

void Window::onPaint() {
  if (m_timer.elapsed() < m_delay / 1000.0)
    return;
  m_timer.restart();

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

  abcg::glViewport(0, 0, m_viewportSize.x, m_viewportSize.y);

  abcg::glUseProgram(m_program);

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

  // Pick a random scale factor (1% to 25%)
  std::uniform_real_distribution rd2(0.01f, 0.25f);
  auto const scale{rd2(m_randomEngine)};
  auto const 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 Window::onPaintUI() {
  abcg::OpenGLWindow::onPaintUI();

  {
    auto const widgetSize{ImVec2(200, 72)};
    ImGui::SetNextWindowPos(ImVec2(m_viewportSize.x - widgetSize.x - 5,
                                   m_viewportSize.y - widgetSize.y - 5));
    ImGui::SetNextWindowSize(widgetSize);
    auto const 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 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);
}

void Window::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 rd(0.0f, 1.0f);
  glm::vec3 const color1{rd(m_randomEngine), rd(m_randomEngine),
                         rd(m_randomEngine)};
  glm::vec3 const 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;
  std::vector<glm::vec3> colors;

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

  // Border vertices
  auto const step{M_PI * 2 / sides};
  for (auto const 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
  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, 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. Os dados que correspondem ao primeiro vértice são sublinhados para facilitar a visualização. Nos exemplos subsequentes, os dados do primeiro vértice também são sublinhados.↩︎

  3. O stride nesse caso também pode ser 8 bytes (4 bytes para \(x\), mais 4 para \(y\)), mas o argumento 0 serve para indicar que os atributos estão agrupados de forma “apertada”.↩︎