Compare commits
3 Commits
e05926ad74
...
0b09aeb5d9
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b09aeb5d9 | |||
| 8a82df59ca | |||
| b24945ec9d |
+5
-10
@@ -21,16 +21,15 @@ class Editor : public sophia::Application {
|
|||||||
|
|
||||||
sophia::SceneHandle sceneHandle = scenes().create();
|
sophia::SceneHandle sceneHandle = scenes().create();
|
||||||
sophia::Scene& scene = scenes().get(sceneHandle);
|
sophia::Scene& scene = scenes().get(sceneHandle);
|
||||||
sophia::Entity camera = createDemoScene(scene);
|
createDemoScene(scene);
|
||||||
|
|
||||||
panels().emplace<SceneHierarchyPanel>(scene, selected);
|
panels().emplace<SceneHierarchyPanel>(scene, selected);
|
||||||
panels().emplace<InspectorPanel>(selected, textures);
|
panels().emplace<InspectorPanel>(selected, textures);
|
||||||
panels().emplace<AssetBrowserPanel>();
|
panels().emplace<AssetBrowserPanel>();
|
||||||
sophia::RenderTargetHandle sceneTarget =
|
sophia::RenderTargetHandle sceneTarget =
|
||||||
resources().create<sophia::RenderTarget>();
|
resources().create<sophia::RenderTarget>();
|
||||||
views().create(
|
views().create({.scene = sceneHandle, .target = sceneTarget});
|
||||||
{.scene = sceneHandle, .target = sceneTarget, .camera = camera});
|
panels().emplace<ViewportPanel>(ui(), sceneTarget, scene, selected);
|
||||||
panels().emplace<ViewportPanel>(ui(), sceneTarget);
|
|
||||||
|
|
||||||
panels().setDefaultLayout(
|
panels().setDefaultLayout(
|
||||||
{.left = {"Scene Hierarchy"},
|
{.left = {"Scene Hierarchy"},
|
||||||
@@ -42,10 +41,8 @@ class Editor : public sophia::Application {
|
|||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
sophia::Entity createDemoScene(sophia::Scene& scene) {
|
void createDemoScene(sophia::Scene& scene) {
|
||||||
sophia::Entity camera = scene.createEntity("Main Camera");
|
scene.createCamera("Main Camera");
|
||||||
camera.addComponent<sophia::CameraComponent>();
|
|
||||||
scene.setActiveCamera(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,
|
||||||
@@ -66,8 +63,6 @@ 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});
|
||||||
|
|
||||||
return camera;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sophia::TextureHandle createCheckerTexture() {
|
sophia::TextureHandle createCheckerTexture() {
|
||||||
|
|||||||
@@ -2,7 +2,86 @@
|
|||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Corners of the unit quad a SpriteComponent's model matrix maps to world.
|
||||||
|
constexpr std::array<glm::vec2, 4> 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&) {
|
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<sophia::CameraComponent>()) {
|
||||||
|
// 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<sophia::CameraComponent>().getViewProjection(
|
||||||
|
camera.getComponent<sophia::TransformComponent>(), 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 struct {
|
||||||
const char* label;
|
const char* label;
|
||||||
GizmoMode mode;
|
GizmoMode mode;
|
||||||
@@ -17,20 +96,14 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
ImGui::Checkbox("Grid", &showGrid);
|
ImGui::Checkbox("Grid", &showGrid);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
void ViewportPanel::drawGrid(ImVec2 origin, ImVec2 size) {
|
||||||
|
|
||||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
||||||
const ImVec2 size = ImGui::GetContentRegionAvail();
|
|
||||||
const ImVec2 end(origin.x + size.x, origin.y + size.y);
|
|
||||||
|
|
||||||
ui.image(target);
|
|
||||||
|
|
||||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
||||||
|
|
||||||
if (showGrid) {
|
|
||||||
constexpr float GRID_STEP = 40.0f;
|
constexpr float GRID_STEP = 40.0f;
|
||||||
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
|
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) {
|
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
|
||||||
drawList->AddLine(ImVec2(origin.x + x, origin.y),
|
drawList->AddLine(ImVec2(origin.x + x, origin.y),
|
||||||
ImVec2(origin.x + x, end.y), gridColor);
|
ImVec2(origin.x + x, end.y), gridColor);
|
||||||
@@ -41,6 +114,89 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
|
void ViewportPanel::handleInput(const ViewportTransform& viewport) {
|
||||||
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
|
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<sophia::TransformComponent>();
|
||||||
|
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<sophia::TransformComponent>();
|
||||||
|
|
||||||
|
if (selected.hasComponent<sophia::SpriteComponent>()) {
|
||||||
|
glm::mat4 model =
|
||||||
|
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
|
||||||
|
|
||||||
|
std::array<ImVec2, 4> 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<std::tuple<sophia::s32, float>> bestOrder;
|
||||||
|
|
||||||
|
scene.forEachEntity([&](sophia::Entity entity) {
|
||||||
|
if (!entity.hasComponent<sophia::SpriteComponent>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& transform = entity.getComponent<sophia::TransformComponent>();
|
||||||
|
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,64 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#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/scene.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <sophia/ui/ui.hpp>
|
#include <sophia/ui/ui.hpp>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
class ViewportPanel : public sophia::Panel {
|
||||||
public:
|
public:
|
||||||
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
|
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target,
|
||||||
|
sophia::Scene& scene, sophia::Entity& selected)
|
||||||
: Panel{{.name = "Viewport",
|
: Panel{{.name = "Viewport",
|
||||||
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
||||||
ImGuiWindowFlags_NoScrollWithMouse}},
|
ImGuiWindowFlags_NoScrollWithMouse}},
|
||||||
ui{ui},
|
ui{ui},
|
||||||
target{target} {}
|
target{target},
|
||||||
|
scene{scene},
|
||||||
|
selected{selected} {}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
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::UI& ui;
|
||||||
sophia::RenderTargetHandle target;
|
sophia::RenderTargetHandle target;
|
||||||
|
sophia::Scene& scene;
|
||||||
|
sophia::Entity& selected;
|
||||||
|
|
||||||
enum class GizmoMode { Move, Rotate, Scale };
|
enum class GizmoMode { Move, Rotate, Scale };
|
||||||
|
|
||||||
GizmoMode gizmoMode = GizmoMode::Move;
|
GizmoMode gizmoMode = GizmoMode::Move;
|
||||||
bool showGrid = true;
|
bool showGrid = true;
|
||||||
|
// Set while a drag that started on the selected entity is moving it.
|
||||||
|
bool dragging = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ struct SpriteComponent {
|
|||||||
|
|
||||||
// Higher draws on top. Ties are broken by the transform's position.z.
|
// Higher draws on top. Ties are broken by the transform's position.z.
|
||||||
s32 sortOrder = 0;
|
s32 sortOrder = 0;
|
||||||
|
|
||||||
|
// Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's
|
||||||
|
// rectangle around its pivot, then by `transform`.
|
||||||
|
glm::mat4 getModel(const TransformComponent& transform) const {
|
||||||
|
return transform.mat4() *
|
||||||
|
glm::translate(glm::mat4{1.f}, glm::vec3{-pivot * size, 0.f}) *
|
||||||
|
glm::scale(glm::mat4{1.f}, glm::vec3{size, 1.f});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CameraComponent {
|
struct CameraComponent {
|
||||||
@@ -98,6 +106,15 @@ struct CameraComponent {
|
|||||||
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
|
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
|
||||||
ortho.nearPlane, ortho.farPlane);
|
ortho.nearPlane, ortho.farPlane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) *
|
||||||
|
glm::mat4_cast(transform.rotation);
|
||||||
|
return getProjection(aspect) * glm::inverse(world);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -13,18 +13,17 @@ class Scene {
|
|||||||
Scene();
|
Scene();
|
||||||
~Scene();
|
~Scene();
|
||||||
|
|
||||||
// Every entity gets a TagComponent (named `name`) and a TransformComponent.
|
|
||||||
Entity createEntity(const std::string& name = {});
|
Entity createEntity(const std::string& name = {});
|
||||||
void destroyEntity(Entity entity);
|
void destroyEntity(Entity entity);
|
||||||
|
|
||||||
|
Entity createCamera(const std::string& name = "Camera");
|
||||||
|
|
||||||
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
|
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
|
||||||
template <typename Fn>
|
template <typename Fn>
|
||||||
void forEachEntity(Fn&& fn);
|
void forEachEntity(Fn&& fn);
|
||||||
|
|
||||||
// The camera a render request uses when it doesn't name one. Null until
|
Entity getActiveCamera();
|
||||||
// set.
|
void setActiveCamera(Entity camera);
|
||||||
entt::entity getActiveCamera() const { return activeCamera; }
|
|
||||||
void setActiveCamera(entt::entity camera) { activeCamera = camera; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Entity;
|
friend class Entity;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class SceneRegistry {
|
|||||||
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
|
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
|
||||||
Scene& get(SceneHandle handle) { return pool.get(handle); }
|
Scene& get(SceneHandle handle) { return pool.get(handle); }
|
||||||
|
|
||||||
|
const Scene& peek(SceneHandle handle) const { return pool.get(handle); }
|
||||||
|
|
||||||
void release(SceneHandle handle) { pool.release(handle); }
|
void release(SceneHandle handle) { pool.release(handle); }
|
||||||
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
|
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ enum class ClearFlags : u8 {
|
|||||||
|
|
||||||
struct SceneView {
|
struct SceneView {
|
||||||
SceneHandle scene = INVALID_HANDLE;
|
SceneHandle scene = INVALID_HANDLE;
|
||||||
RenderTargetHandle target = INVALID_HANDLE;
|
RenderTargetHandle target =
|
||||||
Entity camera;
|
INVALID_HANDLE; // null implies render to swapchain
|
||||||
|
Entity camera; // null implies use scene's active camera
|
||||||
|
|
||||||
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class SceneViewRegistry {
|
|||||||
return pool.get(handle);
|
return pool.get(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SceneView& peek(SceneViewHandle handle) const {
|
||||||
|
return pool.get(handle);
|
||||||
|
}
|
||||||
|
|
||||||
void release(SceneViewHandle handle) {
|
void release(SceneViewHandle handle) {
|
||||||
isRenderQueueDirty = true;
|
isRenderQueueDirty = true;
|
||||||
pool.release(handle);
|
pool.release(handle);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <sophia/application.hpp>
|
#include <sophia/application.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -87,11 +88,12 @@ class Application::Impl {
|
|||||||
PanelRegistry& panelRegistry = this->application.panelRegistry;
|
PanelRegistry& panelRegistry = this->application.panelRegistry;
|
||||||
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
|
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
|
||||||
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
|
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
|
||||||
const SceneViewRegistry& sceneViewRegistryConst =
|
|
||||||
this->application.sceneViewRegistry;
|
|
||||||
|
|
||||||
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
|
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
|
||||||
|
|
||||||
|
// Views already warned about having no camera, so each warns once.
|
||||||
|
std::set<SceneViewHandle> viewsWithoutCamera;
|
||||||
|
|
||||||
auto startTime = std::chrono::high_resolution_clock::now();
|
auto startTime = std::chrono::high_resolution_clock::now();
|
||||||
auto currentTime = startTime;
|
auto currentTime = startTime;
|
||||||
u32 frameIndex = 0;
|
u32 frameIndex = 0;
|
||||||
@@ -148,8 +150,8 @@ class Application::Impl {
|
|||||||
graph.importImage(renderContext.getCurrentImage(), Access::Present);
|
graph.importImage(renderContext.getCurrentImage(), Access::Present);
|
||||||
|
|
||||||
std::map<RenderTargetHandle, ImageRef> viewTargets;
|
std::map<RenderTargetHandle, ImageRef> viewTargets;
|
||||||
for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) {
|
for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
|
||||||
const SceneView& view = sceneViewRegistryConst.get(handle);
|
const SceneView& view = sceneViewRegistry.peek(handle);
|
||||||
if (!sceneRegistry.isAlive(view.scene) ||
|
if (!sceneRegistry.isAlive(view.scene) ||
|
||||||
!renderTargetPool.isAlive(view.target)) {
|
!renderTargetPool.isAlive(view.target)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -172,12 +174,23 @@ class Application::Impl {
|
|||||||
: vk::AttachmentLoadOp::eLoad,
|
: vk::AttachmentLoadOp::eLoad,
|
||||||
{color.r, color.g, color.b, color.a});
|
{color.r, color.g, color.b, color.a});
|
||||||
},
|
},
|
||||||
[&sceneRenderer, &sceneViewRegistryConst,
|
[&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
|
||||||
handle](vk::CommandBuffer commandBuffer) {
|
&viewsWithoutCamera, handle](vk::CommandBuffer commandBuffer) {
|
||||||
const SceneView& view = sceneViewRegistryConst.get(handle);
|
const SceneView& view = sceneViewRegistry.peek(handle);
|
||||||
if (view.camera) {
|
Entity camera =
|
||||||
sceneRenderer.render(commandBuffer, view);
|
view.camera ? view.camera
|
||||||
|
: sceneRegistry.get(view.scene).getActiveCamera();
|
||||||
|
// No camera to look through: the target is just cleared.
|
||||||
|
if (!camera) {
|
||||||
|
if (viewsWithoutCamera.insert(handle).second) {
|
||||||
|
log::warning(
|
||||||
|
"scene view has no camera and its scene has no active "
|
||||||
|
"camera; it will only be cleared (see "
|
||||||
|
"Scene::createCamera)");
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sceneRenderer.render(commandBuffer, view, camera);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,12 +91,13 @@ void ImGuiRenderer::beginFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
||||||
vk::ImageView view =
|
const RenderTarget& target = resources.renderTargetPool.get(handle);
|
||||||
resources.renderTargetPool.get(handle).getImage().getView();
|
vk::ImageView view = target.getImage().getView();
|
||||||
|
|
||||||
auto it = textures.find(handle.index);
|
auto it = textures.find(handle.index);
|
||||||
if (it != textures.end()) {
|
if (it != textures.end()) {
|
||||||
if (it->second.generation == handle.generation && it->second.view == view) {
|
if (it->second.generation == handle.generation &&
|
||||||
|
it->second.version == target.getVersion()) {
|
||||||
return (ImTextureID)it->second.descriptorSet;
|
return (ImTextureID)it->second.descriptorSet;
|
||||||
}
|
}
|
||||||
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
||||||
@@ -107,7 +108,8 @@ ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
|||||||
// left in once the scene pass has finished.
|
// left in once the scene pass has finished.
|
||||||
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
|
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
|
||||||
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
textures[handle.index] = {handle.generation, view, descriptorSet};
|
textures[handle.index] = {handle.generation, target.getVersion(),
|
||||||
|
descriptorSet};
|
||||||
return (ImTextureID)descriptorSet;
|
return (ImTextureID)descriptorSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ void ImGuiRenderer::releaseStaleTextures() {
|
|||||||
|
|
||||||
bool stale =
|
bool stale =
|
||||||
!renderTargetPool.isAlive(handle) ||
|
!renderTargetPool.isAlive(handle) ||
|
||||||
renderTargetPool.get(handle).getImage().getView() != it->second.view;
|
renderTargetPool.get(handle).getVersion() != it->second.version;
|
||||||
if (stale) {
|
if (stale) {
|
||||||
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
||||||
it = textures.erase(it);
|
it = textures.erase(it);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ImGuiRenderer {
|
|||||||
|
|
||||||
struct Texture {
|
struct Texture {
|
||||||
u32 generation;
|
u32 generation;
|
||||||
vk::ImageView view;
|
u32 version;
|
||||||
VkDescriptorSet descriptorSet;
|
VkDescriptorSet descriptorSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,13 @@ SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
|
|||||||
resources{resources},
|
resources{resources},
|
||||||
spriteRenderSystem{device, colorFormat, resources} {}
|
spriteRenderSystem{device, colorFormat, resources} {}
|
||||||
|
|
||||||
SceneRenderer::~SceneRenderer() {}
|
|
||||||
|
|
||||||
void SceneRenderer::beginFrame(u32 frameIndex) {
|
void SceneRenderer::beginFrame(u32 frameIndex) {
|
||||||
this->spriteRenderSystem.beginFrame(frameIndex);
|
this->spriteRenderSystem.beginFrame(frameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
|
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
|
||||||
const SceneView& sceneView) {
|
const SceneView& sceneView, Entity camera) {
|
||||||
const Scene& scene = this->sceneRegistry.get(sceneView.scene);
|
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
|
||||||
const entt::registry& registry = scene.registry;
|
|
||||||
|
|
||||||
entt::entity camera = sceneView.camera;
|
|
||||||
SOPHIA_ASSERT(registry.all_of<CameraComponent>(camera),
|
|
||||||
"camera does not have camera component!");
|
"camera does not have camera component!");
|
||||||
|
|
||||||
vk::Extent2D extent =
|
vk::Extent2D extent =
|
||||||
@@ -34,17 +28,12 @@ void SceneRenderer::render(vk::CommandBuffer commandBuffer,
|
|||||||
float aspect =
|
float aspect =
|
||||||
static_cast<float>(extent.width) / static_cast<float>(extent.height);
|
static_cast<float>(extent.width) / static_cast<float>(extent.height);
|
||||||
|
|
||||||
// The view matrix ignores the camera's scale; zoom is the projection's job.
|
|
||||||
glm::mat4 cameraWorld{1.f};
|
|
||||||
if (const auto* transform = registry.try_get<TransformComponent>(camera)) {
|
|
||||||
cameraWorld = glm::translate(glm::mat4{1.f}, transform->position) *
|
|
||||||
glm::mat4_cast(transform->rotation);
|
|
||||||
}
|
|
||||||
glm::mat4 viewProjection =
|
glm::mat4 viewProjection =
|
||||||
registry.get<CameraComponent>(camera).getProjection(aspect) *
|
camera.getComponent<CameraComponent>().getViewProjection(
|
||||||
glm::inverse(cameraWorld);
|
camera.getComponent<TransformComponent>(), aspect);
|
||||||
|
|
||||||
renderSprites(commandBuffer, scene, viewProjection);
|
renderSprites(commandBuffer, this->sceneRegistry.get(sceneView.scene),
|
||||||
|
viewProjection);
|
||||||
|
|
||||||
// TODO: meshes. Group MeshComponent entities by mesh and draw each group
|
// TODO: meshes. Group MeshComponent entities by mesh and draw each group
|
||||||
// (instanced), as sprites are grouped by texture.
|
// (instanced), as sprites are grouped by texture.
|
||||||
@@ -53,8 +42,7 @@ void SceneRenderer::render(vk::CommandBuffer commandBuffer,
|
|||||||
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
|
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
|
||||||
const Scene& scene,
|
const Scene& scene,
|
||||||
const glm::mat4& viewProjection) {
|
const glm::mat4& viewProjection) {
|
||||||
std::vector<SpriteDrawItem>& items = this->spriteDrawItems;
|
this->spriteDrawItems.clear();
|
||||||
items.clear();
|
|
||||||
|
|
||||||
auto view =
|
auto view =
|
||||||
scene.registry.view<const TransformComponent, const SpriteComponent>();
|
scene.registry.view<const TransformComponent, const SpriteComponent>();
|
||||||
@@ -63,28 +51,22 @@ void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
|
|||||||
? sprite.texture
|
? sprite.texture
|
||||||
: this->resources.whiteTexture;
|
: this->resources.whiteTexture;
|
||||||
|
|
||||||
// Unit quad -> sprite rectangle around its pivot -> world.
|
this->spriteDrawItems.push_back(
|
||||||
glm::mat4 model =
|
{sprite.sortOrder,
|
||||||
transform.mat4() *
|
|
||||||
glm::translate(glm::mat4{1.f},
|
|
||||||
glm::vec3{-sprite.pivot * sprite.size, 0.f}) *
|
|
||||||
glm::scale(glm::mat4{1.f}, glm::vec3{sprite.size, 1.f});
|
|
||||||
|
|
||||||
items.push_back({sprite.sortOrder,
|
|
||||||
transform.position.z,
|
transform.position.z,
|
||||||
texture,
|
texture,
|
||||||
{model, sprite.color, sprite.uvRect}});
|
{sprite.getModel(transform), sprite.color, sprite.uvRect}});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Texture is the last key so that sprites whose order doesn't matter
|
// 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.
|
// relative to each other still end up in as few batches as possible.
|
||||||
std::ranges::sort(items, {}, [](const SpriteDrawItem& item) {
|
std::ranges::sort(this->spriteDrawItems, {}, [](const SpriteDrawItem& item) {
|
||||||
return std::tuple{item.sortOrder, item.depth, item.texture};
|
return std::tuple{item.sortOrder, item.depth, item.texture};
|
||||||
});
|
});
|
||||||
|
|
||||||
this->spriteInstances.clear();
|
this->spriteInstances.clear();
|
||||||
this->spriteBatches.clear();
|
this->spriteBatches.clear();
|
||||||
for (const SpriteDrawItem& item : items) {
|
for (const SpriteDrawItem& item : this->spriteDrawItems) {
|
||||||
auto index = static_cast<u32>(this->spriteInstances.size());
|
auto index = static_cast<u32>(this->spriteInstances.size());
|
||||||
this->spriteInstances.push_back(item.instance);
|
this->spriteInstances.push_back(item.instance);
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ class SceneRenderer {
|
|||||||
|
|
||||||
SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
|
SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
|
||||||
Device& device, vk::Format colorFormat);
|
Device& device, vk::Format colorFormat);
|
||||||
~SceneRenderer();
|
|
||||||
|
|
||||||
// Call once per frame, before any render().
|
// Call once per frame, before any render().
|
||||||
void beginFrame(u32 frameIndex);
|
void beginFrame(u32 frameIndex);
|
||||||
|
|
||||||
// Records `sceneView` into `commandBuffer`. Rendering into the view's
|
// Records `sceneView` as seen through `camera` (an entity of the view's
|
||||||
// target must already have begun.
|
// scene with a CameraComponent) into `commandBuffer`. Rendering into the
|
||||||
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView);
|
// view's target must already have begun.
|
||||||
|
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView,
|
||||||
|
Entity camera);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SpriteDrawItem {
|
struct SpriteDrawItem {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ void RenderTarget::applyPendingResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderTarget::create(u32 width, u32 height) {
|
void RenderTarget::create(u32 width, u32 height) {
|
||||||
|
this->version++;
|
||||||
this->image = Image::Builder(this->device)
|
this->image = Image::Builder(this->device)
|
||||||
.setExtent(width, height)
|
.setExtent(width, height)
|
||||||
.setFormat(this->format)
|
.setFormat(this->format)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class RenderTarget {
|
|||||||
Image& getImage() const { return *this->image; }
|
Image& getImage() const { return *this->image; }
|
||||||
|
|
||||||
vk::Extent2D getExtent() const;
|
vk::Extent2D getExtent() const;
|
||||||
|
u32 getVersion() const { return this->version; }
|
||||||
|
|
||||||
void requestResize(u32 width, u32 height);
|
void requestResize(u32 width, u32 height);
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class RenderTarget {
|
|||||||
std::unique_ptr<Image> image;
|
std::unique_ptr<Image> image;
|
||||||
|
|
||||||
std::optional<vk::Extent2D> pendingExtent;
|
std::optional<vk::Extent2D> pendingExtent;
|
||||||
|
u32 version = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -16,6 +16,24 @@ Entity Scene::createEntity(const std::string& name) {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Entity Scene::createCamera(const std::string& name) {
|
||||||
|
Entity camera = createEntity(name);
|
||||||
|
camera.addComponent<CameraComponent>();
|
||||||
|
if (this->activeCamera == entt::null) {
|
||||||
|
this->activeCamera = camera;
|
||||||
|
}
|
||||||
|
return camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity Scene::getActiveCamera() {
|
||||||
|
if (this->activeCamera == entt::null) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {this->activeCamera, this};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setActiveCamera(Entity camera) { this->activeCamera = camera; }
|
||||||
|
|
||||||
void Scene::destroyEntity(Entity entity) {
|
void Scene::destroyEntity(Entity entity) {
|
||||||
if (this->activeCamera == entity) {
|
if (this->activeCamera == entity) {
|
||||||
this->activeCamera = entt::null;
|
this->activeCamera = entt::null;
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ class Boids2D : public sophia::Application {
|
|||||||
[this](sophia::KeyReleasedEvent& e) {
|
[this](sophia::KeyReleasedEvent& e) {
|
||||||
setScrollKeyHeld(e.getKeyCode(), false);
|
setScrollKeyHeld(e.getKeyCode(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debugUI().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreate() override {
|
void onCreate() override {
|
||||||
@@ -110,8 +112,7 @@ class Boids2D : public sophia::Application {
|
|||||||
sceneHandle = scenes().create();
|
sceneHandle = scenes().create();
|
||||||
sophia::Scene& scene = scenes().get(sceneHandle);
|
sophia::Scene& scene = scenes().get(sceneHandle);
|
||||||
|
|
||||||
cameraEntity = scene.createEntity("Camera");
|
cameraEntity = scene.createCamera();
|
||||||
cameraEntity.addComponent<sophia::CameraComponent>();
|
|
||||||
|
|
||||||
std::uniform_real_distribution<float> tint{0.0f, 1.0f};
|
std::uniform_real_distribution<float> tint{0.0f, 1.0f};
|
||||||
for (Boid& boid : boids) {
|
for (Boid& boid : boids) {
|
||||||
@@ -127,7 +128,6 @@ class Boids2D : public sophia::Application {
|
|||||||
target = resources().create<sophia::RenderTarget>();
|
target = resources().create<sophia::RenderTarget>();
|
||||||
views().create({.scene = sceneHandle,
|
views().create({.scene = sceneHandle,
|
||||||
.target = target,
|
.target = target,
|
||||||
.camera = cameraEntity,
|
|
||||||
.clearColor = {0.01f, 0.01f, 0.02f, 1.0f}});
|
.clearColor = {0.01f, 0.01f, 0.02f, 1.0f}});
|
||||||
|
|
||||||
panels().emplace<ViewportPanel>(ui(), target);
|
panels().emplace<ViewportPanel>(ui(), target);
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ class ImageViewer : public sophia::Application {
|
|||||||
sophia::SceneHandle sceneHandle = scenes().create();
|
sophia::SceneHandle sceneHandle = scenes().create();
|
||||||
sophia::Scene& scene = scenes().get(sceneHandle);
|
sophia::Scene& scene = scenes().get(sceneHandle);
|
||||||
|
|
||||||
camera = scene.createEntity("Camera");
|
camera = scene.createCamera();
|
||||||
camera.addComponent<sophia::CameraComponent>();
|
|
||||||
|
|
||||||
sophia::TextureHandle texture = resources().load<sophia::Texture>(path);
|
sophia::TextureHandle texture = resources().load<sophia::Texture>(path);
|
||||||
imageSize = resources().getExtent(texture);
|
imageSize = resources().getExtent(texture);
|
||||||
@@ -54,7 +53,6 @@ class ImageViewer : public sophia::Application {
|
|||||||
target = resources().create<sophia::RenderTarget>();
|
target = resources().create<sophia::RenderTarget>();
|
||||||
views().create({.scene = sceneHandle,
|
views().create({.scene = sceneHandle,
|
||||||
.target = target,
|
.target = target,
|
||||||
.camera = camera,
|
|
||||||
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
|
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
|
||||||
|
|
||||||
panels().emplace<ViewportPanel>(ui(), target);
|
panels().emplace<ViewportPanel>(ui(), target);
|
||||||
|
|||||||
Reference in New Issue
Block a user