#include "Lightkeeper.h" #include #include #include #include #include #include #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" #include #include #include #include #include "destrum/Components/MeshRendererComponent.h" #include "destrum/Components/Rotator.h" #include "destrum/Components/Spinner.h" #include "destrum/Components/OrbitAndSpin.h" #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/ModelDoc.h" #include "destrum/Components/Animator.h" #include #include #include #include #include #include #include "imgui.h" #include "components/GameComponentRegistry.h" #include "components/PrintComponent.h" #include "destrum/Components/Physics/SphereCollider.h" #include "destrum/Util/ModelDocUtils.h" namespace { std::filesystem::path ResolveScenePath( std::string_view value, const std::filesystem::path& exeDir) { if (value.empty()) { throw std::invalid_argument("Scene path cannot be empty"); } if (value.find("://") != std::string_view::npos) { return AssetFS::GetInstance().GetFullPath(value); } const std::filesystem::path path{value}; return path.is_absolute() ? path : exeDir / path; } } LightKeeper::LightKeeper() : App() { } LightKeeper::~LightKeeper() { if (!cleanedUp) { try { cleanup(); } catch (...) { } } } void LightKeeper::customInit() { RegisterGameComponents(); resources.init(gfxDevice); renderer.init(gfxDevice, resources, m_params.renderSize); const float aspectRatio = static_cast(m_params.renderSize.x) / static_cast(m_params.renderSize.y); camera.setAspectRatio(aspectRatio); ModelDoc::LoadOptions staticModelOptions{}; staticModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; staticModelOptions.loadMaterials = true; staticModelOptions.loadSkeleton = false; staticModelOptions.loadAnimations = false; camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); auto& scene = SceneManager::GetInstance().CreateScene("Main"); // scene.Add(testCube); // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg"); // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg"); const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr"); // // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png"); // const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert"); const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag"); // skyboxCubemap = std::make_unique(); skyboxCubemap->LoadCubeMap(gfxDevice, resources, skyboxID.generic_string()); skyboxCubemap->InitCubemapPipeline(gfxDevice, resources, vertShaderPath.generic_string(), fragShaderPath.generic_string()); skyboxCubemap->CreateCubeMap(gfxDevice, resources); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); const auto planeObj = scene.CreateGameObject("GroundPlane"); const auto planeMeshComp = planeObj->AddComponent(); auto planeModel = ModelDoc::LoadModel( AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string(), staticModelOptions ); ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb"); const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb"); const auto planeMeshID = resources.meshes().addMesh(gfxDevice, planePrimitive.mesh); const auto planeTexturePath = ModelDocUtils::PickTexturePath( planeModel, planePrimitive, AssetFS::GetInstance().GetFullPath("game://grass.png") ); const auto planeTextureID = resources.loadImageFromFile(gfxDevice, planeTexturePath); const auto planeMaterialID = resources.materials().addMaterial({ .baseColor = ModelDocUtils::GetImportedBaseColor( planeModel, planePrimitive), .textureFilteringMode = TextureFilteringMode::Nearest, .diffuseTexture = planeTextureID, .name = ModelDocUtils::GetImportedMaterialName( planeModel, planePrimitive, "GroundPlaneMaterial"), }); planeMeshComp->SetMeshID(planeMeshID); planeMeshComp->SetMaterialID(planeMaterialID); planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); planeObj->AddComponent(glm::vec3{10.0f, 0.5f, 10.0f}); auto* floorRb = planeObj->AddComponent(); floorRb->SetType(RigidbodyType::Static); scene.GetPhysics().RegisterGameObject(*planeObj); const auto CharObj = scene.CreateGameObject("Character"); capybara = CharObj; ModelDoc::LoadOptions characterModelOptions{}; characterModelOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; characterModelOptions.loadMaterials = true; characterModelOptions.loadSkeleton = true; characterModelOptions.loadAnimations = true; // auto charModel = ModelDoc::LoadModel( // AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(), // characterModelOptions // ); // ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx"); // // const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( // charModel, // "engine://cotw-capybara-male/source/capybara.fbx" // ); // // const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); // // const auto charTexturePath = ModelDocUtils::PickTexturePath( // charModel, // charPrimitive, // AssetFS::GetInstance().GetFullPath( // "engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs") // ); // // const auto charTextureID = resources.loadImageFromFile(gfxDevice, charTexturePath); // // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); // const auto charMaterialID = resources.materials().addMaterial({ // .baseColor = ModelDocUtils::GetImportedBaseColor( // charModel, charPrimitive), // .diffuseTexture = charTextureID, // .name = ModelDocUtils::GetImportedMaterialName( // charModel, charPrimitive, "CharacterMaterial"), // }); // // const auto charMeshComp = CharObj->AddComponent(); // charMeshComp->SetMeshID(charMeshID); // charMeshComp->SetMaterialID(charMaterialID); // // const auto animator = CharObj->AddComponent(); // animator->setSkeleton(std::move(charModel.skeleton)); // // std::string firstAnimationName; // // for (auto& clip : charModel.animations) // { // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); // // if (firstAnimationName.empty()) // firstAnimationName = clip.name; // // animator->addClip(std::make_shared(std::move(clip))); // } // // if (!firstAnimationName.empty()) // { // spdlog::info("Playing animation: '{}'", firstAnimationName); // animator->play(firstAnimationName); // } // // animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run"); // // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); // ModelDoc::LoadOptions options{}; // options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; // // auto model = ModelDoc::LoadModel( // AssetFS::GetInstance() // .GetFullPath("engine://multiple_object_test.fbx") // .generic_string(), // options // ); // // ModelDocUtils::LogModelDocSummary(model, "engine://multiple_object_test.fbx"); // // auto root = std::make_shared("MultiMeshModel"); // root->GetTransform().SetWorldPosition(glm::vec3(0.1f, 0.1f, 0.1f)); // scene.Add(root); // // for (std::size_t i = 0; i < model.primitives.size(); ++i) { // const auto &primitive = model.primitives[i]; // // const auto meshID = meshCache.addMesh(gfxDevice, primitive.mesh); // // const auto texturePath = ModelDocUtils::PickTexturePath( // model, // primitive, // AssetFS::GetInstance().GetFullPath("engine://textures/white.png") // ); // // const auto textureID = gfxDevice.loadImageFromFile(texturePath); // // const auto materialID = materialCache.addMaterial(gfxDevice, { // .baseColor = ModelDocUtils::GetImportedBaseColor( // model, primitive), // .diffuseTexture = textureID, // .name = ModelDocUtils::GetImportedMaterialName( // model, // primitive, // primitive.mesh.name + "_Material" // ), // }); // // auto part = std::make_shared( // primitive.mesh.name.empty() // ? "Primitive_" + std::to_string(i) // : primitive.mesh.name // ); // // auto meshComp = part->AddComponent(); // meshComp->SetMeshID(meshID); // meshComp->SetMaterialID(materialID); // // scene.Add(part); // } // { // const auto CharObj = scene.CreateGameObject("Character"); // // ModelDoc::LoadOptions characterOptions{}; // characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; // characterOptions.loadMaterials = true; // characterOptions.loadSkeleton = true; // characterOptions.loadAnimations = false; // // auto charModel = AssetManager::GetInstance().LoadModel("engine://characterMedium.fbx", characterOptions); // // const auto& charPrimitive = // ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( // charModel, // "engine://characterMedium.fbx" // ); // // const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); // // const auto charTextureID = resources.loadImageFromFile(gfxDevice, // AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png") // ); // // const auto charMaterialID = resources.materials().addMaterial({ // .baseColor = ModelDocUtils::GetImportedBaseColor( // charModel, charPrimitive), // .diffuseTexture = charTextureID, // .name = ModelDocUtils::GetImportedMaterialName( // charModel, // charPrimitive, // "CharacterMaterial" // ), // }); // // const auto charMeshComp = CharObj->AddComponent(); // charMeshComp->SetMeshID(charMeshID); // charMeshComp->SetMaterialID(charMaterialID); // // const auto animator = CharObj->AddComponent(); // animator->setSkeleton(charModel.skeleton); // // auto runClips = AssetManager::GetInstance().LoadAnimationClips( // "engine://run.fbx", // charModel.skeleton // ); // // for (auto& clip : runClips) // { // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); // animator->addClip(std::make_shared(std::move(clip))); // } // // if (!runClips.empty()) // { // animator->play("Root|Run"); // } // // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f)); // CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0)); // } // // auto cubeModel = ModelDoc::LoadModel( // AssetFS::GetInstance() // .GetFullPath("engine://cube.fbx") // .generic_string(), // staticModelOptions // ); // ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx"); // // const auto& cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx"); // // // const auto eliasTextuerPath = ModelDocUtils::PickTexturePath( // cubeModel, // cubePrimitive, // AssetFS::GetInstance().GetFullPath("game://grass.png") // ); // // const auto eliasTextueID = resources.loadImageFromFile(gfxDevice, eliasTextuerPath); // const auto eliasMaterialID = resources.materials().addMaterial({ // .baseColor = ModelDocUtils::GetImportedBaseColor( // planeModel, planePrimitive), // .textureFilteringMode = // TextureFilteringMode::Anisotropic, // .diffuseTexture = eliasTextueID, // .name = ModelDocUtils::GetImportedMaterialName( // planeModel, planePrimitive, "GroundPlaneMaterial"), // }); // // const auto cubeMeshID = resources.meshes().addMesh(gfxDevice, cubePrimitive.mesh); // // // // for (int i{0}; i < 100; i++) // // { // // auto cube = std::make_shared("Cube"); // // // // cube->AddComponent(glm::vec3{0.5f}); // // cube->AddComponent(); // // // // auto meshComp = cube->AddComponent(); // // meshComp->SetMeshID(cubeMeshID); // // meshComp->SetMaterialID(eliasMaterialID); // // // // cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, i, 0.0f)); // // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); // // // // scene.Add(cube); // // scene.GetPhysics().RegisterGameObject(*cube); // // } // // // const int cubeCount = 10; // // const float spacing = 1.0f; // // // // for (int x = 0; x < cubeCount; x++) // // { // // for (int y = 0; y < 3; y++) // // { // // for (int z = 0; z < cubeCount; z++) // // { // // auto cube = std::make_shared("Cube"); // // // // cube->AddComponent(glm::vec3{0.5f}); // // cube->AddComponent(); // // // // auto meshComp = cube->AddComponent(); // // meshComp->SetMeshID(cubeMeshID); // // meshComp->SetMaterialID(eliasMaterialID); // // // // cube->GetTransform().SetWorldPosition(glm::vec3( // // (x - cubeCount / 2.0f) * spacing, // // y * spacing + 0, // // (z - cubeCount / 2.0f) * spacing // // )); // // // // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); // // // // scene.Add(cube); // // scene.GetPhysics().RegisterGameObject(*cube); // // } // // } // // } // { auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", staticModelOptions); ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx"); // const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "game://sphere.fbx"); // // // const auto sphereTexturePath = ModelDocUtils::PickTexturePath( // sphereModel, // spherePrimitive, // AssetFS::GetInstance().GetFullPath("game://238882.png") // ); // // const auto sphereTextureID = resources.loadImageFromFile(gfxDevice, sphereTexturePath); // sphereMaterial = resources.materials().addMaterial({ // .baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive), // .textureFilteringMode =TextureFilteringMode::Anisotropic, // .diffuseTexture = sphereTextureID, // .name = ModelDocUtils::GetImportedMaterialName( // planeModel, planePrimitive, // "GroundPlaneMaterial"), // }); // sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh); // // } sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue"); // Trigger zone: a static box with a TriggerLoggerComponent. // Spheres spawned via the "Spawn ball" button fall through it. auto triggerObj = scene.CreateGameObject("TriggerBox"); auto triggerBox = triggerObj->AddComponent(glm::vec3{3.0f, 3.0f, 3.0f}); triggerBox->SetTrigger(true); auto triggerRb = triggerObj->AddComponent(); triggerRb->SetType(RigidbodyType::Static); triggerObj->AddComponent(); triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f}); auto playerObj = scene.CreateGameObject("Player"); playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f)); auto* fps = playerObj->AddComponent(); m_Player = playerObj; m_FPSController = fps; scene.GetPhysics().RegisterGameObject(*playerObj); const auto texPath = AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"); texID = resources.loadImageFromFile(gfxDevice, texPath); } void LightKeeper::customUpdate(float dt) { // LightKeeper owns a camera that hides App::camera; update this instance // after SDL events have been consumed. if (m_FPSController != nullptr && m_FPSController->IsMouseCaptured()) { auto& playerTransform = m_Player->GetTransform(); const glm::vec3 playerPos = playerTransform.GetWorldPosition(); camera.SetRotation(m_FPSController->GetYaw(), m_FPSController->GetPitch()); camera.m_position = playerPos + glm::vec3(0.0f, m_FPSController->GetEyeHeight(), 0.0f); camera.CalculateViewMatrix(); camera.CalculateProjectionMatrix(); } else { camera.Update(dt); } SceneManager::GetInstance().Update(dt); SceneManager::GetInstance().LateUpdate(dt); if (m_params.showDebugUi) { drawDebugMenuBar(); } LineRenderingManager::GetInstance().SubmitLine({0, 0, 0}, {0, 10, 0}, {255, 0, 0, 255}); if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1)) { renderer.setRenderWireframe(!renderer.getRenderWireframe()); } if (m_params.showDebugUi) { ImGui::Begin("Test"); if (ImGui::Button("SPawn ball")) { auto sphere = SceneManager::GetInstance().GetCurrentScene().CreateGameObject("Sphere"); sphere->AddComponent(1.5f); auto rb = sphere->AddComponent(); rb->SetMass(1000); auto meshRenderComp = sphere->AddComponent(); meshRenderComp->SetMaterialID(sphereMaterial); meshRenderComp->SetMeshID(sphereMesh); sphere->GetTransform().SetWorldPosition((std::rand() % 2) - 0.5f, 100, (std::rand() % 2) - 0.5f); sphere->GetTransform().SetWorldScale(glm::vec3{0.015f}); SceneManager::GetInstance().GetCurrentScene().GetPhysics().RefreshGameObject(*sphere); } ImGui::End(); } } void LightKeeper::drawDebugMenuBar() { if (!ImGui::BeginMainMenuBar()) { return; } if (ImGui::BeginMenu("File")) { ImGui::TextDisabled("Scene"); ImGui::SetNextItemWidth(320.0f); ImGui::InputText("Scene path", scenePath, sizeof(scenePath)); ImGui::SetNextItemWidth(320.0f); ImGui::InputText("New scene name", newSceneName, sizeof(newSceneName)); ImGui::Separator(); if (ImGui::MenuItem("New Empty Scene")) { createEmptyScene(); } if (ImGui::MenuItem("Load")) { loadScene(); } if (ImGui::MenuItem("Save")) { saveCurrentScene(); } if (!sceneStatus.empty()) { ImGui::Separator(); ImGui::TextWrapped("%s", sceneStatus.c_str()); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Render")) { ImGui::MenuItem("Box Colliders", nullptr, &renderBoxColliders); ImGui::MenuItem("2D Quads", nullptr, &renderQuads); ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } void LightKeeper::saveCurrentScene() { try { const auto path = ResolveScenePath(scenePath, m_params.exeDir); const auto parent = path.parent_path(); if (!parent.empty()) { std::filesystem::create_directories(parent); } if (SceneSerializer::Save( SceneManager::GetInstance().GetCurrentScene(), path)) { sceneStatus = "Saved scene to " + path.string(); } else { sceneStatus = "Failed to save scene to " + path.string(); } } catch (const std::exception& exception) { sceneStatus = std::string{"Save failed: "} + exception.what(); } } void LightKeeper::loadScene() { try { const auto path = ResolveScenePath(scenePath, m_params.exeDir); if (SceneSerializer::Load( SceneManager::GetInstance().GetCurrentScene(), path)) { sceneStatus = "Loaded scene from " + path.string(); } else { sceneStatus = "Failed to load scene from " + path.string(); } } catch (const std::exception& exception) { sceneStatus = std::string{"Load failed: "} + exception.what(); } } void LightKeeper::createEmptyScene() { try { const std::string name = newSceneName[0] != '\0' ? newSceneName : "EmptyScene"; auto& sceneManager = SceneManager::GetInstance(); const int newSceneIndex = sceneManager.GetSceneCount(); sceneManager.CreateScene(name); sceneManager.SwitchScene(newSceneIndex); sceneStatus = "Created and switched to scene '" + name + "'"; } catch (const std::exception& exception) { sceneStatus = std::string{"New scene failed: "} + exception.what(); } } void LightKeeper::submitBoxColliderDebugLines() { if (!renderBoxColliders) { return; } auto& sceneManager = SceneManager::GetInstance(); if (sceneManager.GetSceneCount() == 0) { return; } Scene& scene = sceneManager.GetCurrentScene(); auto& lineManager = LineRenderingManager::GetInstance(); constexpr std::array, 12> edges{{ {{0, 1}}, {{1, 3}}, {{3, 2}}, {{2, 0}}, {{4, 5}}, {{5, 7}}, {{7, 6}}, {{6, 4}}, {{0, 4}}, {{1, 5}}, {{2, 6}}, {{3, 7}}, }}; for (const auto& objectPtr : scene.GetObjects()) { if (!objectPtr || objectPtr->IsBeingDestroyed() || !objectPtr->IsActiveInHierarchy()) { continue; } const auto* boxCollider = objectPtr->GetComponent(); if (boxCollider == nullptr) { continue; } // Collider dimensions are already in world units. Physics does not // apply the GameObject scale when constructing the shape. const glm::vec3 center = boxCollider->GetCenterOffset(); const glm::vec3 halfExtents = boxCollider->GetHalfExtents(); const std::array localCorners{ center + glm::vec3{-halfExtents.x, -halfExtents.y, -halfExtents.z}, center + glm::vec3{ halfExtents.x, -halfExtents.y, -halfExtents.z}, center + glm::vec3{-halfExtents.x, halfExtents.y, -halfExtents.z}, center + glm::vec3{ halfExtents.x, halfExtents.y, -halfExtents.z}, center + glm::vec3{-halfExtents.x, -halfExtents.y, halfExtents.z}, center + glm::vec3{ halfExtents.x, -halfExtents.y, halfExtents.z}, center + glm::vec3{-halfExtents.x, halfExtents.y, halfExtents.z}, center + glm::vec3{ halfExtents.x, halfExtents.y, halfExtents.z}, }; Transform& transform = objectPtr->GetTransform(); const glm::vec3 worldPosition = transform.GetWorldPosition(); const glm::quat worldRotation = transform.GetWorldRotation(); std::array worldCorners{}; for (std::size_t index = 0; index < localCorners.size(); ++index) { worldCorners[index] = worldPosition + worldRotation * localCorners[index]; } const glm::vec4 color = boxCollider->IsTrigger() ? glm::vec4{1.0f, 0.75f, 0.1f, 1.0f} : glm::vec4{0.1f, 0.8f, 1.0f, 1.0f}; for (const auto& edge : edges) { lineManager.SubmitLine( worldCorners[edge[0]], worldCorners[edge[1]], color); } } } void LightKeeper::customDraw() { const auto cmd = gfxDevice.beginFrame(); if (cmd == VK_NULL_HANDLE) { return; } renderer.beginDrawing(gfxDevice); submitBoxColliderDebugLines(); QuadRenderingManager::GetInstance().SubmitQuad( glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f, glm::vec4{1.0f}, texID); const RenderContext ctx{ .renderer = renderer, .camera = camera, .sceneData = { .camera = camera, .ambientColor = glm::vec3(0.1f), .ambientIntensity = 0.5f, .fogColor = glm::vec3(0.5f), .fogDensity = 0.01f } }; SceneManager::GetInstance().Render(ctx); renderer.endDrawing(); const auto& drawImage = renderer.getDrawImage(); renderer.draw( cmd, gfxDevice, camera, GameRenderer::SceneData{ camera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f }); gfxDevice.endFrame( cmd, drawImage, { .clearColor = {{0.f, 0.f, 0.5f, 1.f}}, .drawImageBlitRect = glm::ivec4{}, .imguiPass = &imguiPass, }); } void LightKeeper::customCleanup() { // auto device = gfxDevice.getDevice().device; // vkDeviceWaitIdle(device); gfxDevice.waitIdle(); SceneManager::GetInstance().Destroy(); if (skyboxCubemap) { skyboxCubemap->cleanup(gfxDevice); skyboxCubemap.reset(); } renderer.cleanup(gfxDevice); } void LightKeeper::customFixedUpdate(float dt) { SceneManager::GetInstance().FixedUpdate(dt); } void LightKeeper::onWindowResize(int newWidth, int newHeight) { renderer.resize(gfxDevice, glm::ivec2{newWidth, newHeight}); const float aspectRatio = static_cast(newWidth) / static_cast(newHeight); camera.setAspectRatio(aspectRatio); }