pbsa-nix/ExampleRenderer.cpp

318 lines
11 KiB
C++
Raw Normal View History

2024-04-16 20:34:15 +02:00
#include "ExampleRenderer.hpp"
#include <QDebug>
#include <QImage>
#include <QMouseEvent>
#include <Eigen/Dense>
#include <Eigen/Geometry>
#include <array>
#include <cmath>
#include "helpers.hpp"
ExampleRenderer::ExampleRenderer(QObject * parent)
: OpenGLRenderer{parent}
{
{
// create/bind a vertex array object to store bindings to array and element buffers
glBindVertexArray(this->icosphereVAO.id());
// build the required vertex and index arrays
std::vector<float> vertices(std::begin(icosahedronVertices), std::end(icosahedronVertices));
std::vector<unsigned> indices(std::begin(icosahedronIndices), std::end(icosahedronIndices));
for(auto k = 4; k--;)
subdivideIcosphere(vertices, indices);
// create/bind an array buffer object and fill it with the vertices we computed (GL_STATIC_DRAW, as this remains unmodified)
glBindBuffer(GL_ARRAY_BUFFER, this->icosphereVertexBuffer.id());
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(sizeof(vertices[0]) * vertices.size()), vertices.data(), GL_STATIC_DRAW);
// set vertex attribute 0 to use the current array buffer
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
// create/bind an element (index) buffer and fill it with the indices we computed (GL_STATIC_DRAW, as this remains unmodified)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->icosphereIndexBuffer.id());
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(sizeof(indices[0]) * indices.size()), indices.data(), GL_STATIC_DRAW);
// unbind the vertex array object as we are done modifying it
glBindVertexArray(0);
// remember the number of indices used (don't need the actual indices/vertices as these are on the GPU now)
this->numIcosphereIndices = static_cast<GLsizei>(indices.size());
}
{
// create temporary shader objects of the correct types
gl::Shader vertexShader{GL_VERTEX_SHADER};
gl::Shader fragmentShader{GL_FRAGMENT_SHADER};
std::vector<char> text;
// load and compile vertex shader
text = loadResource("shaders/icosphere.vert");
vertexShader.compile(text.data(), static_cast<GLint>(text.size()));
// load and compile fragment shader
text = loadResource("shaders/icosphere.frag");
fragmentShader.compile(text.data(), static_cast<GLint>(text.size()));
// link shader program and check for errors. shader objects are no longer required
if(!this->icosphereProgram.link(vertexShader, fragmentShader))
{
qDebug() << "Shader compilation failed:\n" << this->icosphereProgram.infoLog().get();
std::abort();
}
// set which vertex attribute index is bound to position
glBindAttribLocation(this->icosphereProgram.id(), 0, "position");
}
{
// create temporary shader objects of the correct types
gl::Shader vertexShader{GL_VERTEX_SHADER};
gl::Shader fragmentShader{GL_FRAGMENT_SHADER};
std::vector<char> text;
// load and compile vertex shader
text = loadResource("shaders/skybox.vert");
vertexShader.compile(text.data(), static_cast<GLint>(text.size()));
// load and compile fragment shader
text = loadResource("shaders/skybox.frag");
fragmentShader.compile(text.data(), static_cast<GLint>(text.size()));
// link shader program and check for errors. shader objects are no longer required
if(!this->skyboxProgram.link(vertexShader, fragmentShader))
{
qDebug() << "Shader compilation failed:\n" << this->skyboxProgram.infoLog().get();
std::abort();
}
}
{
// load a texture from a Qt resource
auto img = QImage(":/textures/earth_color.jpg").convertToFormat(QImage::Format_RGBA8888).mirrored();
// create/bind texture object
glBindTexture(GL_TEXTURE_2D, this->earthTexture.id());
// upload texture data
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, img.constBits());
// set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if(GLAD_GL_EXT_texture_filter_anisotropic)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 16);
// generate mipmaps (required for GL_TEXTURE_MIN_FILTER: GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D);
}
{
// load a texture from a Qt resource
auto img = QImage(":/textures/moon_color.jpg").convertToFormat(QImage::Format_RGBA8888).mirrored();
// create/bind texture object
glBindTexture(GL_TEXTURE_2D, this->moonTexture.id());
// upload texture data
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, img.constBits());
// set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if(GLAD_GL_EXT_texture_filter_anisotropic)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 16);
// generate mipmaps (required for GL_TEXTURE_MIN_FILTER: GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_2D);
}
{
// enable seamless cube map filtering
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
// create/bind cube map texture object
glBindTexture(GL_TEXTURE_CUBE_MAP, this->starsCubeMap.id());
// load and upload texture images to the individual faces of the cube map
for (auto nameAndTarget : std::array<std::pair<char const *, GLenum>, 6>{{
{":/textures/stars_px.jpg", GL_TEXTURE_CUBE_MAP_POSITIVE_X},
{":/textures/stars_py.jpg", GL_TEXTURE_CUBE_MAP_POSITIVE_Y},
{":/textures/stars_pz.jpg", GL_TEXTURE_CUBE_MAP_POSITIVE_Z},
{":/textures/stars_nx.jpg", GL_TEXTURE_CUBE_MAP_NEGATIVE_X},
{":/textures/stars_ny.jpg", GL_TEXTURE_CUBE_MAP_NEGATIVE_Y},
{":/textures/stars_nz.jpg", GL_TEXTURE_CUBE_MAP_NEGATIVE_Z}
}})
{
auto img = QImage(nameAndTarget.first).convertToFormat(QImage::Format_RGBA8888).mirrored();
glTexImage2D(nameAndTarget.second, 0, GL_SRGB8_ALPHA8, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, img.constBits());
}
// set texture parameters
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
if(GLAD_GL_EXT_texture_filter_anisotropic)
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_ANISOTROPY_EXT, 16);
// generate mipmaps (required for GL_TEXTURE_MIN_FILTER: GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
}
// enable depth tests
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glClearDepth(1.);
// enable back face culling
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
this->timer.start();
}
void ExampleRenderer::resize(int w, int h)
{
this->width = w;
this->height = h;
// update projection matrix to account for (potentially) changed aspect ratio
this->projectionMatrix = calculateInfinitePerspective(
0.78539816339744831, // 45 degrees in radians
static_cast<double>(w) / h,
0.01 // near plane (chosen "at random")
);
}
void ExampleRenderer::render()
{
auto currentTimeNS = this->timer.nsecsElapsed();
auto deltaTimeNS = currentTimeNS - this->lastTimeNS;
this->lastTimeNS = currentTimeNS;
int loc = -1;
// clear depth buffer
glClear(GL_DEPTH_BUFFER_BIT);
// recompute view matrix (camera position and direction) from azimuth/elevation
{
auto sa = std::sin(this->cameraAzimuth);
auto ca = std::cos(this->cameraAzimuth);
auto se = std::sin(this->cameraElevation);
auto ce = std::cos(this->cameraElevation);
auto distance = 4.;
this->viewMatrix = calculateLookAtMatrix(
distance * Eigen::Vector3d{se * ca, se * sa, ce},
{0, 0, 0},
{0, 0, 1}
);
}
// set the shader to be used
glUseProgram(this->icosphereProgram.id());
// set which texture unit is bound to colorTexture
loc = glGetUniformLocation(this->icosphereProgram.id(), "colorTexture");
glUniform1i(loc, 0);
// set projection matrix
loc = glGetUniformLocation(this->icosphereProgram.id(), "projection");
glUniformMatrix4fv(loc, 1, GL_FALSE, this->projectionMatrix.cast<float>().eval().data());
// bind the sphere VAO for drawing
glBindVertexArray(this->icosphereVAO.id());
{
auto tiltAngle = 23.4 * constants::deg_to_rad<double>;
Eigen::Affine3d earthModelTransform = Eigen::AngleAxisd{tiltAngle, Eigen::Vector3d::UnitY()} * Eigen::Affine3d::Identity();
// bind texture to unit 0
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(GL_TEXTURE_2D, this->earthTexture.id());
// set modelView matrix
loc = glGetUniformLocation(this->icosphereProgram.id(), "modelView");
glUniformMatrix4fv(loc, 1, GL_FALSE, (this->viewMatrix * earthModelTransform.matrix()).cast<float>().eval().data());
// draw the sphere's triangles as indexed elements (using unsigned int indices), VAO already bound
glDrawElements(GL_TRIANGLES, this->numIcosphereIndices, GL_UNSIGNED_INT, nullptr);
}
// set a different shader
glUseProgram(this->skyboxProgram.id());
// set which texture unit is bound to colorTexture (different program, so location may be different too!)
loc = glGetUniformLocation(this->skyboxProgram.id(), "colorTexture");
glUniform1i(loc, 0);
// bind cube map texture to unit 0 (unit 0 already active)
glBindTexture(GL_TEXTURE_CUBE_MAP, this->starsCubeMap.id());
// set modelView matrix
loc = glGetUniformLocation(this->skyboxProgram.id(), "modelView");
glUniformMatrix4fv(loc, 1, GL_FALSE, this->viewMatrix.cast<float>().eval().data());
// set projection matrix
loc = glGetUniformLocation(this->skyboxProgram.id(), "projection");
glUniformMatrix4fv(loc, 1, GL_FALSE, this->projectionMatrix.cast<float>().eval().data());
// draw the "skybox"'s triangle as a non-indexed array
glDepthFunc(GL_EQUAL);
glBindVertexArray(this->skyboxVAO.id());
glDrawArrays(GL_TRIANGLES, 0, 3);
glDepthFunc(GL_LESS);
}
void ExampleRenderer::mouseEvent(QMouseEvent * e)
{
auto type = e->type();
auto pos = e->position();
// begin rotation interaction
if(type == QEvent::MouseButtonPress && e->button() == Qt::LeftButton)
{
this->lastPos = pos;
this->rotateInteraction = true;
return;
}
// end rotation interaction
if(type == QEvent::MouseButtonRelease && e->button() == Qt::LeftButton)
{
this->rotateInteraction = false;
return;
}
// perform rotation interaction
if(this->rotateInteraction)
{
auto delta = pos - this->lastPos;
this->lastPos = pos;
// scale rotation depending on window diagonal
auto scale = constants::two_pi<double> / Eigen::Vector2d{this->width, this->height}.norm();
// modify azimuth and elevation by change in mouse position
cameraAzimuth -= scale * delta.x();
cameraAzimuth = std::fmod(cameraAzimuth, constants::two_pi<double>);
cameraElevation -= scale * delta.y();
// limit elevation so up is never collinear with camera view direction
cameraElevation = std::fmax(std::fmin(cameraElevation, constants::pi<double> - 0.01), 0.01);
// tell widget to update itself to account for changed position
this->update();
}
}