#ifndef EDITOR_HPP #define EDITOR_HPP #include #include #include "open_engine/application.hpp" #include "open_engine/logging.hpp" #include "open_engine/renderer/texture.hpp" #include "open_engine/scene/entity.hpp" #include "open_engine/scene/scene_serializer.hpp" #include "panels/content_browser.hpp" #include "panels/scene_hierarchy.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace OpenEngine { enum class PlayState { Edit = 0, Play = 1 }; class CameraController : public NativeScriptableEntity { public: void OnCreate() { }; void OnUpdate() { auto delta = Time::DeltaTime(); if (HasComponent()) { auto& position = GetComponent().translation; if (Input::IsKeyPressed(KeyCode::Up)) position.y += 5.0 * delta; if (Input::IsKeyPressed(KeyCode::Down)) position.y -= 5.0 * delta; if (Input::IsKeyPressed(KeyCode::Right)) position.x += 5.0 * delta; if (Input::IsKeyPressed(KeyCode::Left)) position.x -= 5.0 * delta; } }; void OnDestroy() { }; }; class EditorLayer : public Layer { public: EditorLayer() : Layer("editor_layer") { } ~EditorLayer() {}; void OnAttach() override { OE_PROFILE_FUNCTION(); FramebufferSpecification specs; specs.attachments = { FramebufferTextureFormat::RGBA8, FramebufferTextureFormat::RED_INTEGER, FramebufferTextureFormat::Depth }; specs.width = 1280; specs.height = 720; framebuffer = FrameBuffer::Create(specs); scene = CreateRef(); auto command_line_args = Application::Get().GetCommandLineArgs(); if (command_line_args.count > 1) { auto scene_path = command_line_args[1]; SceneSerializer serializer(scene); serializer.Deserialize(scene_path); } editor_camera = EditorCamera(30.0f, 1920.0f/1080.0f, 0.1f, 1000.0f); icons["play"] = Texture2D::Create("resources/textures/icons/play.png"); icons["stop"] = Texture2D::Create("resources/textures/icons/stop.png"); // TODO: Add file texture. Get free icons and add license //icons["folder"] = Texture2D::Create("resources/textures/icons/folder.png"); /* for (float i = 0; i < 200; i++) { for (float y = 0; y < 200; y++) { Entity entity = scene->CreateEntity("entity"); entities.push_back(entity); auto& tc = entity.AddComponents(); tc.translation = { i / 10, y / 10, 0.0f }; tc.scale = { 0.1f, 0.1f, 1.0f }; auto& sprite = entity.AddComponents(); sprite.color = { i / 100.0f, y / 100.0f, 1.0f, 1.0f }; } } */ scene_hierarchy.Init(scene); } Ref GetIcon(const char* name) { if (icons.contains(name)) return icons[name]; return nullptr; } void OnDetach() override { } void OnUpdate() override { FramebufferSpecification spec = framebuffer->GetSpecification(); if (viewport_size.x > 0.0f && viewport_size.y > 0.0f && (spec.width != viewport_size.x || spec.height != viewport_size.y)) { OE_PROFILE_SCOPE("Setting up Rendering"); framebuffer->Resize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y); editor_camera.SetViewportSize(viewport_size.x, viewport_size.y); scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y); } framebuffer->Bind(); Renderer2D::ResetStats(); RenderCommand::SetClearColor({0.11f, 0.11f, 0.15f, 1.0f}); RenderCommand::Clear(); framebuffer->ClearBufferI(1, -1); switch (state) { case PlayState::Play: { scene->OnUpdateRuntime(); break; } case PlayState::Edit: { if (viewport_focused) editor_camera.OnUpdate(); scene->OnUpdateEditor(editor_camera); break; } } auto [mx, my] = ImGui::GetMousePos(); mx -= viewport_bounds[0].x; my -= viewport_bounds[0].y; auto viewport_size= viewport_bounds[1] - viewport_bounds[0]; my = viewport_size.y - my; int mouse_x = (int)mx; int mouse_y = (int)my; static bool clicked = false; // Mouse Picking if (Input::IsMouseButtonPressed(MouseCode::ButtonLeft) && mouse_x >= 0 && mouse_y >= 0 && mouse_x < (int)viewport_size.x && mouse_y < (int)viewport_size.y && !ImGuizmo::IsOver() && !Input::IsKeyPressed(KeyCode::LeftAlt)) { if (!clicked) { int id = framebuffer->ReadPixel(1, mouse_x, mouse_y); selected_entity = id == -1 ? Entity() : Entity((entt::entity)id, scene.get()); scene_hierarchy.SetSelectedEntity(selected_entity); } clicked = true; } else { clicked = false; selected_entity = scene_hierarchy.GetSelectedEntity(); } framebuffer->Unbind(); } bool EditorKeyBinds(KeyPressedEvent& event) { if (event.GetRepeatCount() > 0) return false; bool control = Input::IsKeyPressed(Key::LeftControl) || Input::IsKeyPressed(Key::RightControl); bool shift = Input::IsKeyPressed(Key::LeftShift) || Input::IsKeyPressed(Key::RightShift); switch(event.GetKeyCode()) { case KeyCode::Q: { guizmo_operation = -1; break; } case KeyCode::W: { guizmo_operation = ImGuizmo::OPERATION::TRANSLATE; break; } case KeyCode::E: { guizmo_operation = ImGuizmo::OPERATION::ROTATE; break; } case KeyCode::R: { guizmo_operation = ImGuizmo::OPERATION::SCALE; break; } default: break; } return true; }; void OnEvent(Event& event) override { OE_PROFILE_FUNCTION(); editor_camera.OnEvent(event); EventDispatcher dispatcher(event); dispatcher.Dispatch(BIND_EVENT_FN(EditorLayer::EditorKeyBinds)); }; float remap(float value, float minInput, float maxInput, float minOutput, float maxOutput) { // 1. Normalize the input to a 0.0 - 1.0 range float t = (value - minInput) / (maxInput - minInput); // 2. Use glm::mix to interpolate between the output range return glm::mix(minOutput, maxOutput, t); }; glm::vec3 ScreenToWorld( float screenX, float screenY, float screenZ, // depth value (0.0 = near plane, 1.0 = far plane) const glm::mat4& view, const glm::mat4& projection, int viewportWidth, int viewportHeight) { // 1. Convert screen coordinates to normalized device coordinates (NDC) // Screen space: origin at top-left, Y points down // NDC space: origin at center, range [-1, 1] for x and y float x = (2.0f * screenX) / viewportWidth - 1.0f; float y = 1.0f - (2.0f * screenY) / viewportHeight; // Flip Y float z = 2.0f * screenZ - 1.0f; // [0,1] -> [-1,1] glm::vec4 clipCoords(x, y, z, 1.0f); // 2. Transform from clip space to view space glm::mat4 invProjection = glm::inverse(projection); glm::vec4 viewCoords = invProjection * clipCoords; // 3. Transform from view space to world space glm::mat4 invView = glm::inverse(view); glm::vec4 worldCoords = invView * viewCoords; // 4. Perspective divide if (worldCoords.w != 0.0f) { worldCoords /= worldCoords.w; } return glm::vec3(worldCoords); }; void DrawViewport() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ 0, 0 }); ImGui::Begin("Viewport"); viewport_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow); viewport_hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow); Application::Get().GetImGuiLayer()->SetBlockEvents((!viewport_focused && !viewport_hovered) || ImGui::IsAnyItemActive()); ImVec2 viewport_panel_size = ImGui::GetContentRegionAvail(); viewport_size = { viewport_panel_size.x, viewport_panel_size.y }; ImVec2 imgui_cursor_position = ImGui::GetCursorScreenPos(); uint32_t texture_id = framebuffer->GetColorAttachmentRendererID(); ImGui::Image((void*)(uint64_t)texture_id, ImVec2{ viewport_size.x, viewport_size.y }, ImVec2{ 0, 1 }, ImVec2{ 1, 0 }); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("CONTENT_BROWSER_PAYLOAD")) { std::filesystem::path path = (const char*)payload->Data; if (path.extension() == ".oes") OpenScene(path); } ImGui::EndDragDropTarget(); } viewport_bounds[0] = { imgui_cursor_position.x, imgui_cursor_position.y }; viewport_bounds[1] = { imgui_cursor_position.x + viewport_size.x, imgui_cursor_position.y + viewport_size.y }; DrawGuizmos(); ImGui::End(); ImGui::PopStyleVar(); }; void DrawStats() { auto stats = Renderer2D::GetStats(); static float time = 0; time += Time::DeltaTime(); if (time >= 1) { time = 0; } ImGui::Begin("Statistics"); ImGui::SeparatorText("Performance"); ImGui::Text("FPS: %0.0f", 1 / Time::DeltaTime()); ImGui::Text("Renderer2D:"); ImGui::Text("\t\tDraw calls: %d", stats.draw_calls); ImGui::Text("\t\tQuad count: %d", stats.quad_count); ImGui::Text("\t\tVertices count: %d", stats.GetTotalVertexCount()); ImGui::Text("\t\tIndices count: %d", stats.GetTotalIndexCount()); if (selected_entity) { ImGui::SeparatorText("Entities"); ImGui::Text("Selected entity:"); ImGui::Text("\t\tname: %s", selected_entity.GetComponents().tag.c_str()); ImGui::Text("\t\tid: %d", (uint32_t)selected_entity); } ImGui::End(); }; void DrawGuizmos() { Entity selected_entity = scene_hierarchy.GetSelectedEntity(); if (selected_entity && selected_entity.HasComponent() && guizmo_operation != -1) { ImGuizmo::SetOrthographic(false); ImGuizmo::SetDrawlist(); ImGuizmo::Enable(!editor_camera.GetMoving()); ImGuizmo::SetRect(viewport_bounds[0].x, viewport_bounds[0].y, viewport_bounds[1].x - viewport_bounds[0].x, viewport_bounds[1].y - viewport_bounds[0].y); const glm::mat4& camera_projection = editor_camera.GetProjection(); glm::mat4 camera_view = editor_camera.GetViewMatrix(); auto& transform_comp = selected_entity.GetComponents(); glm::mat4 transform = GetTransformFromComp(transform_comp); bool snap = Input::IsKeyPressed(KeyCode::LeftControl); float snap_value = 0.1f; if (guizmo_operation == ImGuizmo::OPERATION::ROTATE) snap_value = 10.0f; float snap_values[] = { snap_value, snap_value, snap_value }; ImGuizmo::Manipulate(glm::value_ptr(camera_view), glm::value_ptr(camera_projection), (ImGuizmo::OPERATION)guizmo_operation, ImGuizmo::LOCAL, glm::value_ptr(transform), nullptr, snap ? snap_values : nullptr); if (ImGuizmo::IsUsing()) { glm::vec3 translation, rotation, scale; Math::DecomposeTransform(transform, translation, rotation, scale); glm::vec3 delta_rotation = rotation - transform_comp.rotation; transform_comp.translation = translation; transform_comp.rotation += delta_rotation; transform_comp.scale = scale; } } else guizmo_operation = -1; }; void OpenScene(const std::string& file) { if (!file.empty()) { scene = CreateRef(); scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y); scene_hierarchy.Init(scene); SceneSerializer serializer(scene); serializer.Deserialize(file); } } void DrawPlayBar() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0, 2 }); ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, { 0, 0 }); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); ImGui::PushStyleColor(ImGuiCol_Button, { 0, 0, 0, 0 }); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.200f, 0.207f, 0.286f, 1.0f }); ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0, 0, 0, 0 }); ImGui::Begin("##play_state_bar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar); float size = ImGui::GetContentRegionAvail().y; auto icon = state == PlayState::Play ? icons["stop"] : icons["play"]; ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x * 0.5) - (size * 0.5) ); if (ImGui::ImageButton("##play_button", (ImTextureID)icon->GetID(), { size, size })) { switch (state) { case PlayState::Play : { OnSceneStop(); break; } case PlayState::Edit : { OnScenePlay(); break; } } } ImGui::End(); ImGui::PopStyleColor(3); ImGui::PopStyleVar(3); } void OnImGuiRender() override { OE_PROFILE_FUNCTION(); ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(450, 35)); { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Save Scene As", "Ctrl+S")) { std::string file = FileDialogs::SaveFile("useless"); OE_TRACE("saving to filename: {}", file); if (!file.empty()) { SceneSerializer serializer(scene); serializer.Serialize(file); } } if (ImGui::MenuItem("Open Scene", "Ctrl+O")) { std::string file = FileDialogs::OpenFile("useless"); OE_DEBUG("loading scene: {}", file); OpenScene(file); } if (ImGui::MenuItem("New Scene", "Ctrl+N")) { scene = CreateRef(); scene->OnViewportResize((uint32_t)viewport_size.x, (uint32_t)viewport_size.y); scene_hierarchy.Init(scene); } ImGui::Separator(); if (ImGui::MenuItem("Exit")) Application::Get().Close(); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } } ImGui::DockSpaceOverViewport(); DrawStats(); scene_hierarchy.OnImGuiRender(); browser.OnImGuiRender(); DrawViewport(); DrawPlayBar(); ImGui::ShowDemoWindow(); ImGui::PopStyleVar(); }; void OnScenePlay() { state = PlayState::Play; } void OnSceneStop() { state = PlayState::Edit; } private: ContentBrowserPanel browser; std::vector profiling_results; glm::vec2 world_pos_cursor = { 0.0f, 0.0f }; glm::vec2 viewport_size = { 0.0f, 0.0f }; Ref framebuffer; glm::vec2 viewport_bounds[2]; bool viewport_focused = false, viewport_hovered = false; SceneHierarchy scene_hierarchy; EditorCamera editor_camera; int guizmo_operation = -1; // TODO: Rename this to GameState PlayState state = PlayState::Edit; std::unordered_map> icons; Ref scene; Entity selected_entity; std::vector entities; }; } class EditorApp : public OpenEngine::Application { public: EditorApp(OpenEngine::ApplicationCommandLineArgs args); ~EditorApp(); }; #endif // EDITOR_HPP /* #include #include std::string OpenFile() { char buffer[1024]; // This command opens a native GTK file picker and returns the path FILE* pipe = popen("zenity --file-selection", "r"); if (!pipe) return ""; if (fgets(buffer, sizeof(buffer), pipe) != NULL) { std::string path = buffer; path.erase(path.find_last_not_of("\n") + 1); // Clean newline pclose(pipe); return path; } pclose(pipe); return ""; } */