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 aabcg::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:
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 valoresfloat
, isto é, um arranjo devec2
. Nesse caso, o segundo argumento é a quantidade devec2
que serão copiados. O argumento é1
porquetranslation
não é apenas umvec2
. 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 valorfloat
. Nesse caso, o segundo argumento é simplesmente o valorfloat
que será copiado.
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
parafloat
,int
,unsigned int
ebool
;2
paravec2
,ivec2
,uvec2
,bvec2
;3
paravec3
,ivec3
,uvec3
,bvec3
;4
paravec4
,ivec4
,uvec4
,bvec4
.
{f|i|ui}
define o tipo de dado de cada componente:f
parafloat
,vec2
,vec3
,vec4
;i
paraint
,ivec2
,ivec3
,ivec4
;ui
paraunsigned 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
3.14f);
glUniform1f(loc,
// Variável uniform é um unsigned int ou bool
42);
glUniform1ui(loc,
// Variável uniform é um vec2 ou bvec2
0.0f, 10.5f);
glUniform2f(loc,
// Variável uniform é um ivec4 ou bvec4
1, 2, 10, 3); glUniform4i(loc, -
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};
1, &pi);
glUniform1fv(loc,
// Variável uniform é um unsigned int ou bool
unsigned int answer{42};
1, &answer);
glUniform1uiv(loc,
// Variável uniform é um vec2 ou bvec2
0.0f, 10.5f};
glm::vec2 foo{1, &foo.x);
glUniform2fv(loc,
// Variável uniform é um ivec4[2] ou bvec4[2]
std::array bar{glm::ivec4{-1, 2, 10, 3},
7, -5, 1, 90}};
glm::ivec4{2, &bar.at(0).x); glUniform4iv(loc,
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:
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:
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:
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:
index
: índice do atributo que será modificado. No nosso caso (linha 180) épositionAttribute
.size
: número de componentes do atributo. No nosso caso é2
poisinPosition
é umvec2
, isto é, um atributo de dois componentes.type
: tipo de dado de cada valor do VBO. UsamosGL_FLOAT
pois cada coordenada \(x\) e \(y\) do VBO de posições é umfloat
.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. UsamosGL_FALSE
porque nossas coordenadas são valores do tipofloat
;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 argumento0
indica que não há bytes extras entre uma posição \((x,y)\) e a posição \((x,y)\) do vértice seguinte.pointer
: apesar do nome, não é um ponteiro, mas um deslocamento em bytes que informa qual é a posição do primeiro componente do atributo. Usamosnullptr
, que corresponde a zero, pois não há bytes extras no início do VBO antes da primeira posição \((x,y)\).
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!