adding and integrating ImGuizmo
This commit is contained in:
@@ -11,3 +11,6 @@
|
|||||||
[submodule "third_party/tracy"]
|
[submodule "third_party/tracy"]
|
||||||
path = third_party/tracy
|
path = third_party/tracy
|
||||||
url = https://github.com/wolfpld/tracy.git
|
url = https://github.com/wolfpld/tracy.git
|
||||||
|
[submodule "third_party/ImGuizmo"]
|
||||||
|
path = third_party/ImGuizmo
|
||||||
|
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ add_library(${NAME}_deps INTERFACE)
|
|||||||
|
|
||||||
target_link_libraries(${NAME}_deps INTERFACE
|
target_link_libraries(${NAME}_deps INTERFACE
|
||||||
${ENGINE_NAME}
|
${ENGINE_NAME}
|
||||||
|
imguizmo
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#include "editor_camera.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
#include <sophia/math.hpp>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
|
#include <sophia/scene.hpp>
|
||||||
|
#include <sophia/scene_registry.hpp>
|
||||||
|
#include <sophia/scene_view_registry.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<OpenScene>& 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<OpenScene> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-15
@@ -1,49 +1,86 @@
|
|||||||
#include <iostream>
|
|
||||||
#include <sophia/application.hpp>
|
#include <sophia/application.hpp>
|
||||||
#include <sophia/components.hpp>
|
#include <sophia/components.hpp>
|
||||||
#include <sophia/entity.hpp>
|
#include <sophia/entity.hpp>
|
||||||
|
#include <sophia/primitives.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "editor_context.hpp"
|
||||||
#include "panels/asset_browser_panel.hpp"
|
#include "panels/asset_browser_panel.hpp"
|
||||||
|
#include "panels/game_panel.hpp"
|
||||||
#include "panels/inspector_panel.hpp"
|
#include "panels/inspector_panel.hpp"
|
||||||
#include "panels/scene_hierarchy_panel.hpp"
|
#include "panels/scene_hierarchy_panel.hpp"
|
||||||
#include "panels/scene_panel.hpp"
|
#include "panels/scene_panel.hpp"
|
||||||
|
#include "utilities/logger.hpp"
|
||||||
|
|
||||||
class Editor : public sophia::Application {
|
class Editor : public sophia::Application {
|
||||||
public:
|
public:
|
||||||
Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {}
|
Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {}
|
||||||
|
|
||||||
void onCreate() override {
|
void onCreate() override {
|
||||||
std::cout << "Editor created" << std::endl;
|
log("Editor created");
|
||||||
|
|
||||||
debugUI().show();
|
debugUI().show();
|
||||||
|
|
||||||
textures.emplace_back("Checker", createCheckerTexture());
|
textures.emplace_back("Checker", createCheckerTexture());
|
||||||
|
cubeMesh = createShadedCube();
|
||||||
|
|
||||||
sophia::SceneHandle sceneHandle = scenes().create();
|
sophia::SceneHandle sceneHandle = context.createScene("Demo");
|
||||||
sophia::Scene& scene = scenes().get(sceneHandle);
|
createDemoScene(scenes().get(sceneHandle));
|
||||||
createDemoScene(scene);
|
context.setScene(sceneHandle);
|
||||||
|
|
||||||
panels().emplace<SceneHierarchyPanel>(scene, selected);
|
// The editor's own scene, never edited or rendered. It holds the camera
|
||||||
panels().emplace<InspectorPanel>(selected, textures);
|
// the Scene panel looks through, which a SceneView may take from any
|
||||||
panels().emplace<AssetBrowserPanel>();
|
// 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 =
|
sophia::RenderTargetHandle sceneTarget =
|
||||||
resources().create<sophia::RenderTarget>();
|
resources().create<sophia::RenderTarget>();
|
||||||
sceneViews().create({.scene = sceneHandle, .target = sceneTarget});
|
sophia::SceneViewHandle sceneView = sceneViews().create(
|
||||||
panels().emplace<ScenePanel>(ui(), sceneTarget, scene, selected);
|
{.target = sceneTarget, .camera = editorCamera, .enabled = false});
|
||||||
|
|
||||||
|
// No camera: the Game panel shows the scene through its active one.
|
||||||
|
sophia::RenderTargetHandle gameTarget =
|
||||||
|
resources().create<sophia::RenderTarget>();
|
||||||
|
sophia::SceneViewHandle gameView =
|
||||||
|
sceneViews().create({.target = gameTarget, .enabled = false});
|
||||||
|
|
||||||
|
panels().emplace<SceneHierarchyPanel>(context, cubeMesh);
|
||||||
|
panels().emplace<InspectorPanel>(context, textures);
|
||||||
|
panels().emplace<AssetBrowserPanel>();
|
||||||
|
viewPanels.push_back(
|
||||||
|
{panels().emplace<ScenePanel>(ui(), resources(), context, sceneViews(),
|
||||||
|
sceneView, sceneTarget, editorCamera),
|
||||||
|
sceneView});
|
||||||
|
viewPanels.push_back(
|
||||||
|
{panels().emplace<GamePanel>(ui(), context, sceneViews(), gameView,
|
||||||
|
gameTarget),
|
||||||
|
gameView});
|
||||||
|
|
||||||
panels().setDefaultLayout(
|
panels().setDefaultLayout(
|
||||||
{.left = {"Scene Hierarchy"},
|
{.left = {"Scene Hierarchy"},
|
||||||
.right = {"Inspector", "Frame Stats", "PanelRegistry"},
|
.right = {"Inspector", "Frame Stats", "PanelRegistry"},
|
||||||
.bottom = {"Asset Browser", "Console"},
|
.bottom = {"Asset Browser", "Console"},
|
||||||
.center = {"Scene"}});
|
.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:
|
private:
|
||||||
|
// Adds to a new scene, which already has a camera.
|
||||||
void createDemoScene(sophia::Scene& scene) {
|
void createDemoScene(sophia::Scene& scene) {
|
||||||
scene.createCamera("Main Camera");
|
|
||||||
|
|
||||||
sophia::Entity checker = scene.createEntity("Checker");
|
sophia::Entity checker = scene.createEntity("Checker");
|
||||||
checker.getComponent<sophia::TransformComponent>().position = {-1.5f, 0.f,
|
checker.getComponent<sophia::TransformComponent>().position = {-1.5f, 0.f,
|
||||||
0.f};
|
0.f};
|
||||||
@@ -63,6 +100,27 @@ class Editor : public sophia::Application {
|
|||||||
sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f},
|
sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f},
|
||||||
.size = {2.f, 2.f},
|
.size = {2.f, 2.f},
|
||||||
.sortOrder = 1});
|
.sortOrder = 1});
|
||||||
|
|
||||||
|
sophia::Entity cube = scene.createEntity("Cube");
|
||||||
|
auto& cubeTransform = cube.getComponent<sophia::TransformComponent>();
|
||||||
|
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>(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<sophia::Mesh>(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
sophia::TextureHandle createCheckerTexture() {
|
sophia::TextureHandle createCheckerTexture() {
|
||||||
@@ -79,8 +137,17 @@ class Editor : public sophia::Application {
|
|||||||
return resources().create<sophia::Texture>(SIZE, SIZE, std::move(pixels));
|
return resources().create<sophia::Texture>(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<ViewPanel> viewPanels;
|
||||||
InspectorPanel::TextureList textures;
|
InspectorPanel::TextureList textures;
|
||||||
|
sophia::MeshHandle cubeMesh;
|
||||||
};
|
};
|
||||||
|
|
||||||
SOPHIA_MAIN(Editor)
|
SOPHIA_MAIN(Editor)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#include "game_panel.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
|
#include <sophia/scene_view_registry.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
|
#include <sophia/ui/ui.hpp>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <sophia/components.hpp>
|
#include <sophia/components.hpp>
|
||||||
|
|
||||||
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
||||||
|
selected = context.selected;
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
ImGui::TextDisabled("No entity selected");
|
ImGui::TextDisabled("No entity selected");
|
||||||
return;
|
return;
|
||||||
@@ -16,6 +17,7 @@ void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
drawTransform();
|
drawTransform();
|
||||||
drawCamera();
|
drawCamera();
|
||||||
drawSprite();
|
drawSprite();
|
||||||
|
drawMesh();
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
drawAddComponent();
|
drawAddComponent();
|
||||||
@@ -40,11 +42,13 @@ void InspectorPanel::drawTransform() {
|
|||||||
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
||||||
ImGui::DragFloat3("Position", &transform.position.x, 0.1f);
|
ImGui::DragFloat3("Position", &transform.position.x, 0.1f);
|
||||||
|
|
||||||
// 2D only: edits rotation around Z and drops any X/Y rotation.
|
// Euler angles in degrees, recomputed from the quaternion every frame.
|
||||||
float degrees = glm::degrees(glm::eulerAngles(transform.rotation).z);
|
// Near 90 degrees of pitch the angles can jump between equivalent values
|
||||||
if (ImGui::DragFloat("Rotation", °rees, 1.0f)) {
|
// while dragging.
|
||||||
transform.rotation =
|
// TODO: keep each entity's edited angles, or replace with gizmo rotation.
|
||||||
glm::angleAxis(glm::radians(degrees), glm::vec3{0.f, 0.f, 1.f});
|
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);
|
ImGui::DragFloat3("Scale", &transform.scale.x, 0.1f);
|
||||||
@@ -118,6 +122,29 @@ void InspectorPanel::drawSprite() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InspectorPanel::drawMesh() {
|
||||||
|
if (!selected.hasComponent<sophia::MeshComponent>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool open = true;
|
||||||
|
if (!ImGui::CollapsingHeader("Mesh", &open, ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
if (!open) {
|
||||||
|
selected.removeComponent<sophia::MeshComponent>();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& mesh = selected.getComponent<sophia::MeshComponent>();
|
||||||
|
// TODO: pick the mesh once assets have names.
|
||||||
|
ImGui::Text("Mesh: %u", static_cast<sophia::u32>(mesh.handle.index));
|
||||||
|
ImGui::ColorEdit3("Color", &mesh.color.x);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
selected.removeComponent<sophia::MeshComponent>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void InspectorPanel::drawAddComponent() {
|
void InspectorPanel::drawAddComponent() {
|
||||||
if (ImGui::Button("Add Component")) {
|
if (ImGui::Button("Add Component")) {
|
||||||
ImGui::OpenPopup("AddComponent");
|
ImGui::OpenPopup("AddComponent");
|
||||||
|
|||||||
@@ -8,14 +8,16 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../editor_context.hpp"
|
||||||
|
|
||||||
class InspectorPanel : public sophia::Panel {
|
class InspectorPanel : public sophia::Panel {
|
||||||
public:
|
public:
|
||||||
// Stand-in for an asset browser: the textures a sprite can pick from.
|
// Stand-in for an asset browser: the textures a sprite can pick from.
|
||||||
using TextureList =
|
using TextureList =
|
||||||
std::vector<std::pair<std::string, sophia::TextureHandle>>;
|
std::vector<std::pair<std::string, sophia::TextureHandle>>;
|
||||||
|
|
||||||
InspectorPanel(sophia::Entity& selected, const TextureList& textures)
|
InspectorPanel(EditorContext& context, const TextureList& textures)
|
||||||
: Panel{{.name = "Inspector"}}, selected{selected}, textures{textures} {}
|
: Panel{{.name = "Inspector"}}, context{context}, textures{textures} {}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
@@ -24,8 +26,11 @@ class InspectorPanel : public sophia::Panel {
|
|||||||
void drawTransform();
|
void drawTransform();
|
||||||
void drawCamera();
|
void drawCamera();
|
||||||
void drawSprite();
|
void drawSprite();
|
||||||
|
void drawMesh();
|
||||||
void drawAddComponent();
|
void drawAddComponent();
|
||||||
|
|
||||||
sophia::Entity& selected;
|
EditorContext& context;
|
||||||
|
// The selected entity, for the duration of onUpdate().
|
||||||
|
sophia::Entity selected;
|
||||||
const TextureList& textures;
|
const TextureList& textures;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,18 +2,37 @@
|
|||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <sophia/components.hpp>
|
#include <sophia/components.hpp>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
|
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")) {
|
if (ImGui::Button("+ Sprite")) {
|
||||||
selected = scene.createEntity("Sprite");
|
selected = scene.createEntity("Sprite");
|
||||||
selected.addComponent<sophia::SpriteComponent>();
|
selected.addComponent<sophia::SpriteComponent>();
|
||||||
}
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("+ Cube")) {
|
||||||
|
selected = scene.createEntity("Cube");
|
||||||
|
selected.addComponent<sophia::MeshComponent>(
|
||||||
|
sophia::MeshComponent{.handle = cubeMesh});
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Flat for now: there is no parent/child component yet.
|
// 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 flags = ImGuiTreeNodeFlags_Leaf |
|
||||||
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
||||||
ImGuiTreeNodeFlags_SpanAvailWidth;
|
ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
@@ -30,3 +49,86 @@ void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
ImGui::PopID();
|
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<int>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <sophia/entity.hpp>
|
#include <array>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/scene.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
|
|
||||||
|
#include "../editor_context.hpp"
|
||||||
|
|
||||||
class SceneHierarchyPanel : public sophia::Panel {
|
class SceneHierarchyPanel : public sophia::Panel {
|
||||||
public:
|
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}},
|
: Panel{{.name = "Scene Hierarchy", .locked = true}},
|
||||||
scene{scene},
|
context{context},
|
||||||
selected{selected} {}
|
cubeMesh{cubeMesh} {}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
sophia::Scene& scene;
|
// Switches between open scenes, renames the active one, and creates new
|
||||||
sophia::Entity& selected;
|
// 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<char, 128> renameBuffer{};
|
||||||
};
|
};
|
||||||
|
|||||||
+472
-56
@@ -1,11 +1,20 @@
|
|||||||
#include "scene_panel.hpp"
|
#include "scene_panel.hpp"
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
// After imgui.h, which it relies on.
|
||||||
|
#include <ImGuizmo.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <sophia/aabb.hpp>
|
||||||
#include <sophia/components.hpp>
|
#include <sophia/components.hpp>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -16,17 +25,125 @@ constexpr std::array<glm::vec2, 4> UNIT_QUAD{
|
|||||||
|
|
||||||
const ImU32 SELECTION_COLOR = IM_COL32(255, 170, 40, 255);
|
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<float> 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<float>::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<float> 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
|
} // namespace
|
||||||
|
|
||||||
ImVec2 ScenePanel::ViewportTransform::worldToScreen(
|
ImVec2 ScenePanel::ViewportTransform::clipToScreen(
|
||||||
const glm::vec3& world) const {
|
const glm::vec4& clip) const {
|
||||||
glm::vec4 clip = viewProjection * glm::vec4{world, 1.f};
|
|
||||||
glm::vec2 ndc = glm::vec2{clip} / clip.w;
|
glm::vec2 ndc = glm::vec2{clip} / clip.w;
|
||||||
// Vulkan NDC has +y pointing down, same as the screen.
|
// Vulkan NDC has +y pointing down, same as the screen.
|
||||||
return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x,
|
return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x,
|
||||||
origin.y + (ndc.y * 0.5f + 0.5f) * size.y};
|
origin.y + (ndc.y * 0.5f + 0.5f) * size.y};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<ImVec2> 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(
|
glm::vec2 ScenePanel::ViewportTransform::screenToWorld(
|
||||||
const ImVec2& screen) const {
|
const ImVec2& screen) const {
|
||||||
glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f,
|
glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f,
|
||||||
@@ -35,7 +152,42 @@ glm::vec2 ScenePanel::ViewportTransform::screenToWorld(
|
|||||||
return glm::vec2{world} / world.w;
|
return glm::vec2{world} / world.w;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScenePanel::onUpdate(const sophia::FrameInfo&) {
|
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();
|
drawToolbar();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
@@ -45,35 +197,75 @@ void ScenePanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!context.hasScene()) {
|
||||||
|
ImGui::TextDisabled("No scene open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ui.image(target);
|
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
|
// The image doesn't take mouse input, so without this, dragging in it
|
||||||
// would drag the window instead.
|
// would drag the window instead.
|
||||||
ImGui::SetCursorScreenPos(origin);
|
ImGui::SetCursorScreenPos(origin);
|
||||||
ImGui::InvisibleButton("##viewport", size);
|
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<sophia::TransformComponent>() =
|
||||||
|
editorCamera.getTransform();
|
||||||
|
camera.getComponent<sophia::CameraComponent>() = editorCamera.getCamera();
|
||||||
|
|
||||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||||
drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
|
drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
|
||||||
true);
|
true);
|
||||||
|
|
||||||
if (showGrid) {
|
// 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);
|
drawGrid(origin, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
sophia::Entity camera = scene.getActiveCamera();
|
// Use the panel's size rather than the target's: the target is resized a
|
||||||
if (camera && camera.hasComponent<sophia::CameraComponent>()) {
|
// frame late, and the mouse is over the panel.
|
||||||
// Use the panel's size rather than the target's: the target is resized
|
float aspect = size.x / size.y;
|
||||||
// a frame late, and the mouse is over the panel.
|
glm::mat4 view = sophia::CameraComponent::getView(
|
||||||
float aspect = size.x / size.y;
|
camera.getComponent<sophia::TransformComponent>());
|
||||||
glm::mat4 viewProjection =
|
glm::mat4 viewProjection =
|
||||||
camera.getComponent<sophia::CameraComponent>().getViewProjection(
|
camera.getComponent<sophia::CameraComponent>().getProjection(aspect) *
|
||||||
camera.getComponent<sophia::TransformComponent>(), aspect);
|
view;
|
||||||
|
ViewportTransform viewport{origin, size, viewProjection,
|
||||||
|
glm::inverse(viewProjection)};
|
||||||
|
|
||||||
ViewportTransform viewport{origin, size, viewProjection,
|
handleSelectionInput(viewport);
|
||||||
glm::inverse(viewProjection)};
|
drawSelection(viewport);
|
||||||
handleInput(viewport);
|
|
||||||
drawSelection(viewport);
|
ImGuizmo::SetDrawlist();
|
||||||
}
|
ImGuizmo::SetRect(origin.x, origin.y, size.x, size.y);
|
||||||
|
drawGizmo(view, aspect);
|
||||||
|
|
||||||
drawList->PopClipRect();
|
drawList->PopClipRect();
|
||||||
|
|
||||||
@@ -81,7 +273,106 @@ void ScenePanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
|
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScenePanel::drawToolbar() { ImGui::Checkbox("Grid", &showGrid); }
|
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<float, 3> 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<sophia::CameraComponent>().getProjection(aspect);
|
||||||
|
projection[1][1] *= -1.f;
|
||||||
|
|
||||||
|
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
||||||
|
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<int>(static_cast<sophia::u32>(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) {
|
void ScenePanel::drawGrid(ImVec2 origin, ImVec2 size) {
|
||||||
constexpr float GRID_STEP = 40.0f;
|
constexpr float GRID_STEP = 40.0f;
|
||||||
@@ -99,15 +390,55 @@ void ScenePanel::drawGrid(ImVec2 origin, ImVec2 size) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScenePanel::handleInput(const ViewportTransform& viewport) {
|
void ScenePanel::handleCameraInput(const sophia::FrameInfo& frameInfo,
|
||||||
|
ImVec2 size) {
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
const ImGuiIO& io = ImGui::GetIO();
|
||||||
|
bool is3D = editorCamera.mode == EditorCamera::Mode::Scene3D;
|
||||||
|
glm::vec2 mouseDelta{io.MouseDelta.x, io.MouseDelta.y};
|
||||||
|
|
||||||
if (ImGui::IsItemActivated()) {
|
// The button stays active while any of its mouse buttons is held, even
|
||||||
selected = pick(viewport.screenToWorld(io.MousePos));
|
// once the mouse leaves the panel.
|
||||||
dragging = selected;
|
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<float>(frameInfo.deltaTime));
|
||||||
|
} else if (middleDown || (!is3D && rightDown)) {
|
||||||
|
editorCamera.pan(mouseDelta, size.y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ImGui::IsItemActive()) {
|
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;
|
dragging = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,13 +449,15 @@ void ScenePanel::handleInput(const ViewportTransform& viewport) {
|
|||||||
glm::vec2 delta =
|
glm::vec2 delta =
|
||||||
viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous);
|
viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous);
|
||||||
|
|
||||||
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
auto& transform =
|
||||||
|
context.selected.getComponent<sophia::TransformComponent>();
|
||||||
transform.position.x += delta.x;
|
transform.position.x += delta.x;
|
||||||
transform.position.y += delta.y;
|
transform.position.y += delta.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScenePanel::drawSelection(const ViewportTransform& viewport) {
|
void ScenePanel::drawSelection(const ViewportTransform& viewport) {
|
||||||
|
sophia::Entity selected = context.selected;
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,53 +468,136 @@ void ScenePanel::drawSelection(const ViewportTransform& viewport) {
|
|||||||
if (selected.hasComponent<sophia::SpriteComponent>()) {
|
if (selected.hasComponent<sophia::SpriteComponent>()) {
|
||||||
glm::mat4 model =
|
glm::mat4 model =
|
||||||
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
|
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
|
||||||
|
|
||||||
std::array<ImVec2, 4> corners;
|
|
||||||
for (size_t i = 0; i < UNIT_QUAD.size(); i++) {
|
for (size_t i = 0; i < UNIT_QUAD.size(); i++) {
|
||||||
corners[i] = viewport.worldToScreen(
|
const glm::vec2& from = UNIT_QUAD[i];
|
||||||
glm::vec3{model * glm::vec4{UNIT_QUAD[i], 0.f, 1.f}});
|
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<sophia::AABB> bounds = getMeshBounds(selected)) {
|
||||||
|
glm::mat4 model = transform.mat4();
|
||||||
|
std::array<glm::vec3, 8> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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
|
// 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.
|
// there is to show for entities without a sprite or mesh, such as the
|
||||||
ImVec2 center = viewport.worldToScreen(transform.position);
|
// camera.
|
||||||
drawList->AddCircle(center, 4.0f, SELECTION_COLOR, 0, 2.0f);
|
if (std::optional<ImVec2> center =
|
||||||
|
viewport.worldToScreen(transform.position)) {
|
||||||
|
drawList->AddCircle(*center, 4.0f, SELECTION_COLOR, 0, 2.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sophia::Entity ScenePanel::pick(glm::vec2 world) {
|
void ScenePanel::frameSelection() {
|
||||||
sophia::Entity best;
|
sophia::Entity selected = context.selected;
|
||||||
std::optional<std::tuple<sophia::s32, float>> bestOrder;
|
if (!selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& transform = selected.getComponent<sophia::TransformComponent>();
|
||||||
|
|
||||||
|
// The selection's corners in world space, from its mesh or sprite.
|
||||||
|
std::vector<glm::vec3> points;
|
||||||
|
if (std::optional<sophia::AABB> 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<sophia::SpriteComponent>()) {
|
||||||
|
glm::mat4 model =
|
||||||
|
selected.getComponent<sophia::SpriteComponent>().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<sophia::AABB> ScenePanel::getMeshBounds(sophia::Entity entity) {
|
||||||
|
if (!entity.hasComponent<sophia::MeshComponent>()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
sophia::MeshHandle mesh = entity.getComponent<sophia::MeshComponent>().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<float>::infinity();
|
||||||
|
scene.forEachEntity([&](sophia::Entity entity) {
|
||||||
|
std::optional<sophia::AABB> bounds = getMeshBounds(entity);
|
||||||
|
if (!bounds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
glm::mat4 model = entity.getComponent<sophia::TransformComponent>().mat4();
|
||||||
|
std::optional<float> 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<std::tuple<sophia::s32, float>> topOrder;
|
||||||
scene.forEachEntity([&](sophia::Entity entity) {
|
scene.forEachEntity([&](sophia::Entity entity) {
|
||||||
if (!entity.hasComponent<sophia::SpriteComponent>()) {
|
if (!entity.hasComponent<sophia::SpriteComponent>()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& transform = entity.getComponent<sophia::TransformComponent>();
|
|
||||||
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
|
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
|
||||||
|
glm::mat4 model =
|
||||||
glm::mat4 model = sprite.getModel(transform);
|
sprite.getModel(entity.getComponent<sophia::TransformComponent>());
|
||||||
// Zero size or scale; nothing to hit.
|
std::optional<float> distance =
|
||||||
if (glm::determinant(model) == 0.f) {
|
intersectQuad(ray.origin, ray.direction, model);
|
||||||
|
if (!distance || *distance > meshDistance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::vec4 local =
|
// Negated distance, so nearer compares greater.
|
||||||
glm::inverse(model) * glm::vec4{world, transform.position.z, 1.f};
|
std::tuple order{sprite.sortOrder, -*distance};
|
||||||
if (local.x < 0.f || local.x > 1.f || local.y < 0.f || local.y > 1.f) {
|
if (!topOrder || order > *topOrder) {
|
||||||
return;
|
topSprite = entity;
|
||||||
}
|
topOrder = order;
|
||||||
|
|
||||||
// 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;
|
return topSprite ? topSprite : nearestMesh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,64 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <sophia/aabb.hpp>
|
||||||
#include <sophia/entity.hpp>
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/math.hpp>
|
#include <sophia/math.hpp>
|
||||||
#include <sophia/scene.hpp>
|
#include <sophia/resource_registry_ref.hpp>
|
||||||
|
#include <sophia/scene_view_registry.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <sophia/ui/ui.hpp>
|
#include <sophia/ui/ui.hpp>
|
||||||
|
|
||||||
|
#include "../editor_camera.hpp"
|
||||||
|
#include "../editor_context.hpp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the scene through its active camera. Click a sprite to select it,
|
* Shows the active scene through the editor's own camera.
|
||||||
* drag it to move it (in Move mode).
|
|
||||||
*
|
*
|
||||||
* Picking and moving are 2D: they assume an orthographic camera rotated only
|
* 3D: right-drag orbits, middle-drag pans, the wheel zooms, right-drag with
|
||||||
* around Z, and work in the z = 0 plane.
|
* 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 {
|
class ScenePanel : public sophia::Panel {
|
||||||
public:
|
public:
|
||||||
ScenePanel(sophia::UI& ui, sophia::RenderTargetHandle target,
|
// Renders into `target` through `view`, a SceneView this panel keeps
|
||||||
sophia::Scene& scene, sophia::Entity& selected)
|
// pointed at the active scene. `camera` is the entity the view looks
|
||||||
: Panel{{.name = "Scene",
|
// through; it belongs to a scene of the editor's own, and this panel
|
||||||
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
// drives it from its EditorCamera.
|
||||||
ImGuiWindowFlags_NoScrollWithMouse}},
|
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},
|
ui{ui},
|
||||||
|
resources{resources},
|
||||||
|
context{context},
|
||||||
|
sceneViews{sceneViews},
|
||||||
|
view{view},
|
||||||
target{target},
|
target{target},
|
||||||
scene{scene},
|
camera{camera} {}
|
||||||
selected{selected} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
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
|
// Where the scene image is on screen this frame, and the camera looking
|
||||||
// through it.
|
// through it.
|
||||||
struct ViewportTransform {
|
struct ViewportTransform {
|
||||||
@@ -38,22 +67,57 @@ class ScenePanel : public sophia::Panel {
|
|||||||
glm::mat4 viewProjection;
|
glm::mat4 viewProjection;
|
||||||
glm::mat4 inverseViewProjection;
|
glm::mat4 inverseViewProjection;
|
||||||
|
|
||||||
ImVec2 worldToScreen(const glm::vec3& world) const;
|
// Null for points behind the camera.
|
||||||
|
std::optional<ImVec2> 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;
|
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 drawToolbar();
|
||||||
void drawGrid(ImVec2 origin, ImVec2 size);
|
void drawGrid(ImVec2 origin, ImVec2 size);
|
||||||
void handleInput(const ViewportTransform& viewport);
|
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 drawSelection(const ViewportTransform& viewport);
|
||||||
|
void frameSelection();
|
||||||
|
|
||||||
// The top-most sprite under `world`, or a null Entity.
|
// The bounds of the entity's mesh in the mesh's own space, if it has one.
|
||||||
sophia::Entity pick(glm::vec2 world);
|
std::optional<sophia::AABB> 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::UI& ui;
|
||||||
|
sophia::ResourceRegistryRef resources;
|
||||||
|
EditorContext& context;
|
||||||
|
sophia::SceneViewRegistry& sceneViews;
|
||||||
|
sophia::SceneViewHandle view;
|
||||||
sophia::RenderTargetHandle target;
|
sophia::RenderTargetHandle target;
|
||||||
sophia::Scene& scene;
|
sophia::Entity camera;
|
||||||
sophia::Entity& selected;
|
|
||||||
|
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;
|
bool showGrid = true;
|
||||||
// Set while a drag that started on the selected entity is moving it.
|
// Set while a drag that started on the selected entity is moving it.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
void log(Args... args) {
|
||||||
|
sophia::log::print("\033[1;38;2;170;90;255m[editor]\033[0m", args...);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <sophia/math.hpp>
|
||||||
|
|
||||||
|
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<vec3, 8> getCorners() const {
|
||||||
|
std::array<vec3, 8> 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
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/aabb.hpp>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/math.hpp>
|
#include <sophia/math.hpp>
|
||||||
#include <sophia/mesh_data.hpp>
|
#include <sophia/mesh_data.hpp>
|
||||||
@@ -28,6 +29,10 @@ class ResourceRegistryRef {
|
|||||||
glm::vec2 getExtent(RenderTargetHandle handle) const;
|
glm::vec2 getExtent(RenderTargetHandle handle) const;
|
||||||
glm::vec2 getExtent(TextureHandle 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(RenderTargetHandle handle) const;
|
||||||
bool isAlive(TextureHandle handle) const;
|
bool isAlive(TextureHandle handle) const;
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,20 @@ class Scene {
|
|||||||
Entity getActiveCamera();
|
Entity getActiveCamera();
|
||||||
void setActiveCamera(Entity camera);
|
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:
|
private:
|
||||||
friend class Entity;
|
friend class Entity;
|
||||||
friend class SceneRenderer; // !TEMP remove this
|
friend class SceneRenderer; // !TEMP remove this
|
||||||
|
|
||||||
entt::registry registry;
|
entt::registry registry;
|
||||||
entt::entity activeCamera = entt::null;
|
entt::entity activeCamera = entt::null;
|
||||||
|
bool visible = true;
|
||||||
|
bool active = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ struct SceneView {
|
|||||||
SceneHandle scene = INVALID_HANDLE;
|
SceneHandle scene = INVALID_HANDLE;
|
||||||
RenderTargetHandle target =
|
RenderTargetHandle target =
|
||||||
INVALID_HANDLE; // null implies render to swapchain
|
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.
|
// TODO(#76): not used yet; every view renders to its whole target.
|
||||||
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Panel {
|
|||||||
bool isLocked() const { return config.locked; }
|
bool isLocked() const { return config.locked; }
|
||||||
|
|
||||||
bool isVisible() const { return this->visible; }
|
bool isVisible() const { return this->visible; }
|
||||||
|
bool isShown() const { return this->visible && this->shown; }
|
||||||
void setVisible(bool visible) { this->visible = visible; }
|
void setVisible(bool visible) { this->visible = visible; }
|
||||||
void toggleVisible() { this->visible = !this->visible; }
|
void toggleVisible() { this->visible = !this->visible; }
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ class Panel {
|
|||||||
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
|
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
|
||||||
|
|
||||||
bool visible = true;
|
bool visible = true;
|
||||||
|
bool shown = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Config config;
|
Config config;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* using namespace sophia;
|
* using namespace sophia;
|
||||||
* log::fatal("fatal message")
|
* log::fatal("fatal message")
|
||||||
* log::info("Infomation message")
|
* log::info("Infomation message")
|
||||||
|
* log::print("no prefix, just the message")
|
||||||
*
|
*
|
||||||
* TODO Impl some form of log error handling
|
* TODO Impl some form of log error handling
|
||||||
*/
|
*/
|
||||||
@@ -122,4 +123,24 @@ void trace(Args... args) {
|
|||||||
log(LEVEL::TRACE, 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 <typename First, typename... Rest>
|
||||||
|
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<int>{((void)rest, 0)...};
|
||||||
|
#endif // !NDEBUG
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sophia::log
|
} // namespace sophia::log
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ class Application::Impl {
|
|||||||
const SceneView& view = sceneViewRegistry.peek(handle);
|
const SceneView& view = sceneViewRegistry.peek(handle);
|
||||||
bool toSwapchain = !view.target.isValid();
|
bool toSwapchain = !view.target.isValid();
|
||||||
if (!sceneRegistry.isAlive(view.scene) ||
|
if (!sceneRegistry.isAlive(view.scene) ||
|
||||||
|
!sceneRegistry.get(view.scene).isVisible() ||
|
||||||
(!toSwapchain && !renderTargetPool.isAlive(view.target))) {
|
(!toSwapchain && !renderTargetPool.isAlive(view.target))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ Mesh::getAttributeDescriptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mesh::Mesh(Device& device, const MeshData& data) : device{device} {
|
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);
|
createVertexBuffers(data.vertices);
|
||||||
createIndexBuffers(data.indices);
|
createIndexBuffers(data.indices);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <sophia/aabb.hpp>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/math.hpp>
|
#include <sophia/math.hpp>
|
||||||
#include <sophia/mesh_data.hpp>
|
#include <sophia/mesh_data.hpp>
|
||||||
@@ -26,6 +27,9 @@ class Mesh {
|
|||||||
Mesh(const Mesh&) = delete;
|
Mesh(const Mesh&) = delete;
|
||||||
Mesh& operator=(const Mesh&) = delete;
|
Mesh& operator=(const Mesh&) = delete;
|
||||||
|
|
||||||
|
// Bounds of the vertices, in the mesh's own space.
|
||||||
|
const AABB& getBounds() const { return this->bounds; }
|
||||||
|
|
||||||
void bind(vk::CommandBuffer commandBuffer) const;
|
void bind(vk::CommandBuffer commandBuffer) const;
|
||||||
void draw(vk::CommandBuffer commandBuffer, u32 instanceCount = 1,
|
void draw(vk::CommandBuffer commandBuffer, u32 instanceCount = 1,
|
||||||
u32 firstInstance = 0) const;
|
u32 firstInstance = 0) const;
|
||||||
@@ -36,6 +40,8 @@ class Mesh {
|
|||||||
|
|
||||||
Device& device;
|
Device& device;
|
||||||
|
|
||||||
|
AABB bounds;
|
||||||
|
|
||||||
std::unique_ptr<Buffer> vertexBuffer;
|
std::unique_ptr<Buffer> vertexBuffer;
|
||||||
u32 vertexCount;
|
u32 vertexCount;
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ glm::vec2 ResourceRegistryRef::getExtent(RenderTargetHandle handle) const {
|
|||||||
return {static_cast<float>(extent.width), static_cast<float>(extent.height)};
|
return {static_cast<float>(extent.width), static_cast<float>(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 {
|
bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const {
|
||||||
return registry->renderTargetPool.isAlive(handle);
|
return registry->renderTargetPool.isAlive(handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ void Panel::draw(const FrameInfo& frameInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool open = true;
|
bool open = true;
|
||||||
ImGui::Begin(config.name.c_str(), config.hasCloseButton() ? &open : nullptr,
|
this->shown = ImGui::Begin(config.name.c_str(),
|
||||||
config.getWindowFlags());
|
config.hasCloseButton() ? &open : nullptr,
|
||||||
|
config.getWindowFlags());
|
||||||
onUpdate(frameInfo);
|
onUpdate(frameInfo);
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
|
|||||||
Vendored
+14
@@ -78,3 +78,17 @@ target_link_libraries(imgui PRIVATE
|
|||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
target_compile_options(imgui PRIVATE -fPIC)
|
target_compile_options(imgui PRIVATE -fPIC)
|
||||||
endif()
|
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
|
||||||
|
)
|
||||||
|
|||||||
+1
Submodule third_party/ImGuizmo added at 18cef5e031
Reference in New Issue
Block a user