diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c2921b6b..5865d8b2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -15,7 +15,7 @@ jobs: # Don't fail on empty include (not tracked by git) - run: if ! [ -d include ]; then mkdir include; fi - - run: sudo apt update && sudo apt install -y libglfw3-dev libglm-dev libgl-dev + - run: sudo apt update && sudo apt install -y libglfw3-dev libglm-dev libgl-dev libtinyxml2-dev - run: make format: runs-on: ubuntu-latest @@ -45,4 +45,4 @@ jobs: # Don't fail on empty include (not tracked by git) - run: if ! [ -d include ]; then mkdir include; fi - - run: cppcheck src -I include + - run: cppcheck src -I include -I /usr/include/tinyxml2 diff --git a/Makefile b/Makefile index 8d3f2c10..6f66fe38 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,13 @@ CPPFLAGS := -Iinclude -std=c++20 -Wall -Wextra -pedantic -Wshadow \ $(shell pkg-config --cflags glfw3) -DGLFW_INCLUDE_NONE \ $(shell pkg-config --cflags glm) \ $(shell pkg-config --cflags gl) \ + $(shell pkg-config --cflags tinyxml2) \ -Ilib/include LIBS := -lm \ - $(shell pkg-config --libs glfw3) \ - $(shell pkg-config --libs glm) \ - $(shell pkg-config --libs gl) + $(shell pkg-config --libs glfw3) \ + $(shell pkg-config --libs glm) \ + $(shell pkg-config --libs gl) \ + $(shell pkg-config --libs tinyxml2) DEBUG_CPPFLAGS := -O0 -ggdb3 RELEASE_CPPFLAGS := -O2 @@ -50,7 +52,7 @@ LIB_SOURCES = $(shell find "lib" -name '*.c' -type f) LIB_OBJECTS = $(patsubst lib/%.c, $(OBJDIR)/%.o, $(LIB_SOURCES)) HEADERS = $(shell find "include" -name '*.hpp' -type f) DEPENDS = $(patsubst src/%.cpp, $(DEPDIR)/%.d, $(SOURCES)) -REPORTS = $(patsubst reports/%.tex, %.pdf, $(shell find reports -name '*.tex' -type f)) +REPORTS = $(patsubst reports/%.tex, $(BUILDDIR)/%.pdf, $(shell find reports -name '*.tex' -type f)) ifeq ($(DEBUG), 1) CPPFLAGS += $(DEBUG_CPPFLAGS) diff --git a/include/engine/Entity.hpp b/include/engine/Entity.hpp new file mode 100644 index 00000000..e0f10116 --- /dev/null +++ b/include/engine/Entity.hpp @@ -0,0 +1,34 @@ +/// Copyright 2025 Ana Oliveira, Humberto Gomes, Mariana Rocha, Sara Lopes +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#pragma once + +#include +#include + +#include "engine/Model.hpp" +#include "engine/RenderPipeline.hpp" + +namespace engine { +class Entity { +protected: + std::shared_ptr model; + glm::vec4 color; + // TODO - Phase 2 – Add Geometric Transforms + +public: + Entity(std::shared_ptr _model, const glm::vec4 &color); + void draw(const RenderPipeline &pipeline) const; +}; +} diff --git a/include/engine/Scene.hpp b/include/engine/Scene.hpp new file mode 100644 index 00000000..f3327a10 --- /dev/null +++ b/include/engine/Scene.hpp @@ -0,0 +1,69 @@ +/// Copyright 2025 Ana Oliveira, Humberto Gomes, Mariana Rocha, Sara Lopes +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "engine/Camera.hpp" +#include "engine/Entity.hpp" +#include "engine/RenderPipeline.hpp" + +namespace engine { + +class Scene { +private: + int windowWidth, windowHeight; + std::string windowTitle; + Camera camera; + + // TODO - Phase 2 - add support for groups (linear scene is going to make it harder for phase 3) + std::vector> entities; + +public: + Scene(const std::string &file); + Scene(const Scene &scene) = delete; + Scene(Scene &&scene) = delete; + + int getWindowWidth() const; + int getWindowHeight() const; + Camera &getCamera(); + + void draw(const RenderPipeline &pipeline) const; + void setWindowSize(int width, int height); + +private: + const tinyxml2::XMLElement *getOnlyOneNodeFromXML(const tinyxml2::XMLNode *parent, + const std::string &name); + glm::vec3 getVectorFromXML(const tinyxml2::XMLElement *element); + + void getWindowFromXML(const tinyxml2::XMLElement *worldElement); + void getCameraFromXML(const tinyxml2::XMLElement *worldElement); + + void getEntitiesFromWorldXML( + const std::filesystem::path &sceneDirectory, + std::unordered_map> &loadedModels, + const tinyxml2::XMLElement *worldElement); + void getEntitiesFromGroupXML( + const std::filesystem::path &sceneDirectory, + std::unordered_map> &loadedModels, + const tinyxml2::XMLElement *groupdElement); +}; + +} diff --git a/include/engine/SceneWindow.hpp b/include/engine/SceneWindow.hpp index d8df77d6..01372d58 100644 --- a/include/engine/SceneWindow.hpp +++ b/include/engine/SceneWindow.hpp @@ -16,23 +16,18 @@ #include -#include "engine/Camera.hpp" -#include "engine/Model.hpp" #include "engine/RenderPipeline.hpp" +#include "engine/Scene.hpp" #include "engine/Window.hpp" namespace engine { class SceneWindow : public Window { private: RenderPipeline pipeline; - Camera camera; - - // TODO - remove, these are for testing purposes only - std::unique_ptr model; - glm::vec3 translate; + Scene scene; public: - SceneWindow(); + SceneWindow(const std::string &sceneFile); protected: void onUpdate(float time, float timeElapsed); diff --git a/include/engine/Window.hpp b/include/engine/Window.hpp index 0c381565..417829a5 100644 --- a/include/engine/Window.hpp +++ b/include/engine/Window.hpp @@ -30,8 +30,10 @@ class Window { ~Window(); void runLoop(); - int getWidth(); - int getHeight(); + void resize(int _width, int _height); + + int getWidth() const; + int getHeight() const; protected: GLFWwindow *getHandle(); diff --git a/res/models/taurus.3d b/res/models/taurus.3d new file mode 100644 index 00000000..ed8ecd63 --- /dev/null +++ b/res/models/taurus.3d @@ -0,0 +1 @@ +Só para a pasta não apagar. \ No newline at end of file diff --git a/res/scenes/scene_box.xml b/res/scenes/scene_box.xml new file mode 100644 index 00000000..96f231b1 --- /dev/null +++ b/res/scenes/scene_box.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/scenes/scene_plane.xml b/res/scenes/scene_plane.xml new file mode 100644 index 00000000..ea73eca3 --- /dev/null +++ b/res/scenes/scene_plane.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/scenes/scene_sphere.xml b/res/scenes/scene_sphere.xml new file mode 100644 index 00000000..364829bf --- /dev/null +++ b/res/scenes/scene_sphere.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/engine/Entity.cpp b/src/engine/Entity.cpp new file mode 100644 index 00000000..d393194a --- /dev/null +++ b/src/engine/Entity.cpp @@ -0,0 +1,31 @@ +/// Copyright 2025 Ana Oliveira, Humberto Gomes, Mariana Rocha, Sara Lopes +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "engine/Entity.hpp" + +#include "engine/Model.hpp" + +namespace engine { + +Entity::Entity(std::shared_ptr _model, const glm::vec4 &_color) { + this->model = _model; + this->color = _color; +} + +void Entity::draw(const RenderPipeline &pipeline) const { + pipeline.setColor(color); + this->model->draw(); +} + +} diff --git a/src/engine/Model.cpp b/src/engine/Model.cpp index ba4a7d93..45e891d5 100644 --- a/src/engine/Model.cpp +++ b/src/engine/Model.cpp @@ -57,4 +57,5 @@ void Model::draw() const { glBindVertexArray(this->vao); glDrawElements(GL_TRIANGLES, this->vertexCount, GL_UNSIGNED_INT, nullptr); } + } diff --git a/src/engine/RenderPipeline.cpp b/src/engine/RenderPipeline.cpp index b6fb882d..5da46e38 100644 --- a/src/engine/RenderPipeline.cpp +++ b/src/engine/RenderPipeline.cpp @@ -114,4 +114,5 @@ void RenderPipeline::assertProgramLinking() const { throw std::runtime_error("Program linking error: " + logMessage); } } + } diff --git a/src/engine/Scene.cpp b/src/engine/Scene.cpp new file mode 100644 index 00000000..f8084d08 --- /dev/null +++ b/src/engine/Scene.cpp @@ -0,0 +1,179 @@ +/// Copyright 2025 Ana Oliveira, Humberto Gomes, Mariana Rocha, Sara Lopes +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include + +#include "engine/Camera.hpp" +#include "engine/Entity.hpp" +#include "engine/RenderPipeline.hpp" +#include "engine/Scene.hpp" + +namespace engine { + +Scene::Scene(const std::string &file) { + const std::filesystem::path sceneDirectory = std::filesystem::path(file).parent_path(); + std::unordered_map> loadedModels; + + tinyxml2::XMLDocument doc; + if (doc.LoadFile(file.c_str()) != tinyxml2::XML_SUCCESS) { + throw std::runtime_error("Failed to load scene XML file"); + } + + const tinyxml2::XMLElement *worldElement = this->getOnlyOneNodeFromXML(&doc, "world"); + this->getWindowFromXML(worldElement); + this->getCameraFromXML(worldElement); + this->getEntitiesFromWorldXML(sceneDirectory, loadedModels, worldElement); +} + +const tinyxml2::XMLElement *Scene::getOnlyOneNodeFromXML(const tinyxml2::XMLNode *parent, + const std::string &name) { + + const tinyxml2::XMLElement *child = parent->FirstChildElement(name.c_str()); + if (!child) { + throw std::runtime_error("<" + name + "> element not found in scene XML"); + } + + const tinyxml2::XMLElement *child2 = child->NextSiblingElement(name.c_str()); + if (child2) { + throw std::runtime_error("More than one <" + name + "> element in scene XML"); + } + + return child; +} + +glm::vec3 Scene::getVectorFromXML(const tinyxml2::XMLElement *element) { + float x = element->FloatAttribute("x", NAN); + float y = element->FloatAttribute("y", NAN); + float z = element->FloatAttribute("z", NAN); + + if (std::isnan(x) || std::isnan(y) || std::isnan(z)) { + std::string name = element->Name(); + throw std::runtime_error("Invalid vector in <" + name + "> in scene XML file"); + } + + return glm::vec3(x, y, z); +} + +void Scene::getWindowFromXML(const tinyxml2::XMLElement *worldElement) { + const tinyxml2::XMLElement *windowElement = this->getOnlyOneNodeFromXML(worldElement, "window"); + this->windowWidth = windowElement->IntAttribute("width", -1); + this->windowHeight = windowElement->IntAttribute("height", -1); + + if (this->windowWidth < 0 || this->windowHeight < 0) { + throw std::runtime_error("Invalid / unknown window width / height in scene XML file"); + } +} + +void Scene::getCameraFromXML(const tinyxml2::XMLElement *worldElement) { + const tinyxml2::XMLElement *cameraElement = this->getOnlyOneNodeFromXML(worldElement, "camera"); + + // View matrix + const glm::vec3 position = + this->getVectorFromXML(this->getOnlyOneNodeFromXML(cameraElement, "position")); + const glm::vec3 lookAt = + this->getVectorFromXML(this->getOnlyOneNodeFromXML(cameraElement, "lookAt")); + const glm::vec3 up = this->getVectorFromXML(this->getOnlyOneNodeFromXML(cameraElement, "up")); + + // Projection matrix + const tinyxml2::XMLElement *projectionElement = + this->getOnlyOneNodeFromXML(cameraElement, "projection"); + + const float fov = projectionElement->FloatAttribute("fov", NAN); + const float near = projectionElement->FloatAttribute("near", NAN); + const float far = projectionElement->FloatAttribute("far", NAN); + + if (std::isnan(fov) || std::isnan(near) || std::isnan(far)) { + throw std::runtime_error("Invalid in scene XML file"); + } + + this->camera = Camera(position, lookAt, up, fov, near, far); +} + +void Scene::getEntitiesFromWorldXML( + const std::filesystem::path &sceneDirectory, + std::unordered_map> &loadedModels, + const tinyxml2::XMLElement *worldElement) { + + const tinyxml2::XMLElement *groupElement = worldElement->FirstChildElement("group"); + while (groupElement) { + this->getEntitiesFromGroupXML(sceneDirectory, loadedModels, groupElement); + groupElement = worldElement->NextSiblingElement("group"); + } +} + +void Scene::getEntitiesFromGroupXML( + const std::filesystem::path &sceneDirectory, + std::unordered_map> &loadedModels, + const tinyxml2::XMLElement *groupElement) { + + const tinyxml2::XMLElement *innerGroupElement = groupElement->FirstChildElement("group"); + while (innerGroupElement) { + this->getEntitiesFromGroupXML(sceneDirectory, loadedModels, innerGroupElement); + innerGroupElement = innerGroupElement->NextSiblingElement("group"); + } + + const tinyxml2::XMLElement *modelsElement = groupElement->FirstChildElement("models"); + if (!modelsElement) + return; // is optional + + const tinyxml2::XMLElement *modelElement = modelsElement->FirstChildElement("model"); + while (modelElement) { + const char *file = modelElement->Attribute("file"); + if (!file) { + throw std::runtime_error("Invalid in scene XML file"); + } + + std::string modelPath = std::filesystem::canonical(sceneDirectory / file); + auto it = loadedModels.find(modelPath); + std::shared_ptr model; + if (it == loadedModels.end()) { + utils::WavefrontOBJ object(modelPath); + model = std::make_shared(object); + loadedModels[modelPath] = model; + } else { + model = it->second; + } + + // TODO - color + std::unique_ptr entity = std::make_unique(model, glm::vec4(1.0f)); + entities.push_back(std::move(entity)); + + modelElement = modelElement->NextSiblingElement("model"); + } +} + +void Scene::draw(const RenderPipeline &pipeline) const { + for (const std::unique_ptr &entity : this->entities) { + entity->draw(pipeline); + } +} + +int Scene::getWindowWidth() const { + return this->windowWidth; +} + +int Scene::getWindowHeight() const { + return this->windowHeight; +} + +Camera &Scene::getCamera() { + return this->camera; +} + +void Scene::setWindowSize(int width, int height) { + this->windowWidth = width; + this->windowHeight = height; +} + +} diff --git a/src/engine/SceneWindow.cpp b/src/engine/SceneWindow.cpp index e128166d..c8a03f8d 100644 --- a/src/engine/SceneWindow.cpp +++ b/src/engine/SceneWindow.cpp @@ -14,28 +14,25 @@ #include #include +#include #include "engine/SceneWindow.hpp" namespace engine { -SceneWindow::SceneWindow() : Window("CG 2024/25", 640, 480), pipeline(), translate() { - // Only do this once, as we have a single shader program - // TODO - in the future, remove this, as this is just for testing - camera = Camera(glm::vec3(0.0f, 0.0f, 5.0f), - glm::vec3(0.0f, 0.0f, 0.0f), - glm::vec3(0.0f, 1.0f, 0.0f), - 60.0f, - 0.01f, - 1000.0f); +SceneWindow::SceneWindow(const std::string &sceneFile) : + Window(sceneFile, 640, 480), pipeline(), scene(sceneFile) { + this->resize(scene.getWindowWidth(), scene.getWindowHeight()); + // Only do this once, as we have a single shader program this->pipeline.use(); - // TODO - in the future, remove this, as this is just for testing - const utils::WavefrontOBJ object("box.3d"); - this->model = std::make_unique(object); + // TODO - remove when all of the figures are using the right-hand rule + glFrontFace(GL_CCW); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINES); } void SceneWindow::onUpdate(float time, float timeElapsed) { @@ -46,25 +43,27 @@ void SceneWindow::onUpdate(float time, float timeElapsed) { const int left = glfwGetKey(windowHandle, GLFW_KEY_A); const int right = glfwGetKey(windowHandle, GLFW_KEY_D); - const float cameraSpeed = 2.5f; const glm::vec3 direction((right - left), 0.0f, (down - up)); - camera.move(direction * cameraSpeed, timeElapsed); - // this->translate += direction; + this->scene.getCamera().move(direction, timeElapsed); } void SceneWindow::onRender() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.f, 0.f, 0.f, 1.f); - glm::mat4 cameraMatrix = camera.getCameraMatrix(static_cast(getWidth()) / getHeight()); + float time = glfwGetTime() * 100; + glm::mat4 rot = glm::rotate(glm::mat4(1.0f), glm::radians(time), glm::vec3(0.0f, 1.0f, 0.0f)); + + glm::mat4 cameraMatrix = + scene.getCamera().getCameraMatrix(static_cast(this->getWidth()) / this->getHeight()); - this->pipeline.setCameraMatrix(cameraMatrix); - this->pipeline.setColor(glm::vec4(1.0f, 0.0f, 0.0f, 1.0f)); - this->model->draw(); + this->pipeline.setCameraMatrix(cameraMatrix * rot); + this->scene.draw(this->pipeline); } void SceneWindow::onResize(int _width, int _height) { glViewport(0, 0, _width, _height); + scene.setWindowSize(_width, _height); } } diff --git a/src/engine/Window.cpp b/src/engine/Window.cpp index 80960488..538d040a 100644 --- a/src/engine/Window.cpp +++ b/src/engine/Window.cpp @@ -18,8 +18,10 @@ #include "engine/Window.hpp" namespace engine { + Window::Window(const std::string &title, int argWidth, int argHeight) : width(argWidth), height(argHeight) { + if (!glfwInit()) { throw std::runtime_error("Failed to initialize GLFW"); } @@ -27,6 +29,7 @@ Window::Window(const std::string &title, int argWidth, int argHeight) : glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); this->handle = glfwCreateWindow(argWidth, argHeight, title.c_str(), NULL, NULL); if (!this->handle) { glfwTerminate(); @@ -51,8 +54,10 @@ Window::~Window() { } void Window::runLoop() { - double time = glfwGetTime(); + glfwShowWindow(this->handle); this->onResize(this->width, this->height); + + double time = glfwGetTime(); while (!glfwWindowShouldClose(this->handle)) { glfwSetWindowSizeCallback(this->handle, [](GLFWwindow *_handle, int _width, int _height) { Window *window = reinterpret_cast(glfwGetWindowUserPointer(_handle)); @@ -71,11 +76,15 @@ void Window::runLoop() { } } -int Window::getWidth() { +void Window::resize(int _width, int _height) { + glfwSetWindowSize(this->handle, _width, _height); +} + +int Window::getWidth() const { return this->width; } -int Window::getHeight() { +int Window::getHeight() const { return this->height; } diff --git a/src/engine/main.cpp b/src/engine/main.cpp index 1ca37fc3..6c1d4fe9 100644 --- a/src/engine/main.cpp +++ b/src/engine/main.cpp @@ -12,12 +12,20 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#include + +#include "engine/Scene.hpp" #include "engine/SceneWindow.hpp" namespace engine { int main(int argc, char **argv) { - SceneWindow window = SceneWindow(); + if (argc <= 1) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + SceneWindow window(argv[1]); window.runLoop(); return 0; }