diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index 3fed965..1dc3880 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -2,9 +2,15 @@ set(SRCS main.cpp Window.cpp Window.h + Scene/Scene.cpp + Scene/Scene.h + Scene/Camera.cpp + Scene/Camera.h Shaders/diffuse.fs Shaders/diffuse.vs + Shaders/model.fs + Shaders/model.vs Textures/voronoi.png resources.qrc diff --git a/src/App/Models/pallas_cat.glb b/src/App/Models/pallas_cat.glb new file mode 100644 index 0000000..e2ae0b4 Binary files /dev/null and b/src/App/Models/pallas_cat.glb differ diff --git a/src/App/Scene/Camera.cpp b/src/App/Scene/Camera.cpp new file mode 100644 index 0000000..3ebb4e9 --- /dev/null +++ b/src/App/Scene/Camera.cpp @@ -0,0 +1,60 @@ +#include +#include "Camera.h" + +Camera::Camera() +{ + updateVectors(); +} + +void Camera::setAspect(float aspect) +{ + aspect_ = aspect; +} + +void Camera::move(float forward, float right, float dt) +{ + const float v = speed_ * dt; + position_ += front_ * forward * v; + position_ += right_ * right * v; +} + +void Camera::rotate(float dx, float dy) +{ + dx *= sensitivity_; + dy *= sensitivity_; + + yaw_ += dx; + pitch_ += dy; + + pitch_ = qBound(-89.0f, pitch_, 89.0f); + updateVectors(); +} + +void Camera::updateVectors() +{ + const float yawRad = qDegreesToRadians(yaw_); + const float pitchRad = qDegreesToRadians(pitch_); + + front_ = QVector3D( + std::cos(yawRad) * std::cos(pitchRad), + std::sin(pitchRad), + std::sin(yawRad) * std::cos(pitchRad) + ).normalized(); + + right_ = QVector3D::crossProduct(front_, QVector3D(0, 1, 0)).normalized(); + up_ = QVector3D::crossProduct(right_, front_).normalized(); +} + +QMatrix4x4 Camera::view() const +{ + QMatrix4x4 v; + v.lookAt(position_, position_ + front_, up_); + return v; +} + +QMatrix4x4 Camera::projection() const +{ + QMatrix4x4 p; + p.perspective(fov_, aspect_, 0.1f, 1000.0f); + return p; +} diff --git a/src/App/Scene/Camera.h b/src/App/Scene/Camera.h new file mode 100644 index 0000000..2e7e087 --- /dev/null +++ b/src/App/Scene/Camera.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +class Camera +{ +public: + Camera(); + + void setAspect(float aspect); + + void move(float forward, float right, float dt); + void rotate(float dx, float dy); + + QMatrix4x4 view() const; + QMatrix4x4 projection() const; + + QVector3D position() const { return position_; } + QVector3D direction() const { return front_; } + +private: + void updateVectors(); + +private: + QVector3D position_{0.0f, 1.0f, 3.0f}; + QVector3D front_{0.0f, 0.0f, -1.0f}; + QVector3D right_{1.0f, 0.0f, 0.0f}; + QVector3D up_{0.0f, 1.0f, 0.0f}; + + float yaw_ = -90.0f; + float pitch_ = 0.0f; + + float fov_ = 60.0f; + float aspect_ = 1.0f; + + float speed_ = 3.0f; + float sensitivity_ = 0.1f; +}; diff --git a/src/App/Scene/Lihgt.h b/src/App/Scene/Lihgt.h new file mode 100644 index 0000000..53d374c --- /dev/null +++ b/src/App/Scene/Lihgt.h @@ -0,0 +1,22 @@ +#include +#include + +struct DirectionalLight { + QVector3D direction = QVector3D(-0.2f, -1.0f, -0.3f); + QVector3D color = QVector3D(1.0f, 1.0f, 1.0f); + float intensity = 1.0f; +}; + +struct SpotLight { + QVector3D position = QVector3D(0.0f, 1.0f, 4.0f); + QVector3D direction = QVector3D(0.0f, 0.1f, -1.0f).normalized(); + + float innerCutoff = qCos(qDegreesToRadians(12.5f)); + float outerCutoff = qCos(qDegreesToRadians(17.5f)); + + QVector3D color = QVector3D(1.0f, 0.0f, 0.0f); + + float constant = 1.0f; + float linear = 0.09f; + float quadratic = 0.032f; +}; \ No newline at end of file diff --git a/src/App/Scene/Scene.cpp b/src/App/Scene/Scene.cpp new file mode 100644 index 0000000..644bc89 --- /dev/null +++ b/src/App/Scene/Scene.cpp @@ -0,0 +1,254 @@ +#include "Scene.h" +#include +#include +#include + +Scene::Scene() = default; + +void Mesh::createGLObjects(QOpenGLShaderProgram* shader) +{ + if (!shader) return; + + vao = std::make_unique(); + vao->create(); + vao->bind(); + + vbo = std::make_unique(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(vertices.data(), static_cast(vertices.size() * sizeof(Vertex))); + + ibo = std::make_unique(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), static_cast(indices.size() * sizeof(uint32_t))); + + shader->bind(); + shader->enableAttributeArray(0); + shader->setAttributeBuffer(0, GL_FLOAT, offsetof(Vertex, position), 3, sizeof(Vertex)); + + shader->enableAttributeArray(1); + shader->setAttributeBuffer(1, GL_FLOAT, offsetof(Vertex, normal), 3, sizeof(Vertex)); + + shader->enableAttributeArray(2); + shader->setAttributeBuffer(2, GL_FLOAT, offsetof(Vertex, texCoord), 2, sizeof(Vertex)); + + vao->release(); + vbo->release(); + ibo->release(); + shader->release(); +} + +void Mesh::destroyGLObjects() +{ + if (vbo) vbo->destroy(); + if (ibo) ibo->destroy(); + if (vao) vao->destroy(); +} + +void Scene::update(float dt) +{ + float forward = 0.0f; + float right = 0.0f; + + if(moveForward) forward += 1.0f; + if(moveBackward) forward -= 1.0f; + if(moveRight) right += 1.0f; + if(moveLeft) right -= 1.0f; + + camera_.move(forward, right, dt); +} + +void Scene::applyLights(QOpenGLShaderProgram& shader) +{ + shader.setUniformValue("viewPos", camera_.position()); + + shader.setUniformValue("dirLight.direction", dirLight_.direction); + shader.setUniformValue("dirLight.color", dirLight_.color); + shader.setUniformValue("dirLight.intensity", dirLight_.intensity); + + shader.setUniformValue("spotLight.position", spotLight_.position); + shader.setUniformValue("spotLight.direction", spotLight_.direction); + + shader.setUniformValue("spotLight.innerCutoff", spotLight_.innerCutoff); + shader.setUniformValue("spotLight.outerCutoff", spotLight_.outerCutoff); + + shader.setUniformValue("spotLight.color", spotLight_.color); + + shader.setUniformValue("spotLight.constant", spotLight_.constant); + shader.setUniformValue("spotLight.linear", spotLight_.linear); + shader.setUniformValue("spotLight.quadratic", spotLight_.quadratic); +} + +bool Scene::loadGLB(const QString& filename, QOpenGLShaderProgram* shader) +{ + qDebug() << "Loading GLB:" << filename; + + tinygltf::Model model; + tinygltf::TinyGLTF loader; + std::string err, warn; + + QResource res(filename); + if (!res.isValid()) { + qWarning() << "Scene::loadGLB: invalid resource" << filename; + return false; + } + + QByteArray data(reinterpret_cast(res.data()), res.size()); + qDebug() << "Resource size:" << data.size() << "bytes"; + + bool ret = loader.LoadBinaryFromMemory(&model, &err, &warn, + reinterpret_cast(data.data()), + static_cast(data.size()), ""); + if (!warn.empty()) qDebug() << "WARN:" << QString::fromStdString(warn); + if (!err.empty()) qDebug() << "ERR:" << QString::fromStdString(err); + if (!ret) return false; + + qDebug() << "Loaded model:" << model.meshes.size() << "meshes," + << model.nodes.size() << "nodes"; + + meshes_.clear(); + + std::vector> textures; + for (size_t i = 0; i < model.images.size(); ++i) + { + const auto& image = model.images[i]; + QImage qimg; + if (image.component == 3) + qimg = QImage(image.image.data(), image.width, image.height, QImage::Format_RGB888); + else if (image.component == 4) + qimg = QImage(image.image.data(), image.width, image.height, QImage::Format_RGBA8888); + else { + qWarning() << "Unsupported image format for texture" << i; + continue; + } + + auto tex = std::make_unique(qimg); + tex->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear); + tex->setWrapMode(QOpenGLTexture::Repeat); + tex->generateMipMaps(); + + qDebug() << "Loaded texture" << i << "size:" << qimg.width() << "x" << qimg.height(); + + textures.push_back(std::move(tex)); + } + + for (size_t meshIndex = 0; meshIndex < model.meshes.size(); ++meshIndex) + { + const auto& mesh = model.meshes[meshIndex]; + qDebug() << "Mesh" << meshIndex << "has" << mesh.primitives.size() << "primitives"; + + for (size_t primIndex = 0; primIndex < mesh.primitives.size(); ++primIndex) + { + const auto& prim = mesh.primitives[primIndex]; + qDebug() << "Primitive" << primIndex; + + auto m = std::make_unique(); + + auto posIt = prim.attributes.find("POSITION"); + if (posIt == prim.attributes.end()) { + qWarning() << "Primitive has no POSITION attribute, skipping"; + continue; + } + const auto& posAccessor = model.accessors[posIt->second]; + const auto& posBufferView = model.bufferViews[posAccessor.bufferView]; + const auto& posBuffer = model.buffers[posBufferView.buffer]; + const float* positions = reinterpret_cast(posBuffer.data.data() + posBufferView.byteOffset + posAccessor.byteOffset); + + size_t vertexCount = posAccessor.count; + m->vertices.resize(vertexCount); + + for (size_t i = 0; i < vertexCount; ++i) { + m->vertices[i].position[0] = positions[i*3 + 0]; + m->vertices[i].position[1] = positions[i*3 + 1]; + m->vertices[i].position[2] = positions[i*3 + 2]; + } + qDebug() << "Vertex count:" << vertexCount; + + QVector3D min, max; + for (auto& v : m->vertices) + { + min.setX(std::min(min.x(), v.position[0])); + min.setY(std::min(min.y(), v.position[1])); + min.setZ(std::min(min.z(), v.position[2])); + + max.setX(std::max(max.x(), v.position[0])); + max.setY(std::max(max.y(), v.position[1])); + max.setZ(std::max(max.z(), v.position[2])); + } + + m->center = (min + max) * 0.5f; + m->radius = (max - m->center).length(); + + auto normIt = prim.attributes.find("NORMAL"); + if (normIt != prim.attributes.end()) { + const auto& normAccessor = model.accessors[normIt->second]; + const auto& normBufferView = model.bufferViews[normAccessor.bufferView]; + const auto& normBuffer = model.buffers[normBufferView.buffer]; + const float* normals = reinterpret_cast(normBuffer.data.data() + normBufferView.byteOffset + normAccessor.byteOffset); + for (size_t i = 0; i < vertexCount; ++i) { + m->vertices[i].normal[0] = normals[i*3 + 0]; + m->vertices[i].normal[1] = normals[i*3 + 1]; + m->vertices[i].normal[2] = normals[i*3 + 2]; + } + } + + auto uvIt = prim.attributes.find("TEXCOORD_0"); + if (uvIt != prim.attributes.end()) { + const auto& uvAccessor = model.accessors[uvIt->second]; + const auto& uvBufferView = model.bufferViews[uvAccessor.bufferView]; + const auto& uvBuffer = model.buffers[uvBufferView.buffer]; + const float* uvs = reinterpret_cast(uvBuffer.data.data() + uvBufferView.byteOffset + uvAccessor.byteOffset); + for (size_t i = 0; i < vertexCount; ++i) { + m->vertices[i].texCoord[0] = uvs[i*2 + 0]; + m->vertices[i].texCoord[1] = uvs[i*2 + 1]; + } + } + + if (prim.indices >= 0) { + const auto& idxAccessor = model.accessors[prim.indices]; + const auto& idxBufferView = model.bufferViews[idxAccessor.bufferView]; + const auto& idxBuffer = model.buffers[idxBufferView.buffer]; + + m->indices.resize(idxAccessor.count); + if (idxAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + qDebug() << "Primitive has short indices"; + const uint16_t* buf = reinterpret_cast(idxBuffer.data.data() + idxBufferView.byteOffset + idxAccessor.byteOffset); + for (size_t i=0; iindices[i] = buf[i]; + } else if (idxAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + qDebug() << "Primitive has unsigned int indices"; + const uint32_t* buf = reinterpret_cast(idxBuffer.data.data() + idxBufferView.byteOffset + idxAccessor.byteOffset); + for (size_t i=0; iindices[i] = buf[i]; + } else { + qWarning() << "Unsupported index component type"; + continue; + } + } else { + m->indices.resize(vertexCount); + for (size_t i = 0; i < vertexCount; ++i) m->indices[i] = static_cast(i); + qDebug() << "Primitive has no indices, generated default indices"; + } + + if (prim.material >= 0 && prim.material < static_cast(model.materials.size())) { + const auto& material = model.materials[prim.material]; + if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) { + int texIndex = material.pbrMetallicRoughness.baseColorTexture.index; + if (texIndex < static_cast(textures.size())) { + m->texture = std::move(textures[texIndex]); + qDebug() << "Assigned texture" << texIndex << "to mesh" << meshIndex; + } else { + qWarning() << "Texture index out of range for mesh" << meshIndex; + } + } + } + + m->createGLObjects(shader); + meshes_.push_back(std::move(m)); + } + } + + qDebug() << "Total meshes loaded:" << meshes_.size(); + return true; +} diff --git a/src/App/Scene/Scene.h b/src/App/Scene/Scene.h new file mode 100644 index 0000000..833f0f7 --- /dev/null +++ b/src/App/Scene/Scene.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include "Camera.h" +#include "Lihgt.h" + +struct Vertex +{ + float position[3]{0,0,0}; + float normal[3]{0,0,0}; + float texCoord[2]{0,0}; +}; + +struct Mesh +{ + std::vector vertices; + std::vector indices; + QVector3D position{0,0,-2}; + + QVector3D center; + float radius; + + std::unique_ptr vao; + std::unique_ptr vbo; + std::unique_ptr ibo; + + std::unique_ptr texture; + + void createGLObjects(QOpenGLShaderProgram* shader); + void destroyGLObjects(); +}; + +class Scene +{ +public: + Scene(); + + void update(float dt); + + Camera& camera() { return camera_; } + const Camera& camera() const { return camera_; } + + const std::vector>& meshes() const { return meshes_; } + std::vector>& meshes() { return meshes_; } + + void applyLights(QOpenGLShaderProgram& shader); + bool loadGLB(const QString& filename, QOpenGLShaderProgram* shader); + + bool moveForward{false}; + bool moveBackward{false}; + bool moveLeft{false}; + bool moveRight{false}; + + //tempurarily public, change tp getters/setters later + DirectionalLight dirLight_; + SpotLight spotLight_; + +private: + Camera camera_; + std::vector> meshes_; +}; diff --git a/src/App/Shaders/model.fs b/src/App/Shaders/model.fs new file mode 100644 index 0000000..ed0af48 --- /dev/null +++ b/src/App/Shaders/model.fs @@ -0,0 +1,78 @@ +#version 330 core + +in vec3 FragPos; +in vec3 Normal; +in vec2 TexCoord; + +out vec4 FragColor; + +uniform sampler2D diffuseTexture; +uniform vec3 viewPos; + +struct DirLight { + vec3 direction; + vec3 color; + float intensity; +}; +uniform DirLight dirLight; + +struct SpotLight { + vec3 position; + vec3 direction; + + float innerCutoff; + float outerCutoff; + + vec3 color; + + float constant; + float linear; + float quadratic; +}; +uniform SpotLight spotLight; + +void main() +{ + vec3 N = normalize(Normal); + vec3 V = normalize(viewPos - FragPos); + vec3 albedo = texture(diffuseTexture, TexCoord).rgb; + + vec3 Ld = normalize(-dirLight.direction); + + vec3 ambientD = 0.1 * dirLight.color * dirLight.intensity; + + float diffD = max(dot(N, Ld), 0.0); + vec3 diffuseD = diffD * dirLight.color * dirLight.intensity; + + vec3 reflectD = reflect(-Ld, N); + float specD = pow(max(dot(V, reflectD), 0.0), 32.0); + vec3 specularD = specD * dirLight.color * dirLight.intensity; + + vec3 result = ambientD + diffuseD + specularD; + + vec3 Ls = normalize(spotLight.position - FragPos); + + float theta = dot(Ls, normalize(-spotLight.direction)); + float epsilon = spotLight.innerCutoff - spotLight.outerCutoff; + float intensity = clamp((theta - spotLight.outerCutoff) / epsilon, 0.0, 1.0); + + float distance = length(spotLight.position - FragPos); + float attenuation = 1.0 / ( + spotLight.constant + + spotLight.linear * distance + + spotLight.quadratic * distance * distance + ); + + vec3 ambientS = 0.05 * spotLight.color; + + float diffS = max(dot(N, Ls), 0.0); + vec3 diffuseS = diffS * spotLight.color; + + vec3 reflectS = reflect(-Ls, N); + float specS = pow(max(dot(V, reflectS), 0.0), 32.0); + vec3 specularS = specS * spotLight.color; + + result += (ambientS + diffuseS + specularS) * intensity * attenuation; + + FragColor = vec4(result * albedo, 1.0); +} diff --git a/src/App/Shaders/model.vs b/src/App/Shaders/model.vs new file mode 100644 index 0000000..20c56b1 --- /dev/null +++ b/src/App/Shaders/model.vs @@ -0,0 +1,31 @@ +#version 330 core + +layout(location = 0) in vec3 aPosition; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aTexCoord; + +uniform mat4 mvp; +uniform mat4 model; +uniform mat3 normalMatrix; + +uniform float morphFactor; +uniform vec3 modelCenter; +uniform float sphereRadius; + +out vec3 FragPos; +out vec3 Normal; +out vec2 TexCoord; + +void main() +{ + vec3 dir = normalize(aPosition - modelCenter); + vec3 spherePos = modelCenter + dir * sphereRadius; + + vec3 morphedPos = mix(aPosition, spherePos, morphFactor); + + FragPos = vec3(model * vec4(morphedPos, 1.0)); + Normal = normalize(normalMatrix * normalize(mix(aNormal, dir, morphFactor))); + + TexCoord = aTexCoord; + gl_Position = mvp * vec4(morphedPos, 1.0); +} diff --git a/src/App/Window.cpp b/src/App/Window.cpp index 5c607c5..b3b7f83 100644 --- a/src/App/Window.cpp +++ b/src/App/Window.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include @@ -15,18 +18,6 @@ #include -namespace -{ - -constexpr std::array vertices = { - 0.0f, 0.707f, 1.f, 0.f, 0.f, 0.0f, 0.0f, - -0.5f, -0.5f, 0.f, 1.f, 0.f, 0.5f, 1.0f, - 0.5f, -0.5f, 0.f, 0.f, 1.f, 1.0f, 0.0f, -}; -constexpr std::array indices = {0, 1, 2}; - -}// namespace - Window::Window() noexcept { const auto formatFPS = [](const auto value) { @@ -36,16 +27,36 @@ Window::Window() noexcept auto fps = new QLabel(formatFPS(0), this); fps->setStyleSheet("QLabel { color : white; }"); + auto controlsLabel = new QLabel( + "Controls:\n" + " - WASD: Move camera\n" + " - Mouse Drag (LMB): Rotate camera\n" + " - L: Snap spotlight to camera\n" + " - F: Free camera mode", + this + ); + controlsLabel->setStyleSheet("color: white;"); + controlsLabel->setWordWrap(true); + controlsLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + auto layout = new QVBoxLayout(); - layout->addWidget(fps, 1); + layout->addWidget(fps, 0); + layout->addWidget(controlsLabel, 0); + layout->addStretch(1); + + createControlPanel(); + layout->addWidget(controlPanel_, 0); setLayout(layout); timer_.start(); + fpsTimer_.start(); connect(this, &Window::updateUI, [=] { fps->setText(formatFPS(ui_.fps)); }); + + setMouseTracking(true); } Window::~Window() @@ -60,57 +71,21 @@ Window::~Window() void Window::onInit() { - // Configure shaders program_ = std::make_unique(this); - program_->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/Shaders/diffuse.vs"); - program_->addShaderFromSourceFile(QOpenGLShader::Fragment, - ":/Shaders/diffuse.fs"); + program_->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/Shaders/model.vs"); + program_->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/Shaders/model.fs"); program_->link(); - // Create VAO object - vao_.create(); - vao_.bind(); - - // Create VBO - vbo_.create(); - vbo_.bind(); - vbo_.setUsagePattern(QOpenGLBuffer::StaticDraw); - vbo_.allocate(vertices.data(), static_cast(vertices.size() * sizeof(GLfloat))); - - // Create IBO - ibo_.create(); - ibo_.bind(); - ibo_.setUsagePattern(QOpenGLBuffer::StaticDraw); - ibo_.allocate(indices.data(), static_cast(indices.size() * sizeof(GLuint))); - - texture_ = std::make_unique(QImage(":/Textures/voronoi.png")); - texture_->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear); - texture_->setWrapMode(QOpenGLTexture::WrapMode::Repeat); - - // Bind attributes - program_->bind(); - - program_->enableAttributeArray(0); - program_->setAttributeBuffer(0, GL_FLOAT, 0, 2, static_cast(7 * sizeof(GLfloat))); - - program_->enableAttributeArray(1); - program_->setAttributeBuffer(1, GL_FLOAT, static_cast(2 * sizeof(GLfloat)), 3, - static_cast(7 * sizeof(GLfloat))); - - program_->enableAttributeArray(2); - program_->setAttributeBuffer(2, GL_FLOAT, static_cast(5 * sizeof(GLfloat)), 2, - static_cast(7 * sizeof(GLfloat))); - mvpUniform_ = program_->uniformLocation("mvp"); - // Release all - program_->release(); - - vao_.release(); - - ibo_.release(); - vbo_.release(); + scene_ = std::make_unique(); + if (!scene_->loadGLB(":/Models/pallas_cat.glb", program_.get())) { + qWarning() << "Failed to load GLB model"; + } else { + qDebug() << "Loaded" << scene_->meshes().size() << "meshes"; + } + // Еnable depth test and face culling glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); @@ -126,29 +101,72 @@ void Window::onRender() // Clear buffers glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // Calculate MVP matrix - model_.setToIdentity(); - model_.translate(0, 0, -2); - view_.setToIdentity(); - const auto mvp = projection_ * view_ * model_; - - // Bind VAO and shader program program_->bind(); - vao_.bind(); - // Update uniform value - program_->setUniformValue(mvpUniform_, mvp); + if (spotLightFollowsCamera_) { + scene_->spotLight_.position = scene_->camera().position(); + scene_->spotLight_.direction = scene_->camera().direction(); + } + + scene_->applyLights(*program_); + + static qint64 lastTime = timer_.elapsed(); + qint64 now = timer_.elapsed(); + float dt = float(now - lastTime) / 1000.0f; + lastTime = now; + + scene_->update(dt); - // Activate texture unit and bind texture - glActiveTexture(GL_TEXTURE0); - texture_->bind(); + //qDebug() << "Rendering" << scene_->meshes().size() << "meshes"; + + for (size_t i = 0; i < scene_->meshes().size(); ++i) { + auto& mesh = scene_->meshes()[i]; + + if (!mesh->vao || !mesh->vao->isCreated()) { + qWarning() << "Mesh" << i << "VAO is not valid!"; + continue; + } - // Draw - glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr); + QMatrix4x4 model; + model.setToIdentity(); + + float scaleFactor = 0.1f; + model.scale(scaleFactor); + model.rotate(-90.0f, 1.0f, 0.0f, 0.0f); + model.translate(mesh->position); + + QMatrix4x4 mvp = scene_->camera().projection() * scene_->camera().view() * model; + QMatrix3x3 normalMatrix = model.normalMatrix(); + + program_->setUniformValue(mvpUniform_, mvp); + program_->setUniformValue("model", model); + program_->setUniformValue("normalMatrix", normalMatrix); + + program_->setUniformValue("morphFactor", morphFactor_); + program_->setUniformValue("sphereRadius", mesh->radius); + program_->setUniformValue("modelCenter", mesh->center); + + if (mesh->texture) + { + mesh->texture->bind(0); + int diffuseLoc = program_->uniformLocation("diffuseTexture"); + program_->setUniformValue(diffuseLoc, 0); + // qDebug() << "Mesh" << i << "binding texture"; + } + + if (mesh->vao) + { + mesh->vao->bind(); + glDrawElements(GL_TRIANGLES, static_cast(mesh->indices.size()), GL_UNSIGNED_INT, nullptr); + mesh->vao->release(); + } + + if (mesh->texture) + { + mesh->texture->release(); + } + } - // Release VAO and shader program - texture_->release(); - vao_.release(); program_->release(); ++frameCount_; @@ -164,14 +182,91 @@ void Window::onResize(const size_t width, const size_t height) { // Configure viewport glViewport(0, 0, static_cast(width), static_cast(height)); + scene_->camera().setAspect(float(width) / float(height)); +} + +void Window::mousePressEvent(QMouseEvent* e) +{ + if (cameraMode_ == CameraMode::ClickToRotate && e->button() == Qt::LeftButton) + { + rotating_ = true; + lastMousePos_ = e->pos(); + } +} + +void Window::mouseReleaseEvent(QMouseEvent* e) +{ + if (cameraMode_ == CameraMode::ClickToRotate && e->button() == Qt::LeftButton) + { + rotating_ = false; + firstMouse_ = true; + } +} + +void Window::mouseMoveEvent(QMouseEvent* e) +{ + if (firstMouse_) + { + lastMousePos_ = e->pos(); + firstMouse_ = false; + return; + } + + float dx = e->pos().x() - lastMousePos_.x(); + float dy = lastMousePos_.y() - e->pos().y(); - // Configure matrix - const auto aspect = static_cast(width) / static_cast(height); - const auto zNear = 0.1f; - const auto zFar = 100.0f; - const auto fov = 60.0f; - projection_.setToIdentity(); - projection_.perspective(fov, aspect, zNear, zFar); + if (cameraMode_ == CameraMode::FreeLook || (cameraMode_ == CameraMode::ClickToRotate && rotating_)) + { + scene_->camera().rotate(dx, dy); + } + + lastMousePos_ = e->pos(); + + if (cameraMode_ == CameraMode::FreeLook) + { + QPoint center = rect().center(); + QCursor::setPos(mapToGlobal(center)); + lastMousePos_ = center; + } +} + +void Window::keyPressEvent(QKeyEvent* e) +{ + if(e->key() == Qt::Key_F) + { + if (cameraMode_ == CameraMode::ClickToRotate) { + cameraMode_ = CameraMode::FreeLook; + grabMouse(); + } else { + cameraMode_ = CameraMode::ClickToRotate; + releaseMouse(); + } + + firstMouse_ = true; + setCursor(cameraMode_ == CameraMode::FreeLook ? Qt::BlankCursor : Qt::ArrowCursor); + } + + if(e->key() == Qt::Key_L) { + spotLightFollowsCamera_ = !spotLightFollowsCamera_; + if (spotLightFollowsCamera_) { + qDebug() << "Spot light now follows camera"; + } else { + qDebug() << "Spot light is independent"; + } + } + + if(e->key() == Qt::Key_W) scene_->moveForward = true; + if(e->key() == Qt::Key_S) scene_->moveBackward = true; + if(e->key() == Qt::Key_A) scene_->moveLeft = true; + if(e->key() == Qt::Key_D) scene_->moveRight = true; +} + +void Window::keyReleaseEvent(QKeyEvent* e) +{ + if(e->key() == Qt::Key_W) scene_->moveForward = false; + if(e->key() == Qt::Key_S) scene_->moveBackward = false; + if(e->key() == Qt::Key_A) scene_->moveLeft = false; + if(e->key() == Qt::Key_D) scene_->moveRight = false; } Window::PerfomanceMetricsGuard::PerfomanceMetricsGuard(std::function callback) @@ -191,9 +286,9 @@ auto Window::captureMetrics() -> PerfomanceMetricsGuard { return PerfomanceMetricsGuard{ [&] { - if (timer_.elapsed() >= 1000) + if (fpsTimer_.elapsed() >= 1000) { - const auto elapsedSeconds = static_cast(timer_.restart()) / 1000.0f; + const auto elapsedSeconds = static_cast(fpsTimer_.restart()) / 1000.0f; ui_.fps = static_cast(std::round(frameCount_ / elapsedSeconds)); frameCount_ = 0; emit updateUI(); @@ -201,3 +296,104 @@ auto Window::captureMetrics() -> PerfomanceMetricsGuard } }; } + +void Window::createControlPanel() +{ + controlPanel_ = new QWidget(this); + controlPanel_->setFixedWidth(260); + controlPanel_->setStyleSheet("background:#2b2b2b; color:white;"); + + auto* layout = new QVBoxLayout(controlPanel_); + layout->setSpacing(8); + + auto* dirGroup = new QGroupBox("Directional Light"); + dirGroup->setStyleSheet("QGroupBox { font-weight: bold; }"); + auto* dirLayout = new QVBoxLayout(dirGroup); + + dirLayout->addWidget(new QLabel("Intensity")); + + dirIntensitySlider_ = new QSlider(Qt::Horizontal); + dirIntensitySlider_->setRange(0, 200); + dirIntensitySlider_->setValue(100); + dirLayout->addWidget(dirIntensitySlider_); + + QObject::connect(dirIntensitySlider_, &QSlider::valueChanged, [this](int v) { + float k = v / 100.0f; + scene_->dirLight_.color = QVector3D(k, k, k); + update(); + }); + + layout->addWidget(dirGroup); + + auto* spotGroup = new QGroupBox("Spot Light"); + spotGroup->setStyleSheet("QGroupBox { font-weight: bold; }"); + auto* spotLayout = new QVBoxLayout(spotGroup); + + spotLayout->addWidget(new QLabel("Inner angle")); + spotInnerSlider_ = new QSlider(Qt::Horizontal); + spotInnerSlider_->setRange(1, 45); + spotInnerSlider_->setValue(12); + spotLayout->addWidget(spotInnerSlider_); + + spotLayout->addWidget(new QLabel("Outer angle")); + spotOuterSlider_ = new QSlider(Qt::Horizontal); + spotOuterSlider_->setRange(1, 60); + spotOuterSlider_->setValue(17); + spotLayout->addWidget(spotOuterSlider_); + + QObject::connect(spotInnerSlider_, &QSlider::valueChanged, [this](int v) { + scene_->spotLight_.innerCutoff = qCos(qDegreesToRadians(float(v))); + update(); + }); + + QObject::connect(spotOuterSlider_, &QSlider::valueChanged, [this](int v) { + scene_->spotLight_.outerCutoff = qCos(qDegreesToRadians(float(v))); + update(); + }); + + auto* colorPalette = new QWidget; + auto* paletteLayout = new QHBoxLayout(colorPalette); + paletteLayout->setSpacing(4); + paletteLayout->setContentsMargins(0,0,0,0); + + std::vector colors = { Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::magenta }; + + for (const auto& c : colors) { + auto* colorBtn = new QPushButton; + colorBtn->setFixedSize(24,24); + colorBtn->setStyleSheet(QString("background-color:%1; border:1px solid white;").arg(c.name())); + colorBtn->setCursor(Qt::PointingHandCursor); + + QObject::connect(colorBtn, &QPushButton::clicked, [this, c]() { + scene_->spotLight_.color = QVector3D(c.redF(), c.greenF(), c.blueF()); + update(); + }); + + paletteLayout->addWidget(colorBtn); + } + + spotLayout->addWidget(new QLabel("Spot Color:")); + spotLayout->addWidget(colorPalette); + + layout->addWidget(spotGroup); + layout->addStretch(); + + auto* morphGroup = new QGroupBox("Morphing"); + morphGroup->setStyleSheet("QGroupBox { font-weight: bold; }"); + auto* morphLayout = new QVBoxLayout(morphGroup); + + morphLayout->addWidget(new QLabel("Shpereness")); + + morphSlider_ = new QSlider(Qt::Horizontal); + morphSlider_->setRange(0, 100); + morphSlider_->setValue(0); + morphLayout->addWidget(morphSlider_); + + QObject::connect(morphSlider_, &QSlider::valueChanged, [this](int value) + { + morphFactor_ = value / 100.0f; + // qDebug() << "Morph factor:" << morphFactor_; + }); + + layout->addWidget(morphGroup); +} diff --git a/src/App/Window.h b/src/App/Window.h index 1102e2f..553abe9 100644 --- a/src/App/Window.h +++ b/src/App/Window.h @@ -8,10 +8,16 @@ #include #include #include +#include +#include #include #include +#include "Scene/Scene.h" + +enum class CameraMode { ClickToRotate, FreeLook }; + class Window final : public fgl::GLWidget { Q_OBJECT @@ -24,6 +30,12 @@ class Window final : public fgl::GLWidget void onRender() override; void onResize(size_t width, size_t height) override; + void mousePressEvent(QMouseEvent * event) override; + void mouseReleaseEvent(QMouseEvent * event) override; + void mouseMoveEvent(QMouseEvent * event) override; + void keyPressEvent(QKeyEvent * event) override; + void keyReleaseEvent(QKeyEvent * event) override; + private: class PerfomanceMetricsGuard final { @@ -43,6 +55,7 @@ class Window final : public fgl::GLWidget private: [[nodiscard]] PerfomanceMetricsGuard captureMetrics(); + void createControlPanel(); signals: void updateUI(); @@ -62,6 +75,7 @@ class Window final : public fgl::GLWidget std::unique_ptr program_; QElapsedTimer timer_; + QElapsedTimer fpsTimer_; size_t frameCount_ = 0; struct { @@ -69,4 +83,25 @@ class Window final : public fgl::GLWidget } ui_; bool animated_ = true; + + std::unique_ptr scene_; + + CameraMode cameraMode_ = CameraMode::ClickToRotate; + bool rotating_ = false; + QPoint lastMousePos_; + bool firstMouse_ = true; + + float morphFactor_ = 0.0f; + + QWidget* controlPanel_ = nullptr; + + QSlider* dirIntensitySlider_ = nullptr; + + QSlider* spotIntensitySlider_ = nullptr; + QSlider* spotInnerSlider_ = nullptr; + QSlider* spotOuterSlider_ = nullptr; + QPushButton* spotColorButton_ = nullptr; + QSlider* morphSlider_ = nullptr; + + bool spotLightFollowsCamera_ = false; }; diff --git a/src/App/resources.qrc b/src/App/resources.qrc index 41ba765..fbab4b0 100644 --- a/src/App/resources.qrc +++ b/src/App/resources.qrc @@ -1,12 +1,16 @@ + - - Models/chess.glb - - - Textures/voronoi.png - - - Shaders/diffuse.fs - Shaders/diffuse.vs - - \ No newline at end of file + + Models/chess.glb + Models/pallas_cat.glb + + + Textures/voronoi.png + + + Shaders/diffuse.fs + Shaders/diffuse.vs + Shaders/model.fs + Shaders/model.vs + +