#include "RenderWindow.h" #include #include #include #include #include "Components/GameObject.h" #include "Components/mesh.h" #include "Components/transform.h" #include "Components/CameraComponent.h" #include "Engine/AssetManager.h" #include "Engine/Settings.h" #include "Engine/InputManager.h" #include "Engine/KeyCode.h" #include "Rendering/Shader.h" #include "ImGuizmo.h" #include "gcml.h" #include "Icons.h" #include "imgui.h" #define CAM_FOV 45.0f #define CAM_NEAR_PLAIN 0.1f #define CAM_FAR_PLAIN 2048.0f float editorYaw = -90.0f; // Horizontal angle, initialized to face along negative Z-axis float editorPitch = 0.0f; // Vertical angle float editorDistance = 5.0f; // Distance from the target glm::vec3 editorTarget(0.0f, 0.0f, 0.0f); // The point the camera orbits around // Configuration Parameters const float rotationSpeed = 0.1f; // Sensitivity for mouse rotation const float zoomSpeed = 2.0f; // Sensitivity for zooming const float movementSpeed = 5.0f; // Speed for panning const float minZoom = 2.0f; // Minimum zoom distance const float maxZoom = 20.0f; // Maximum zoom distance // Managers extern AssetManager g_AssetManager; extern Settings g_SettingsManager; extern InputManager g_InputManager; // Settings extern bool DrawBBBO; // GameObjects extern std::vector> g_GameObjects; extern std::shared_ptr g_RuntimeCameraObject; extern GameObject *g_SelectedObject; // Profilers extern int g_GPU_Triangles_drawn_to_screen; extern int g_GPU_Draw_Calls; // Simple container for six planes struct FrustumPlanes { glm::vec4 planes[6]; // Each plane is (A,B,C,D) }; // Extract 6 planes from the combined view-projection matrix inline FrustumPlanes ExtractFrustumPlanes(const glm::mat4 &vp) { FrustumPlanes frustum; // Left frustum.planes[0] = glm::vec4( vp[0][3] + vp[0][0], vp[1][3] + vp[1][0], vp[2][3] + vp[2][0], vp[3][3] + vp[3][0]); // Right frustum.planes[1] = glm::vec4( vp[0][3] - vp[0][0], vp[1][3] - vp[1][0], vp[2][3] - vp[2][0], vp[3][3] - vp[3][0]); // Bottom frustum.planes[2] = glm::vec4( vp[0][3] + vp[0][1], vp[1][3] + vp[1][1], vp[2][3] + vp[2][1], vp[3][3] + vp[3][1]); // Top frustum.planes[3] = glm::vec4( vp[0][3] - vp[0][1], vp[1][3] - vp[1][1], vp[2][3] - vp[2][1], vp[3][3] - vp[3][1]); // Near frustum.planes[4] = glm::vec4( vp[0][3] + vp[0][2], vp[1][3] + vp[1][2], vp[2][3] + vp[2][2], vp[3][3] + vp[3][2]); // Far frustum.planes[5] = glm::vec4( vp[0][3] - vp[0][2], vp[1][3] - vp[1][2], vp[2][3] - vp[2][2], vp[3][3] - vp[3][2]); // Normalize planes (optional but recommended) for (int i = 0; i < 6; i++) { float length = glm::length(glm::vec3( frustum.planes[i].x, frustum.planes[i].y, frustum.planes[i].z)); if (length > 0.0f) { frustum.planes[i] /= length; } } return frustum; } inline bool IsBoxInFrustum(const glm::vec3 &bmin, const glm::vec3 &bmax, const FrustumPlanes &frustum) { // Build the 8 corners in world space // (We assume bmin/bmax are already in world space.) std::array corners = { glm::vec3(bmin.x, bmin.y, bmin.z), glm::vec3(bmin.x, bmin.y, bmax.z), glm::vec3(bmin.x, bmax.y, bmin.z), glm::vec3(bmin.x, bmax.y, bmax.z), glm::vec3(bmax.x, bmin.y, bmin.z), glm::vec3(bmax.x, bmin.y, bmax.z), glm::vec3(bmax.x, bmax.y, bmin.z), glm::vec3(bmax.x, bmax.y, bmax.z)}; // For each plane, check if all corners are behind it. // If yes => box is completely outside. for (int p = 0; p < 6; p++) { const glm::vec4 &plane = frustum.planes[p]; int outCount = 0; for (const auto &corner : corners) { // Plane eq: A*x + B*y + C*z + D float distance = plane.x * corner.x + plane.y * corner.y + plane.z * corner.z + plane.w; if (distance < 0.0f) outCount++; } // If all corners are 'behind' the plane, box is outside if (outCount == 8) return false; } // Otherwise, it's at least partially inside return true; } bool PlayPauseButton(const char *label, bool *isPlaying, ImVec2 Size) { // Define button size // Begin the button if (ImGui::Button(label, Size)) { // Toggle the state *isPlaying = !(*isPlaying); return true; // Indicate that the state was toggled } // Add tooltip if (ImGui::IsItemHovered()) { ImGui::SetTooltip(*isPlaying ? "Pause (Space)" : "Play (Space)"); } // Get the current window's draw list ImDrawList *draw_list = ImGui::GetWindowDrawList(); // Get the position of the button ImVec2 button_pos = ImGui::GetItemRectMin(); ImVec2 button_size = ImGui::GetItemRectSize(); ImVec2 center = ImVec2(button_pos.x + button_size.x * 0.5f, button_pos.y + button_size.y * 0.5f); // Define icon size float icon_size = 0.4f * Size.x; float half_icon_size = icon_size / 2.0f; // Define colors ImU32 icon_color = ImGui::GetColorU32(ImGuiCol_Text); if (*isPlaying) { // Draw Pause Icon (two vertical bars) float bar_width = 4.0f; float spacing = 0.1f * Size.x; // Left bar ImVec2 left_bar_p1 = ImVec2(center.x - spacing - bar_width, center.y - half_icon_size); ImVec2 left_bar_p2 = ImVec2(center.x - spacing, center.y + half_icon_size); draw_list->AddRectFilled(left_bar_p1, left_bar_p2, icon_color, 2.0f); // Right bar ImVec2 right_bar_p1 = ImVec2(center.x + spacing, center.y - half_icon_size); ImVec2 right_bar_p2 = ImVec2(center.x + spacing + bar_width, center.y + half_icon_size); draw_list->AddRectFilled(right_bar_p1, right_bar_p2, icon_color, 2.0f); } else { // Draw Play Icon (triangle) ImVec2 p1 = ImVec2(center.x - half_icon_size, center.y - half_icon_size); ImVec2 p2 = ImVec2(center.x - half_icon_size, center.y + half_icon_size); ImVec2 p3 = ImVec2(center.x + half_icon_size, center.y); draw_list->AddTriangleFilled(p1, p2, p3, icon_color); } return false; // No toggle occurred } // Enum for gizmo operations enum GizmoOperation { GIZMO_TRANSLATE, GIZMO_ROTATE, GIZMO_SCALE }; // Initialize with a default operation GizmoOperation currentOperation = GIZMO_TRANSLATE; void RenderWindow::Show(bool *GameRunning, double deltaTime) { // Begin the ImGui window with an icon and label ImGui::Begin(ICON_FA_GAMEPAD " Editor##EditorWindow"); ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(false); ImGuizmo::SetDrawlist(); // Initialize OpenGL resources if not already done if (!m_Initialized) { InitGLResources(); m_Initialized = true; } // Get the available size for rendering within the window ImVec2 size = ImGui::GetContentRegionAvail(); int w = static_cast(size.x); int h = static_cast(size.y); // Check if there's space to render if (w > 0 && h > 0) { // Resize the FBO if the window size has changed if (w != m_LastWidth || h != m_LastHeight) { m_FBO.Create(w, h); m_LastWidth = w; m_LastHeight = h; } // Render the 3D scene to the FBO RenderSceneToFBO(GameRunning); // Display the rendered scene as an image in ImGui // Correctly cast the texture ID for OpenGL ImGui::Image(m_FBO.GetTextureID(), size, ImVec2(0, 0), ImVec2(1, 1)); // Calculate button position to place it slightly right and down from the top-left of the image ImVec2 imagePos = ImGui::GetItemRectMin(); // Add an offset to position the button ImVec2 buttonOffset(10.0f, 10.0f); // 10 pixels right and 10 pixels down ImVec2 buttonPos = ImVec2(imagePos.x + buttonOffset.x, imagePos.y + buttonOffset.y); // Set cursor position for the button ImGui::SetCursorScreenPos(buttonPos); // Dynamically calculate button size based on window size float buttonWidth = size.x * 0.05f; // 5% of the window width ImVec2 buttonSize = ImVec2(buttonWidth, buttonWidth); // Render the Play/Pause button with the calculated size PlayPauseButton("##PlayPauseButton", GameRunning, buttonSize); // --------------------------------------------------- // *** Gizmo Operation Selection UI *** // --------------------------------------------------- // --------------------------------------------------- // *** Integrate ImGuizmo for Manipulating Objects *** // --------------------------------------------------- // Ensure a GameObject is selected if (g_SelectedObject) { // Retrieve the TransformComponent from the selected object auto transform = g_SelectedObject->GetComponent(); if (transform) { // Get the current transformation matrix glm::mat4 modelMatrix = transform->GetTransformMatrix(); // Obtain view and projection matrices from the active camera glm::mat4 viewMatrix; glm::mat4 projectionMatrix; // Camera Selection and Setup Logic (e.g., within your rendering or update function) if (m_ActiveCamera) { // Use the existing active camera viewMatrix = m_ActiveCamera->GetViewMatrix(); projectionMatrix = m_ActiveCamera->GetProjectionMatrix(); } else { m_ActiveCamera = m_EditorCamera->GetComponent(); // 1. Mouse Input for Rotation if (g_InputManager.IsMouseButtonPressed(MouseButton::RIGHT)) { float deltaX = g_InputManager.GetMouseDeltaX(); float deltaY = g_InputManager.GetMouseDeltaY(); editorYaw += deltaX * rotationSpeed; editorPitch += deltaY * rotationSpeed; // Clamp the pitch to prevent flipping if (editorPitch > 89.0f) editorPitch = 89.0f; if (editorPitch < -89.0f) editorPitch = -89.0f; } // 2. Scroll Input for Zooming float scrollDelta = g_InputManager.GetScrollDelta(); editorDistance -= scrollDelta * zoomSpeed; editorDistance = glm::clamp(editorDistance, minZoom, maxZoom); // 3. Keyboard Input for Panning (WASD) glm::vec3 forward = glm::normalize(editorTarget - m_EditorCamera->GetComponent()->GetPosition()); glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0.0f, 1.0f, 0.0f))); glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); deltaTime = static_cast(deltaTime); if (g_InputManager.IsKeyPressed(KeyCode::W)) editorTarget += up.y * movementSpeed * deltaTime; if (g_InputManager.IsKeyPressed(KeyCode::S)) editorTarget -= up.y * movementSpeed * deltaTime; if (g_InputManager.IsKeyPressed(KeyCode::A)) editorTarget -= right.x * movementSpeed * deltaTime; if (g_InputManager.IsKeyPressed(KeyCode::D)) editorTarget += right.x * movementSpeed * deltaTime; // 4. Calculate the New Camera Position glm::vec3 direction; direction.x = cos(glm::radians(editorYaw)) * cos(glm::radians(editorPitch)); direction.y = sin(glm::radians(editorPitch)); direction.z = sin(glm::radians(editorYaw)) * cos(glm::radians(editorPitch)); direction = glm::normalize(direction); glm::vec3 newPosition = editorTarget - direction * editorDistance; // 5. Update the Editor Camera's Transform auto editorTransform = m_EditorCamera->GetComponent(); editorTransform->SetPosition(newPosition.x,newPosition.y,newPosition.z); editorTransform->LookAt(editorTarget, up); // 6. Retrieve Updated Matrices viewMatrix = m_ActiveCamera->GetViewMatrix(); projectionMatrix = m_ActiveCamera->GetProjectionMatrix(); static ImGuizmo::OPERATION currentOperation = ImGuizmo::TRANSLATE; if (ImGui::IsWindowFocused() && ImGui::IsWindowHovered()) { if (ImGui::IsKeyPressed(ImGuiKey_T)) { currentOperation = ImGuizmo::TRANSLATE; } if (ImGui::IsKeyPressed(ImGuiKey_R)) { currentOperation = ImGuizmo::ROTATE; } if (ImGui::IsKeyPressed(ImGuiKey_S)) { currentOperation = ImGuizmo::SCALE; } } // Define snap settings bool snap = false; // Enable snapping if needed float snapValue = 0.1f; // Snap increment // Set the ImGuizmo rectangle to the window's position and size ImVec2 windowPos = ImGui::GetWindowPos(); ImVec2 windowSize = ImGui::GetWindowSize(); ImGuizmo::SetRect(windowPos.x, windowPos.y, windowSize.x, windowSize.y); // Render the gizmo and handle user interaction projectionMatrix[1][1] *= -1.0f; // Flip Image Internaly ImGuizmo::Manipulate( glm::value_ptr(viewMatrix), glm::value_ptr(projectionMatrix), currentOperation, ImGuizmo::LOCAL, glm::value_ptr(modelMatrix), nullptr, // Optional delta matrix snap ? &snapValue : nullptr // Optional snap values ); // Check if the gizmo is being used (i.e., if the user is interacting with it) if (ImGuizmo::IsUsing()) { // Update the TransformComponent with the modified matrix transform->SetTransformMatrix(modelMatrix); } } } } } else { // Display message if there's insufficient space to render the scene ImGui::Text("No space to render."); } // End the ImGui window ImGui::End(); } void RenderWindow::InitGLResources() { // ---------------------------------------------------- // 1) Load SHADER from the asset manager // ---------------------------------------------------- // throw this in here cus we dont have a constructor m_ActiveCamera = nullptr; m_EditorCamera = std::make_shared(-1, "EditorCamera"); // Setup the editor camera m_EditorCamera->AddComponent(std::make_shared()); m_EditorCamera->AddComponent(std::make_shared()); { std::shared_ptr shaderAsset = g_AssetManager.loadAsset(AssetType::SHADER, "assets/shaders/UnlitMaterial"); if (!shaderAsset) { fprintf(stderr, "[RenderWindow] Failed to load shader via AssetManager.\n"); return; } // Cast back to your Shader class m_ShaderPtr = shaderAsset.get(); } { std::shared_ptr shaderAsset = g_AssetManager.loadAsset(AssetType::SHADER, "assets/shaders/Line"); if (!shaderAsset) { fprintf(stderr, "[RenderWindow] Failed to load shader via AssetManager.\n"); return; } // Cast back to your Shader class m_LineShaderPtr = shaderAsset.get(); } // ---------------------------------------------------- // 3) Load TEXTURE from the asset manager // ---------------------------------------------------- { std::shared_ptr texAsset = g_AssetManager.loadAsset(AssetType::TEXTURE, "assets/textures/wood.png"); if (!texAsset) { fprintf(stderr, "[RenderWindow] Failed to load texture.\n"); } else { // Cast from void* to GLuint m_TextureID = *texAsset; // Assign the GLuint value } } // ---------------------------------------------------- // 4) Initialize GameObjects // ---------------------------------------------------- } void CheckOpenGLError(const std::string &location) { GLenum err; bool hasError = false; while ((err = glGetError()) != GL_NO_ERROR) { std::cerr << "[OpenGL Error] (" << err << ") at " << location << std::endl; hasError = true; } if (hasError) { // Optionally, you can throw an exception or handle the error as needed } } #include // For glm::value_ptr #include // Ensure is included void RenderWindow::RenderSceneToFBO(bool *GameRunning) { m_RotationAngle += 0.001f; // Spin per frame // 1) Bind the FBO and set up viewport m_FBO.Bind(); glViewport(0, 0, m_LastWidth, m_LastHeight); #if TRANSPERANCY glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); #endif glEnable(GL_DEPTH_TEST); glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 2) Check our main rendering shader if (!m_ShaderPtr) { DEBUG_PRINT("[RenderWindow] Shader pointer is null. Cannot render."); m_FBO.Unbind(); return; } m_ShaderPtr->Use(); // 3) Obtain view/projection from the active camera (or fallback) std::shared_ptr activeCamera = nullptr; glm::mat4 view, proj; if (*GameRunning && g_RuntimeCameraObject) { activeCamera = g_RuntimeCameraObject; } if (activeCamera) { // TODO: Add camera Movement in editor view = activeCamera->GetViewMatrix(); proj = activeCamera->GetProjectionMatrix(); m_ActiveCamera = activeCamera; } else { // Fallback if no camera view = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -5.f)); float aspect = (m_LastHeight != 0) ? (float)m_LastWidth / (float)m_LastHeight : 1.0f; proj = glm::perspective(glm::radians(CAM_FOV), aspect, CAM_NEAR_PLAIN, CAM_FAR_PLAIN); m_ActiveCamera = nullptr; } // 4) Extract frustum planes for culling glm::mat4 vp = proj * view; FrustumPlanes frustum = ExtractFrustumPlanes(vp); // 5) Iterate over each GameObject and render glm::vec4 LineColor = g_SettingsManager.S_LineColor; for (auto &obj : g_GameObjects) { glm::mat4 model = glm::mat4(1.f); auto transform = obj->GetComponent(); auto mesh = obj->GetComponent(); if (transform && mesh) { // 5A) Build the Model matrix model = glm::translate(model, transform->position); model = glm::rotate(model, glm::radians(transform->rotation.x), glm::vec3(1.f, 0.f, 0.f)); model = glm::rotate(model, glm::radians(transform->rotation.y), glm::vec3(0.f, 1.f, 0.f)); model = glm::rotate(model, glm::radians(transform->rotation.z), glm::vec3(0.f, 0.f, 1.f)); model = glm::scale(model, transform->scale); // 5B) For each submesh for (auto &submesh : mesh->submeshes) { // Check if VAO is valid if (submesh.vao == 0) { DEBUG_PRINT("[RenderWindow] Warning: Submesh VAO is not initialized."); continue; } // ------------------------- // *** FRUSTUM CULLING *** // Transform submesh bounding box from local -> world space glm::vec3 worldMin(FLT_MAX), worldMax(-FLT_MAX); std::array corners = { glm::vec3(submesh.minExtents.x, submesh.minExtents.y, submesh.minExtents.z), glm::vec3(submesh.minExtents.x, submesh.minExtents.y, submesh.maxExtents.z), glm::vec3(submesh.minExtents.x, submesh.maxExtents.y, submesh.minExtents.z), glm::vec3(submesh.minExtents.x, submesh.maxExtents.y, submesh.maxExtents.z), glm::vec3(submesh.maxExtents.x, submesh.minExtents.y, submesh.minExtents.z), glm::vec3(submesh.maxExtents.x, submesh.minExtents.y, submesh.maxExtents.z), glm::vec3(submesh.maxExtents.x, submesh.maxExtents.y, submesh.minExtents.z), glm::vec3(submesh.maxExtents.x, submesh.maxExtents.y, submesh.maxExtents.z)}; for (auto &c : corners) { // Transform corner by the model matrix glm::vec4 worldPos = model * glm::vec4(c, 1.0f); // Expand worldMin / worldMax worldMin.x = std::min(worldMin.x, worldPos.x); worldMin.y = std::min(worldMin.y, worldPos.y); worldMin.z = std::min(worldMin.z, worldPos.z); worldMax.x = std::max(worldMax.x, worldPos.x); worldMax.y = std::max(worldMax.y, worldPos.y); worldMax.z = std::max(worldMax.z, worldPos.z); } // Now test that box against frustum if (!IsBoxInFrustum(worldMin, worldMax, frustum)) { // The submesh is completely outside the camera's view => skip continue; } // ------------------------- // 5C) Compute MVP and pass to shader glm::mat4 mvp = vp * model; m_ShaderPtr->SetMat4("uMVP", mvp); m_ShaderPtr->SetMat4("uModel", model); // 5D) Update tri count g_GPU_Triangles_drawn_to_screen += static_cast(submesh.indices.size() / 3); // 5E) Bind textures const int MAX_DIFFUSE = 32; int textureUnit = 0; for (auto &texture : submesh.textures) { if (texture.type == "texture_diffuse") { if (textureUnit >= MAX_DIFFUSE) { DEBUG_PRINT("[RenderWindow] Warning: Exceeded maximum diffuse textures."); break; } glActiveTexture(GL_TEXTURE0 + textureUnit); glBindTexture(GL_TEXTURE_2D, texture.id); std::string uniformName = "uTextures.texture_diffuse[" + std::to_string(textureUnit) + "]"; m_ShaderPtr->SetInt(uniformName, textureUnit); textureUnit++; } } // Assign default texture to unused slots for (int i = textureUnit; i < MAX_DIFFUSE; ++i) { std::string uniformName = "uTextures.texture_diffuse[" + std::to_string(i) + "]"; m_ShaderPtr->SetInt(uniformName, 0); } m_ShaderPtr->SetInt("uNumDiffuseTextures", textureUnit); // 5F) Draw submesh (filled) glBindVertexArray(submesh.vao); glDrawElements(GL_TRIANGLES, static_cast(submesh.indices.size()), GL_UNSIGNED_INT, nullptr); g_GPU_Draw_Calls++; glBindVertexArray(0); glActiveTexture(GL_TEXTURE0); // ---------------------------------------- // 5G) OPTIONAL: Draw a bounding box outline // ---------------------------------------- if (DrawBBBO && m_LineShaderPtr && submesh.bboxVAO != 0) { // Use a simpler line shader that doesn't rely on textures m_LineShaderPtr->Use(); // Recompute the MVP for the bounding box (same model, same vp) glm::mat4 bboxMVP = vp * model; m_LineShaderPtr->SetMat4("uMVP", bboxMVP); m_LineShaderPtr->SetVec4("uColor", LineColor); glLineWidth(2.0f); // If your line shader has a uniform color: // m_LineShaderPtr->SetVec4("uColor", glm::vec4(1,1,0,1)); // e.g., yellow // Draw the bounding box in wireframe lines glBindVertexArray(submesh.bboxVAO); // We assume submesh.bboxVertexCount is the number of line-vertices glDrawArrays(GL_LINES, 0, submesh.bboxVertexCount); glBindVertexArray(0); glUseProgram(0); } } } } #if TRANSPERANCY glDisable(GL_BLEND); #endif glUseProgram(0); // 6) Unbind the FBO m_FBO.Unbind(); }