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 e 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 e em . Se for 2.0, o tamanho será dobrado em e em .
O resultado de inPosition * scale
é somado com translation
. Isso significa que, após a mudança de escala, a geometria será deslocada pelas coordenadas 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:
Figura 5.1: Resultado da transformação dos vértices de um triângulo usando diferentes fatores de escala e translaçã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 .
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 . Na linha 73, scale
é um fator de escala aleatório no intervalo .
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:
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:
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:
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:
onde é o ângulo (angle
) que varia de a usando um tamanho do passo (step
) igual à divisão de 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 e do VBO de posições é umfloat
.normalized
: flag que indica se valores inteiros devem ser normalizados para (para valores com sinal) ou (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 e a posição 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 .
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 em sequência. Assim, para um triângulo (três vértices), o VBO é um arranjo no formato:
onde cada grupo de é a posição de um vértice, e tanto quanto 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:
onde cada grupo de define a cor de um vértice, e , e 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:
Agora, o atributo de posição tem um stride que corresponde à quantidade de bytes contida em . 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 também tem um stride de 20 bytes. Entretanto, pointer
precisa ser 8
, pois e formam 8 bytes antes do início do primeiro grupo de .
Suponha agora um único VBO no formato a seguir:
O stride da posição pode ser 0
, pois após um grupo de há imediatamente outro 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 ocorre apenas depois de três grupos de .
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!