diff --git a/.gitmodules b/.gitmodules index 0109930..c4d9b71 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "third_party/tracy"] path = third_party/tracy url = https://github.com/wolfpld/tracy.git +[submodule "third_party/ImGuizmo"] + path = third_party/ImGuizmo + url = https://github.com/CedricGuillemet/ImGuizmo.git diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 151e498..cf177ec 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(${NAME}_deps INTERFACE) target_link_libraries(${NAME}_deps INTERFACE ${ENGINE_NAME} + imguizmo ) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) diff --git a/editor/editor_camera.cpp b/editor/editor_camera.cpp new file mode 100644 index 0000000..6f4afd7 --- /dev/null +++ b/editor/editor_camera.cpp @@ -0,0 +1,99 @@ +#include "editor_camera.hpp" + +#include +#include + +namespace { + +constexpr float ORBIT_RADIANS_PER_PIXEL = 0.005f; +const float MAX_PITCH = glm::radians(89.f); +// Each wheel step moves this fraction of the way towards the pivot. +constexpr float ZOOM_STEP = 0.1f; +constexpr float MIN_DISTANCE = 0.05f; +constexpr float MIN_ORTHO_HEIGHT = 0.01f; +// Fly speed per unit of distance from the pivot, so it suits any scale. +constexpr float FLY_SPEED = 1.5f; + +} // namespace + +void EditorCamera::orbit(glm::vec2 pixels) { + if (mode != Mode::Scene3D) { + return; + } + yaw -= pixels.x * ORBIT_RADIANS_PER_PIXEL; + pitch = std::clamp(pitch - pixels.y * ORBIT_RADIANS_PER_PIXEL, -MAX_PITCH, + MAX_PITCH); +} + +void EditorCamera::pan(glm::vec2 pixels, float viewportHeight) { + if (viewportHeight <= 0.f) { + return; + } + + // World units one pixel covers at the pivot's depth. + float unitsPerPixel = + mode == Mode::Scene3D + ? 2.f * distance * std::tan(fov / 2.f) / viewportHeight + : orthoHeight / viewportHeight; + + // Screen y runs down, world y up. Dragging moves the scene with the mouse, + // so the camera moves the other way. + glm::vec3 offset{-pixels.x * unitsPerPixel, pixels.y * unitsPerPixel, 0.f}; + pivot += mode == Mode::Scene3D ? getRotation() * offset : offset; +} + +void EditorCamera::zoom(float steps) { + float scale = std::pow(1.f - ZOOM_STEP, steps); + if (mode == Mode::Scene3D) { + distance = std::max(distance * scale, MIN_DISTANCE); + } else { + orthoHeight = std::max(orthoHeight * scale, MIN_ORTHO_HEIGHT); + } +} + +void EditorCamera::fly(glm::vec3 direction, float deltaTime) { + if (mode != Mode::Scene3D || direction == glm::vec3{0.f}) { + return; + } + float speed = FLY_SPEED * std::max(distance, 1.f); + pivot += getRotation() * glm::normalize(direction) * speed * deltaTime; +} + +void EditorCamera::frame(glm::vec3 target, float radius) { + pivot = target; + radius = std::max(radius, 0.1f); + // Far enough that the sphere fits in the vertical field of view. + distance = radius / std::sin(fov / 2.f); + orthoHeight = radius * 2.5f; +} + +sophia::TransformComponent EditorCamera::getTransform() const { + sophia::TransformComponent transform; + if (mode == Mode::Scene3D) { + transform.rotation = getRotation(); + transform.position = + pivot + transform.rotation * glm::vec3{0.f, 0.f, distance}; + } else { + transform.position = pivot; + } + return transform; +} + +sophia::CameraComponent EditorCamera::getCamera() const { + sophia::CameraComponent camera; + if (mode == Mode::Scene3D) { + camera.projection = sophia::CameraComponent::Projection::Perspective; + camera.perspective.fov = fov; + // Room to zoom far out without the scene clipping. + camera.perspective.farPlane = 1000.f; + } else { + camera.projection = sophia::CameraComponent::Projection::Orthographic; + camera.ortho.height = orthoHeight; + } + return camera; +} + +glm::quat EditorCamera::getRotation() const { + return glm::angleAxis(yaw, glm::vec3{0.f, 1.f, 0.f}) * + glm::angleAxis(pitch, glm::vec3{1.f, 0.f, 0.f}); +} diff --git a/editor/editor_camera.hpp b/editor/editor_camera.hpp new file mode 100644 index 0000000..52ca28b --- /dev/null +++ b/editor/editor_camera.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +/** + * The Scene panel's camera. Not part of any scene the editor edits, and never + * saved with one. + * + * In 3D it orbits a pivot point at some distance; in 2D it looks straight + * down -Z at the pivot through an orthographic projection, like a 2D game + * camera. + */ +class EditorCamera { + public: + enum class Mode { Scene3D, Scene2D }; + + Mode mode = Mode::Scene3D; + + // Turns around the pivot by a mouse movement in pixels. 3D only. + void orbit(glm::vec2 pixels); + // Moves the pivot so the scene follows a mouse movement in pixels across a + // viewport `viewportHeight` pixels tall. + void pan(glm::vec2 pixels, float viewportHeight); + // Moves in (positive) or out by mouse wheel steps. + void zoom(float steps); + // Moves the pivot along the camera's axes: x right, y up, z backwards. + // `direction` needn't be normalized. 3D only. + void fly(glm::vec3 direction, float deltaTime); + // Centers on `target`, backing off enough to see a sphere of `radius`. + void frame(glm::vec3 target, float radius); + + sophia::TransformComponent getTransform() const; + sophia::CameraComponent getCamera() const; + + private: + glm::quat getRotation() const; + + glm::vec3 pivot{0.f, 0.f, 0.f}; + + // 3D: looking at the pivot from in front, a little to the right and above. + float yaw = glm::radians(30.f); + float pitch = glm::radians(-25.f); + float distance = 8.f; + float fov = glm::radians(60.f); + + // 2D: world units visible from the bottom of the view to the top. + float orthoHeight = 10.f; +}; diff --git a/editor/editor_context.hpp b/editor/editor_context.hpp new file mode 100644 index 0000000..92a5f13 --- /dev/null +++ b/editor/editor_context.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/** + * What the editor is working on, shared by its panels. Panels read it every + * frame instead of holding on to a scene, so switching scenes is one call. + */ +class EditorContext { + public: + // A scene the editor has open, and can switch to. + struct OpenScene { + sophia::SceneHandle handle; + std::string name; + }; + + explicit EditorContext(sophia::SceneRegistry& scenes) : scenes{scenes} {} + + // Creates an open scene with a "Main Camera", without switching to it. + sophia::SceneHandle createScene(std::string name) { + sophia::SceneHandle handle = scenes.create(); + scenes.get(handle).createCamera("Main Camera"); + openScenes.push_back({handle, std::move(name)}); + return handle; + } + + const std::vector& getOpenScenes() const { return openScenes; } + + // Null if `handle` isn't an open scene. + const std::string* getSceneName(sophia::SceneHandle handle) const { + for (const OpenScene& open : openScenes) { + if (open.handle == handle) { + return &open.name; + } + } + return nullptr; + } + + void renameScene(sophia::SceneHandle handle, std::string name) { + for (OpenScene& open : openScenes) { + if (open.handle == handle) { + open.name = std::move(name); + } + } + } + + bool hasScene() const { return scenes.isAlive(activeScene); } + sophia::SceneHandle getSceneHandle() const { return activeScene; } + sophia::Scene& getScene() { return scenes.get(activeScene); } + + // Also clears the selection, which belonged to the previous scene. + void setScene(sophia::SceneHandle handle) { + activeScene = handle; + selected = {}; + } + + // Null when nothing is selected. Always an entity of the active scene. + sophia::Entity selected; + + private: + sophia::SceneRegistry& scenes; + sophia::SceneHandle activeScene = sophia::INVALID_HANDLE; + // Kept open even when not active: with no saving yet, closing one would + // lose it. + std::vector openScenes; +}; + +// Points `view` at the context's active scene, and renders it only while +// there is one and its panel is `shown`. Call every frame from the panel that +// owns the view. +inline void syncSceneView(EditorContext& context, + sophia::SceneViewRegistry& sceneViews, + sophia::SceneViewHandle view, bool shown) { + bool enabled = shown && context.hasScene(); + const sophia::SceneView& current = sceneViews.peek(view); + // get() re-sorts the render queue, so only use it when something changed. + if (current.enabled != enabled || + (context.hasScene() && current.scene != context.getSceneHandle())) { + sophia::SceneView& updated = sceneViews.get(view); + updated.enabled = enabled; + updated.scene = context.getSceneHandle(); + } +} diff --git a/editor/main.cpp b/editor/main.cpp index 98c410e..e9d6550 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -1,49 +1,86 @@ -#include #include #include #include +#include +#include +#include "editor_context.hpp" #include "panels/asset_browser_panel.hpp" +#include "panels/game_panel.hpp" #include "panels/inspector_panel.hpp" #include "panels/scene_hierarchy_panel.hpp" -#include "panels/viewport_panel.hpp" +#include "panels/scene_panel.hpp" +#include "utilities/logger.hpp" class Editor : public sophia::Application { public: Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {} void onCreate() override { - std::cout << "Editor created" << std::endl; + log("Editor created"); debugUI().show(); textures.emplace_back("Checker", createCheckerTexture()); + cubeMesh = createShadedCube(); - sophia::SceneHandle sceneHandle = scenes().create(); - sophia::Scene& scene = scenes().get(sceneHandle); - createDemoScene(scene); + sophia::SceneHandle sceneHandle = context.createScene("Demo"); + createDemoScene(scenes().get(sceneHandle)); + context.setScene(sceneHandle); - panels().emplace(scene, selected); - panels().emplace(selected, textures); - panels().emplace(); + // The editor's own scene, never edited or rendered. It holds the camera + // the Scene panel looks through, which a SceneView may take from any + // scene. + sophia::SceneHandle editorSceneHandle = scenes().create(); + sophia::Entity editorCamera = + scenes().get(editorSceneHandle).createCamera("Editor Camera"); + + // Views start without a scene; their panels point them at the active + // scene each frame. sophia::RenderTargetHandle sceneTarget = resources().create(); - sceneViews().create({.scene = sceneHandle, .target = sceneTarget}); - panels().emplace(ui(), sceneTarget, scene, selected); + sophia::SceneViewHandle sceneView = sceneViews().create( + {.target = sceneTarget, .camera = editorCamera, .enabled = false}); + + // No camera: the Game panel shows the scene through its active one. + sophia::RenderTargetHandle gameTarget = + resources().create(); + sophia::SceneViewHandle gameView = + sceneViews().create({.target = gameTarget, .enabled = false}); + + panels().emplace(context, cubeMesh); + panels().emplace(context, textures); + panels().emplace(); + viewPanels.push_back( + {panels().emplace(ui(), resources(), context, sceneViews(), + sceneView, sceneTarget, editorCamera), + sceneView}); + viewPanels.push_back( + {panels().emplace(ui(), context, sceneViews(), gameView, + gameTarget), + gameView}); panels().setDefaultLayout( {.left = {"Scene Hierarchy"}, .right = {"Inspector", "Frame Stats", "PanelRegistry"}, .bottom = {"Asset Browser", "Console"}, - .center = {"Viewport"}}); + .center = {"Scene", "Game"}}); } - void onUpdate(const sophia::FrameInfo& frameInfo) override {} + void onUpdate(const sophia::FrameInfo& frameInfo) override { + // A closed panel's onUpdate() doesn't run, so it can't stop its view + // rendering itself. + for (const ViewPanel& viewPanel : viewPanels) { + if (!panels().get(viewPanel.panel).isVisible() && + sceneViews().peek(viewPanel.view).enabled) { + sceneViews().get(viewPanel.view).enabled = false; + } + } + } private: + // Adds to a new scene, which already has a camera. void createDemoScene(sophia::Scene& scene) { - scene.createCamera("Main Camera"); - sophia::Entity checker = scene.createEntity("Checker"); checker.getComponent().position = {-1.5f, 0.f, 0.f}; @@ -63,6 +100,27 @@ class Editor : public sophia::Application { sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f}, .size = {2.f, 2.f}, .sortOrder = 1}); + + sophia::Entity cube = scene.createEntity("Cube"); + auto& cubeTransform = cube.getComponent(); + cubeTransform.position = {0.f, 2.5f, 0.f}; + cubeTransform.rotation = + glm::quat{glm::radians(glm::vec3{30.f, 45.f, 0.f})}; + cube.addComponent(sophia::MeshComponent{ + .handle = cubeMesh, .color = {0.4f, 0.8f, 0.5f, 1.f}}); + } + + // Meshes are unlit, so a plain cube is a flat silhouette. Bake a fixed + // light into its vertex colors so its faces read in 3D until there's real + // lighting; MeshComponent::color still tints it. + sophia::MeshHandle createShadedCube() { + const glm::vec3 lightDirection = glm::normalize(glm::vec3{0.4f, 1.f, 0.7f}); + sophia::MeshData data = sophia::primitives::cube(); + for (sophia::MeshVertex& vertex : data.vertices) { + float light = glm::dot(vertex.normal, lightDirection) * 0.5f + 0.5f; + vertex.color = glm::vec4{glm::vec3{0.35f + 0.65f * light}, 1.f}; + } + return resources().create(data); } sophia::TextureHandle createCheckerTexture() { @@ -79,8 +137,17 @@ class Editor : public sophia::Application { return resources().create(SIZE, SIZE, std::move(pixels)); } - sophia::Entity selected; + // A panel showing a scene view, which should only render while the panel + // is on screen. + struct ViewPanel { + sophia::PanelHandle panel; + sophia::SceneViewHandle view; + }; + + EditorContext context{scenes()}; + std::vector viewPanels; InspectorPanel::TextureList textures; + sophia::MeshHandle cubeMesh; }; SOPHIA_MAIN(Editor) diff --git a/editor/panels/game_panel.cpp b/editor/panels/game_panel.cpp new file mode 100644 index 0000000..4b193cc --- /dev/null +++ b/editor/panels/game_panel.cpp @@ -0,0 +1,20 @@ +#include "game_panel.hpp" + +#include + +void GamePanel::onUpdate(const sophia::FrameInfo&) { + syncSceneView(context, sceneViews, view, isShown()); + + if (!context.hasScene()) { + ImGui::TextDisabled("No scene open"); + return; + } + + const ImVec2 origin = ImGui::GetCursorScreenPos(); + ui.image(target); + + if (!context.getScene().getActiveCamera()) { + ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f)); + ImGui::TextDisabled("The scene has no active camera"); + } +} diff --git a/editor/panels/game_panel.hpp b/editor/panels/game_panel.hpp new file mode 100644 index 0000000..687d8b6 --- /dev/null +++ b/editor/panels/game_panel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../editor_context.hpp" + +/** + * Shows the active scene through its own active camera: what the game will + * see. + */ +class GamePanel : public sophia::Panel { + public: + // Renders into `target` through `view`, a SceneView with no camera of its + // own that this panel keeps pointed at the active scene. + GamePanel(sophia::UI& ui, EditorContext& context, + sophia::SceneViewRegistry& sceneViews, sophia::SceneViewHandle view, + sophia::RenderTargetHandle target) + : Panel{{.name = "Game", + .windowFlags = ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse}}, + ui{ui}, + context{context}, + sceneViews{sceneViews}, + view{view}, + target{target} {} + + void onUpdate(const sophia::FrameInfo& frameInfo) override; + + private: + sophia::UI& ui; + EditorContext& context; + sophia::SceneViewRegistry& sceneViews; + sophia::SceneViewHandle view; + sophia::RenderTargetHandle target; +}; diff --git a/editor/panels/inspector_panel.cpp b/editor/panels/inspector_panel.cpp index 4592a5c..09eb269 100644 --- a/editor/panels/inspector_panel.cpp +++ b/editor/panels/inspector_panel.cpp @@ -6,6 +6,7 @@ #include void InspectorPanel::onUpdate(const sophia::FrameInfo&) { + selected = context.selected; if (!selected) { ImGui::TextDisabled("No entity selected"); return; @@ -16,6 +17,7 @@ void InspectorPanel::onUpdate(const sophia::FrameInfo&) { drawTransform(); drawCamera(); drawSprite(); + drawMesh(); ImGui::Separator(); drawAddComponent(); @@ -40,11 +42,13 @@ void InspectorPanel::drawTransform() { auto& transform = selected.getComponent(); ImGui::DragFloat3("Position", &transform.position.x, 0.1f); - // 2D only: edits rotation around Z and drops any X/Y rotation. - float degrees = glm::degrees(glm::eulerAngles(transform.rotation).z); - if (ImGui::DragFloat("Rotation", °rees, 1.0f)) { - transform.rotation = - glm::angleAxis(glm::radians(degrees), glm::vec3{0.f, 0.f, 1.f}); + // Euler angles in degrees, recomputed from the quaternion every frame. + // Near 90 degrees of pitch the angles can jump between equivalent values + // while dragging. + // TODO: keep each entity's edited angles, or replace with gizmo rotation. + glm::vec3 degrees = glm::degrees(glm::eulerAngles(transform.rotation)); + if (ImGui::DragFloat3("Rotation", °rees.x, 1.0f)) { + transform.rotation = glm::quat{glm::radians(degrees)}; } ImGui::DragFloat3("Scale", &transform.scale.x, 0.1f); @@ -118,6 +122,29 @@ void InspectorPanel::drawSprite() { } } +void InspectorPanel::drawMesh() { + if (!selected.hasComponent()) { + return; + } + + bool open = true; + if (!ImGui::CollapsingHeader("Mesh", &open, ImGuiTreeNodeFlags_DefaultOpen)) { + if (!open) { + selected.removeComponent(); + } + return; + } + + auto& mesh = selected.getComponent(); + // TODO: pick the mesh once assets have names. + ImGui::Text("Mesh: %u", static_cast(mesh.handle.index)); + ImGui::ColorEdit3("Color", &mesh.color.x); + + if (!open) { + selected.removeComponent(); + } +} + void InspectorPanel::drawAddComponent() { if (ImGui::Button("Add Component")) { ImGui::OpenPopup("AddComponent"); diff --git a/editor/panels/inspector_panel.hpp b/editor/panels/inspector_panel.hpp index 429d302..3816ed4 100644 --- a/editor/panels/inspector_panel.hpp +++ b/editor/panels/inspector_panel.hpp @@ -8,14 +8,16 @@ #include #include +#include "../editor_context.hpp" + class InspectorPanel : public sophia::Panel { public: // Stand-in for an asset browser: the textures a sprite can pick from. using TextureList = std::vector>; - InspectorPanel(sophia::Entity& selected, const TextureList& textures) - : Panel{{.name = "Inspector"}}, selected{selected}, textures{textures} {} + InspectorPanel(EditorContext& context, const TextureList& textures) + : Panel{{.name = "Inspector"}}, context{context}, textures{textures} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; @@ -24,8 +26,11 @@ class InspectorPanel : public sophia::Panel { void drawTransform(); void drawCamera(); void drawSprite(); + void drawMesh(); void drawAddComponent(); - sophia::Entity& selected; + EditorContext& context; + // The selected entity, for the duration of onUpdate(). + sophia::Entity selected; const TextureList& textures; }; diff --git a/editor/panels/scene_hierarchy_panel.cpp b/editor/panels/scene_hierarchy_panel.cpp index fc22caa..ebc41f5 100644 --- a/editor/panels/scene_hierarchy_panel.cpp +++ b/editor/panels/scene_hierarchy_panel.cpp @@ -2,18 +2,37 @@ #include +#include #include +#include void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) { + drawSceneSelector(); + ImGui::Separator(); + + if (!context.hasScene()) { + ImGui::TextDisabled("No scene open"); + return; + } + + sophia::Scene& scene = context.getScene(); + sophia::Entity& selected = context.selected; + if (ImGui::Button("+ Sprite")) { selected = scene.createEntity("Sprite"); selected.addComponent(); } + ImGui::SameLine(); + if (ImGui::Button("+ Cube")) { + selected = scene.createEntity("Cube"); + selected.addComponent( + sophia::MeshComponent{.handle = cubeMesh}); + } ImGui::Separator(); // Flat for now: there is no parent/child component yet. - scene.forEachEntity([this](sophia::Entity entity) { + scene.forEachEntity([&selected](sophia::Entity entity) { ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth; @@ -30,3 +49,86 @@ void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) { ImGui::PopID(); }); } + +namespace { +// See SceneHierarchyPanel::renameFocusFrames. +constexpr int RENAME_FOCUS_FRAMES = 2; +} // namespace + +void SceneHierarchyPanel::drawSceneSelector() { + // Leave room for the button: its label, its padding, and the gap to it. + constexpr const char* ADD_SCENE_LABEL = "Add Scene"; + const ImGuiStyle& style = ImGui::GetStyle(); + ImGui::SetNextItemWidth(-(ImGui::CalcTextSize(ADD_SCENE_LABEL).x + + style.FramePadding.x * 2.f + style.ItemSpacing.x)); + + const std::string* current = context.getSceneName(context.getSceneHandle()); + bool startRename = false; + + if (renamingScene && current) { + drawSceneRename(); + } else { + renamingScene = false; + if (ImGui::BeginCombo("##scene", current ? current->c_str() : "No scene")) { + for (const EditorContext::OpenScene& open : context.getOpenScenes()) { + ImGui::PushID(static_cast(open.handle.index)); + bool isCurrent = open.handle == context.getSceneHandle(); + if (ImGui::Selectable(open.name.c_str(), isCurrent) && !isCurrent) { + context.setScene(open.handle); + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } + + if (current && ImGui::BeginPopupContextItem("##sceneMenu")) { + startRename = ImGui::MenuItem("Rename", "F2"); + ImGui::EndPopup(); + } + startRename = startRename || (current && ImGui::IsWindowFocused() && + !ImGui::GetIO().WantTextInput && + ImGui::IsKeyPressed(ImGuiKey_F2)); + } + + ImGui::SameLine(); + if (ImGui::Button(ADD_SCENE_LABEL)) { + std::string name = "Untitled " + std::to_string(++this->untitledSceneCount); + context.setScene(context.createScene(std::move(name))); + startRename = false; + } + + if (startRename) { + renamingScene = true; + renameFocusFrames = RENAME_FOCUS_FRAMES; + std::snprintf(renameBuffer.data(), renameBuffer.size(), "%s", + current->c_str()); + } +} + +void SceneHierarchyPanel::drawSceneRename() { + if (renameFocusFrames == RENAME_FOCUS_FRAMES) { + ImGui::SetKeyboardFocusHere(); + } + + ImGui::InputText("##sceneName", renameBuffer.data(), renameBuffer.size(), + ImGuiInputTextFlags_AutoSelectAll); + + bool active = ImGui::IsItemActive(); + if (active) { + renameFocusFrames = 0; + } else if (renameFocusFrames > 0) { + renameFocusFrames--; + return; + } + + // Escape reverts the text before deactivating, so committing whatever is + // left when the field closes covers Enter, clicking away and Escape alike. + if (!active) { + std::string name = renameBuffer.data(); + // Keep the old name rather than leave the scene nameless. + if (name.find_first_not_of(' ') != std::string::npos) { + context.renameScene(context.getSceneHandle(), std::move(name)); + } + renamingScene = false; + } +} diff --git a/editor/panels/scene_hierarchy_panel.hpp b/editor/panels/scene_hierarchy_panel.hpp index c732068..8cf14b3 100644 --- a/editor/panels/scene_hierarchy_panel.hpp +++ b/editor/panels/scene_hierarchy_panel.hpp @@ -1,20 +1,38 @@ #pragma once -#include +#include #include -#include +#include #include +#include "../editor_context.hpp" + class SceneHierarchyPanel : public sophia::Panel { public: - SceneHierarchyPanel(sophia::Scene& scene, sophia::Entity& selected) + // `cubeMesh` is what "+ Cube" gives new entities. + SceneHierarchyPanel(EditorContext& context, sophia::MeshHandle cubeMesh) : Panel{{.name = "Scene Hierarchy", .locked = true}}, - scene{scene}, - selected{selected} {} + context{context}, + cubeMesh{cubeMesh} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; private: - sophia::Scene& scene; - sophia::Entity& selected; + // Switches between open scenes, renames the active one, and creates new + // ones. + void drawSceneSelector(); + // Replaces the selector with a text field editing the active scene's name. + void drawSceneRename(); + + EditorContext& context; + sophia::MeshHandle cubeMesh; + // For naming new scenes "Untitled 1", "Untitled 2", ... + int untitledSceneCount = 0; + + // Set while the scene selector is a text field for renaming. + bool renamingScene = false; + // Frames the text field has left to become active after renaming starts; + // focus can take a frame to land. If it never does, renaming ends. + int renameFocusFrames = 0; + std::array renameBuffer{}; }; diff --git a/editor/panels/scene_panel.cpp b/editor/panels/scene_panel.cpp new file mode 100644 index 0000000..698d113 --- /dev/null +++ b/editor/panels/scene_panel.cpp @@ -0,0 +1,603 @@ +#include "scene_panel.hpp" + +#include +// After imgui.h, which it relies on. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Corners of the unit quad a SpriteComponent's model matrix maps to world. +constexpr std::array UNIT_QUAD{ + glm::vec2{0.f, 0.f}, glm::vec2{1.f, 0.f}, glm::vec2{1.f, 1.f}, + glm::vec2{0.f, 1.f}}; + +const ImU32 SELECTION_COLOR = IM_COL32(255, 170, 40, 255); + +// Holding Ctrl snaps the gizmo to these steps. +constexpr float TRANSLATE_SNAP = 0.5f; +constexpr float ROTATE_SNAP_DEGREES = 15.f; +constexpr float SCALE_SNAP = 0.1f; + +// Writes a matrix built from a translation, rotation and scale (no shear) +// back into `transform`. A mirroring matrix comes back with a negative x +// scale. +void decompose(const glm::mat4& matrix, sophia::TransformComponent& transform) { + transform.position = glm::vec3{matrix[3]}; + + glm::vec3 scale{glm::length(glm::vec3{matrix[0]}), + glm::length(glm::vec3{matrix[1]}), + glm::length(glm::vec3{matrix[2]})}; + if (glm::determinant(glm::mat3{matrix}) < 0.f) { + scale.x = -scale.x; + } + transform.scale = scale; + + // A zero scale loses the axis, and with it the rotation; keep the old one. + if (scale.x == 0.f || scale.y == 0.f || scale.z == 0.f) { + return; + } + glm::mat3 rotation{glm::vec3{matrix[0]} / scale.x, + glm::vec3{matrix[1]} / scale.y, + glm::vec3{matrix[2]} / scale.z}; + transform.rotation = glm::normalize(glm::quat_cast(rotation)); +} + +// How far along a ray (from `origin`, along the normalized `direction`) it +// enters `bounds` placed by `model`, or null if it misses. A ray starting +// inside hits at 0. +std::optional intersectBox(const glm::vec3& origin, + const glm::vec3& direction, + const glm::mat4& model, + const sophia::AABB& bounds) { + if (glm::determinant(model) == 0.f) { + return std::nullopt; + } + + // Test in the box's space, where it's axis-aligned. The direction isn't + // renormalized, so distances along it are still world distances. + glm::mat4 inverse = glm::inverse(model); + glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}}; + glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}}; + + float enter = 0.f; + float exit = std::numeric_limits::infinity(); + for (int axis = 0; axis < 3; axis++) { + if (std::abs(localDirection[axis]) < 1e-8f) { + // Parallel to this pair of faces: hits only if already between them. + if (localOrigin[axis] < bounds.min[axis] || + localOrigin[axis] > bounds.max[axis]) { + return std::nullopt; + } + continue; + } + + float toMin = (bounds.min[axis] - localOrigin[axis]) / localDirection[axis]; + float toMax = (bounds.max[axis] - localOrigin[axis]) / localDirection[axis]; + if (toMin > toMax) { + std::swap(toMin, toMax); + } + enter = std::max(enter, toMin); + exit = std::min(exit, toMax); + if (enter > exit) { + return std::nullopt; + } + } + return enter; +} + +// How far along a ray it hits the unit quad (0, 0)-(1, 1) in the XY plane, +// placed by `model`, or null if it misses. +std::optional intersectQuad(const glm::vec3& origin, + const glm::vec3& direction, + const glm::mat4& model) { + if (glm::determinant(model) == 0.f) { + return std::nullopt; + } + + glm::mat4 inverse = glm::inverse(model); + glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}}; + glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}}; + // Edge on to the quad. + if (std::abs(localDirection.z) < 1e-8f) { + return std::nullopt; + } + + float distance = -localOrigin.z / localDirection.z; + if (distance < 0.f) { + return std::nullopt; + } + glm::vec3 hit = localOrigin + distance * localDirection; + if (hit.x < 0.f || hit.x > 1.f || hit.y < 0.f || hit.y > 1.f) { + return std::nullopt; + } + return distance; +} + +} // namespace + +ImVec2 ScenePanel::ViewportTransform::clipToScreen( + const glm::vec4& clip) const { + glm::vec2 ndc = glm::vec2{clip} / clip.w; + // Vulkan NDC has +y pointing down, same as the screen. + return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x, + origin.y + (ndc.y * 0.5f + 0.5f) * size.y}; +} + +std::optional ScenePanel::ViewportTransform::worldToScreen( + const glm::vec3& world) const { + glm::vec4 clip = viewProjection * glm::vec4{world, 1.f}; + if (clip.w <= 0.f) { + return std::nullopt; + } + return clipToScreen(clip); +} + +glm::vec2 ScenePanel::ViewportTransform::screenToWorld( + const ImVec2& screen) const { + glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f, + (screen.y - origin.y) / size.y * 2.f - 1.f}; + glm::vec4 world = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f}; + return glm::vec2{world} / world.w; +} + +ScenePanel::Ray ScenePanel::ViewportTransform::screenToRay( + const ImVec2& screen) const { + glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f, + (screen.y - origin.y) / size.y * 2.f - 1.f}; + // Depth runs 0 at the near plane to 1 at the far one. + glm::vec4 nearPoint = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f}; + glm::vec4 farPoint = inverseViewProjection * glm::vec4{ndc, 1.f, 1.f}; + glm::vec3 from = glm::vec3{nearPoint} / nearPoint.w; + glm::vec3 to = glm::vec3{farPoint} / farPoint.w; + return {from, glm::normalize(to - from)}; +} + +void ScenePanel::ViewportTransform::drawLine(ImDrawList* drawList, + const glm::vec3& a, + const glm::vec3& b, ImU32 color, + float thickness) const { + // Just in front of the camera. Points behind it project mirrored, so the + // part of the line there is cut off rather than drawn. + constexpr float MIN_W = 1e-4f; + + glm::vec4 clipA = viewProjection * glm::vec4{a, 1.f}; + glm::vec4 clipB = viewProjection * glm::vec4{b, 1.f}; + if (clipA.w < MIN_W && clipB.w < MIN_W) { + return; + } + if (clipA.w < MIN_W) { + clipA = glm::mix(clipA, clipB, (MIN_W - clipA.w) / (clipB.w - clipA.w)); + } else if (clipB.w < MIN_W) { + clipB = glm::mix(clipB, clipA, (MIN_W - clipB.w) / (clipA.w - clipB.w)); + } + drawList->AddLine(clipToScreen(clipA), clipToScreen(clipB), color, thickness); +} + +void ScenePanel::onUpdate(const sophia::FrameInfo& frameInfo) { + syncSceneView(context, sceneViews, view, isShown()); + + drawToolbar(); + ImGui::Separator(); + + const ImVec2 origin = ImGui::GetCursorScreenPos(); + const ImVec2 size = ImGui::GetContentRegionAvail(); + if (size.x < 1.0f || size.y < 1.0f) { + return; + } + + if (!context.hasScene()) { + ImGui::TextDisabled("No scene open"); + return; + } + + ui.image(target); + + // Rolls over ImGuizmo's "mouse is over a handle" state, so IsOver() below + // reports last frame's. + ImGuizmo::BeginFrame(); + // A handle can't be grabbed while any ImGui item is hovered, so while the + // mouse is over one, leave out the viewport button, and keep the window + // still so dragging the handle doesn't drag it. Unless a camera drag is + // already under way: dropping the button would end it. + bool overGizmo = + (ImGuizmo::IsOver() || ImGuizmo::IsUsing()) && !viewportActive; + getConfig().windowFlags = + overGizmo ? WINDOW_FLAGS | ImGuiWindowFlags_NoMove : WINDOW_FLAGS; + + // The image doesn't take mouse input, so without this, dragging in it + // would drag the window instead. + ImGui::SetCursorScreenPos(origin); + if (overGizmo) { + ImGui::Dummy(size); + } else { + ImGui::InvisibleButton("##viewport", size, + ImGuiButtonFlags_MouseButtonLeft | + ImGuiButtonFlags_MouseButtonRight | + ImGuiButtonFlags_MouseButtonMiddle); + } + viewportActive = !overGizmo && ImGui::IsItemActive(); + + handleCameraInput(frameInfo, size); + handleGizmoHotkeys(); + + // Drive the view's camera from the editor camera. The scene view renders + // after the panels update, so this frame already uses it. + camera.getComponent() = + editorCamera.getTransform(); + camera.getComponent() = editorCamera.getCamera(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y), + true); + + // Screen-space, so it only lines up with anything in 2D. + // TODO: a world-space grid on the XZ plane for 3D, once there's debug + // drawing in scene views. + if (showGrid && editorCamera.mode == EditorCamera::Mode::Scene2D) { + drawGrid(origin, size); + } + + // Use the panel's size rather than the target's: the target is resized a + // frame late, and the mouse is over the panel. + float aspect = size.x / size.y; + glm::mat4 view = sophia::CameraComponent::getView( + camera.getComponent()); + glm::mat4 viewProjection = + camera.getComponent().getProjection(aspect) * + view; + ViewportTransform viewport{origin, size, viewProjection, + glm::inverse(viewProjection)}; + + handleSelectionInput(viewport); + drawSelection(viewport); + + ImGuizmo::SetDrawlist(); + ImGuizmo::SetRect(origin.x, origin.y, size.x, size.y); + drawGizmo(view, aspect); + + drawList->PopClipRect(); + + ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f)); + ImGui::TextDisabled("%.0f x %.0f", size.x, size.y); +} + +void ScenePanel::drawToolbar() { + bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D; + if (ImGui::Checkbox("2D", &is2D)) { + editorCamera.mode = + is2D ? EditorCamera::Mode::Scene2D : EditorCamera::Mode::Scene3D; + dragging = false; + } + ImGui::SameLine(); + ImGui::BeginDisabled(!is2D); + ImGui::Checkbox("Grid", &showGrid); + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + const struct { + const char* label; + GizmoOperation operation; + } operations[] = {{"Move (W)", GizmoOperation::Translate}, + {"Rotate (E)", GizmoOperation::Rotate}, + {"Scale (R)", GizmoOperation::Scale}}; + for (const auto& entry : operations) { + if (ImGui::RadioButton(entry.label, gizmoOperation == entry.operation)) { + gizmoOperation = entry.operation; + } + ImGui::SameLine(); + } + if (ImGui::Button(gizmoLocal ? "Local" : "World")) { + gizmoLocal = !gizmoLocal; + } +} + +void ScenePanel::handleGizmoHotkeys() { + const ImGuiIO& io = ImGui::GetIO(); + // W/E/Q fly the camera while the right button is held. + if (!ImGui::IsWindowFocused() || io.WantTextInput || + ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + return; + } + if (ImGui::IsKeyPressed(ImGuiKey_W, false)) { + gizmoOperation = GizmoOperation::Translate; + } else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) { + gizmoOperation = GizmoOperation::Rotate; + } else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) { + gizmoOperation = GizmoOperation::Scale; + } +} + +void ScenePanel::drawGizmo(const glm::mat4& view, float aspect) { + sophia::Entity selected = context.selected; + if (!selected) { + return; + } + + bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D; + + // In 2D, only the axes that keep things in the XY plane. + ImGuizmo::OPERATION operation{}; + float snapStep = 0.f; + switch (gizmoOperation) { + case GizmoOperation::Translate: + operation = is2D ? ImGuizmo::TRANSLATE_X | ImGuizmo::TRANSLATE_Y + : ImGuizmo::TRANSLATE; + snapStep = TRANSLATE_SNAP; + break; + case GizmoOperation::Rotate: + operation = is2D ? ImGuizmo::ROTATE_Z : ImGuizmo::ROTATE; + snapStep = ROTATE_SNAP_DEGREES; + break; + case GizmoOperation::Scale: + operation = + is2D ? ImGuizmo::SCALE_X | ImGuizmo::SCALE_Y : ImGuizmo::SCALE; + snapStep = SCALE_SNAP; + break; + } + std::array snap{snapStep, snapStep, snapStep}; + bool snapping = ImGui::GetIO().KeyCtrl; + + // ImGuizmo maps clip space to the screen the OpenGL way, with +y up, so it + // needs the projection without the Y flip CameraComponent adds for Vulkan. + glm::mat4 projection = + camera.getComponent().getProjection(aspect); + projection[1][1] *= -1.f; + + auto& transform = selected.getComponent(); + glm::mat4 model = transform.mat4(); + + ImGuizmo::SetOrthographic(is2D); + // Per entity, so a drag in progress doesn't carry over to a new selection. + ImGuizmo::PushID(static_cast(static_cast(selected))); + bool changed = ImGuizmo::Manipulate( + glm::value_ptr(view), glm::value_ptr(projection), operation, + gizmoLocal ? ImGuizmo::LOCAL : ImGuizmo::WORLD, glm::value_ptr(model), + nullptr, snapping ? snap.data() : nullptr); + ImGuizmo::PopID(); + + if (changed) { + decompose(model, transform); + } +} + +void ScenePanel::drawGrid(ImVec2 origin, ImVec2 size) { + constexpr float GRID_STEP = 40.0f; + const ImU32 gridColor = IM_COL32(255, 255, 255, 18); + const ImVec2 end(origin.x + size.x, origin.y + size.y); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + for (float x = GRID_STEP; x < size.x; x += GRID_STEP) { + drawList->AddLine(ImVec2(origin.x + x, origin.y), + ImVec2(origin.x + x, end.y), gridColor); + } + for (float y = GRID_STEP; y < size.y; y += GRID_STEP) { + drawList->AddLine(ImVec2(origin.x, origin.y + y), + ImVec2(end.x, origin.y + y), gridColor); + } +} + +void ScenePanel::handleCameraInput(const sophia::FrameInfo& frameInfo, + ImVec2 size) { + const ImGuiIO& io = ImGui::GetIO(); + bool is3D = editorCamera.mode == EditorCamera::Mode::Scene3D; + glm::vec2 mouseDelta{io.MouseDelta.x, io.MouseDelta.y}; + + // The button stays active while any of its mouse buttons is held, even + // once the mouse leaves the panel. + if (ImGui::IsItemActive()) { + bool rightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); + bool middleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); + + if (is3D && rightDown) { + editorCamera.orbit(mouseDelta); + + glm::vec3 direction{0.f}; + if (ImGui::IsKeyDown(ImGuiKey_D)) direction.x += 1.f; + if (ImGui::IsKeyDown(ImGuiKey_A)) direction.x -= 1.f; + if (ImGui::IsKeyDown(ImGuiKey_E)) direction.y += 1.f; + if (ImGui::IsKeyDown(ImGuiKey_Q)) direction.y -= 1.f; + if (ImGui::IsKeyDown(ImGuiKey_S)) direction.z += 1.f; + if (ImGui::IsKeyDown(ImGuiKey_W)) direction.z -= 1.f; + editorCamera.fly(direction, static_cast(frameInfo.deltaTime)); + } else if (middleDown || (!is3D && rightDown)) { + editorCamera.pan(mouseDelta, size.y); + } + } + + if (ImGui::IsItemHovered() && io.MouseWheel != 0.f) { + editorCamera.zoom(io.MouseWheel); + } + + if (ImGui::IsWindowFocused() && !io.WantTextInput && + ImGui::IsKeyPressed(ImGuiKey_F, false)) { + frameSelection(); + } +} + +void ScenePanel::handleSelectionInput(const ViewportTransform& viewport) { + const ImGuiIO& io = ImGui::GetIO(); + + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + context.selected = pick(viewport.screenToRay(io.MousePos)); + // Dragging the selection around is 2D only; in 3D, use the gizmo. + dragging = + context.selected && editorCamera.mode == EditorCamera::Mode::Scene2D; + } + + if (!ImGui::IsItemActive() || !ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + dragging = false; + return; + } + + if (dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) { + ImVec2 previous(io.MousePos.x - io.MouseDelta.x, + io.MousePos.y - io.MouseDelta.y); + glm::vec2 delta = + viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous); + + auto& transform = + context.selected.getComponent(); + transform.position.x += delta.x; + transform.position.y += delta.y; + } +} + +void ScenePanel::drawSelection(const ViewportTransform& viewport) { + sophia::Entity selected = context.selected; + if (!selected) { + return; + } + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const auto& transform = selected.getComponent(); + + if (selected.hasComponent()) { + glm::mat4 model = + selected.getComponent().getModel(transform); + for (size_t i = 0; i < UNIT_QUAD.size(); i++) { + const glm::vec2& from = UNIT_QUAD[i]; + const glm::vec2& to = UNIT_QUAD[(i + 1) % UNIT_QUAD.size()]; + viewport.drawLine(drawList, glm::vec3{model * glm::vec4{from, 0.f, 1.f}}, + glm::vec3{model * glm::vec4{to, 0.f, 1.f}}, + SELECTION_COLOR, 2.0f); + } + } + + if (std::optional bounds = getMeshBounds(selected)) { + glm::mat4 model = transform.mat4(); + std::array corners = bounds->getCorners(); + for (glm::vec3& corner : corners) { + corner = glm::vec3{model * glm::vec4{corner, 1.f}}; + } + // Corners whose indices differ in one bit share an edge. + for (int i = 0; i < 8; i++) { + for (int bit : {1, 2, 4}) { + if ((i & bit) == 0) { + viewport.drawLine(drawList, corners[i], corners[i | bit], + SELECTION_COLOR, 2.0f); + } + } + } + } + + // Mark the transform's position (the sprite's pivot), which is also all + // there is to show for entities without a sprite or mesh, such as the + // camera. + if (std::optional center = + viewport.worldToScreen(transform.position)) { + drawList->AddCircle(*center, 4.0f, SELECTION_COLOR, 0, 2.0f); + } +} + +void ScenePanel::frameSelection() { + sophia::Entity selected = context.selected; + if (!selected) { + return; + } + + const auto& transform = selected.getComponent(); + + // The selection's corners in world space, from its mesh or sprite. + std::vector points; + if (std::optional bounds = getMeshBounds(selected)) { + glm::mat4 model = transform.mat4(); + for (const glm::vec3& corner : bounds->getCorners()) { + points.push_back(glm::vec3{model * glm::vec4{corner, 1.f}}); + } + } + if (selected.hasComponent()) { + glm::mat4 model = + selected.getComponent().getModel(transform); + for (const glm::vec2& corner : UNIT_QUAD) { + points.push_back(glm::vec3{model * glm::vec4{corner, 0.f, 1.f}}); + } + } + + if (points.empty()) { + // Nothing with a size: frame a unit around its position. + editorCamera.frame(transform.position, 0.5f); + return; + } + + glm::vec3 min = points.front(); + glm::vec3 max = points.front(); + for (const glm::vec3& point : points) { + min = glm::min(min, point); + max = glm::max(max, point); + } + editorCamera.frame((min + max) * 0.5f, glm::length(max - min) * 0.5f); +} + +std::optional ScenePanel::getMeshBounds(sophia::Entity entity) { + if (!entity.hasComponent()) { + return std::nullopt; + } + sophia::MeshHandle mesh = entity.getComponent().handle; + if (!resources.isAlive(mesh)) { + return std::nullopt; + } + return resources.getBounds(mesh); +} + +sophia::Entity ScenePanel::pick(const Ray& ray) { + sophia::Scene& scene = context.getScene(); + + // Meshes are opaque and depth tested, so the nearest one hides anything + // behind it. + sophia::Entity nearestMesh; + float meshDistance = std::numeric_limits::infinity(); + scene.forEachEntity([&](sophia::Entity entity) { + std::optional bounds = getMeshBounds(entity); + if (!bounds) { + return; + } + glm::mat4 model = entity.getComponent().mat4(); + std::optional distance = + intersectBox(ray.origin, ray.direction, model, *bounds); + if (distance && *distance < meshDistance) { + nearestMesh = entity; + meshDistance = *distance; + } + }); + + // Sprites in front of that mesh draw over it, in the renderer's order: + // higher sortOrder on top, then nearer. + sophia::Entity topSprite; + std::optional> topOrder; + scene.forEachEntity([&](sophia::Entity entity) { + if (!entity.hasComponent()) { + return; + } + const auto& sprite = entity.getComponent(); + glm::mat4 model = + sprite.getModel(entity.getComponent()); + std::optional distance = + intersectQuad(ray.origin, ray.direction, model); + if (!distance || *distance > meshDistance) { + return; + } + + // Negated distance, so nearer compares greater. + std::tuple order{sprite.sortOrder, -*distance}; + if (!topOrder || order > *topOrder) { + topSprite = entity; + topOrder = order; + } + }); + + return topSprite ? topSprite : nearestMesh; +} diff --git a/editor/panels/scene_panel.hpp b/editor/panels/scene_panel.hpp new file mode 100644 index 0000000..3097cce --- /dev/null +++ b/editor/panels/scene_panel.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../editor_camera.hpp" +#include "../editor_context.hpp" + +/** + * Shows the active scene through the editor's own camera. + * + * 3D: right-drag orbits, middle-drag pans, the wheel zooms, right-drag with + * WASD/QE flies. 2D: right- or middle-drag pans, the wheel zooms. F frames + * the selection in both. + * + * The selection gets a transform gizmo: W moves, E rotates, R scales, and + * holding Ctrl snaps. Clicking selects the mesh or sprite under the mouse; + * in 2D, dragging it also moves it. + */ +class ScenePanel : public sophia::Panel { + public: + // Renders into `target` through `view`, a SceneView this panel keeps + // pointed at the active scene. `camera` is the entity the view looks + // through; it belongs to a scene of the editor's own, and this panel + // drives it from its EditorCamera. + ScenePanel(sophia::UI& ui, sophia::ResourceRegistryRef resources, + EditorContext& context, sophia::SceneViewRegistry& sceneViews, + sophia::SceneViewHandle view, sophia::RenderTargetHandle target, + sophia::Entity camera) + : Panel{{.name = "Scene", .windowFlags = WINDOW_FLAGS}}, + ui{ui}, + resources{resources}, + context{context}, + sceneViews{sceneViews}, + view{view}, + target{target}, + camera{camera} {} + + void onUpdate(const sophia::FrameInfo& frameInfo) override; + + private: + static constexpr ImGuiWindowFlags WINDOW_FLAGS = + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + + enum class GizmoOperation { Translate, Rotate, Scale }; + + // A half-line in world space. `direction` is normalized, so distances + // along it are in world units. + struct Ray { + glm::vec3 origin; + glm::vec3 direction; + }; + + // Where the scene image is on screen this frame, and the camera looking + // through it. + struct ViewportTransform { + ImVec2 origin; + ImVec2 size; + glm::mat4 viewProjection; + glm::mat4 inverseViewProjection; + + // Null for points behind the camera. + std::optional worldToScreen(const glm::vec3& world) const; + // Where `screen` is on the camera's near plane. Only the x and y, which + // is all that's needed in 2D. + glm::vec2 screenToWorld(const ImVec2& screen) const; + // From the near plane through `screen`, into the scene. + Ray screenToRay(const ImVec2& screen) const; + + // Draws a world-space line, cut off where it passes behind the camera. + void drawLine(ImDrawList* drawList, const glm::vec3& a, const glm::vec3& b, + ImU32 color, float thickness) const; + + private: + ImVec2 clipToScreen(const glm::vec4& clip) const; + }; + + void drawToolbar(); + void drawGrid(ImVec2 origin, ImVec2 size); + void handleCameraInput(const sophia::FrameInfo& frameInfo, ImVec2 size); + void handleSelectionInput(const ViewportTransform& viewport); + void handleGizmoHotkeys(); + // Draws the transform gizmo on the selection, and applies any change made + // with it. `view` is the editor camera's, and `aspect` the viewport's + // width over its height. + void drawGizmo(const glm::mat4& view, float aspect); + void drawSelection(const ViewportTransform& viewport); + void frameSelection(); + + // The bounds of the entity's mesh in the mesh's own space, if it has one. + std::optional getMeshBounds(sophia::Entity entity); + + // The entity drawn in front along `ray`, or a null Entity. Only meshes and + // sprites can be hit. + sophia::Entity pick(const Ray& ray); + + sophia::UI& ui; + sophia::ResourceRegistryRef resources; + EditorContext& context; + sophia::SceneViewRegistry& sceneViews; + sophia::SceneViewHandle view; + sophia::RenderTargetHandle target; + sophia::Entity camera; + + EditorCamera editorCamera; + + GizmoOperation gizmoOperation = GizmoOperation::Translate; + // Whether the gizmo works along the entity's own axes or the world's. + bool gizmoLocal = true; + // Whether the viewport button was active last frame, i.e. a camera drag or + // click is under way, which the gizmo mustn't interrupt. + bool viewportActive = false; + + bool showGrid = true; + // Set while a drag that started on the selected entity is moving it. + bool dragging = false; +}; diff --git a/editor/panels/viewport_panel.cpp b/editor/panels/viewport_panel.cpp deleted file mode 100644 index f8b793d..0000000 --- a/editor/panels/viewport_panel.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#include "viewport_panel.hpp" - -#include - -#include -#include -#include -#include - -namespace { - -// Corners of the unit quad a SpriteComponent's model matrix maps to world. -constexpr std::array UNIT_QUAD{ - glm::vec2{0.f, 0.f}, glm::vec2{1.f, 0.f}, glm::vec2{1.f, 1.f}, - glm::vec2{0.f, 1.f}}; - -const ImU32 SELECTION_COLOR = IM_COL32(255, 170, 40, 255); - -} // namespace - -ImVec2 ViewportPanel::ViewportTransform::worldToScreen( - const glm::vec3& world) const { - glm::vec4 clip = viewProjection * glm::vec4{world, 1.f}; - glm::vec2 ndc = glm::vec2{clip} / clip.w; - // Vulkan NDC has +y pointing down, same as the screen. - return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x, - origin.y + (ndc.y * 0.5f + 0.5f) * size.y}; -} - -glm::vec2 ViewportPanel::ViewportTransform::screenToWorld( - const ImVec2& screen) const { - glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f, - (screen.y - origin.y) / size.y * 2.f - 1.f}; - glm::vec4 world = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f}; - return glm::vec2{world} / world.w; -} - -void ViewportPanel::onUpdate(const sophia::FrameInfo&) { - drawToolbar(); - ImGui::Separator(); - - const ImVec2 origin = ImGui::GetCursorScreenPos(); - const ImVec2 size = ImGui::GetContentRegionAvail(); - if (size.x < 1.0f || size.y < 1.0f) { - return; - } - - ui.image(target); - - // The image doesn't take mouse input, so without this, dragging in it - // would drag the window instead. - ImGui::SetCursorScreenPos(origin); - ImGui::InvisibleButton("##viewport", size); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y), - true); - - if (showGrid) { - drawGrid(origin, size); - } - - sophia::Entity camera = scene.getActiveCamera(); - if (camera && camera.hasComponent()) { - // Use the panel's size rather than the target's: the target is resized - // a frame late, and the mouse is over the panel. - float aspect = size.x / size.y; - glm::mat4 viewProjection = - camera.getComponent().getViewProjection( - camera.getComponent(), aspect); - - ViewportTransform viewport{origin, size, viewProjection, - glm::inverse(viewProjection)}; - handleInput(viewport); - drawSelection(viewport); - } - - drawList->PopClipRect(); - - ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f)); - ImGui::TextDisabled("%.0f x %.0f", size.x, size.y); -} - -void ViewportPanel::drawToolbar() { - const struct { - const char* label; - GizmoMode mode; - } modes[] = {{"Move", GizmoMode::Move}, - {"Rotate", GizmoMode::Rotate}, - {"Scale", GizmoMode::Scale}}; - - for (const auto& entry : modes) { - if (ImGui::RadioButton(entry.label, gizmoMode == entry.mode)) { - gizmoMode = entry.mode; - } - ImGui::SameLine(); - } - ImGui::Checkbox("Grid", &showGrid); -} - -void ViewportPanel::drawGrid(ImVec2 origin, ImVec2 size) { - constexpr float GRID_STEP = 40.0f; - const ImU32 gridColor = IM_COL32(255, 255, 255, 18); - const ImVec2 end(origin.x + size.x, origin.y + size.y); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - for (float x = GRID_STEP; x < size.x; x += GRID_STEP) { - drawList->AddLine(ImVec2(origin.x + x, origin.y), - ImVec2(origin.x + x, end.y), gridColor); - } - for (float y = GRID_STEP; y < size.y; y += GRID_STEP) { - drawList->AddLine(ImVec2(origin.x, origin.y + y), - ImVec2(end.x, origin.y + y), gridColor); - } -} - -void ViewportPanel::handleInput(const ViewportTransform& viewport) { - const ImGuiIO& io = ImGui::GetIO(); - - if (ImGui::IsItemActivated()) { - selected = pick(viewport.screenToWorld(io.MousePos)); - dragging = selected && gizmoMode == GizmoMode::Move; - } - - if (!ImGui::IsItemActive()) { - dragging = false; - return; - } - - if (dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) { - ImVec2 previous(io.MousePos.x - io.MouseDelta.x, - io.MousePos.y - io.MouseDelta.y); - glm::vec2 delta = - viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous); - - auto& transform = selected.getComponent(); - transform.position.x += delta.x; - transform.position.y += delta.y; - } -} - -void ViewportPanel::drawSelection(const ViewportTransform& viewport) { - if (!selected) { - return; - } - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const auto& transform = selected.getComponent(); - - if (selected.hasComponent()) { - glm::mat4 model = - selected.getComponent().getModel(transform); - - std::array corners; - for (size_t i = 0; i < UNIT_QUAD.size(); i++) { - corners[i] = viewport.worldToScreen( - glm::vec3{model * glm::vec4{UNIT_QUAD[i], 0.f, 1.f}}); - } - drawList->AddQuad(corners[0], corners[1], corners[2], corners[3], - SELECTION_COLOR, 2.0f); - } - - // Mark the transform's position (the sprite's pivot), which is also all - // there is to show for entities without a sprite, such as the camera. - ImVec2 center = viewport.worldToScreen(transform.position); - drawList->AddCircle(center, 4.0f, SELECTION_COLOR, 0, 2.0f); -} - -sophia::Entity ViewportPanel::pick(glm::vec2 world) { - sophia::Entity best; - std::optional> bestOrder; - - scene.forEachEntity([&](sophia::Entity entity) { - if (!entity.hasComponent()) { - return; - } - - const auto& transform = entity.getComponent(); - const auto& sprite = entity.getComponent(); - - glm::mat4 model = sprite.getModel(transform); - // Zero size or scale; nothing to hit. - if (glm::determinant(model) == 0.f) { - return; - } - - glm::vec4 local = - glm::inverse(model) * glm::vec4{world, transform.position.z, 1.f}; - if (local.x < 0.f || local.x > 1.f || local.y < 0.f || local.y > 1.f) { - return; - } - - // Same ordering the renderer draws in, so this is the one on top. - std::tuple order{sprite.sortOrder, transform.position.z}; - if (!bestOrder || order >= *bestOrder) { - best = entity; - bestOrder = order; - } - }); - - return best; -} diff --git a/editor/panels/viewport_panel.hpp b/editor/panels/viewport_panel.hpp deleted file mode 100644 index 408b539..0000000 --- a/editor/panels/viewport_panel.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -/** - * Shows the scene through its active camera. Click a sprite to select it, - * drag it to move it (in Move mode). - * - * Picking and moving are 2D: they assume an orthographic camera rotated only - * around Z, and work in the z = 0 plane. - */ -class ViewportPanel : public sophia::Panel { - public: - ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target, - sophia::Scene& scene, sophia::Entity& selected) - : Panel{{.name = "Viewport", - .windowFlags = ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse}}, - ui{ui}, - target{target}, - scene{scene}, - selected{selected} {} - - void onUpdate(const sophia::FrameInfo& frameInfo) override; - - private: - // Where the scene image is on screen this frame, and the camera looking - // through it. - struct ViewportTransform { - ImVec2 origin; - ImVec2 size; - glm::mat4 viewProjection; - glm::mat4 inverseViewProjection; - - ImVec2 worldToScreen(const glm::vec3& world) const; - glm::vec2 screenToWorld(const ImVec2& screen) const; - }; - - void drawToolbar(); - void drawGrid(ImVec2 origin, ImVec2 size); - void handleInput(const ViewportTransform& viewport); - void drawSelection(const ViewportTransform& viewport); - - // The top-most sprite under `world`, or a null Entity. - sophia::Entity pick(glm::vec2 world); - - sophia::UI& ui; - sophia::RenderTargetHandle target; - sophia::Scene& scene; - sophia::Entity& selected; - - enum class GizmoMode { Move, Rotate, Scale }; - - GizmoMode gizmoMode = GizmoMode::Move; - bool showGrid = true; - // Set while a drag that started on the selected entity is moving it. - bool dragging = false; -}; diff --git a/editor/utilities/logger.hpp b/editor/utilities/logger.hpp new file mode 100644 index 0000000..846896f --- /dev/null +++ b/editor/utilities/logger.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +template +void log(Args... args) { + sophia::log::print("\033[1;38;2;170;90;255m[editor]\033[0m", args...); +} diff --git a/engine/include/sophia/aabb.hpp b/engine/include/sophia/aabb.hpp new file mode 100644 index 0000000..a875e61 --- /dev/null +++ b/engine/include/sophia/aabb.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace sophia { + +/** @brief Axis-Aligned Bounding Box + * + * An axis-aligned box, from its smallest corner to its largest. + */ +struct AABB { + vec3 min{0.f, 0.f, 0.f}; + vec3 max{0.f, 0.f, 0.f}; + + vec3 getCenter() const { return (min + max) * 0.5f; } + vec3 getSize() const { return max - min; } + + // The 8 corners. Bit 0 of the index picks x from max, bit 1 y, bit 2 z. + std::array getCorners() const { + std::array corners; + for (int i = 0; i < 8; i++) { + corners[i] = {(i & 1) ? max.x : min.x, (i & 2) ? max.y : min.y, + (i & 4) ? max.z : min.z}; + } + return corners; + } +}; + +} // namespace sophia diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 834f874..72aaae6 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -53,7 +52,6 @@ class Application { DebugUI& debugUI() { return this->_debugUI; } Canvas canvas; - Camera2D camera; std::vector args; ConsoleCommandRegistry consoleCommandRegistry; diff --git a/engine/include/sophia/camera_2d.hpp b/engine/include/sophia/camera_2d.hpp deleted file mode 100644 index 88a87f0..0000000 --- a/engine/include/sophia/camera_2d.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace sophia { - -class Camera2D { - public: - glm::vec2 position{0.f, 0.f}; - - void setProjection(float left, float right, float bottom, float top) { - projection = glm::ortho(left, right, bottom, top); - } - - const glm::mat4& getProjection() const { return projection; } - - glm::mat4 getView() const { - return glm::translate(glm::mat4(1.f), glm::vec3(-position, 0.f)); - } - - glm::mat4 getViewProjection() const { return projection * getView(); } - - private: - glm::mat4 projection{1.f}; -}; - -} // namespace sophia diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index ab44893..5183b4a 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -43,8 +43,15 @@ struct TransformComponent { } }; +/** + * Draws a mesh, placed by the entity's TransformComponent. Opaque and unlit + * for now. + */ struct MeshComponent { MeshHandle handle; + // Multiplied with the mesh's vertex colors. Alpha is ignored. Stands in for + // a material until there are materials. + glm::vec4 color{1.f, 1.f, 1.f, 1.f}; }; /** @@ -58,13 +65,17 @@ struct SpriteComponent { // Size in world units, before the transform's scale. glm::vec2 size{1.f, 1.f}; - // Point on the sprite, from (0, 0) to (1, 1) across its size, that sits at - // the transform's position and that it rotates around. + // Point on the sprite, from (0, 0) at its bottom-left to (1, 1) at its + // top-right, that sits at the transform's position and that it rotates + // around. glm::vec2 pivot{0.5f, 0.5f}; - // Region of the texture to draw, as normalized (u0, v0, u1, v1). + // Region of the texture to draw, as normalized (u0, v0, u1, v1) with + // (0, 0) at the texture's top-left. glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f}; - // Higher draws on top. Ties are broken by the transform's position.z. + // Higher draws on top, whatever the depth. Sprites with the same sortOrder + // draw back to front from the camera; the transform's position.z is depth + // like any other axis, not a layer. s32 sortOrder = 0; // Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's @@ -79,8 +90,8 @@ struct SpriteComponent { struct CameraComponent { struct Orthographic { float height = 10.f; - float nearPlane = -1.f; - float farPlane = 1.f; + float nearPlane = -1000.f; + float farPlane = 1000.f; }; struct Perspective { @@ -95,25 +106,37 @@ struct CameraComponent { Orthographic ortho; Perspective perspective; + // View -> clip space. World +Y is up on screen: Vulkan's clip space has +Y + // pointing down, so the projection flips it. glm::mat4 getProjection(float aspect) const { + glm::mat4 result; if (projection == Projection::Perspective) { - return glm::perspective(perspective.fov, aspect, perspective.nearPlane, - perspective.farPlane); + result = glm::perspective(perspective.fov, aspect, perspective.nearPlane, + perspective.farPlane); + } else { + float halfHeight = ortho.height / 2.f; + float halfWidth = halfHeight * aspect; + result = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, + ortho.nearPlane, ortho.farPlane); } - float halfHeight = ortho.height / 2.f; - float halfWidth = halfHeight * aspect; - return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, - ortho.nearPlane, ortho.farPlane); + result[1][1] *= -1.f; + return result; } - // World -> clip space for this camera placed by `transform`. The view - // ignores the transform's scale; zoom is the projection's job. - glm::mat4 getViewProjection(const TransformComponent& transform, - float aspect) const { + // World -> view space for this camera placed by `transform`. Ignores the + // transform's scale; zoom is the projection's job. The camera looks down + // view-space -Z. + static glm::mat4 getView(const TransformComponent& transform) { glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) * glm::mat4_cast(transform.rotation); - return getProjection(aspect) * glm::inverse(world); + return glm::inverse(world); + } + + // World -> clip space for this camera placed by `transform`. + glm::mat4 getViewProjection(const TransformComponent& transform, + float aspect) const { + return getProjection(aspect) * getView(transform); } }; diff --git a/engine/include/sophia/frame_info.hpp b/engine/include/sophia/frame_info.hpp index 78f92bb..9a6158e 100644 --- a/engine/include/sophia/frame_info.hpp +++ b/engine/include/sophia/frame_info.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -11,7 +10,6 @@ struct FrameInfo { double elapsedTime; double deltaTime; glm::vec2 currentFramebufferExtent; - const Camera2D& camera; }; } // namespace sophia diff --git a/engine/include/sophia/mesh_data.hpp b/engine/include/sophia/mesh_data.hpp new file mode 100644 index 0000000..ab979c4 --- /dev/null +++ b/engine/include/sophia/mesh_data.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +// Layout must match the per-vertex inputs in mesh.vert. +struct MeshVertex { + vec3 position{0.f, 0.f, 0.f}; + vec3 normal{0.f, 0.f, 1.f}; + // Texture coordinates, with (0, 0) at the texture's top-left. + vec2 uv{0.f, 0.f}; + vec4 color{1.f, 1.f, 1.f, 1.f}; +}; + +/** + * Geometry on the CPU, uploaded with `resources().create(data)`. + * Triangles wind counter-clockwise when seen from the front; back faces are + * culled. See primitives.hpp for ready-made shapes. + */ +struct MeshData { + std::vector vertices; + // Empty draws the vertices in order, three per triangle. + std::vector indices; +}; + +} // namespace sophia diff --git a/engine/include/sophia/primitives.hpp b/engine/include/sophia/primitives.hpp new file mode 100644 index 0000000..bfdddf5 --- /dev/null +++ b/engine/include/sophia/primitives.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +/** + * Ready-made shapes, centered on the origin. They return MeshData rather than + * a mesh so they can be adjusted before uploading: + * + * MeshHandle cube = resources().create(primitives::cube()); + */ +namespace sophia::primitives { + +// An axis-aligned cube `size` across. Each face has its own four vertices so +// it gets its own normal and UVs. +MeshData cube(float size = 1.f, vec4 color = vec4{1.f}); + +} // namespace sophia::primitives diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp index 241593d..30667f3 100644 --- a/engine/include/sophia/resource_registry_ref.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -1,8 +1,9 @@ #pragma once -#include +#include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ class ResourceRegistryRef { glm::vec2 getExtent(RenderTargetHandle handle) const; glm::vec2 getExtent(TextureHandle handle) const; + // In the mesh's own space. + AABB getBounds(MeshHandle handle) const; + + bool isAlive(MeshHandle handle) const; bool isAlive(RenderTargetHandle handle) const; bool isAlive(TextureHandle handle) const; @@ -40,9 +45,7 @@ class ResourceRegistryRef { explicit ResourceRegistryRef(ResourceRegistry& registry) : registry{®istry} {} - MeshHandle createResource(std::type_identity, - std::vector vertices, - std::vector indices = {}); + MeshHandle createResource(std::type_identity, const MeshData& data); RenderTargetHandle createResource(std::type_identity, u32 width = 1, u32 height = 1); diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp index 5571a53..f957663 100644 --- a/engine/include/sophia/scene.hpp +++ b/engine/include/sophia/scene.hpp @@ -25,12 +25,20 @@ class Scene { Entity getActiveCamera(); void setActiveCamera(Entity camera); + bool isVisible() const { return this->visible; } + void setVisible(bool visible) { this->visible = visible; } + + bool isActive() const { return this->active; } + void setActive(bool active) { this->active = active; } + private: friend class Entity; friend class SceneRenderer; // !TEMP remove this entt::registry registry; entt::entity activeCamera = entt::null; + bool visible = true; + bool active = true; }; } // namespace sophia diff --git a/engine/include/sophia/scene_view.hpp b/engine/include/sophia/scene_view.hpp index af5860c..cdff21b 100644 --- a/engine/include/sophia/scene_view.hpp +++ b/engine/include/sophia/scene_view.hpp @@ -19,8 +19,11 @@ struct SceneView { SceneHandle scene = INVALID_HANDLE; RenderTargetHandle target = INVALID_HANDLE; // null implies render to swapchain - Entity camera; // null implies use scene's active camera + // Null implies the scene's active camera. May belong to another scene, + // e.g. an editor camera that isn't part of the scene being shown. + Entity camera; + // TODO(#76): not used yet; every view renders to its whole target. vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f}; vec4 clearColor{0.f, 0.f, 0.f, 1.f}; diff --git a/engine/include/sophia/ui/panel.hpp b/engine/include/sophia/ui/panel.hpp index 078104e..ecd2406 100644 --- a/engine/include/sophia/ui/panel.hpp +++ b/engine/include/sophia/ui/panel.hpp @@ -48,6 +48,7 @@ class Panel { bool isLocked() const { return config.locked; } bool isVisible() const { return this->visible; } + bool isShown() const { return this->visible && this->shown; } void setVisible(bool visible) { this->visible = visible; } void toggleVisible() { this->visible = !this->visible; } @@ -55,6 +56,7 @@ class Panel { virtual void onUpdate(const FrameInfo& frameInfo) = 0; bool visible = true; + bool shown = false; private: Config config; diff --git a/engine/include/sophia/util/logger.hpp b/engine/include/sophia/util/logger.hpp index 46d4bf1..cc3110d 100644 --- a/engine/include/sophia/util/logger.hpp +++ b/engine/include/sophia/util/logger.hpp @@ -5,6 +5,7 @@ * using namespace sophia; * log::fatal("fatal message") * log::info("Infomation message") + * log::print("no prefix, just the message") * * TODO Impl some form of log error handling */ @@ -122,4 +123,24 @@ void trace(Args... args) { log(LEVEL::TRACE, args...); } +// Like the functions above, but with no "[LEVEL]" prefix: just the arguments, +// separated by spaces, then a newline. With none, an empty line. +inline void print() { +#ifndef NDEBUG + std::cout << std::endl; +#endif // !NDEBUG +} + +template +void print(First first, Rest... rest) { +#ifndef NDEBUG + std::cout << first; + ((std::cout << ' ' << rest), ...); + std::cout << std::endl; +#else + (void)first; + (void)std::initializer_list{((void)rest, 0)...}; +#endif // !NDEBUG +} + } // namespace sophia::log diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 88ca4dd..51d3b2d 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -92,8 +94,9 @@ class Application::Impl { imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); - // Views already warned about having no camera, so each warns once. + // Views already warned about, so each warns once. std::set viewsWithoutCamera; + std::set viewsOverwritingOthers; auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = startTime; @@ -117,17 +120,20 @@ class Application::Impl { vk::Extent2D extent = renderContext.getExtent(); FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, glm::vec2(static_cast(extent.width), - static_cast(extent.height)), - this->application.camera}; + static_cast(extent.height))}; { ZoneScopedN("Application::onUpdate"); this->application.onUpdate(frameInfo); } - this->application.camera.setProjection( - -frameInfo.currentFramebufferExtent.x / 2.f, - frameInfo.currentFramebufferExtent.x / 2.f, - -frameInfo.currentFramebufferExtent.y / 2.f, - frameInfo.currentFramebufferExtent.y / 2.f); + + // Canvas draws in window pixels, origin at the center. + // TODO: replace with Debug/Gizmos drawn into scene views through their + // cameras. + glm::mat4 canvasProjection = + glm::ortho(-frameInfo.currentFramebufferExtent.x / 2.f, + frameInfo.currentFramebufferExtent.x / 2.f, + -frameInfo.currentFramebufferExtent.y / 2.f, + frameInfo.currentFramebufferExtent.y / 2.f); vk::CommandBuffer commandBuffer = renderContext.beginFrame(); if (!commandBuffer) { @@ -152,37 +158,79 @@ class Application::Impl { this->drawCalls.end()); this->drawCalls.clear(); - RenderGraph graph; + RenderGraph graph{renderContext.getTracyContext()}; ImageRef backbuffer = graph.importImage(renderContext.getCurrentImage(), Access::Present); - std::map viewTargets; + struct ViewTarget { + ImageRef color; + ImageRef depth; + }; + std::map viewTargets; + // Views without a target render straight into the backbuffer. + std::optional swapchainTarget; for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) { const SceneView& view = sceneViewRegistry.peek(handle); + bool toSwapchain = !view.target.isValid(); if (!sceneRegistry.isAlive(view.scene) || - !renderTargetPool.isAlive(view.target)) { + !sceneRegistry.get(view.scene).isVisible() || + (!toSwapchain && !renderTargetPool.isAlive(view.target))) { continue; } - auto [it, inserted] = viewTargets.try_emplace(view.target); - if (inserted) { - it->second = - graph.importImage(renderTargetPool.get(view.target).getImage(), - Access::SampledRead); + ViewTarget target; + vk::Extent2D targetExtent; + // Whether this is the first view this frame to render to its target. + bool firstOnTarget = false; + if (toSwapchain) { + firstOnTarget = !swapchainTarget; + if (!swapchainTarget) { + swapchainTarget = { + backbuffer, + graph.importImage(renderContext.getCurrentDepthImage(), + Access::DepthWrite)}; + } + target = *swapchainTarget; + targetExtent = renderContext.getExtent(); + } else { + RenderTarget& renderTarget = renderTargetPool.get(view.target); + auto [it, inserted] = viewTargets.try_emplace(view.target); + firstOnTarget = inserted; + if (inserted) { + it->second = { + graph.importImage(renderTarget.getImage(), Access::SampledRead), + graph.importImage(renderTarget.getDepthImage(), + Access::DepthWrite)}; + } + target = it->second; + targetExtent = renderTarget.getExtent(); + } + + if (!firstOnTarget && hasFlag(view.clearFlags, ClearFlags::Color) && + viewsOverwritingOthers.insert(handle).second) { + log::warning( + "scene view clears its target after another view already " + "rendered to it this frame, hiding that view (give it clearFlags " + "without Color, or render it first with a lower priority)"); } const vec4& color = view.clearColor; graph.addPass( - "scene view", + "scene view " + std::to_string(handle.index), [&](RenderGraph::PassBuilder& pass) { - pass.writeColor(it->second, + pass.writeColor(target.color, hasFlag(view.clearFlags, ClearFlags::Color) ? vk::AttachmentLoadOp::eClear : vk::AttachmentLoadOp::eLoad, {color.r, color.g, color.b, color.a}); + pass.writeDepth(target.depth, + hasFlag(view.clearFlags, ClearFlags::Depth) + ? vk::AttachmentLoadOp::eClear + : vk::AttachmentLoadOp::eLoad); }, [&sceneRenderer, &sceneRegistry, &sceneViewRegistry, - &viewsWithoutCamera, handle](vk::CommandBuffer commandBuffer) { + &viewsWithoutCamera, handle, + targetExtent](vk::CommandBuffer commandBuffer) { ZoneScopedN("scene view pass"); const SceneView& view = sceneViewRegistry.peek(handle); Entity camera = @@ -198,22 +246,26 @@ class Application::Impl { } return; } - sceneRenderer.render(commandBuffer, view, camera); + sceneRenderer.render(commandBuffer, view, camera, targetExtent); }); } graph.addPass( "main", [&](RenderGraph::PassBuilder& pass) { - pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear, + // Keep what scene views drew into the backbuffer. + pass.writeColor(backbuffer, + swapchainTarget ? vk::AttachmentLoadOp::eLoad + : vk::AttachmentLoadOp::eClear, CLEAR_COLOR); for (const auto& [handle, target] : viewTargets) { - pass.sample(target); + pass.sample(target.color); } }, [&](vk::CommandBuffer commandBuffer) { ZoneScopedN("main pass"); - modelRenderSystem.render(commandBuffer, instances, frameInfo); + modelRenderSystem.render(commandBuffer, instances, + canvasProjection); imguiRenderer.endFrame(commandBuffer); }); @@ -239,14 +291,15 @@ class Application::Impl { } void pushMesh(std::vector vertices, std::vector indices) { - Mesh::Builder builder{}; - builder.vertices.reserve(vertices.size()); + MeshData data; + data.vertices.reserve(vertices.size()); for (const CanvasVertex& vertex : vertices) { - builder.vertices.push_back({vertex.position, vertex.color}); + data.vertices.push_back( + {.position = {vertex.position, 0.f}, .color = {vertex.color, 1.f}}); } - builder.indices = std::move(indices); + data.indices = std::move(indices); - this->meshes.push_back(std::make_unique(this->device, builder)); + this->meshes.push_back(std::make_unique(this->device, data)); } void clearMeshes() { diff --git a/engine/src/renderer/mesh_render_system.cpp b/engine/src/renderer/mesh_render_system.cpp new file mode 100644 index 0000000..71f19ed --- /dev/null +++ b/engine/src/renderer/mesh_render_system.cpp @@ -0,0 +1,153 @@ +#include "renderer/mesh_render_system.hpp" + +#include +#include +#include + +#include "shaders/mesh.frag.spv.h" +#include "shaders/mesh.vert.spv.h" + +namespace sophia { + +namespace { +// Mesh::getAttributeDescriptions() takes locations below this. +constexpr u32 FIRST_INSTANCE_LOCATION = 4; +} // namespace + +MeshRenderSystem::MeshRenderSystem(Device& device, vk::Format colorFormat, + vk::Format depthFormat, + ResourceRegistry& resources) + : device{device}, resources{resources} { + createPipelineLayout(); + createPipeline(colorFormat, depthFormat); + createInstanceBuffers(); +} + +MeshRenderSystem::~MeshRenderSystem() { + log::trace("destroyed sophia::MeshRenderSystem"); +} + +void MeshRenderSystem::beginFrame(u32 frameIndex) { + this->frameIndex = frameIndex; + this->instanceCursor = 0; +} + +void MeshRenderSystem::render(vk::CommandBuffer commandBuffer, + const glm::mat4& viewProjection, + std::span instances, + std::span batches) { + ZoneScoped; + if (instances.empty()) { + return; + } + + u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor; + if (instances.size() > available) { + log::warning("MeshRenderSystem: frame instance buffer full, dropping", + instances.size() - available, "meshes"); + } + + Buffer& buffer = *this->instanceBuffers[this->frameIndex]; + u32 base = this->instanceCursor; + u32 count = std::min(static_cast(instances.size()), available); + buffer.writeToBuffer(const_cast(instances.data()), + count * sizeof(Instance), base * sizeof(Instance)); + this->instanceCursor += count; + + this->graphicsPipeline->bind(commandBuffer); + + PushConstantData push{viewProjection}; + commandBuffer.pushConstants(this->pipelineLayout.get(), + vk::ShaderStageFlagBits::eVertex, 0, + sizeof(PushConstantData), &push); + + // Binding 1 stays bound across batches; each mesh rebinds binding 0. + vk::Buffer instanceBuffer = buffer.getBuffer(); + vk::DeviceSize offset = 0; + commandBuffer.bindVertexBuffers(1, 1, &instanceBuffer, &offset); + + for (const Batch& batch : batches) { + if (batch.firstInstance >= count) { + break; + } + u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance); + + const Mesh& mesh = this->resources.meshPool.get(batch.mesh); + mesh.bind(commandBuffer); + mesh.draw(commandBuffer, batchCount, base + batch.firstInstance); + } +} + +void MeshRenderSystem::createPipelineLayout() { + vk::PushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(PushConstantData); + + vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {}; + pipelineLayoutInfo.setLayoutCount = 0; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + try { + this->pipelineLayout = + this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo); + log::trace("created vk::PipelineLayout"); + } catch (const vk::SystemError& err) { + log::fatal("failed to create vk::PipelineLayout"); + throw std::runtime_error("failed to create vk::PipelineLayout"); + } +} + +void MeshRenderSystem::createPipeline(vk::Format colorFormat, + vk::Format depthFormat) { + auto vertexShader = Shader::Builder() + .fromSPIRV(shaders::mesh_vert_spv) + .setStage(ShaderStage::VERTEX) + .build(); + auto fragmentShader = Shader::Builder() + .fromSPIRV(shaders::mesh_frag_spv) + .setStage(ShaderStage::FRAGMENT) + .build(); + + std::vector bindings = + Mesh::getBindingDescriptions(); + bindings.push_back({1, sizeof(Instance), vk::VertexInputRate::eInstance}); + + std::vector attributes = + Mesh::getAttributeDescriptions(); + // A mat4 attribute takes four consecutive locations, one per column. + for (u32 column = 0; column < 4; column++) { + attributes.push_back({FIRST_INSTANCE_LOCATION + column, 1, + vk::Format::eR32G32B32A32Sfloat, + static_cast(offsetof(Instance, model) + + column * sizeof(glm::vec4))}); + } + attributes.push_back({FIRST_INSTANCE_LOCATION + 4, 1, + vk::Format::eR32G32B32A32Sfloat, + static_cast(offsetof(Instance, color))}); + + this->graphicsPipeline = GraphicsPipeline::Builder(this->device) + .setVertexShader(*vertexShader) + .setFragmentShader(*fragmentShader) + .setPipelineLayout(this->pipelineLayout.get()) + .setColorAttachmentFormats({colorFormat}) + .setDepthAttachmentFormat(depthFormat) + .setVertexInput(bindings, attributes) + .setDepthTest(true, true) + .setCullMode(vk::CullModeFlagBits::eBack) + .build(); +} + +void MeshRenderSystem::createInstanceBuffers() { + for (std::unique_ptr& buffer : this->instanceBuffers) { + buffer = std::make_unique( + this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent); + buffer->map(); + } +} + +} // namespace sophia diff --git a/engine/src/renderer/mesh_render_system.hpp b/engine/src/renderer/mesh_render_system.hpp new file mode 100644 index 0000000..da3498c --- /dev/null +++ b/engine/src/renderer/mesh_render_system.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "renderer/render_context.hpp" +#include "resources/resource_registry.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" + +namespace sophia { + +/** + * Draws batches of opaque meshes, depth tested and back-face culled. Knows + * nothing about scenes: callers hand it instances grouped into runs that + * share a mesh. + * + * Unlit for now: each pixel is the vertex color times the instance color. + * Normals and UVs reach the vertex shader but aren't used yet. + * + * Instances are written to a per-frame host-visible buffer, so every + * render() call within one frame appends to it; call beginFrame() once per + * frame before any render(). + */ +class MeshRenderSystem { + public: + // Layout must match the per-instance inputs in mesh.vert. + struct Instance { + glm::mat4 model{1.f}; + glm::vec4 color{1.f}; + }; + + // A run of consecutive instances of the same mesh. + struct Batch { + MeshHandle mesh; + u32 firstInstance; + u32 instanceCount; + }; + + static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768; + + MeshRenderSystem(const MeshRenderSystem&) = delete; + MeshRenderSystem& operator=(const MeshRenderSystem&) = delete; + + MeshRenderSystem(Device& device, vk::Format colorFormat, + vk::Format depthFormat, ResourceRegistry& resources); + ~MeshRenderSystem(); + + void beginFrame(u32 frameIndex); + + void render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection, + std::span instances, + std::span batches); + + private: + struct PushConstantData { + glm::mat4 viewProjection{1.f}; + }; + + void createPipelineLayout(); + void createPipeline(vk::Format colorFormat, vk::Format depthFormat); + void createInstanceBuffers(); + + Device& device; + ResourceRegistry& resources; + + vk::UniquePipelineLayout pipelineLayout; + std::unique_ptr graphicsPipeline; + + std::array, RenderContext::MAX_FRAMES_IN_FLIGHT> + instanceBuffers; + u32 frameIndex = 0; + u32 instanceCursor = 0; +}; + +} // namespace sophia diff --git a/engine/src/renderer/model_render_system.cpp b/engine/src/renderer/model_render_system.cpp index 0495b06..a6565dc 100644 --- a/engine/src/renderer/model_render_system.cpp +++ b/engine/src/renderer/model_render_system.cpp @@ -25,8 +25,8 @@ ModelRenderSystem::ModelRenderSystem(Device& device, .setStage(ShaderStage::FRAGMENT) .build(); - auto bindingDescriptions = Mesh::Vertex::getBindingDescriptions(); - auto attributeDescriptions = Mesh::Vertex::getAttributeDescriptions(); + auto bindingDescriptions = Mesh::getBindingDescriptions(); + auto attributeDescriptions = Mesh::getAttributeDescriptions(); this->graphicsPipeline = GraphicsPipeline::Builder(device) @@ -44,10 +44,8 @@ ModelRenderSystem::~ModelRenderSystem() { void ModelRenderSystem::render(vk::CommandBuffer commandBuffer, std::span instances, - const FrameInfo& frameInfo) { + const glm::mat4& viewProjection) { ZoneScoped; - glm::mat4 viewProjection = frameInfo.camera.getViewProjection(); - this->graphicsPipeline->bind(commandBuffer); for (const MeshInstance& instance : instances) { diff --git a/engine/src/renderer/model_render_system.hpp b/engine/src/renderer/model_render_system.hpp index 7238a6f..1ac6348 100644 --- a/engine/src/renderer/model_render_system.hpp +++ b/engine/src/renderer/model_render_system.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include "renderer/render_context.hpp" @@ -32,7 +32,7 @@ class ModelRenderSystem { void render(vk::CommandBuffer commandBuffer, std::span instances, - const FrameInfo& frameInfo); + const glm::mat4& viewProjection); private: void createPipelineLayout(); diff --git a/engine/src/renderer/render_context.cpp b/engine/src/renderer/render_context.cpp index 73d7b51..e5c39b0 100644 --- a/engine/src/renderer/render_context.cpp +++ b/engine/src/renderer/render_context.cpp @@ -14,9 +14,13 @@ RenderContext::RenderContext(Window& window, Device& device) : window{window}, device{device} { createFrames(); recreateSwapchain(); + createTracyContext(); } -RenderContext::~RenderContext() { this->device.waitIdle(); } +RenderContext::~RenderContext() { + this->device.waitIdle(); + TracyVkDestroy(this->tracyContext); +} vk::CommandBuffer RenderContext::beginFrame() { ZoneScoped; @@ -75,6 +79,9 @@ vk::CommandBuffer RenderContext::beginFrame() { throw std::runtime_error("failed to begin recording command buffer"); } + TracyVkCollect(this->tracyContext, + static_cast(frame.commandBuffer)); + return frame.commandBuffer; } @@ -158,6 +165,27 @@ void RenderContext::recreateSwapchain() { this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{}); } +void RenderContext::createTracyContext() { +#ifdef TRACY_ENABLE + // Tracy records and submits a one-off command buffer to line up GPU and CPU + // clocks, and waits for it. + vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(), + vk::CommandBufferLevel::ePrimary, 1}; + vk::CommandBuffer commandBuffer = + this->device.get()->allocateCommandBuffers(allocInfo).front(); + + this->tracyContext = TracyVkContext( + static_cast(this->device.getPhysicalDevice()), + static_cast(*this->device.get()), + static_cast(this->device.getGraphicsQueue()), + static_cast(commandBuffer)); + TracyVkContextName(this->tracyContext, "Graphics", 8); + + this->device.get()->freeCommandBuffers(this->device.getCommandPool(), + commandBuffer); +#endif +} + void RenderContext::createFrames() { vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(), vk::CommandBufferLevel::ePrimary, diff --git a/engine/src/renderer/render_context.hpp b/engine/src/renderer/render_context.hpp index d5667d1..00330a8 100644 --- a/engine/src/renderer/render_context.hpp +++ b/engine/src/renderer/render_context.hpp @@ -6,6 +6,9 @@ #include #include +/* AFTER VULKAN */ +#include + #include "vulkan/device.hpp" #include "vulkan/swapchain.hpp" #include "window.hpp" @@ -35,11 +38,17 @@ class RenderContext { return this->swapchain->getImage(static_cast(this->currentImageIndex)); } + Image& getCurrentDepthImage() const { + return this->swapchain->getDepthImage( + static_cast(this->currentImageIndex)); + } + vk::Extent2D getExtent() const { return this->swapchain->getExtent(); } vk::Format getImageFormat() const { return this->swapchain->getImageFormat(); } u32 getFrameIndex() const { return this->currentFrameIndex; } + TracyVkCtx getTracyContext() const { return this->tracyContext; } u32 getImageCount() const { return static_cast(this->swapchain->imageCount()); } @@ -47,6 +56,7 @@ class RenderContext { private: void recreateSwapchain(); void createFrames(); + void createTracyContext(); Window& window; Device& device; @@ -58,6 +68,8 @@ class RenderContext { std::vector renderFinishedSemaphores; std::vector imagesInFlight; + TracyVkCtx tracyContext = nullptr; + u32 currentFrameIndex = 0; u32 currentImageIndex = 0; bool isFrameStarted = false; diff --git a/engine/src/renderer/render_graph.cpp b/engine/src/renderer/render_graph.cpp index af5b978..170bba9 100644 --- a/engine/src/renderer/render_graph.cpp +++ b/engine/src/renderer/render_graph.cpp @@ -71,6 +71,10 @@ void RenderGraph::flush(vk::CommandBuffer commandBuffer, } void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { + ZoneTransientN(cpuZone, pass.name.c_str(), true); + TracyVkZoneTransient(this->tracyContext, gpuZone, + static_cast(commandBuffer), + pass.name.c_str(), this->tracyContext != nullptr); beginDebugLabel(commandBuffer, pass.name.c_str()); PendingBarriers pending; @@ -79,7 +83,7 @@ void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { } this->flush(commandBuffer, pending); - if (pass.colorAttachments.empty()) { + if (pass.colorAttachments.empty() && !pass.depthAttachment) { pass.execute(commandBuffer); } else { this->recordRendering(commandBuffer, pass); @@ -92,9 +96,10 @@ void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) { std::vector attachments; attachments.reserve(pass.colorAttachments.size()); - vk::Extent3D imageExtent = - this->images[pass.colorAttachments.front().image.index] - .image->getExtent(); + ImageRef sizedBy = pass.colorAttachments.empty() + ? pass.depthAttachment->image + : pass.colorAttachments.front().image; + vk::Extent3D imageExtent = this->images[sizedBy.index].image->getExtent(); vk::Extent2D extent{imageExtent.width, imageExtent.height}; for (const ColorAttachment& color : pass.colorAttachments) { @@ -119,6 +124,24 @@ void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) { renderingInfo.colorAttachmentCount = static_cast(attachments.size()); renderingInfo.pColorAttachments = attachments.data(); + vk::RenderingAttachmentInfo depthAttachment{}; + if (pass.depthAttachment) { + const Image& image = *this->images[pass.depthAttachment->image.index].image; + SOPHIA_ASSERT(image.getExtent().width == extent.width && + image.getExtent().height == extent.height, + "Pass", pass.name, + "has a depth attachment of a different size"); + + depthAttachment.imageView = image.getView(); + depthAttachment.imageLayout = + vk::ImageLayout::eDepthStencilAttachmentOptimal; + depthAttachment.loadOp = pass.depthAttachment->loadOp; + depthAttachment.storeOp = vk::AttachmentStoreOp::eStore; + depthAttachment.clearValue.depthStencil = + vk::ClearDepthStencilValue{pass.depthAttachment->clearDepth, 0}; + renderingInfo.pDepthAttachment = &depthAttachment; + } + commandBuffer.beginRendering(renderingInfo); vk::Viewport viewport{0.0f, @@ -143,6 +166,15 @@ RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor( return *this; } +RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeDepth( + ImageRef image, vk::AttachmentLoadOp loadOp, float clearDepth) { + SOPHIA_ASSERT(!this->pass.depthAttachment, "Pass", this->pass.name, + "already has a depth attachment"); + this->use(image, Access::DepthWrite); + this->pass.depthAttachment = DepthAttachment{image, loadOp, clearDepth}; + return *this; +} + RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) { return this->use(image, Access::SampledRead); } diff --git a/engine/src/renderer/render_graph.hpp b/engine/src/renderer/render_graph.hpp index be2e5ef..a9a190f 100644 --- a/engine/src/renderer/render_graph.hpp +++ b/engine/src/renderer/render_graph.hpp @@ -2,10 +2,13 @@ #include #include +#include #include #include #include #include +// After Vulkan, which it expects to already be declared. +#include #include "vulkan/access.hpp" #include "vulkan/image.hpp" @@ -31,7 +34,7 @@ using ClearColor = std::array; * buffer. Passes run in the order they were added; the graph's job is to * work out the barriers and layout transitions between them from what each * pass says it does to each image, and to begin/end rendering for passes that - * write color attachments. + * write color or depth attachments. * * Built fresh every frame: * @@ -53,7 +56,8 @@ class RenderGraph { using SetupFn = std::function; using ExecuteFn = std::function; - RenderGraph() = default; + explicit RenderGraph(TracyVkCtx tracyContext = nullptr) + : tracyContext{tracyContext} {} RenderGraph(const RenderGraph&) = delete; RenderGraph& operator=(const RenderGraph&) = delete; @@ -67,8 +71,8 @@ class RenderGraph { /** * Adds a pass. `setup` runs immediately and declares what the pass does to * images; `execute` runs later, from execute(), with the barriers already - * recorded and (if the pass writes color) rendering begun. `name` labels - * the pass's commands in debugging tools such as RenderDoc. + * recorded and (if the pass writes color or depth) rendering begun. `name` + * labels the pass's commands in debugging tools such as RenderDoc. */ void addPass(std::string name, const SetupFn& setup, ExecuteFn execute); @@ -89,10 +93,17 @@ class RenderGraph { ClearColor clearColor; }; + struct DepthAttachment { + ImageRef image; + vk::AttachmentLoadOp loadOp; + float clearDepth; + }; + struct Pass { std::string name; std::vector uses; std::vector colorAttachments; + std::optional depthAttachment; ExecuteFn execute; }; @@ -113,6 +124,7 @@ class RenderGraph { void recordPass(vk::CommandBuffer commandBuffer, Pass& pass); void recordRendering(vk::CommandBuffer commandBuffer, Pass& pass); + TracyVkCtx tracyContext; std::vector images; std::vector passes; }; @@ -129,6 +141,15 @@ class RenderGraph::PassBuilder { vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear, ClearColor clearColor = {0.f, 0.f, 0.f, 1.f}); + /** + * Renders with `image` as the depth attachment, which must be the same size + * as any color attachments. At most one per pass. + */ + PassBuilder& writeDepth( + ImageRef image, + vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear, + float clearDepth = 1.f); + /** Samples `image` in a shader. */ PassBuilder& sample(ImageRef image); diff --git a/engine/src/renderer/renderer.cpp b/engine/src/renderer/renderer.cpp new file mode 100644 index 0000000..8a9deeb --- /dev/null +++ b/engine/src/renderer/renderer.cpp @@ -0,0 +1,9 @@ +#include "renderer/renderer.hpp" + +namespace sophia { + +Renderer::Renderer() {} + +Renderer::~Renderer() {} + +} // namespace sophia diff --git a/engine/src/renderer/renderer.hpp b/engine/src/renderer/renderer.hpp new file mode 100644 index 0000000..800ce87 --- /dev/null +++ b/engine/src/renderer/renderer.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "vulkan/buffer.hpp" + +namespace sophia { + +struct DrawPacket { + /* Geometry */ + // BufferHandle vertexBuffer; + // BufferHandle indexBuffer; + u32 indexCount; + u32 firstIndex; + + /* Shader & state */ + // PipelineHandle pipelineState; // Contains shaders, blend state, depth + // settings + + /* Bindings */ + // DescriptorSetHandle resourceBindings; // Textures, Uniform Buffers, + // Storage Buffers + + /* Transform / Constants */ + u32 pushConstantData[16]; +}; + +struct RenderView { + mat4 view; + mat4 projection; + vec2 viewport; + + RenderTargetHandle target; +}; + +// struct ComputePacket { +// ComputePipelineHandle pipeline; +// DescriptorSetHandle +// resourceBindings; // Includes storage buffers, uniforms, textures + +// // Grid dimensions +// uint32_t groupCountX = 1; +// uint32_t groupCountY = 1; +// uint32_t groupCountZ = 1; + +// // Optional push constants for dynamic per-dispatch parameters +// uint8_t pushConstants[64]; +// uint32_t pushConstantSize = 0; +// }; + +// !TODO move to render graph +enum class RenderPassType { Graphics, Compute }; + +// !TODO move to render graph (???) +struct RenderPass { + std::string name; + RenderPassType type; + + // Declared for automatic barrier injection & resource synchronization + std::vector readBuffers; // BufferHandle instead? + std::vector writeBuffers; // BufferHandle instead? + + // Commands in this pass + // std::vector computeDispatches; + std::vector opaqueDrawPackets; + std::vector transparentDrawPackets; +}; + +struct RenderPacket { + RenderView view; + std::vector passes; +}; + +class Renderer { + public: + Renderer(const Renderer&) = delete; + Renderer& operator=(const Renderer&) = delete; + + Renderer(); + ~Renderer(); + + // PipelineHandle createPipeline(); + + private: +}; + +} // namespace sophia diff --git a/engine/src/renderer/scene_renderer.cpp b/engine/src/renderer/scene_renderer.cpp index 92e2563..537640f 100644 --- a/engine/src/renderer/scene_renderer.cpp +++ b/engine/src/renderer/scene_renderer.cpp @@ -13,73 +13,149 @@ SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry, vk::Format colorFormat) : sceneRegistry{sceneRegistry}, resources{resources}, - spriteRenderSystem{device, colorFormat, resources} {} + meshRenderSystem{device, colorFormat, + resources.renderTargetPool.getDepthFormat(), resources}, + spriteRenderSystem{device, colorFormat, + resources.renderTargetPool.getDepthFormat(), + resources} {} void SceneRenderer::beginFrame(u32 frameIndex) { ZoneScoped; + this->meshRenderSystem.beginFrame(frameIndex); this->spriteRenderSystem.beginFrame(frameIndex); } void SceneRenderer::render(vk::CommandBuffer commandBuffer, - const SceneView& sceneView, Entity camera) { + const SceneView& sceneView, Entity camera, + vk::Extent2D extent) { ZoneScoped; SOPHIA_ASSERT(camera.hasComponent(), "camera does not have camera component!"); - vk::Extent2D extent = - this->resources.renderTargetPool.get(sceneView.target).getExtent(); float aspect = static_cast(extent.width) / static_cast(extent.height); - glm::mat4 viewProjection = - camera.getComponent().getViewProjection( - camera.getComponent(), aspect); + glm::mat4 view = + CameraComponent::getView(camera.getComponent()); + glm::mat4 projection = + camera.getComponent().getProjection(aspect); - renderSprites(commandBuffer, this->sceneRegistry.get(sceneView.scene), - viewProjection); + const Scene& scene = this->sceneRegistry.get(sceneView.scene); + glm::mat4 viewProjection = projection * view; - // TODO: meshes. Group MeshComponent entities by mesh and draw each group - // (instanced), as sprites are grouped by texture. + // Opaque first, so sprites blend over the meshes behind them. + renderMeshes(commandBuffer, scene, viewProjection); + renderSprites(commandBuffer, scene, view, viewProjection); +} + +void SceneRenderer::renderMeshes(vk::CommandBuffer commandBuffer, + const Scene& scene, + const glm::mat4& viewProjection) { + ZoneScoped; + this->meshDrawItems.clear(); + + { + ZoneScopedN("meshes: gather"); + auto meshes = + scene.registry.view(); + for (auto [entity, transform, mesh] : meshes.each()) { + if (!this->resources.meshPool.isAlive(mesh.handle)) { + continue; + } + this->meshDrawItems.push_back( + {mesh.handle, {transform.mat4(), mesh.color}}); + } + } + + { + ZoneScopedN("meshes: sort"); + // Opaque, so order only matters for batching: one batch per mesh. + std::ranges::sort(this->meshDrawItems, {}, + [](const MeshDrawItem& item) { return item.mesh; }); + } + + { + ZoneScopedN("meshes: batch"); + this->meshInstances.clear(); + this->meshBatches.clear(); + for (const MeshDrawItem& item : this->meshDrawItems) { + auto index = static_cast(this->meshInstances.size()); + this->meshInstances.push_back(item.instance); + + if (this->meshBatches.empty() || + this->meshBatches.back().mesh != item.mesh) { + this->meshBatches.push_back({item.mesh, index, 0}); + } + this->meshBatches.back().instanceCount++; + } + } + + TracyPlot("mesh instances", static_cast(this->meshInstances.size())); + TracyPlot("mesh batches", static_cast(this->meshBatches.size())); + + this->meshRenderSystem.render(commandBuffer, viewProjection, + this->meshInstances, this->meshBatches); } void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer, - const Scene& scene, + const Scene& scene, const glm::mat4& view, const glm::mat4& viewProjection) { + ZoneScoped; this->spriteDrawItems.clear(); - auto view = - scene.registry.view(); - for (auto [entity, transform, sprite] : view.each()) { - TextureHandle texture = this->resources.texturePool.isAlive(sprite.texture) - ? sprite.texture - : this->resources.whiteTexture; + { + ZoneScopedN("sprites: gather"); + auto sprites = + scene.registry.view(); + for (auto [entity, transform, sprite] : sprites.each()) { + TextureHandle texture = + this->resources.texturePool.isAlive(sprite.texture) + ? sprite.texture + : this->resources.whiteTexture; - this->spriteDrawItems.push_back( - {sprite.sortOrder, - transform.position.z, - texture, - {sprite.getModel(transform), sprite.color, sprite.uvRect}}); - } + // Distance in front of the camera, which looks down view-space -Z. + float depth = -(view * glm::vec4{transform.position, 1.f}).z; - // Texture is the last key so that sprites whose order doesn't matter - // relative to each other still end up in as few batches as possible. - std::ranges::sort(this->spriteDrawItems, {}, [](const SpriteDrawItem& item) { - return std::tuple{item.sortOrder, item.depth, item.texture}; - }); - - this->spriteInstances.clear(); - this->spriteBatches.clear(); - for (const SpriteDrawItem& item : this->spriteDrawItems) { - auto index = static_cast(this->spriteInstances.size()); - this->spriteInstances.push_back(item.instance); - - if (this->spriteBatches.empty() || - this->spriteBatches.back().texture != item.texture) { - this->spriteBatches.push_back({item.texture, index, 0}); + this->spriteDrawItems.push_back( + {sprite.sortOrder, + depth, + texture, + {sprite.getModel(transform), sprite.color, sprite.uvRect}}); } - this->spriteBatches.back().instanceCount++; } + { + ZoneScopedN("sprites: sort"); + // Within a sortOrder, back to front, so nearer sprites blend over farther + // ones. Texture is the last key so that sprites whose order doesn't + // matter relative to each other still end up in as few batches as + // possible. + std::ranges::sort( + this->spriteDrawItems, {}, [](const SpriteDrawItem& item) { + return std::tuple{item.sortOrder, -item.depth, item.texture}; + }); + } + + { + ZoneScopedN("sprites: batch"); + this->spriteInstances.clear(); + this->spriteBatches.clear(); + for (const SpriteDrawItem& item : this->spriteDrawItems) { + auto index = static_cast(this->spriteInstances.size()); + this->spriteInstances.push_back(item.instance); + + if (this->spriteBatches.empty() || + this->spriteBatches.back().texture != item.texture) { + this->spriteBatches.push_back({item.texture, index, 0}); + } + this->spriteBatches.back().instanceCount++; + } + } + + TracyPlot("sprite instances", + static_cast(this->spriteInstances.size())); + TracyPlot("sprite batches", static_cast(this->spriteBatches.size())); + this->spriteRenderSystem.render(commandBuffer, viewProjection, this->spriteInstances, this->spriteBatches); } diff --git a/engine/src/renderer/scene_renderer.hpp b/engine/src/renderer/scene_renderer.hpp index 2b1bf52..f57c66a 100644 --- a/engine/src/renderer/scene_renderer.hpp +++ b/engine/src/renderer/scene_renderer.hpp @@ -4,6 +4,7 @@ #include #include +#include "renderer/mesh_render_system.hpp" #include "renderer/sprite_render_system.hpp" #include "resources/resource_registry.hpp" @@ -27,27 +28,39 @@ class SceneRenderer { // Records `sceneView` as seen through `camera` (an entity of the view's // scene with a CameraComponent) into `commandBuffer`. Rendering into the - // view's target must already have begun. + // view's target, which is `extent` in size, must already have begun. void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView, - Entity camera); + Entity camera, vk::Extent2D extent); private: + struct MeshDrawItem { + MeshHandle mesh; + MeshRenderSystem::Instance instance; + }; + struct SpriteDrawItem { s32 sortOrder; + // Distance in front of the camera. float depth; TextureHandle texture; SpriteRenderSystem::Instance instance; }; + void renderMeshes(vk::CommandBuffer commandBuffer, const Scene& scene, + const glm::mat4& viewProjection); void renderSprites(vk::CommandBuffer commandBuffer, const Scene& scene, - const glm::mat4& viewProjection); + const glm::mat4& view, const glm::mat4& viewProjection); const SceneRegistry& sceneRegistry; ResourceRegistry& resources; + MeshRenderSystem meshRenderSystem; SpriteRenderSystem spriteRenderSystem; // Reused across frames to avoid reallocating. + std::vector meshDrawItems; + std::vector meshInstances; + std::vector meshBatches; std::vector spriteDrawItems; std::vector spriteInstances; std::vector spriteBatches; diff --git a/engine/src/renderer/sprite_render_system.cpp b/engine/src/renderer/sprite_render_system.cpp index 20c9aea..932b0ec 100644 --- a/engine/src/renderer/sprite_render_system.cpp +++ b/engine/src/renderer/sprite_render_system.cpp @@ -16,11 +16,12 @@ constexpr u32 VERTICES_PER_SPRITE = 6; } // namespace SpriteRenderSystem::SpriteRenderSystem(Device& device, vk::Format colorFormat, + vk::Format depthFormat, ResourceRegistry& resources) : device{device}, resources{resources} { createDescriptors(); createPipelineLayout(); - createPipeline(colorFormat); + createPipeline(colorFormat, depthFormat); createInstanceBuffers(); } @@ -147,7 +148,8 @@ void SpriteRenderSystem::createPipelineLayout() { } } -void SpriteRenderSystem::createPipeline(vk::Format colorFormat) { +void SpriteRenderSystem::createPipeline(vk::Format colorFormat, + vk::Format depthFormat) { auto vertexShader = Shader::Builder() .fromSPIRV(shaders::sprite_vert_spv) .setStage(ShaderStage::VERTEX) @@ -172,14 +174,17 @@ void SpriteRenderSystem::createPipeline(vk::Format colorFormat) { attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat, static_cast(offsetof(Instance, uvRect))}); - this->graphicsPipeline = GraphicsPipeline::Builder(this->device) - .setVertexShader(*vertexShader) - .setFragmentShader(*fragmentShader) - .setPipelineLayout(this->pipelineLayout.get()) - .setColorAttachmentFormats({colorFormat}) - .setVertexInput(bindings, attributes) - .setAlphaBlending() - .build(); + this->graphicsPipeline = + GraphicsPipeline::Builder(this->device) + .setVertexShader(*vertexShader) + .setFragmentShader(*fragmentShader) + .setPipelineLayout(this->pipelineLayout.get()) + .setColorAttachmentFormats({colorFormat}) + .setDepthAttachmentFormat(depthFormat) + .setVertexInput(bindings, attributes) + .setAlphaBlending() + .setDepthTest(true, false, vk::CompareOp::eLessOrEqual) + .build(); } void SpriteRenderSystem::createInstanceBuffers() { diff --git a/engine/src/renderer/sprite_render_system.hpp b/engine/src/renderer/sprite_render_system.hpp index 293caae..993a6de 100644 --- a/engine/src/renderer/sprite_render_system.hpp +++ b/engine/src/renderer/sprite_render_system.hpp @@ -20,6 +20,8 @@ namespace sophia { /** * Draws batches of textured quads. Knows nothing about scenes: callers hand * it instances already in draw order, grouped into runs that share a texture. + * Sprites are depth tested, so opaque geometry in front hides them, but + * don't write depth, since they blend. * * Instances are written to a per-frame host-visible buffer, so every * render() call within one frame appends to it; call beginFrame() once per @@ -47,7 +49,7 @@ class SpriteRenderSystem { SpriteRenderSystem& operator=(const SpriteRenderSystem&) = delete; SpriteRenderSystem(Device& device, vk::Format colorFormat, - ResourceRegistry& resources); + vk::Format depthFormat, ResourceRegistry& resources); ~SpriteRenderSystem(); void beginFrame(u32 frameIndex); @@ -68,7 +70,7 @@ class SpriteRenderSystem { void createDescriptors(); void createPipelineLayout(); - void createPipeline(vk::Format colorFormat); + void createPipeline(vk::Format colorFormat, vk::Format depthFormat); void createInstanceBuffers(); vk::DescriptorSet getDescriptorSet(TextureHandle handle); diff --git a/engine/src/resources/mesh.cpp b/engine/src/resources/mesh.cpp index eba9e28..c521c7d 100644 --- a/engine/src/resources/mesh.cpp +++ b/engine/src/resources/mesh.cpp @@ -5,35 +5,32 @@ namespace sophia { -std::vector -Mesh::Vertex::getBindingDescriptions() { - std::vector bindingDescriptions(1); - bindingDescriptions[0].binding = 0; - bindingDescriptions[0].stride = sizeof(Vertex); - bindingDescriptions[0].inputRate = vk::VertexInputRate::eVertex; - - return bindingDescriptions; +std::vector Mesh::getBindingDescriptions() { + return {{0, sizeof(MeshVertex), vk::VertexInputRate::eVertex}}; } std::vector -Mesh::Vertex::getAttributeDescriptions() { - std::vector attributeDescriptions{}; - - attributeDescriptions.push_back( - {0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)}); - attributeDescriptions.push_back( - {1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}); - // attributeDescriptions.push_back( - // {2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal)}); - // attributeDescriptions.push_back( - // {3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, uv)}); - - return attributeDescriptions; +Mesh::getAttributeDescriptions() { + return { + {0, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, position)}, + {1, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, normal)}, + {2, 0, vk::Format::eR32G32Sfloat, offsetof(MeshVertex, uv)}, + {3, 0, vk::Format::eR32G32B32A32Sfloat, offsetof(MeshVertex, color)}, + }; } -Mesh::Mesh(Device& device, const Builder& builder) : device{device} { - createVertexBuffers(builder.vertices); - createIndexBuffers(builder.indices); +Mesh::Mesh(Device& device, const MeshData& data) : device{device} { + if (!data.vertices.empty()) { + this->bounds = {data.vertices.front().position, + data.vertices.front().position}; + for (const MeshVertex& vertex : data.vertices) { + this->bounds.min = glm::min(this->bounds.min, vertex.position); + this->bounds.max = glm::max(this->bounds.max, vertex.position); + } + } + + createVertexBuffers(data.vertices); + createIndexBuffers(data.indices); } Mesh::~Mesh() = default; @@ -49,15 +46,17 @@ void Mesh::bind(vk::CommandBuffer commandBuffer) const { } } -void Mesh::draw(vk::CommandBuffer commandBuffer) const { +void Mesh::draw(vk::CommandBuffer commandBuffer, u32 instanceCount, + u32 firstInstance) const { if (this->hasIndexBuffer) { - commandBuffer.drawIndexed(this->indexCount, 1, 0, 0, 0); + commandBuffer.drawIndexed(this->indexCount, instanceCount, 0, 0, + firstInstance); } else { - commandBuffer.draw(this->vertexCount, 1, 0, 0); + commandBuffer.draw(this->vertexCount, instanceCount, 0, firstInstance); } } -void Mesh::createVertexBuffers(const std::vector& vertices) { +void Mesh::createVertexBuffers(const std::vector& vertices) { this->vertexCount = static_cast(vertices.size()); SOPHIA_ASSERT(this->vertexCount >= 3, "vertex count must be at least 3"); diff --git a/engine/src/resources/mesh.hpp b/engine/src/resources/mesh.hpp index 86bd302..a193651 100644 --- a/engine/src/resources/mesh.hpp +++ b/engine/src/resources/mesh.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include #include #include @@ -13,47 +15,33 @@ namespace sophia { class Mesh { public: - struct Vertex { - glm::vec2 position{}; - glm::vec3 color{}; - // glm::vec3 normal{}; - // glm::vec2 uv{}; + // Binding 0 (per vertex, MeshVertex) for pipelines that draw meshes. + static std::vector + getBindingDescriptions(); + static std::vector + getAttributeDescriptions(); - static std::vector - getBindingDescriptions(); - static std::vector - getAttributeDescriptions(); - - // bool operator==(const Vertex& other) const { - // return position == other.position && color == other.color; - // } - }; - - struct Builder { - std::vector vertices{}; - std::vector indices{}; - - // void loadModel(const std::string& filepath); - }; - - Mesh(Device& device, const Mesh::Builder& builder); + Mesh(Device& device, const MeshData& data); ~Mesh(); Mesh(const Mesh&) = delete; Mesh& operator=(const Mesh&) = delete; - // static std::unique_ptr createMeshFromFile( - // Device& device, const std::string& filepath); + // Bounds of the vertices, in the mesh's own space. + const AABB& getBounds() const { return this->bounds; } void bind(vk::CommandBuffer commandBuffer) const; - void draw(vk::CommandBuffer commandBuffer) const; + void draw(vk::CommandBuffer commandBuffer, u32 instanceCount = 1, + u32 firstInstance = 0) const; private: - void createVertexBuffers(const std::vector& vertices); + void createVertexBuffers(const std::vector& vertices); void createIndexBuffers(const std::vector& indices); Device& device; + AABB bounds; + std::unique_ptr vertexBuffer; u32 vertexCount; diff --git a/engine/src/resources/pools/render_target_pool.cpp b/engine/src/resources/pools/render_target_pool.cpp index d34817c..f265cfe 100644 --- a/engine/src/resources/pools/render_target_pool.cpp +++ b/engine/src/resources/pools/render_target_pool.cpp @@ -5,10 +5,16 @@ namespace sophia { RenderTargetPool::RenderTargetPool(Device& device, vk::Format format) - : device{device}, format{format} {} + : device{device}, + format{format}, + // Depth-only formats, so the images have no stencil aspect to track. + depthFormat{device.findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eX8D24UnormPack32}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment)} {} RenderTargetHandle RenderTargetPool::create(u32 width, u32 height) { - return pool.create(device, format, width, height); + return pool.create(device, format, depthFormat, width, height); } void RenderTargetPool::release(RenderTargetHandle handle) { diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index 5f4b525..8fee7b2 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -24,11 +24,15 @@ class RenderTargetPool { bool isAlive(RenderTargetHandle handle) const { return pool.isAlive(handle); } + vk::Format getFormat() const { return format; } + vk::Format getDepthFormat() const { return depthFormat; } + void applyPendingResizes(); private: Device& device; vk::Format format; + vk::Format depthFormat; Pool pool{"RenderTarget"}; }; diff --git a/engine/src/resources/primitives.cpp b/engine/src/resources/primitives.cpp new file mode 100644 index 0000000..9747fd3 --- /dev/null +++ b/engine/src/resources/primitives.cpp @@ -0,0 +1,55 @@ +#include +#include + +namespace sophia::primitives { + +MeshData cube(float size, vec4 color) { + struct Face { + vec3 normal; + // Axes across the face, with u x v = normal, so corners taken + // counter-clockwise in (u, v) wind counter-clockwise seen from outside. + vec3 u; + vec3 v; + }; + + constexpr std::array FACES{{ + {{1.f, 0.f, 0.f}, {0.f, 0.f, -1.f}, {0.f, 1.f, 0.f}}, + {{-1.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, {0.f, 1.f, 0.f}}, + {{0.f, 1.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 0.f, -1.f}}, + {{0.f, -1.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 0.f, 1.f}}, + {{0.f, 0.f, 1.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}}, + {{0.f, 0.f, -1.f}, {-1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}}, + }}; + + constexpr std::array CORNERS{vec2{-1.f, -1.f}, vec2{1.f, -1.f}, + vec2{1.f, 1.f}, vec2{-1.f, 1.f}}; + + float half = size / 2.f; + + MeshData data; + data.vertices.reserve(FACES.size() * CORNERS.size()); + data.indices.reserve(FACES.size() * 6); + + for (const Face& face : FACES) { + auto base = static_cast(data.vertices.size()); + + for (vec2 corner : CORNERS) { + MeshVertex vertex; + vertex.position = + (face.normal + corner.x * face.u + corner.y * face.v) * half; + vertex.normal = face.normal; + // Texture v runs down while the face's v runs up. + vertex.uv = {(corner.x + 1.f) / 2.f, (1.f - corner.y) / 2.f}; + vertex.color = color; + data.vertices.push_back(vertex); + } + + for (u32 index : {0u, 1u, 2u, 2u, 3u, 0u}) { + data.indices.push_back(base + index); + } + } + + return data; +} + +} // namespace sophia::primitives diff --git a/engine/src/resources/render_target.cpp b/engine/src/resources/render_target.cpp index 23cccb7..a90c6ba 100644 --- a/engine/src/resources/render_target.cpp +++ b/engine/src/resources/render_target.cpp @@ -4,9 +4,9 @@ namespace sophia { -RenderTarget::RenderTarget(Device& device, vk::Format format, u32 width, - u32 height) - : device{device}, format{format} { +RenderTarget::RenderTarget(Device& device, vk::Format format, + vk::Format depthFormat, u32 width, u32 height) + : device{device}, format{format}, depthFormat{depthFormat} { create(std::max(width, 1u), std::max(height, 1u)); } @@ -46,8 +46,17 @@ void RenderTarget::create(u32 width, u32 height) { .asColorAttachment() .withView() .build(); + this->depthImage = Image::Builder(this->device) + .setExtent(width, height) + .asDepthAttachment() + .setFormat(this->depthFormat) + .withView() + .build(); } -void RenderTarget::destroy() { this->image.reset(); } +void RenderTarget::destroy() { + this->image.reset(); + this->depthImage.reset(); +} } // namespace sophia diff --git a/engine/src/resources/render_target.hpp b/engine/src/resources/render_target.hpp index c545c52..2e9e3c0 100644 --- a/engine/src/resources/render_target.hpp +++ b/engine/src/resources/render_target.hpp @@ -11,20 +11,22 @@ namespace sophia { /** - * A place rendering can go. Currently always backed by an offscreen color - * image. + * A place rendering can go: an offscreen color image, plus a depth image of + * the same size. * * Owned by the ResourceRegistry and addressed through a RenderTargetHandle. */ class RenderTarget { public: - RenderTarget(Device& device, vk::Format format, u32 width, u32 height); + RenderTarget(Device& device, vk::Format format, vk::Format depthFormat, + u32 width, u32 height); ~RenderTarget(); RenderTarget(const RenderTarget&) = delete; RenderTarget& operator=(const RenderTarget&) = delete; Image& getImage() const { return *this->image; } + Image& getDepthImage() const { return *this->depthImage; } vk::Extent2D getExtent() const; u32 getVersion() const { return this->version; } @@ -43,8 +45,10 @@ class RenderTarget { Device& device; vk::Format format; + vk::Format depthFormat; std::unique_ptr image; + std::unique_ptr depthImage; std::optional pendingExtent; u32 version = 0; diff --git a/engine/src/resources/resource_registry_ref.cpp b/engine/src/resources/resource_registry_ref.cpp index 346f7c8..a329abf 100644 --- a/engine/src/resources/resource_registry_ref.cpp +++ b/engine/src/resources/resource_registry_ref.cpp @@ -14,7 +14,7 @@ MeshHandle ResourceRegistryRef::load(const std::string& path) { (void)path; SOPHIA_ASSERT(false, "Mesh::load(path) is not implemented yet - no model file " - "format is supported; build one via Mesh::Builder for now"); + "format is supported; build a MeshData instead"); return MeshHandle{}; } @@ -40,6 +40,8 @@ TextureHandle ResourceRegistryRef::load(const std::string& path) { template <> void ResourceRegistryRef::release(MeshHandle handle) { + // Frames in flight may still be drawing it. + registry->device.waitIdle(); registry->meshPool.release(handle); } @@ -57,17 +59,9 @@ void ResourceRegistryRef::release(TextureHandle handle) { registry->texturePool.release(handle); } -MeshHandle ResourceRegistryRef::createResource( - std::type_identity, std::vector vertices, - std::vector indices) { - Mesh::Builder builder{}; - builder.vertices.reserve(vertices.size()); - for (const CanvasVertex& vertex : vertices) { - builder.vertices.push_back({vertex.position, vertex.color}); - } - builder.indices = std::move(indices); - - return registry->meshPool.create(registry->device, builder); +MeshHandle ResourceRegistryRef::createResource(std::type_identity, + const MeshData& data) { + return registry->meshPool.create(registry->device, data); } RenderTargetHandle ResourceRegistryRef::createResource( @@ -92,6 +86,14 @@ glm::vec2 ResourceRegistryRef::getExtent(RenderTargetHandle handle) const { return {static_cast(extent.width), static_cast(extent.height)}; } +AABB ResourceRegistryRef::getBounds(MeshHandle handle) const { + return registry->meshPool.get(handle).getBounds(); +} + +bool ResourceRegistryRef::isAlive(MeshHandle handle) const { + return registry->meshPool.isAlive(handle); +} + bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const { return registry->renderTargetPool.isAlive(handle); } diff --git a/engine/src/scene/scene_render_packet_builder.cpp b/engine/src/scene/scene_render_packet_builder.cpp new file mode 100644 index 0000000..db1e30d --- /dev/null +++ b/engine/src/scene/scene_render_packet_builder.cpp @@ -0,0 +1,9 @@ +#include "scene/scene_render_packet_builder.hpp" + +namespace sophia { + +void SceneRenderPacketBuilder::build(SceneHandle sceneHandle, + RenderTargetHandle renderTargetHandle, + RenderPacket& renderPacket) {} + +} // namespace sophia diff --git a/engine/src/scene/scene_render_packet_builder.hpp b/engine/src/scene/scene_render_packet_builder.hpp new file mode 100644 index 0000000..50c2a08 --- /dev/null +++ b/engine/src/scene/scene_render_packet_builder.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "renderer/renderer.hpp" + +namespace sophia { + +class SceneRenderPacketBuilder { + public: + static void build(SceneHandle sceneHandle, + RenderTargetHandle renderTargetHandle, + RenderPacket& renderPacket); +}; + +} // namespace sophia diff --git a/engine/src/shaders/fullscreen.vert.spv.h b/engine/src/shaders/fullscreen.vert.spv.h index 08b2353..cd66c96 100644 --- a/engine/src/shaders/fullscreen.vert.spv.h +++ b/engine/src/shaders/fullscreen.vert.spv.h @@ -6,95 +6,66 @@ namespace sophia::shaders { inline constexpr std::array fullscreen_vert_spv = { - 0x07230203, 0x00010000, 0x000d000b, 0x00000032, - 0x00000000, 0x00020011, 0x00000001, 0x0006000b, - 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, - 0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, - 0x00000000, 0x00000018, 0x00000022, 0x0000002c, - 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, - 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, - 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, - 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, - 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, - 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, - 0x00000000, 0x00050005, 0x0000000c, 0x69736f70, - 0x6e6f6974, 0x00000073, 0x00030005, 0x00000015, - 0x00736f70, 0x00060005, 0x00000018, 0x565f6c67, - 0x65747265, 0x646e4978, 0x00007865, 0x00060005, - 0x00000020, 0x505f6c67, 0x65567265, 0x78657472, - 0x00000000, 0x00060006, 0x00000020, 0x00000000, - 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, - 0x00000020, 0x00000001, 0x505f6c67, 0x746e696f, - 0x657a6953, 0x00000000, 0x00070006, 0x00000020, - 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, - 0x0065636e, 0x00070006, 0x00000020, 0x00000003, - 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, - 0x00030005, 0x00000022, 0x00000000, 0x00030005, - 0x0000002c, 0x00007675, 0x00040047, 0x00000018, - 0x0000000b, 0x0000002a, 0x00030047, 0x00000020, - 0x00000002, 0x00050048, 0x00000020, 0x00000000, - 0x0000000b, 0x00000000, 0x00050048, 0x00000020, - 0x00000001, 0x0000000b, 0x00000001, 0x00050048, - 0x00000020, 0x00000002, 0x0000000b, 0x00000003, - 0x00050048, 0x00000020, 0x00000003, 0x0000000b, - 0x00000004, 0x00040047, 0x0000002c, 0x0000001e, - 0x00000000, 0x00020013, 0x00000002, 0x00030021, - 0x00000003, 0x00000002, 0x00030016, 0x00000006, - 0x00000020, 0x00040017, 0x00000007, 0x00000006, - 0x00000002, 0x00040015, 0x00000008, 0x00000020, - 0x00000000, 0x0004002b, 0x00000008, 0x00000009, - 0x00000006, 0x0004001c, 0x0000000a, 0x00000007, - 0x00000009, 0x00040020, 0x0000000b, 0x00000006, - 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, - 0x00000006, 0x0004002b, 0x00000006, 0x0000000d, - 0xbf800000, 0x0005002c, 0x00000007, 0x0000000e, - 0x0000000d, 0x0000000d, 0x0004002b, 0x00000006, - 0x0000000f, 0x3f800000, 0x0005002c, 0x00000007, - 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, - 0x00000007, 0x00000011, 0x0000000d, 0x0000000f, - 0x0005002c, 0x00000007, 0x00000012, 0x0000000f, - 0x0000000f, 0x0009002c, 0x0000000a, 0x00000013, - 0x0000000e, 0x00000010, 0x00000011, 0x00000011, - 0x00000010, 0x00000012, 0x00040020, 0x00000014, - 0x00000007, 0x00000007, 0x00040015, 0x00000016, - 0x00000020, 0x00000001, 0x00040020, 0x00000017, - 0x00000001, 0x00000016, 0x0004003b, 0x00000017, - 0x00000018, 0x00000001, 0x00040020, 0x0000001a, - 0x00000006, 0x00000007, 0x00040017, 0x0000001d, - 0x00000006, 0x00000004, 0x0004002b, 0x00000008, - 0x0000001e, 0x00000001, 0x0004001c, 0x0000001f, - 0x00000006, 0x0000001e, 0x0006001e, 0x00000020, - 0x0000001d, 0x00000006, 0x0000001f, 0x0000001f, - 0x00040020, 0x00000021, 0x00000003, 0x00000020, - 0x0004003b, 0x00000021, 0x00000022, 0x00000003, - 0x0004002b, 0x00000016, 0x00000023, 0x00000000, - 0x0004002b, 0x00000006, 0x00000025, 0x00000000, - 0x00040020, 0x00000029, 0x00000003, 0x0000001d, - 0x00040020, 0x0000002b, 0x00000003, 0x00000007, - 0x0004003b, 0x0000002b, 0x0000002c, 0x00000003, - 0x0004002b, 0x00000006, 0x0000002e, 0x3f000000, - 0x00050036, 0x00000002, 0x00000004, 0x00000000, - 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, - 0x00000014, 0x00000015, 0x00000007, 0x0003003e, - 0x0000000c, 0x00000013, 0x0004003d, 0x00000016, - 0x00000019, 0x00000018, 0x00050041, 0x0000001a, - 0x0000001b, 0x0000000c, 0x00000019, 0x0004003d, - 0x00000007, 0x0000001c, 0x0000001b, 0x0003003e, - 0x00000015, 0x0000001c, 0x0004003d, 0x00000007, - 0x00000024, 0x00000015, 0x00050051, 0x00000006, - 0x00000026, 0x00000024, 0x00000000, 0x00050051, - 0x00000006, 0x00000027, 0x00000024, 0x00000001, - 0x00070050, 0x0000001d, 0x00000028, 0x00000026, - 0x00000027, 0x00000025, 0x0000000f, 0x00050041, - 0x00000029, 0x0000002a, 0x00000022, 0x00000023, - 0x0003003e, 0x0000002a, 0x00000028, 0x0004003d, - 0x00000007, 0x0000002d, 0x00000015, 0x0005008e, - 0x00000007, 0x0000002f, 0x0000002d, 0x0000002e, - 0x00050050, 0x00000007, 0x00000030, 0x0000002e, - 0x0000002e, 0x00050081, 0x00000007, 0x00000031, - 0x0000002f, 0x00000030, 0x0003003e, 0x0000002c, - 0x00000031, 0x000100fd, 0x00010038, + 0x07230203, 0x00010000, 0x000d000b, 0x00000032, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000018, 0x00000022, 0x0000002c, + 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, + 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, + 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, + 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x00050005, 0x0000000c, 0x69736f70, 0x6e6f6974, 0x00000073, + 0x00030005, 0x00000015, 0x00736f70, 0x00060005, 0x00000018, 0x565f6c67, + 0x65747265, 0x646e4978, 0x00007865, 0x00060005, 0x00000020, 0x505f6c67, + 0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x00000020, 0x00000000, + 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, 0x00000020, 0x00000001, + 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x00000020, + 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, + 0x00000020, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, + 0x00030005, 0x00000022, 0x00000000, 0x00030005, 0x0000002c, 0x00007675, + 0x00040047, 0x00000018, 0x0000000b, 0x0000002a, 0x00030047, 0x00000020, + 0x00000002, 0x00050048, 0x00000020, 0x00000000, 0x0000000b, 0x00000000, + 0x00050048, 0x00000020, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, + 0x00000020, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x00000020, + 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x0000002c, 0x0000001e, + 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, + 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, + 0x00000002, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, + 0x00000008, 0x00000009, 0x00000006, 0x0004001c, 0x0000000a, 0x00000007, + 0x00000009, 0x00040020, 0x0000000b, 0x00000006, 0x0000000a, 0x0004003b, + 0x0000000b, 0x0000000c, 0x00000006, 0x0004002b, 0x00000006, 0x0000000d, + 0xbf800000, 0x0005002c, 0x00000007, 0x0000000e, 0x0000000d, 0x0000000d, + 0x0004002b, 0x00000006, 0x0000000f, 0x3f800000, 0x0005002c, 0x00000007, + 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, 0x00000007, 0x00000011, + 0x0000000d, 0x0000000f, 0x0005002c, 0x00000007, 0x00000012, 0x0000000f, + 0x0000000f, 0x0009002c, 0x0000000a, 0x00000013, 0x0000000e, 0x00000010, + 0x00000011, 0x00000011, 0x00000010, 0x00000012, 0x00040020, 0x00000014, + 0x00000007, 0x00000007, 0x00040015, 0x00000016, 0x00000020, 0x00000001, + 0x00040020, 0x00000017, 0x00000001, 0x00000016, 0x0004003b, 0x00000017, + 0x00000018, 0x00000001, 0x00040020, 0x0000001a, 0x00000006, 0x00000007, + 0x00040017, 0x0000001d, 0x00000006, 0x00000004, 0x0004002b, 0x00000008, + 0x0000001e, 0x00000001, 0x0004001c, 0x0000001f, 0x00000006, 0x0000001e, + 0x0006001e, 0x00000020, 0x0000001d, 0x00000006, 0x0000001f, 0x0000001f, + 0x00040020, 0x00000021, 0x00000003, 0x00000020, 0x0004003b, 0x00000021, + 0x00000022, 0x00000003, 0x0004002b, 0x00000016, 0x00000023, 0x00000000, + 0x0004002b, 0x00000006, 0x00000025, 0x00000000, 0x00040020, 0x00000029, + 0x00000003, 0x0000001d, 0x00040020, 0x0000002b, 0x00000003, 0x00000007, + 0x0004003b, 0x0000002b, 0x0000002c, 0x00000003, 0x0004002b, 0x00000006, + 0x0000002e, 0x3f000000, 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000014, 0x00000015, + 0x00000007, 0x0003003e, 0x0000000c, 0x00000013, 0x0004003d, 0x00000016, + 0x00000019, 0x00000018, 0x00050041, 0x0000001a, 0x0000001b, 0x0000000c, + 0x00000019, 0x0004003d, 0x00000007, 0x0000001c, 0x0000001b, 0x0003003e, + 0x00000015, 0x0000001c, 0x0004003d, 0x00000007, 0x00000024, 0x00000015, + 0x00050051, 0x00000006, 0x00000026, 0x00000024, 0x00000000, 0x00050051, + 0x00000006, 0x00000027, 0x00000024, 0x00000001, 0x00070050, 0x0000001d, + 0x00000028, 0x00000026, 0x00000027, 0x00000025, 0x0000000f, 0x00050041, + 0x00000029, 0x0000002a, 0x00000022, 0x00000023, 0x0003003e, 0x0000002a, + 0x00000028, 0x0004003d, 0x00000007, 0x0000002d, 0x00000015, 0x0005008e, + 0x00000007, 0x0000002f, 0x0000002d, 0x0000002e, 0x00050050, 0x00000007, + 0x00000030, 0x0000002e, 0x0000002e, 0x00050081, 0x00000007, 0x00000031, + 0x0000002f, 0x00000030, 0x0003003e, 0x0000002c, 0x00000031, 0x000100fd, + 0x00010038, }; } // namespace sophia::shaders diff --git a/engine/src/shaders/mesh.frag b/engine/src/shaders/mesh.frag new file mode 100644 index 0000000..cf2b987 --- /dev/null +++ b/engine/src/shaders/mesh.frag @@ -0,0 +1,9 @@ +#version 450 + +layout (location = 0) in vec4 fragColor; + +layout (location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor.rgb, 1.0); +} diff --git a/engine/src/shaders/mesh.frag.spv.h b/engine/src/shaders/mesh.frag.spv.h new file mode 100644 index 0000000..b393715 --- /dev/null +++ b/engine/src/shaders/mesh.frag.spv.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace sophia::shaders { + +inline constexpr std::array mesh_frag_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000014, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000b, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, + 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, + 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, + 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, + 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f, + 0x726f6c6f, 0x00000000, 0x00050005, 0x0000000b, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, + 0x0000000b, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, + 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, + 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040020, + 0x0000000a, 0x00000001, 0x00000007, 0x0004003b, 0x0000000a, 0x0000000b, + 0x00000001, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x0004002b, + 0x00000006, 0x0000000f, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000007, + 0x0000000d, 0x0000000b, 0x0008004f, 0x0000000c, 0x0000000e, 0x0000000d, + 0x0000000d, 0x00000000, 0x00000001, 0x00000002, 0x00050051, 0x00000006, + 0x00000010, 0x0000000e, 0x00000000, 0x00050051, 0x00000006, 0x00000011, + 0x0000000e, 0x00000001, 0x00050051, 0x00000006, 0x00000012, 0x0000000e, + 0x00000002, 0x00070050, 0x00000007, 0x00000013, 0x00000010, 0x00000011, + 0x00000012, 0x0000000f, 0x0003003e, 0x00000009, 0x00000013, 0x000100fd, + 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/mesh.vert b/engine/src/shaders/mesh.vert new file mode 100644 index 0000000..1ae202a --- /dev/null +++ b/engine/src/shaders/mesh.vert @@ -0,0 +1,23 @@ +#version 450 + +// Per vertex; layout must match MeshVertex. Normal and UV are unused until +// there is lighting and texturing. +layout (location = 0) in vec3 position; +layout (location = 1) in vec3 normal; +layout (location = 2) in vec2 uv; +layout (location = 3) in vec4 color; + +// Per instance; layout must match MeshRenderSystem::Instance. +layout (location = 4) in mat4 model; +layout (location = 8) in vec4 instanceColor; + +layout (location = 0) out vec4 fragColor; + +layout (push_constant) uniform Push { + mat4 viewProjection; +} push; + +void main() { + gl_Position = push.viewProjection * model * vec4(position, 1.0); + fragColor = color * instanceColor; +} diff --git a/engine/src/shaders/mesh.vert.spv.h b/engine/src/shaders/mesh.vert.spv.h new file mode 100644 index 0000000..55d9a24 --- /dev/null +++ b/engine/src/shaders/mesh.vert.spv.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +namespace sophia::shaders { + +inline constexpr std::array mesh_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000032, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000d000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000018, 0x0000001d, + 0x00000027, 0x00000029, 0x0000002b, 0x0000002e, 0x00000031, 0x00030003, + 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, + 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, + 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, + 0x74636572, 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, + 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, 0x78657472, 0x00000000, + 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69, + 0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, + 0x00000000, 0x00070006, 0x0000000b, 0x00000002, 0x435f6c67, 0x4470696c, + 0x61747369, 0x0065636e, 0x00070006, 0x0000000b, 0x00000003, 0x435f6c67, + 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x0000000d, 0x00000000, + 0x00040005, 0x00000011, 0x68737550, 0x00000000, 0x00070006, 0x00000011, + 0x00000000, 0x77656976, 0x6a6f7250, 0x69746365, 0x00006e6f, 0x00040005, + 0x00000013, 0x68737570, 0x00000000, 0x00040005, 0x00000018, 0x65646f6d, + 0x0000006c, 0x00050005, 0x0000001d, 0x69736f70, 0x6e6f6974, 0x00000000, + 0x00050005, 0x00000027, 0x67617266, 0x6f6c6f43, 0x00000072, 0x00040005, + 0x00000029, 0x6f6c6f63, 0x00000072, 0x00060005, 0x0000002b, 0x74736e69, + 0x65636e61, 0x6f6c6f43, 0x00000072, 0x00040005, 0x0000002e, 0x6d726f6e, + 0x00006c61, 0x00030005, 0x00000031, 0x00007675, 0x00030047, 0x0000000b, + 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, + 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, + 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, + 0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011, 0x00000002, + 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011, + 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, + 0x00000023, 0x00000000, 0x00040047, 0x00000018, 0x0000001e, 0x00000004, + 0x00040047, 0x0000001d, 0x0000001e, 0x00000000, 0x00040047, 0x00000027, + 0x0000001e, 0x00000000, 0x00040047, 0x00000029, 0x0000001e, 0x00000003, + 0x00040047, 0x0000002b, 0x0000001e, 0x00000008, 0x00040047, 0x0000002e, + 0x0000001e, 0x00000001, 0x00040047, 0x00000031, 0x0000001e, 0x00000002, + 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008, + 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, + 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a, + 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, + 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, + 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, 0x00040018, 0x00000010, + 0x00000007, 0x00000004, 0x0003001e, 0x00000011, 0x00000010, 0x00040020, + 0x00000012, 0x00000009, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, + 0x00000009, 0x00040020, 0x00000014, 0x00000009, 0x00000010, 0x00040020, + 0x00000017, 0x00000001, 0x00000010, 0x0004003b, 0x00000017, 0x00000018, + 0x00000001, 0x00040017, 0x0000001b, 0x00000006, 0x00000003, 0x00040020, + 0x0000001c, 0x00000001, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, + 0x00000001, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x00040020, + 0x00000025, 0x00000003, 0x00000007, 0x0004003b, 0x00000025, 0x00000027, + 0x00000003, 0x00040020, 0x00000028, 0x00000001, 0x00000007, 0x0004003b, + 0x00000028, 0x00000029, 0x00000001, 0x0004003b, 0x00000028, 0x0000002b, + 0x00000001, 0x0004003b, 0x0000001c, 0x0000002e, 0x00000001, 0x00040017, + 0x0000002f, 0x00000006, 0x00000002, 0x00040020, 0x00000030, 0x00000001, + 0x0000002f, 0x0004003b, 0x00000030, 0x00000031, 0x00000001, 0x00050036, + 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, + 0x00000010, 0x00000016, 0x00000015, 0x0004003d, 0x00000010, 0x00000019, + 0x00000018, 0x00050092, 0x00000010, 0x0000001a, 0x00000016, 0x00000019, + 0x0004003d, 0x0000001b, 0x0000001e, 0x0000001d, 0x00050051, 0x00000006, + 0x00000020, 0x0000001e, 0x00000000, 0x00050051, 0x00000006, 0x00000021, + 0x0000001e, 0x00000001, 0x00050051, 0x00000006, 0x00000022, 0x0000001e, + 0x00000002, 0x00070050, 0x00000007, 0x00000023, 0x00000020, 0x00000021, + 0x00000022, 0x0000001f, 0x00050091, 0x00000007, 0x00000024, 0x0000001a, + 0x00000023, 0x00050041, 0x00000025, 0x00000026, 0x0000000d, 0x0000000f, + 0x0003003e, 0x00000026, 0x00000024, 0x0004003d, 0x00000007, 0x0000002a, + 0x00000029, 0x0004003d, 0x00000007, 0x0000002c, 0x0000002b, 0x00050085, + 0x00000007, 0x0000002d, 0x0000002a, 0x0000002c, 0x0003003e, 0x00000027, + 0x0000002d, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.frag b/engine/src/shaders/model_render_system.frag index 1eb43db..1bddc15 100644 --- a/engine/src/shaders/model_render_system.frag +++ b/engine/src/shaders/model_render_system.frag @@ -1,9 +1,9 @@ #version 450 -layout (location = 0) in vec3 fragColor; +layout (location = 0) in vec4 fragColor; layout (location = 0) out vec4 outColor; void main() { - outColor = vec4(fragColor, 1.0); + outColor = fragColor; } diff --git a/engine/src/shaders/model_render_system.frag.spv.h b/engine/src/shaders/model_render_system.frag.spv.h index e5f1c2f..1becc40 100644 --- a/engine/src/shaders/model_render_system.frag.spv.h +++ b/engine/src/shaders/model_render_system.frag.spv.h @@ -5,31 +5,36 @@ namespace sophia::shaders { -inline constexpr std::array model_render_system_frag_spv = { - 0x07230203, 0x00010000, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, - 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, - 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004, - 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, - 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, - 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, - 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, - 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, - 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f, - 0x726f6c6f, 0x00000000, 0x00050005, 0x0000000c, 0x67617266, 0x6f6c6f43, - 0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, - 0x0000000c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, - 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, - 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, - 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040017, - 0x0000000a, 0x00000006, 0x00000003, 0x00040020, 0x0000000b, 0x00000001, - 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b, - 0x00000006, 0x0000000e, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, - 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a, - 0x0000000d, 0x0000000c, 0x00050051, 0x00000006, 0x0000000f, 0x0000000d, - 0x00000000, 0x00050051, 0x00000006, 0x00000010, 0x0000000d, 0x00000001, - 0x00050051, 0x00000006, 0x00000011, 0x0000000d, 0x00000002, 0x00070050, - 0x00000007, 0x00000012, 0x0000000f, 0x00000010, 0x00000011, 0x0000000e, - 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +inline constexpr std::array model_render_system_frag_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x0000000d, + 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, + 0x00000000, 0x00000009, 0x0000000b, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, + 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, + 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, + 0x7269645f, 0x69746365, 0x00006576, 0x00080004, + 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, + 0x69645f65, 0x74636572, 0x00657669, 0x00040005, + 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, + 0x00000009, 0x4374756f, 0x726f6c6f, 0x00000000, + 0x00050005, 0x0000000b, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040047, 0x00000009, 0x0000001e, + 0x00000000, 0x00040047, 0x0000000b, 0x0000001e, + 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, + 0x00000020, 0x00040017, 0x00000007, 0x00000006, + 0x00000004, 0x00040020, 0x00000008, 0x00000003, + 0x00000007, 0x0004003b, 0x00000008, 0x00000009, + 0x00000003, 0x00040020, 0x0000000a, 0x00000001, + 0x00000007, 0x0004003b, 0x0000000a, 0x0000000b, + 0x00000001, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x0004003d, 0x00000007, 0x0000000c, 0x0000000b, + 0x0003003e, 0x00000009, 0x0000000c, 0x000100fd, + 0x00010038, }; } // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.vert b/engine/src/shaders/model_render_system.vert index 4895054..34dfbe9 100644 --- a/engine/src/shaders/model_render_system.vert +++ b/engine/src/shaders/model_render_system.vert @@ -1,9 +1,10 @@ #version 450 -layout (location = 0) in vec2 position; -layout (location = 1) in vec3 color; +// Per vertex; locations match Mesh::getAttributeDescriptions(). +layout (location = 0) in vec3 position; +layout (location = 3) in vec4 color; -layout (location = 0) out vec3 fragColor; +layout (location = 0) out vec4 fragColor; layout (push_constant) uniform Push { mat4 transform; @@ -11,6 +12,6 @@ layout (push_constant) uniform Push { } push; void main() { - gl_Position = vec4(push.transform * vec4(position, 0.0, 1.0)); + gl_Position = push.transform * vec4(position, 1.0); fragColor = color; } diff --git a/engine/src/shaders/model_render_system.vert.spv.h b/engine/src/shaders/model_render_system.vert.spv.h index 0ad2b04..3194f41 100644 --- a/engine/src/shaders/model_render_system.vert.spv.h +++ b/engine/src/shaders/model_render_system.vert.spv.h @@ -5,12 +5,12 @@ namespace sophia::shaders { -inline constexpr std::array model_render_system_vert_spv = { - 0x07230203, 0x00010000, 0x000d000b, 0x00000029, 0x00000000, 0x00020011, +inline constexpr std::array model_render_system_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000027, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0009000f, 0x00000000, - 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025, - 0x00000027, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000023, + 0x00000025, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, @@ -24,8 +24,8 @@ inline constexpr std::array model_render_system_vert_spv = { 0x00060006, 0x00000011, 0x00000000, 0x6e617274, 0x726f6673, 0x0000006d, 0x00050006, 0x00000011, 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, 0x00000013, 0x68737570, 0x00000000, 0x00050005, 0x00000019, 0x69736f70, - 0x6e6f6974, 0x00000000, 0x00050005, 0x00000025, 0x67617266, 0x6f6c6f43, - 0x00000072, 0x00040005, 0x00000027, 0x6f6c6f63, 0x00000072, 0x00030047, + 0x6e6f6974, 0x00000000, 0x00050005, 0x00000023, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040005, 0x00000025, 0x6f6c6f63, 0x00000072, 0x00030047, 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, @@ -34,8 +34,8 @@ inline constexpr std::array model_render_system_vert_spv = { 0x00000011, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000011, 0x00000001, 0x00000023, 0x00000040, 0x00040047, 0x00000019, 0x0000001e, 0x00000000, - 0x00040047, 0x00000025, 0x0000001e, 0x00000000, 0x00040047, 0x00000027, - 0x0000001e, 0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00040047, 0x00000023, 0x0000001e, 0x00000000, 0x00040047, 0x00000025, + 0x0000001e, 0x00000003, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, @@ -46,24 +46,23 @@ inline constexpr std::array model_render_system_vert_spv = { 0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0004001e, 0x00000011, 0x00000010, 0x00000007, 0x00040020, 0x00000012, 0x00000009, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, 0x00000009, 0x00040020, 0x00000014, - 0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000002, + 0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000003, 0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, - 0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x00000000, - 0x0004002b, 0x00000006, 0x0000001c, 0x3f800000, 0x00040020, 0x00000021, - 0x00000003, 0x00000007, 0x00040017, 0x00000023, 0x00000006, 0x00000003, - 0x00040020, 0x00000024, 0x00000003, 0x00000023, 0x0004003b, 0x00000024, - 0x00000025, 0x00000003, 0x00040020, 0x00000026, 0x00000001, 0x00000023, - 0x0004003b, 0x00000026, 0x00000027, 0x00000001, 0x00050036, 0x00000002, + 0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x3f800000, + 0x00040020, 0x00000021, 0x00000003, 0x00000007, 0x0004003b, 0x00000021, + 0x00000023, 0x00000003, 0x00040020, 0x00000024, 0x00000001, 0x00000007, + 0x0004003b, 0x00000024, 0x00000025, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010, 0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019, - 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000000, 0x00050051, - 0x00000006, 0x0000001e, 0x0000001a, 0x00000001, 0x00070050, 0x00000007, - 0x0000001f, 0x0000001d, 0x0000001e, 0x0000001b, 0x0000001c, 0x00050091, - 0x00000007, 0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, - 0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, - 0x0004003d, 0x00000023, 0x00000028, 0x00000027, 0x0003003e, 0x00000025, - 0x00000028, 0x000100fd, 0x00010038, + 0x00050051, 0x00000006, 0x0000001c, 0x0000001a, 0x00000000, 0x00050051, + 0x00000006, 0x0000001d, 0x0000001a, 0x00000001, 0x00050051, 0x00000006, + 0x0000001e, 0x0000001a, 0x00000002, 0x00070050, 0x00000007, 0x0000001f, + 0x0000001c, 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007, + 0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, 0x00000022, + 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, 0x0004003d, + 0x00000007, 0x00000026, 0x00000025, 0x0003003e, 0x00000023, 0x00000026, + 0x000100fd, 0x00010038, }; } // namespace sophia::shaders diff --git a/engine/src/shaders/sprite.vert b/engine/src/shaders/sprite.vert index d217501..3aba928 100644 --- a/engine/src/shaders/sprite.vert +++ b/engine/src/shaders/sprite.vert @@ -22,10 +22,8 @@ void main() { vec2 corner = CORNERS[gl_VertexIndex]; gl_Position = push.viewProjection * model * vec4(corner, 0.0, 1.0); - // Draw order comes from the CPU-side sort, so flatten depth; sprites should - // never be clipped by the camera's near/far planes. - gl_Position.z = 0.0; fragColor = color; - fragUv = mix(uvRect.xy, uvRect.zw, corner); + // The quad's y runs up and the texture's v runs down. + fragUv = mix(uvRect.xy, uvRect.zw, vec2(corner.x, 1.0 - corner.y)); } diff --git a/engine/src/shaders/sprite.vert.spv.h b/engine/src/shaders/sprite.vert.spv.h index 081edfa..5a7e6b2 100644 --- a/engine/src/shaders/sprite.vert.spv.h +++ b/engine/src/shaders/sprite.vert.spv.h @@ -5,12 +5,12 @@ namespace sophia::shaders { -inline constexpr std::array sprite_vert_spv = { - 0x07230203, 0x00010000, 0x000d000b, 0x00000045, 0x00000000, 0x00020011, +inline constexpr std::array sprite_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000049, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000c000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x00000016, 0x00000021, 0x0000002b, - 0x00000038, 0x0000003a, 0x0000003d, 0x0000003e, 0x00030003, 0x00000002, + 0x00000035, 0x00000037, 0x0000003a, 0x0000003b, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, @@ -26,9 +26,9 @@ inline constexpr std::array sprite_vert_spv = { 0x00000021, 0x00000000, 0x00040005, 0x00000024, 0x68737550, 0x00000000, 0x00070006, 0x00000024, 0x00000000, 0x77656976, 0x6a6f7250, 0x69746365, 0x00006e6f, 0x00040005, 0x00000026, 0x68737570, 0x00000000, 0x00040005, - 0x0000002b, 0x65646f6d, 0x0000006c, 0x00050005, 0x00000038, 0x67617266, - 0x6f6c6f43, 0x00000072, 0x00040005, 0x0000003a, 0x6f6c6f63, 0x00000072, - 0x00040005, 0x0000003d, 0x67617266, 0x00007655, 0x00040005, 0x0000003e, + 0x0000002b, 0x65646f6d, 0x0000006c, 0x00050005, 0x00000035, 0x67617266, + 0x6f6c6f43, 0x00000072, 0x00040005, 0x00000037, 0x6f6c6f63, 0x00000072, + 0x00040005, 0x0000003a, 0x67617266, 0x00007655, 0x00040005, 0x0000003b, 0x65527675, 0x00007463, 0x00040047, 0x00000016, 0x0000000b, 0x0000002a, 0x00030047, 0x0000001f, 0x00000002, 0x00050048, 0x0000001f, 0x00000000, 0x0000000b, 0x00000000, 0x00050048, 0x0000001f, 0x00000001, 0x0000000b, @@ -37,9 +37,9 @@ inline constexpr std::array sprite_vert_spv = { 0x00000024, 0x00000002, 0x00040048, 0x00000024, 0x00000000, 0x00000005, 0x00050048, 0x00000024, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000024, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000002b, - 0x0000001e, 0x00000000, 0x00040047, 0x00000038, 0x0000001e, 0x00000000, - 0x00040047, 0x0000003a, 0x0000001e, 0x00000004, 0x00040047, 0x0000003d, - 0x0000001e, 0x00000001, 0x00040047, 0x0000003e, 0x0000001e, 0x00000005, + 0x0000001e, 0x00000000, 0x00040047, 0x00000035, 0x0000001e, 0x00000000, + 0x00040047, 0x00000037, 0x0000001e, 0x00000004, 0x00040047, 0x0000003a, + 0x0000001e, 0x00000001, 0x00040047, 0x0000003b, 0x0000001e, 0x00000005, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000002, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x0000000a, @@ -63,12 +63,12 @@ inline constexpr std::array sprite_vert_spv = { 0x00000009, 0x00000024, 0x0004003b, 0x00000025, 0x00000026, 0x00000009, 0x00040020, 0x00000027, 0x00000009, 0x00000023, 0x00040020, 0x0000002a, 0x00000001, 0x00000023, 0x0004003b, 0x0000002a, 0x0000002b, 0x00000001, - 0x00040020, 0x00000033, 0x00000003, 0x0000001c, 0x0004002b, 0x0000000a, - 0x00000035, 0x00000002, 0x00040020, 0x00000036, 0x00000003, 0x00000006, - 0x0004003b, 0x00000033, 0x00000038, 0x00000003, 0x00040020, 0x00000039, - 0x00000001, 0x0000001c, 0x0004003b, 0x00000039, 0x0000003a, 0x00000001, - 0x00040020, 0x0000003c, 0x00000003, 0x00000007, 0x0004003b, 0x0000003c, - 0x0000003d, 0x00000003, 0x0004003b, 0x00000039, 0x0000003e, 0x00000001, + 0x00040020, 0x00000033, 0x00000003, 0x0000001c, 0x0004003b, 0x00000033, + 0x00000035, 0x00000003, 0x00040020, 0x00000036, 0x00000001, 0x0000001c, + 0x0004003b, 0x00000036, 0x00000037, 0x00000001, 0x00040020, 0x00000039, + 0x00000003, 0x00000007, 0x0004003b, 0x00000039, 0x0000003a, 0x00000003, + 0x0004003b, 0x00000036, 0x0000003b, 0x00000001, 0x0004002b, 0x0000000a, + 0x00000040, 0x00000000, 0x00040020, 0x00000041, 0x00000007, 0x00000006, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003b, 0x00000018, 0x00000019, 0x00000007, 0x0004003d, 0x00000014, 0x00000017, @@ -83,16 +83,18 @@ inline constexpr std::array sprite_vert_spv = { 0x00070050, 0x0000001c, 0x00000031, 0x0000002f, 0x00000030, 0x0000000d, 0x0000000f, 0x00050091, 0x0000001c, 0x00000032, 0x0000002d, 0x00000031, 0x00050041, 0x00000033, 0x00000034, 0x00000021, 0x00000022, 0x0003003e, - 0x00000034, 0x00000032, 0x00060041, 0x00000036, 0x00000037, 0x00000021, - 0x00000022, 0x00000035, 0x0003003e, 0x00000037, 0x0000000d, 0x0004003d, - 0x0000001c, 0x0000003b, 0x0000003a, 0x0003003e, 0x00000038, 0x0000003b, - 0x0004003d, 0x0000001c, 0x0000003f, 0x0000003e, 0x0007004f, 0x00000007, - 0x00000040, 0x0000003f, 0x0000003f, 0x00000000, 0x00000001, 0x0004003d, - 0x0000001c, 0x00000041, 0x0000003e, 0x0007004f, 0x00000007, 0x00000042, - 0x00000041, 0x00000041, 0x00000002, 0x00000003, 0x0004003d, 0x00000007, - 0x00000043, 0x00000009, 0x0008000c, 0x00000007, 0x00000044, 0x00000001, - 0x0000002e, 0x00000040, 0x00000042, 0x00000043, 0x0003003e, 0x0000003d, - 0x00000044, 0x000100fd, 0x00010038, + 0x00000034, 0x00000032, 0x0004003d, 0x0000001c, 0x00000038, 0x00000037, + 0x0003003e, 0x00000035, 0x00000038, 0x0004003d, 0x0000001c, 0x0000003c, + 0x0000003b, 0x0007004f, 0x00000007, 0x0000003d, 0x0000003c, 0x0000003c, + 0x00000000, 0x00000001, 0x0004003d, 0x0000001c, 0x0000003e, 0x0000003b, + 0x0007004f, 0x00000007, 0x0000003f, 0x0000003e, 0x0000003e, 0x00000002, + 0x00000003, 0x00050041, 0x00000041, 0x00000042, 0x00000009, 0x00000040, + 0x0004003d, 0x00000006, 0x00000043, 0x00000042, 0x00050041, 0x00000041, + 0x00000044, 0x00000009, 0x0000001d, 0x0004003d, 0x00000006, 0x00000045, + 0x00000044, 0x00050083, 0x00000006, 0x00000046, 0x0000000f, 0x00000045, + 0x00050050, 0x00000007, 0x00000047, 0x00000043, 0x00000046, 0x0008000c, + 0x00000007, 0x00000048, 0x00000001, 0x0000002e, 0x0000003d, 0x0000003f, + 0x00000047, 0x0003003e, 0x0000003a, 0x00000048, 0x000100fd, 0x00010038, }; } // namespace sophia::shaders diff --git a/engine/src/ui/panel.cpp b/engine/src/ui/panel.cpp index 0fc6ebc..dce694c 100644 --- a/engine/src/ui/panel.cpp +++ b/engine/src/ui/panel.cpp @@ -47,8 +47,9 @@ void Panel::draw(const FrameInfo& frameInfo) { } bool open = true; - ImGui::Begin(config.name.c_str(), config.hasCloseButton() ? &open : nullptr, - config.getWindowFlags()); + this->shown = ImGui::Begin(config.name.c_str(), + config.hasCloseButton() ? &open : nullptr, + config.getWindowFlags()); onUpdate(frameInfo); ImGui::End(); diff --git a/engine/src/vulkan/device.hpp b/engine/src/vulkan/device.hpp index 9967f24..e8e0d08 100644 --- a/engine/src/vulkan/device.hpp +++ b/engine/src/vulkan/device.hpp @@ -48,6 +48,7 @@ class Device { void waitIdle() { this->device->waitIdle(); } + vk::PhysicalDevice getPhysicalDevice() const { return this->physicalDevice; } VmaAllocator getAllocator() const { return this->allocator; } vk::CommandPool getCommandPool() const { return this->commandPool; } vk::SurfaceKHR getSurface() const { return surface; } diff --git a/engine/src/vulkan/pipelines/graphics_pipeline.cpp b/engine/src/vulkan/pipelines/graphics_pipeline.cpp index 2e578e9..8a1d1b9 100644 --- a/engine/src/vulkan/pipelines/graphics_pipeline.cpp +++ b/engine/src/vulkan/pipelines/graphics_pipeline.cpp @@ -101,6 +101,21 @@ GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending( return *this; } +GraphicsPipeline::Builder& GraphicsPipeline::Builder::setDepthTest( + bool test, bool write, vk::CompareOp compareOp) { + this->depthTest = test; + this->depthWrite = write; + this->depthCompareOp = compareOp; + return *this; +} + +GraphicsPipeline::Builder& GraphicsPipeline::Builder::setCullMode( + vk::CullModeFlags cullMode, vk::FrontFace frontFace) { + this->cullMode = cullMode; + this->frontFace = frontFace; + return *this; +} + std::unique_ptr GraphicsPipeline::Builder::build() const { if (this->vertexShader == nullptr || this->fragmentShader == nullptr) { log::fatal( @@ -130,26 +145,14 @@ std::unique_ptr GraphicsPipeline::Builder::build() const { "setDepthAttachmentFormat() was not called"); } - return std::unique_ptr(new GraphicsPipeline( - this->device, *this->vertexShader, *this->fragmentShader, - this->pipelineLayout, this->colorAttachmentFormats, - this->depthAttachmentFormat, this->stencilAttachmentFormat, - this->bindingDescriptions, this->attributeDescriptions, - this->alphaBlending)); + return std::unique_ptr(new GraphicsPipeline(*this)); } -GraphicsPipeline::GraphicsPipeline( - Device& device, Shader& vertexShader, Shader& fragmentShader, - vk::PipelineLayout pipelineLayout, - std::vector colorAttachmentFormats, - vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat, - std::vector bindingDescriptions, - std::vector attributeDescriptions, - bool alphaBlending) - : Pipeline(device, vk::PipelineBindPoint::eGraphics) { +GraphicsPipeline::GraphicsPipeline(const Builder& builder) + : Pipeline(builder.device, vk::PipelineBindPoint::eGraphics) { setDefaultGraphicsPipelineConfig(); - if (alphaBlending) { + if (builder.alphaBlending) { vk::PipelineColorBlendAttachmentState& blend = this->config.colorBlendAttachment; blend.blendEnable = vk::True; @@ -159,17 +162,24 @@ GraphicsPipeline::GraphicsPipeline( blend.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; } - this->config.bindingDescriptions = std::move(bindingDescriptions); - this->config.attributeDescriptions = std::move(attributeDescriptions); - this->config.pipelineLayout = pipelineLayout; - this->config.colorAttachmentFormats = std::move(colorAttachmentFormats); - this->config.depthAttachmentFormat = depthAttachmentFormat; - this->config.stencilAttachmentFormat = stencilAttachmentFormat; + this->config.depthStencilInfo.depthTestEnable = builder.depthTest; + this->config.depthStencilInfo.depthWriteEnable = builder.depthWrite; + this->config.depthStencilInfo.depthCompareOp = builder.depthCompareOp; + + this->config.rasterizationInfo.cullMode = builder.cullMode; + this->config.rasterizationInfo.frontFace = builder.frontFace; + + this->config.bindingDescriptions = builder.bindingDescriptions; + this->config.attributeDescriptions = builder.attributeDescriptions; + this->config.pipelineLayout = builder.pipelineLayout; + this->config.colorAttachmentFormats = builder.colorAttachmentFormats; + this->config.depthAttachmentFormat = builder.depthAttachmentFormat; + this->config.stencilAttachmentFormat = builder.stencilAttachmentFormat; vk::UniqueShaderModule vertexShaderModule = - createShaderModule(vertexShader.getSpirv()); + createShaderModule(builder.vertexShader->getSpirv()); vk::UniqueShaderModule fragmentShaderModule = - createShaderModule(fragmentShader.getSpirv()); + createShaderModule(builder.fragmentShader->getSpirv()); log::trace("created graphics shader modules"); vk::PipelineShaderStageCreateInfo shaderStages[] = { @@ -245,7 +255,7 @@ void GraphicsPipeline::setDefaultGraphicsPipelineConfig() { this->config.rasterizationInfo.polygonMode = vk::PolygonMode::eFill; this->config.rasterizationInfo.lineWidth = 1.0f; this->config.rasterizationInfo.cullMode = vk::CullModeFlagBits::eNone; - this->config.rasterizationInfo.frontFace = vk::FrontFace::eClockwise; + this->config.rasterizationInfo.frontFace = vk::FrontFace::eCounterClockwise; this->config.rasterizationInfo.depthBiasEnable = vk::False; this->config.rasterizationInfo.depthBiasConstantFactor = 0.0f; this->config.rasterizationInfo.depthBiasClamp = 0.0f; diff --git a/engine/src/vulkan/pipelines/graphics_pipeline.hpp b/engine/src/vulkan/pipelines/graphics_pipeline.hpp index fe0d46e..469d398 100644 --- a/engine/src/vulkan/pipelines/graphics_pipeline.hpp +++ b/engine/src/vulkan/pipelines/graphics_pipeline.hpp @@ -41,14 +41,7 @@ class GraphicsPipeline : public Pipeline { ~GraphicsPipeline() override; private: - GraphicsPipeline( - Device& device, Shader& vertexShader, Shader& fragmentShader, - vk::PipelineLayout pipelineLayout, - std::vector colorAttachmentFormats, - vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat, - std::vector bindingDescriptions, - std::vector attributeDescriptions, - bool alphaBlending); + explicit GraphicsPipeline(const Builder& builder); void setDefaultGraphicsPipelineConfig(); @@ -72,6 +65,14 @@ class GraphicsPipeline::Builder { std::vector attributeDescriptions); // Straight (non-premultiplied) "over" blending on every color attachment. Builder& setAlphaBlending(bool enabled = true); + // Only matters with a depth attachment. Defaults to testing and writing + // with eLess. + Builder& setDepthTest(bool test, bool write, + vk::CompareOp compareOp = vk::CompareOp::eLess); + // Defaults to no culling, with counter-clockwise triangles facing front. + Builder& setCullMode( + vk::CullModeFlags cullMode, + vk::FrontFace frontFace = vk::FrontFace::eCounterClockwise); std::unique_ptr build() const; @@ -87,6 +88,13 @@ class GraphicsPipeline::Builder { std::vector bindingDescriptions{}; std::vector attributeDescriptions{}; bool alphaBlending = false; + bool depthTest = true; + bool depthWrite = true; + vk::CompareOp depthCompareOp = vk::CompareOp::eLess; + vk::CullModeFlags cullMode = vk::CullModeFlagBits::eNone; + vk::FrontFace frontFace = vk::FrontFace::eCounterClockwise; + + friend class GraphicsPipeline; }; } // namespace sophia diff --git a/engine/src/vulkan/swapchain.cpp b/engine/src/vulkan/swapchain.cpp index 67744d9..66fe4b0 100644 --- a/engine/src/vulkan/swapchain.cpp +++ b/engine/src/vulkan/swapchain.cpp @@ -215,8 +215,7 @@ vk::Extent2D Swapchain::chooseExtent( vk::Format Swapchain::findDepthFormat() { return this->device.findSupportedFormat( - {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, - vk::Format::eD24UnormS8Uint}, + {vk::Format::eD32Sfloat, vk::Format::eX8D24UnormPack32}, vk::ImageTiling::eOptimal, vk::FormatFeatureFlagBits::eDepthStencilAttachment); } diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index f2f19e8..1dc11d1 100644 --- a/examples/boids_2d/main.cpp +++ b/examples/boids_2d/main.cpp @@ -141,8 +141,8 @@ class Boids2D : public sophia::Application { cameraEntity.getComponent().position; if (scrollLeft) cameraPosition.x -= scrollDistance; if (scrollRight) cameraPosition.x += scrollDistance; - if (scrollUp) cameraPosition.y -= scrollDistance; - if (scrollDown) cameraPosition.y += scrollDistance; + if (scrollUp) cameraPosition.y += scrollDistance; + if (scrollDown) cameraPosition.y -= scrollDistance; // One world unit per pixel, whatever size the viewport is. cameraEntity.getComponent().ortho.height = diff --git a/examples/cube/CMakeLists.txt b/examples/cube/CMakeLists.txt new file mode 100644 index 0000000..21f6a84 --- /dev/null +++ b/examples/cube/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(cube main.cpp) + +target_link_libraries(cube PRIVATE ${PROJECT_NAME}) diff --git a/examples/cube/main.cpp b/examples/cube/main.cpp new file mode 100644 index 0000000..82228a4 --- /dev/null +++ b/examples/cube/main.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include + +// A spinning cube seen through a perspective camera. +class Cube : public sophia::Application { + public: + Cube() : Application{{.width = 800, .height = 600, .name = "Cube"}} { + debugUI().showFrameStats(true); + } + + void onCreate() override { + sophia::SceneHandle sceneHandle = scenes().create(); + sophia::Scene& scene = scenes().get(sceneHandle); + + sophia::Entity camera = scene.createCamera(); + camera.getComponent().projection = + sophia::CameraComponent::Projection::Perspective; + camera.getComponent().position = {0.f, 0.f, + 3.f}; + + // Unlit, so color each face by its normal to tell them apart. + sophia::MeshData data = sophia::primitives::cube(); + for (sophia::MeshVertex& vertex : data.vertices) { + vertex.color = glm::vec4{vertex.normal * 0.5f + 0.5f, 1.f}; + } + + cube = scene.createEntity("Cube"); + cube.addComponent(sophia::MeshComponent{ + .handle = resources().create(data)}); + + sceneViews().create({ + .scene = sceneHandle, + }); + } + + void onUpdate(const sophia::FrameInfo& frameInfo) override { + auto angle = static_cast(frameInfo.elapsedTime); + cube.getComponent().rotation = + glm::angleAxis(angle, glm::normalize(glm::vec3{1.f, 1.f, 0.f})); + } + + private: + sophia::Entity cube; +}; + +SOPHIA_MAIN(Cube) diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index d8cb1c7..26d4260 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -78,3 +78,17 @@ target_link_libraries(imgui PRIVATE if(UNIX AND NOT APPLE) target_compile_options(imgui PRIVATE -fPIC) endif() + +# ImGuizmo: only the transform gizmo. The repo's own CMakeLists also builds +# its other widgets, and doesn't link imgui when used as a subdirectory. +add_library(imguizmo STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/ImGuizmo/src/ImGuizmo.cpp +) + +target_include_directories(imguizmo PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/ImGuizmo/src +) + +target_link_libraries(imguizmo PUBLIC + imgui +) diff --git a/third_party/ImGuizmo b/third_party/ImGuizmo new file mode 160000 index 0000000..18cef5e --- /dev/null +++ b/third_party/ImGuizmo @@ -0,0 +1 @@ +Subproject commit 18cef5e031d8c6973d80284c67f60549fafd78c1