From a21a842b5ba140dcc02612bf01ffdac6f7c2af9e Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Fri, 25 Sep 2026 22:59:01 -0700 Subject: [PATCH] Initial claude dump --- editor/main.cpp | 61 +++++- editor/panels/inspector_panel.cpp | 141 +++++++++++-- editor/panels/inspector_panel.hpp | 28 ++- editor/panels/scene_hierarchy_panel.cpp | 40 ++-- editor/panels/scene_hierarchy_panel.hpp | 28 +-- engine/include/sophia/components.hpp | 21 ++ engine/include/sophia/entity.hpp | 7 + engine/include/sophia/handles.hpp | 2 + .../include/sophia/resource_registry_ref.hpp | 11 + engine/include/sophia/scene.hpp | 7 +- engine/src/application.cpp | 11 +- engine/src/rendering/render_context.hpp | 1 + engine/src/rendering/scene_renderer.cpp | 128 +++++++----- engine/src/rendering/scene_renderer.hpp | 32 ++- engine/src/rendering/sprite_render_system.cpp | 194 ++++++++++++++++++ engine/src/rendering/sprite_render_system.hpp | 94 +++++++++ engine/src/resources/resource_registry.hpp | 13 +- .../src/resources/resource_registry_ref.cpp | 50 +++++ engine/src/resources/texture.cpp | 32 +++ engine/src/resources/texture.hpp | 38 ++++ engine/src/scene/scene.cpp | 22 +- engine/src/shaders/sprite.frag | 12 ++ engine/src/shaders/sprite.frag.spv.h | 42 ++++ engine/src/shaders/sprite.vert | 31 +++ engine/src/shaders/sprite.vert.spv.h | 98 +++++++++ examples/boids_2d/main.cpp | 115 +++++++++-- 26 files changed, 1101 insertions(+), 158 deletions(-) create mode 100644 engine/src/rendering/sprite_render_system.cpp create mode 100644 engine/src/rendering/sprite_render_system.hpp create mode 100644 engine/src/resources/texture.cpp create mode 100644 engine/src/resources/texture.hpp create mode 100644 engine/src/shaders/sprite.frag create mode 100644 engine/src/shaders/sprite.frag.spv.h create mode 100644 engine/src/shaders/sprite.vert create mode 100644 engine/src/shaders/sprite.vert.spv.h diff --git a/editor/main.cpp b/editor/main.cpp index 7e0eb49..81fb414 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "panels/asset_browser_panel.hpp" #include "panels/inspector_panel.hpp" @@ -15,12 +17,19 @@ class Editor : public sophia::Application { debugUI().show(); - panels().emplace(); - panels().emplace(); + textures.emplace_back("Checker", createCheckerTexture()); + + sophia::SceneHandle sceneHandle = scenes().create(); + sophia::Scene& scene = scenes().get(sceneHandle); + sophia::Entity camera = createDemoScene(scene); + + panels().emplace(scene, selected); + panels().emplace(selected, textures); panels().emplace(); sophia::RenderTargetHandle sceneTarget = resources().create(); - views().create({.scene = scenes().create(), .target = sceneTarget}); + views().create( + {.scene = sceneHandle, .target = sceneTarget, .camera = camera}); panels().emplace(ui(), sceneTarget); panels().setDefaultLayout( @@ -31,6 +40,52 @@ class Editor : public sophia::Application { } void onUpdate(const sophia::FrameInfo& frameInfo) override {} + + private: + sophia::Entity createDemoScene(sophia::Scene& scene) { + sophia::Entity camera = scene.createEntity("Main Camera"); + camera.addComponent(); + scene.setActiveCamera(camera); + + sophia::Entity checker = scene.createEntity("Checker"); + checker.getComponent().position = {-1.5f, 0.f, + 0.f}; + checker.addComponent(sophia::SpriteComponent{ + .texture = textures[0].second, .size = {2.f, 2.f}}); + + sophia::Entity red = scene.createEntity("Red"); + red.getComponent().position = {1.f, -0.5f, 0.f}; + red.addComponent(sophia::SpriteComponent{ + .color = {0.9f, 0.25f, 0.25f, 1.f}, .size = {2.f, 2.f}}); + + // Drawn over "Red" because of its higher sort order. + sophia::Entity blue = scene.createEntity("Blue (translucent)"); + blue.getComponent().position = {1.75f, 0.25f, + 0.f}; + blue.addComponent( + sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f}, + .size = {2.f, 2.f}, + .sortOrder = 1}); + + return camera; + } + + sophia::TextureHandle createCheckerTexture() { + constexpr sophia::u32 SIZE = 8; + std::vector pixels(SIZE * SIZE * 4); + for (sophia::u32 y = 0; y < SIZE; y++) { + for (sophia::u32 x = 0; x < SIZE; x++) { + sophia::u8 value = ((x + y) % 2 == 0) ? 230 : 60; + sophia::u8* pixel = &pixels[(y * SIZE + x) * 4]; + pixel[0] = pixel[1] = pixel[2] = value; + pixel[3] = 255; + } + } + return resources().create(SIZE, SIZE, std::move(pixels)); + } + + sophia::Entity selected; + InspectorPanel::TextureList textures; }; SOPHIA_MAIN(Editor) diff --git a/editor/panels/inspector_panel.cpp b/editor/panels/inspector_panel.cpp index 3fe5f83..4592a5c 100644 --- a/editor/panels/inspector_panel.cpp +++ b/editor/panels/inspector_panel.cpp @@ -3,30 +3,135 @@ #include #include +#include void InspectorPanel::onUpdate(const sophia::FrameInfo&) { - char nameBuf[128]; - std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str()); - if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { - name = nameBuf; + if (!selected) { + ImGui::TextDisabled("No entity selected"); + return; } - ImGui::Text("Tag: %s", tag.c_str()); + + drawTag(); + ImGui::Separator(); + drawTransform(); + drawCamera(); + drawSprite(); ImGui::Separator(); + drawAddComponent(); +} - if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::DragFloat3("Position", &position.x, 0.1f); - ImGui::DragFloat3("Rotation", &rotation.x, 0.1f); - ImGui::DragFloat3("Scale", &scale.x, 0.1f); - } +void InspectorPanel::drawTag() { + auto& tag = selected.getComponent(); - if (ImGui::CollapsingHeader("Mesh Renderer", - ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Mesh: %s", meshName.c_str()); - ImGui::Text("Material: %s", materialName.c_str()); - } - - if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("%s", scriptName.c_str()); + char nameBuf[128]; + std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str()); + if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { + tag.tag = nameBuf; + } +} + +void InspectorPanel::drawTransform() { + if (!selected.hasComponent() || + !ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { + return; + } + + auto& transform = selected.getComponent(); + ImGui::DragFloat3("Position", &transform.position.x, 0.1f); + + // 2D only: edits rotation around Z and drops any X/Y rotation. + float degrees = glm::degrees(glm::eulerAngles(transform.rotation).z); + if (ImGui::DragFloat("Rotation", °rees, 1.0f)) { + transform.rotation = + glm::angleAxis(glm::radians(degrees), glm::vec3{0.f, 0.f, 1.f}); + } + + ImGui::DragFloat3("Scale", &transform.scale.x, 0.1f); +} + +void InspectorPanel::drawCamera() { + if (!selected.hasComponent() || + !ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { + return; + } + + auto& camera = selected.getComponent(); + int projection = static_cast(camera.projection); + if (ImGui::Combo("Projection", &projection, "Orthographic\0Perspective\0")) { + camera.projection = + static_cast(projection); + } + + if (camera.projection == sophia::CameraComponent::Projection::Orthographic) { + ImGui::DragFloat("Height", &camera.ortho.height, 0.1f, 0.01f, 10000.f); + } else { + float fov = glm::degrees(camera.perspective.fov); + if (ImGui::DragFloat("FOV", &fov, 0.5f, 1.f, 179.f)) { + camera.perspective.fov = glm::radians(fov); + } + } +} + +void InspectorPanel::drawSprite() { + if (!selected.hasComponent()) { + return; + } + + bool open = true; + if (!ImGui::CollapsingHeader("Sprite", &open, + ImGuiTreeNodeFlags_DefaultOpen)) { + if (!open) { + selected.removeComponent(); + } + return; + } + + auto& sprite = selected.getComponent(); + + const char* current = "None"; + for (const auto& [name, handle] : textures) { + if (handle == sprite.texture) { + current = name.c_str(); + } + } + if (ImGui::BeginCombo("Texture", current)) { + if (ImGui::Selectable("None", !sprite.texture.isValid())) { + sprite.texture = sophia::INVALID_HANDLE; + } + for (const auto& [name, handle] : textures) { + if (ImGui::Selectable(name.c_str(), handle == sprite.texture)) { + sprite.texture = handle; + } + } + ImGui::EndCombo(); + } + + ImGui::ColorEdit4("Color", &sprite.color.x); + ImGui::DragFloat2("Size", &sprite.size.x, 0.05f); + ImGui::SliderFloat2("Pivot", &sprite.pivot.x, 0.f, 1.f); + ImGui::DragFloat4("UV Rect", &sprite.uvRect.x, 0.01f); + ImGui::DragInt("Sort Order", &sprite.sortOrder); + + if (!open) { + selected.removeComponent(); + } +} + +void InspectorPanel::drawAddComponent() { + if (ImGui::Button("Add Component")) { + ImGui::OpenPopup("AddComponent"); + } + + if (ImGui::BeginPopup("AddComponent")) { + if (!selected.hasComponent() && + ImGui::MenuItem("Sprite")) { + selected.addComponent(); + } + if (!selected.hasComponent() && + ImGui::MenuItem("Camera")) { + selected.addComponent(); + } + ImGui::EndPopup(); } } diff --git a/editor/panels/inspector_panel.hpp b/editor/panels/inspector_panel.hpp index 3a080c1..429d302 100644 --- a/editor/panels/inspector_panel.hpp +++ b/editor/panels/inspector_panel.hpp @@ -1,25 +1,31 @@ #pragma once -#include +#include #include +#include #include #include +#include +#include class InspectorPanel : public sophia::Panel { public: - InspectorPanel() : Panel{{.name = "Inspector"}} {} + // Stand-in for an asset browser: the textures a sprite can pick from. + using TextureList = + std::vector>; + + InspectorPanel(sophia::Entity& selected, const TextureList& textures) + : Panel{{.name = "Inspector"}}, selected{selected}, textures{textures} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; private: - std::string name = "Player"; - std::string tag = "Player"; + void drawTag(); + void drawTransform(); + void drawCamera(); + void drawSprite(); + void drawAddComponent(); - glm::vec3 position{0.0f, 1.0f, 0.0f}; - glm::vec3 rotation{0.0f, 0.0f, 0.0f}; - glm::vec3 scale{1.0f, 1.0f, 1.0f}; - - std::string meshName = "player_capsule.fbx"; - std::string materialName = "player_mat"; - std::string scriptName = "PlayerController.lua"; + sophia::Entity& selected; + const TextureList& textures; }; diff --git a/editor/panels/scene_hierarchy_panel.cpp b/editor/panels/scene_hierarchy_panel.cpp index 08cf3c4..fc22caa 100644 --- a/editor/panels/scene_hierarchy_panel.cpp +++ b/editor/panels/scene_hierarchy_panel.cpp @@ -2,31 +2,31 @@ #include -void SceneHierarchyPanel::drawNode(const Node& node) { - if (node.children.empty()) { +#include + +void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) { + if (ImGui::Button("+ Sprite")) { + selected = scene.createEntity("Sprite"); + selected.addComponent(); + } + + ImGui::Separator(); + + // Flat for now: there is no parent/child component yet. + scene.forEachEntity([this](sophia::Entity entity) { ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth; - if (node.name == selected) { + if (entity == selected) { flags |= ImGuiTreeNodeFlags_Selected; } - ImGui::TreeNodeEx(node.name.c_str(), flags); + + const std::string& name = entity.getComponent().tag; + ImGui::PushID(static_cast(static_cast(entity))); + ImGui::TreeNodeEx(name.c_str(), flags); if (ImGui::IsItemClicked()) { - selected = node.name; + selected = entity; } - return; - } - - if (ImGui::TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) { - for (const Node& child : node.children) { - drawNode(child); - } - ImGui::TreePop(); - } -} - -void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) { - for (const Node& node : nodes) { - drawNode(node); - } + ImGui::PopID(); + }); } diff --git a/editor/panels/scene_hierarchy_panel.hpp b/editor/panels/scene_hierarchy_panel.hpp index 2833b32..c732068 100644 --- a/editor/panels/scene_hierarchy_panel.hpp +++ b/editor/panels/scene_hierarchy_panel.hpp @@ -1,32 +1,20 @@ #pragma once +#include #include +#include #include -#include -#include class SceneHierarchyPanel : public sophia::Panel { public: - SceneHierarchyPanel() : Panel{{.name = "Scene Hierarchy", .locked = true}} {} + SceneHierarchyPanel(sophia::Scene& scene, sophia::Entity& selected) + : Panel{{.name = "Scene Hierarchy", .locked = true}}, + scene{scene}, + selected{selected} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; private: - struct Node { - std::string name; - std::vector children; - }; - - void drawNode(const Node& node); - - std::vector nodes = { - {"Main Camera", {}}, - {"Directional Light", {}}, - {"Ground", {}}, - {"Player", {}}, - {"Enemies", {{"Enemy_01", {}}, {"Enemy_02", {}}, {"Enemy_03", {}}}}, - {"Props", {{"Crate_01", {}}, {"Crate_02", {}}}}, - }; - - std::string selected; + sophia::Scene& scene; + sophia::Entity& selected; }; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index 9b39b76..eae6ebd 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -47,6 +47,27 @@ struct MeshComponent { MeshHandle handle; }; +/** + * A textured quad in the XY plane, placed by the entity's TransformComponent. + * Only rotation around Z is meaningful. + */ +struct SpriteComponent { + // Invalid draws a solid quad in `color`. + TextureHandle texture = INVALID_HANDLE; + glm::vec4 color{1.f, 1.f, 1.f, 1.f}; + + // Size in world units, before the transform's scale. + glm::vec2 size{1.f, 1.f}; + // Point on the sprite, from (0, 0) to (1, 1) across its size, that sits at + // the transform's position and that it rotates around. + glm::vec2 pivot{0.5f, 0.5f}; + // Region of the texture to draw, as normalized (u0, v0, u1, v1). + glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f}; + + // Higher draws on top. Ties are broken by the transform's position.z. + s32 sortOrder = 0; +}; + struct CameraComponent { struct Orthographic { float height = 10.f; diff --git a/engine/include/sophia/entity.hpp b/engine/include/sophia/entity.hpp index 0c3950d..971d49f 100644 --- a/engine/include/sophia/entity.hpp +++ b/engine/include/sophia/entity.hpp @@ -59,4 +59,11 @@ class Entity { Scene* scene = nullptr; }; +template +void Scene::forEachEntity(Fn&& fn) { + for (auto [handle] : registry.storage().each()) { + fn(Entity{handle, this}); + } +} + } // namespace sophia diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index 298b51b..7855be0 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -38,11 +38,13 @@ class Panel; class RenderTarget; class Scene; struct SceneView; +class Texture; using MeshHandle = Handle; using PanelHandle = Handle; using RenderTargetHandle = Handle; using SceneHandle = Handle; using SceneViewHandle = Handle; +using TextureHandle = Handle; } // namespace sophia diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp index 9ea4200..241593d 100644 --- a/engine/include/sophia/resource_registry_ref.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -26,8 +26,10 @@ class ResourceRegistryRef { void requestResize(RenderTargetHandle handle, u32 width, u32 height); glm::vec2 getExtent(RenderTargetHandle handle) const; + glm::vec2 getExtent(TextureHandle handle) const; bool isAlive(RenderTargetHandle handle) const; + bool isAlive(TextureHandle handle) const; template void release(Handle handle); @@ -45,16 +47,25 @@ class ResourceRegistryRef { RenderTargetHandle createResource(std::type_identity, u32 width = 1, u32 height = 1); + TextureHandle createResource(std::type_identity, u32 width, + u32 height, std::vector pixels); + ResourceRegistry* registry; }; template <> MeshHandle ResourceRegistryRef::load(const std::string& path); +template <> +TextureHandle ResourceRegistryRef::load(const std::string& path); + template <> void ResourceRegistryRef::release(MeshHandle handle); template <> void ResourceRegistryRef::release(RenderTargetHandle handle); +template <> +void ResourceRegistryRef::release(TextureHandle handle); + } // namespace sophia diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp index 687b50a..2e4af2c 100644 --- a/engine/include/sophia/scene.hpp +++ b/engine/include/sophia/scene.hpp @@ -13,9 +13,14 @@ class Scene { Scene(); ~Scene(); - void createEntity(const std::string& name = {}); + // Every entity gets a TagComponent (named `name`) and a TransformComponent. + Entity createEntity(const std::string& name = {}); void destroyEntity(Entity entity); + /** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */ + template + void forEachEntity(Fn&& fn); + // The camera a render request uses when it doesn't name one. Null until // set. entt::entity getActiveCamera() const { return activeCamera; } diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 84ae53e..dadc490 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -65,7 +65,8 @@ class Application::Impl { void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; SceneRenderer sceneRenderer{this->application.sceneRegistry, - this->resourceRegistry}; + this->resourceRegistry, device, + renderContext.getImageFormat()}; ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), renderContext.getImageCount(), this->resourceRegistry}; @@ -128,6 +129,7 @@ class Application::Impl { } imguiRenderer.beginFrame(); + sceneRenderer.beginFrame(renderContext.getFrameIndex()); panelRegistry.update(frameInfo); this->application.onRender(); @@ -170,10 +172,11 @@ class Application::Impl { : vk::AttachmentLoadOp::eLoad, {color.r, color.g, color.b, color.a}); }, - [&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) { - const SceneView& view = sceneViewRegistry.get(handle); + [&sceneRenderer, &sceneViewRegistryConst, + handle](vk::CommandBuffer commandBuffer) { + const SceneView& view = sceneViewRegistryConst.get(handle); if (view.camera) { - sceneRenderer.render(view); + sceneRenderer.render(commandBuffer, view); } }); } diff --git a/engine/src/rendering/render_context.hpp b/engine/src/rendering/render_context.hpp index 25001e1..d5667d1 100644 --- a/engine/src/rendering/render_context.hpp +++ b/engine/src/rendering/render_context.hpp @@ -39,6 +39,7 @@ class RenderContext { vk::Format getImageFormat() const { return this->swapchain->getImageFormat(); } + u32 getFrameIndex() const { return this->currentFrameIndex; } u32 getImageCount() const { return static_cast(this->swapchain->imageCount()); } diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp index fc1e8be..71d6ee6 100644 --- a/engine/src/rendering/scene_renderer.cpp +++ b/engine/src/rendering/scene_renderer.cpp @@ -1,74 +1,102 @@ #include "rendering/scene_renderer.hpp" +#include #include #include +#include namespace sophia { -namespace { -struct PushConstantData { - glm::mat4 transform{1.f}; - glm::vec4 color{1.f}; -}; -} // namespace - SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry, - ResourceRegistry& resources) - : sceneRegistry{sceneRegistry}, resources{resources} {} + ResourceRegistry& resources, Device& device, + vk::Format colorFormat) + : sceneRegistry{sceneRegistry}, + resources{resources}, + spriteRenderSystem{device, colorFormat, resources} {} SceneRenderer::~SceneRenderer() {} -void SceneRenderer::render(SceneView sceneView) { - auto& handle = sceneView.scene; - auto& scene = this->sceneRegistry.get(handle); +void SceneRenderer::beginFrame(u32 frameIndex) { + this->spriteRenderSystem.beginFrame(frameIndex); +} - auto& camera = sceneView.camera; - SOPHIA_ASSERT(camera.hasComponent(), +void SceneRenderer::render(vk::CommandBuffer commandBuffer, + const SceneView& sceneView) { + const Scene& scene = this->sceneRegistry.get(sceneView.scene); + const entt::registry& registry = scene.registry; + + entt::entity camera = sceneView.camera; + SOPHIA_ASSERT(registry.all_of(camera), "camera does not have camera component!"); - auto& registry = scene.registry; + vk::Extent2D extent = + this->resources.renderTargetPool.get(sceneView.target).getExtent(); + float aspect = + static_cast(extent.width) / static_cast(extent.height); - auto view = registry.view(); + // 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(camera)) { + cameraWorld = glm::translate(glm::mat4{1.f}, transform->position) * + glm::mat4_cast(transform->rotation); + } + glm::mat4 viewProjection = + registry.get(camera).getProjection(aspect) * + glm::inverse(cameraWorld); - for (auto entity : view) { - auto& transform = view.get(entity); - log::info(transform); + renderSprites(commandBuffer, scene, viewProjection); + + // TODO: meshes. Group MeshComponent entities by mesh and draw each group + // (instanced), as sprites are grouped by texture. +} + +void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer, + const Scene& scene, + const glm::mat4& viewProjection) { + std::vector& items = this->spriteDrawItems; + items.clear(); + + auto view = + scene.registry.view(); + for (auto [entity, transform, sprite] : view.each()) { + TextureHandle texture = this->resources.texturePool.isAlive(sprite.texture) + ? sprite.texture + : this->resources.whiteTexture; + + // Unit quad -> sprite rectangle around its pivot -> world. + glm::mat4 model = + 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, + texture, + {model, sprite.color, sprite.uvRect}}); } - // Get all mesh - // For each mesh, get all entities(ES) that have a transform and that mesh - // For e in ES: add e's transform component to a list + // Texture is the last key so that sprites whose order doesn't matter + // relative to each other still end up in as few batches as possible. + std::ranges::sort(items, {}, [](const SpriteDrawItem& item) { + return std::tuple{item.sortOrder, item.depth, item.texture}; + }); - // We now have - /* - M = mesh - T = transform + this->spriteInstances.clear(); + this->spriteBatches.clear(); + for (const SpriteDrawItem& item : items) { + auto index = static_cast(this->spriteInstances.size()); + this->spriteInstances.push_back(item.instance); - Work Batch - |- M0 - | |- T0 - | \- T2 - | - |- M1 - | |- T1 - | \- T5 - | - \- M2 - |- T3 - \- T4 + if (this->spriteBatches.empty() || + this->spriteBatches.back().texture != item.texture) { + this->spriteBatches.push_back({item.texture, index, 0}); + } + this->spriteBatches.back().instanceCount++; + } - */ - - /* - for batch in Work Batch: - batch.mesh.bind() - - for transform in batch.transforms: - commandBuffer.pushConstants(transform) - batch.mesh.draw(commandBuffer) - - NOTE: Use instanced rendering? - */ + this->spriteRenderSystem.render(commandBuffer, viewProjection, + this->spriteInstances, this->spriteBatches); } } // namespace sophia diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp index abaf69b..e7c50a4 100644 --- a/engine/src/rendering/scene_renderer.hpp +++ b/engine/src/rendering/scene_renderer.hpp @@ -2,28 +2,54 @@ #include #include +#include +#include "rendering/sprite_render_system.hpp" #include "resources/resource_registry.hpp" namespace sophia { /** * !TEMP to be decoupled once scene batching and rendering impl + * + * Turns a scene's components into draw calls for a SceneView. */ class SceneRenderer { public: SceneRenderer(const SceneRenderer&) = delete; SceneRenderer& operator=(const SceneRenderer&) = delete; - SceneRenderer(const SceneRegistry& sceneRegistry, - ResourceRegistry& resources); + SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources, + Device& device, vk::Format colorFormat); ~SceneRenderer(); - void render(SceneView sceneView); + // Call once per frame, before any render(). + void beginFrame(u32 frameIndex); + + // Records `sceneView` into `commandBuffer`. Rendering into the view's + // target must already have begun. + void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView); private: + struct SpriteDrawItem { + s32 sortOrder; + float depth; + TextureHandle texture; + SpriteRenderSystem::Instance instance; + }; + + void renderSprites(vk::CommandBuffer commandBuffer, const Scene& scene, + const glm::mat4& viewProjection); + const SceneRegistry& sceneRegistry; ResourceRegistry& resources; + + SpriteRenderSystem spriteRenderSystem; + + // Reused across frames to avoid reallocating. + std::vector spriteDrawItems; + std::vector spriteInstances; + std::vector spriteBatches; }; } // namespace sophia diff --git a/engine/src/rendering/sprite_render_system.cpp b/engine/src/rendering/sprite_render_system.cpp new file mode 100644 index 0000000..98fca65 --- /dev/null +++ b/engine/src/rendering/sprite_render_system.cpp @@ -0,0 +1,194 @@ +#include "rendering/sprite_render_system.hpp" + +#include +#include + +#include "shaders/sprite.frag.spv.h" +#include "shaders/sprite.vert.spv.h" +#include "vulkan/descriptors/descriptor_writer.hpp" + +namespace sophia { + +namespace { +constexpr u32 MAX_TEXTURES = 1024; +constexpr u32 VERTICES_PER_SPRITE = 6; +} // namespace + +SpriteRenderSystem::SpriteRenderSystem(Device& device, vk::Format colorFormat, + ResourceRegistry& resources) + : device{device}, resources{resources} { + createDescriptors(); + createPipelineLayout(); + createPipeline(colorFormat); + createInstanceBuffers(); +} + +SpriteRenderSystem::~SpriteRenderSystem() { + log::trace("destroyed sophia::SpriteRenderSystem"); +} + +void SpriteRenderSystem::beginFrame(u32 frameIndex) { + this->frameIndex = frameIndex; + this->instanceCursor = 0; +} + +void SpriteRenderSystem::render(vk::CommandBuffer commandBuffer, + const glm::mat4& viewProjection, + std::span instances, + std::span batches) { + if (instances.empty()) { + return; + } + + u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor; + if (instances.size() > available) { + log::warning("SpriteRenderSystem: frame instance buffer full, dropping", + instances.size() - available, "sprites"); + } + + Buffer& buffer = *this->instanceBuffers[this->frameIndex]; + u32 base = this->instanceCursor; + u32 count = std::min(static_cast(instances.size()), available); + buffer.writeToBuffer(const_cast(instances.data()), + count * sizeof(Instance), base * sizeof(Instance)); + this->instanceCursor += count; + + this->graphicsPipeline->bind(commandBuffer); + + PushConstantData push{viewProjection}; + commandBuffer.pushConstants(this->pipelineLayout.get(), + vk::ShaderStageFlagBits::eVertex, 0, + sizeof(PushConstantData), &push); + + vk::Buffer vertexBuffer = buffer.getBuffer(); + vk::DeviceSize offset = 0; + commandBuffer.bindVertexBuffers(0, 1, &vertexBuffer, &offset); + + for (const Batch& batch : batches) { + if (batch.firstInstance >= count) { + break; + } + u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance); + + vk::DescriptorSet set = getDescriptorSet(batch.texture); + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + this->pipelineLayout.get(), 0, 1, &set, 0, + nullptr); + commandBuffer.draw(VERTICES_PER_SPRITE, batchCount, 0, + base + batch.firstInstance); + } +} + +vk::DescriptorSet SpriteRenderSystem::getDescriptorSet(TextureHandle handle) { + auto it = this->descriptorSets.find(handle.index); + if (it != this->descriptorSets.end() && + it->second.generation == handle.generation) { + return it->second.set; + } + + const Texture& texture = this->resources.texturePool.get(handle); + vk::DescriptorImageInfo imageInfo = texture.descriptorInfo(); + DescriptorWriter writer{*this->setLayout, *this->descriptorPool}; + writer.writeImage(0, &imageInfo); + + if (it == this->descriptorSets.end()) { + vk::DescriptorSet set; + bool allocated = writer.build(set); + SOPHIA_ASSERT(allocated, "SpriteRenderSystem: out of texture descriptors"); + this->descriptorSets.emplace(static_cast(handle.index), + CachedDescriptorSet{handle.generation, set}); + return set; + } + + // The slot was reused by a new texture. Releasing a texture waits for the + // device to go idle, so no in-flight frame can still be using this set. + writer.overwrite(it->second.set); + it->second.generation = handle.generation; + return it->second.set; +} + +void SpriteRenderSystem::createDescriptors() { + this->setLayout = + DescriptorSetLayout::Builder(this->device) + .addBinding(0, vk::DescriptorType::eCombinedImageSampler, + vk::ShaderStageFlagBits::eFragment) + .build(); + + this->descriptorPool = + DescriptorPool::Builder(this->device) + .setMaxSets(MAX_TEXTURES) + .addPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_TEXTURES) + .build(); +} + +void SpriteRenderSystem::createPipelineLayout() { + vk::PushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(PushConstantData); + + vk::DescriptorSetLayout setLayout = this->setLayout->getDescriptorSetLayout(); + + vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {}; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &setLayout; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + try { + this->pipelineLayout = + this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo); + log::trace("created vk::PipelineLayout"); + } catch (const vk::SystemError& err) { + log::fatal("failed to create vk::PipelineLayout"); + throw std::runtime_error("failed to create vk::PipelineLayout"); + } +} + +void SpriteRenderSystem::createPipeline(vk::Format colorFormat) { + auto vertexShader = Shader::Builder() + .fromSPIRV(shaders::sprite_vert_spv) + .setStage(ShaderStage::VERTEX) + .build(); + auto fragmentShader = Shader::Builder() + .fromSPIRV(shaders::sprite_frag_spv) + .setStage(ShaderStage::FRAGMENT) + .build(); + + std::vector bindings{ + {0, sizeof(Instance), vk::VertexInputRate::eInstance}}; + + std::vector attributes; + // A mat4 attribute takes four consecutive locations, one per column. + for (u32 column = 0; column < 4; column++) { + attributes.push_back({column, 0, vk::Format::eR32G32B32A32Sfloat, + static_cast(offsetof(Instance, model) + + column * sizeof(glm::vec4))}); + } + attributes.push_back({4, 0, vk::Format::eR32G32B32A32Sfloat, + static_cast(offsetof(Instance, color))}); + attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat, + static_cast(offsetof(Instance, uvRect))}); + + this->graphicsPipeline = GraphicsPipeline::Builder(this->device) + .setVertexShader(*vertexShader) + .setFragmentShader(*fragmentShader) + .setPipelineLayout(this->pipelineLayout.get()) + .setColorAttachmentFormats({colorFormat}) + .setVertexInput(bindings, attributes) + .setAlphaBlending() + .build(); +} + +void SpriteRenderSystem::createInstanceBuffers() { + for (std::unique_ptr& buffer : this->instanceBuffers) { + buffer = std::make_unique( + this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent); + buffer->map(); + } +} + +} // namespace sophia diff --git a/engine/src/rendering/sprite_render_system.hpp b/engine/src/rendering/sprite_render_system.hpp new file mode 100644 index 0000000..d3109f0 --- /dev/null +++ b/engine/src/rendering/sprite_render_system.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "rendering/render_context.hpp" +#include "resources/resource_registry.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/descriptors/descriptor_pool.hpp" +#include "vulkan/descriptors/descriptor_set_layout.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" + +namespace sophia { + +/** + * Draws batches of textured quads. Knows nothing about scenes: callers hand + * it instances already in draw order, grouped into runs that share a texture. + * + * Instances are written to a per-frame host-visible buffer, so every + * render() call within one frame appends to it; call beginFrame() once per + * frame before any render(). + */ +class SpriteRenderSystem { + public: + // Layout must match the per-instance inputs in sprite.vert. + struct Instance { + glm::mat4 model{1.f}; + glm::vec4 color{1.f}; + glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f}; + }; + + // A run of consecutive instances drawn with the same texture. + struct Batch { + TextureHandle texture; + u32 firstInstance; + u32 instanceCount; + }; + + static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768; + + SpriteRenderSystem(const SpriteRenderSystem&) = delete; + SpriteRenderSystem& operator=(const SpriteRenderSystem&) = delete; + + SpriteRenderSystem(Device& device, vk::Format colorFormat, + ResourceRegistry& resources); + ~SpriteRenderSystem(); + + void beginFrame(u32 frameIndex); + + void render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection, + std::span instances, + std::span batches); + + private: + struct PushConstantData { + glm::mat4 viewProjection{1.f}; + }; + + struct CachedDescriptorSet { + u32 generation; + vk::DescriptorSet set; + }; + + void createDescriptors(); + void createPipelineLayout(); + void createPipeline(vk::Format colorFormat); + void createInstanceBuffers(); + + vk::DescriptorSet getDescriptorSet(TextureHandle handle); + + Device& device; + ResourceRegistry& resources; + + std::unique_ptr setLayout; + std::unique_ptr descriptorPool; + // One set per texture slot, keyed by handle index. A generation mismatch + // means the slot was reused by a new texture, so the set is rewritten. + std::unordered_map descriptorSets; + + vk::UniquePipelineLayout pipelineLayout; + std::unique_ptr graphicsPipeline; + + std::array, RenderContext::MAX_FRAMES_IN_FLIGHT> + instanceBuffers; + u32 frameIndex = 0; + u32 instanceCursor = 0; +}; + +} // namespace sophia diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp index 3912cf7..2cbca9a 100644 --- a/engine/src/resources/resource_registry.hpp +++ b/engine/src/resources/resource_registry.hpp @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include "resources/mesh.hpp" #include "resources/pools/render_target_pool.hpp" +#include "resources/texture.hpp" #include "vulkan/device.hpp" namespace sophia { @@ -15,13 +17,20 @@ class ResourceRegistry { ResourceRegistry& operator=(const ResourceRegistry&) = delete; ResourceRegistry(Device& device, vk::Format renderTargetFormat) - : device{device}, renderTargetPool{device, renderTargetFormat} {} + : device{device}, renderTargetPool{device, renderTargetFormat} { + constexpr std::array WHITE{0xFF, 0xFF, 0xFF, 0xFF}; + whiteTexture = texturePool.create(device, 1, 1, WHITE); + } Device& device; Pool meshPool{"Mesh"}; RenderTargetPool renderTargetPool; - // TexturePool textures; + Pool texturePool{"Texture"}; // MaterialPool materials; + + // 1x1 white. Stands in for sprites without a texture, so they draw as a + // solid quad in their tint color. + TextureHandle whiteTexture = INVALID_HANDLE; }; } // namespace sophia diff --git a/engine/src/resources/resource_registry_ref.cpp b/engine/src/resources/resource_registry_ref.cpp index 6d963a8..346f7c8 100644 --- a/engine/src/resources/resource_registry_ref.cpp +++ b/engine/src/resources/resource_registry_ref.cpp @@ -1,5 +1,9 @@ +#include + #include #include +#include +#include #include "resources/resource_registry.hpp" @@ -14,6 +18,26 @@ MeshHandle ResourceRegistryRef::load(const std::string& path) { return MeshHandle{}; } +template <> +TextureHandle ResourceRegistryRef::load(const std::string& path) { + int width = 0; + int height = 0; + int channels = 0; + // Always decode to RGBA8, whatever the file has. + stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, 4); + if (pixels == nullptr) { + log::fatal("failed to load texture", path, "-", stbi_failure_reason()); + throw std::runtime_error("failed to load texture: " + path); + } + + auto size = static_cast(width) * static_cast(height) * 4; + TextureHandle handle = registry->texturePool.create( + registry->device, static_cast(width), static_cast(height), + std::span{pixels, size}); + stbi_image_free(pixels); + return handle; +} + template <> void ResourceRegistryRef::release(MeshHandle handle) { registry->meshPool.release(handle); @@ -24,6 +48,15 @@ void ResourceRegistryRef::release(RenderTargetHandle handle) { registry->renderTargetPool.release(handle); } +template <> +void ResourceRegistryRef::release(TextureHandle handle) { + SOPHIA_ASSERT(handle != registry->whiteTexture, + "the built-in white texture cannot be released"); + // Frames in flight may still be sampling it. + registry->device.waitIdle(); + registry->texturePool.release(handle); +} + MeshHandle ResourceRegistryRef::createResource( std::type_identity, std::vector vertices, std::vector indices) { @@ -42,6 +75,13 @@ RenderTargetHandle ResourceRegistryRef::createResource( return registry->renderTargetPool.create(width, height); } +TextureHandle ResourceRegistryRef::createResource(std::type_identity, + u32 width, u32 height, + std::vector pixels) { + return registry->texturePool.create(registry->device, width, height, + std::span{pixels}); +} + void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width, u32 height) { registry->renderTargetPool.get(handle).requestResize(width, height); @@ -56,4 +96,14 @@ bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const { return registry->renderTargetPool.isAlive(handle); } +glm::vec2 ResourceRegistryRef::getExtent(TextureHandle handle) const { + const Texture& texture = registry->texturePool.get(handle); + return {static_cast(texture.getWidth()), + static_cast(texture.getHeight())}; +} + +bool ResourceRegistryRef::isAlive(TextureHandle handle) const { + return registry->texturePool.isAlive(handle); +} + } // namespace sophia diff --git a/engine/src/resources/texture.cpp b/engine/src/resources/texture.cpp new file mode 100644 index 0000000..0da52c1 --- /dev/null +++ b/engine/src/resources/texture.cpp @@ -0,0 +1,32 @@ +#include "resources/texture.hpp" + +#include +#include + +namespace sophia { + +Texture::Texture(Device& device, u32 width, u32 height, + std::span pixels) { + SOPHIA_ASSERT(width > 0 && height > 0, "texture must not be empty"); + SOPHIA_ASSERT(pixels.size() == static_cast(width) * height * 4, + "texture pixel data must be width * height RGBA8 texels"); + + this->image = Image::Builder(device) + .setExtent(width, height) + .setFormat(vk::Format::eR8G8B8A8Srgb) + .setUsage(vk::ImageUsageFlagBits::eSampled) + .setInitialData(pixels.data(), pixels.size()) + .withView() + .withSampler(vk::Filter::eLinear, + vk::SamplerAddressMode::eClampToEdge) + .build(); +} + +Texture::~Texture() { log::trace("destroyed sophia::Texture"); } + +vk::DescriptorImageInfo Texture::descriptorInfo() const { + return {this->image->getSampler(), this->image->getView(), + vk::ImageLayout::eShaderReadOnlyOptimal}; +} + +} // namespace sophia diff --git a/engine/src/resources/texture.hpp b/engine/src/resources/texture.hpp new file mode 100644 index 0000000..d3f1873 --- /dev/null +++ b/engine/src/resources/texture.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +#include "vulkan/device.hpp" +#include "vulkan/image.hpp" + +namespace sophia { + +/** + * A sampled, read-only RGBA8 (sRGB) image. + * + * Owned by the ResourceRegistry and addressed through a TextureHandle. + */ +class Texture { + public: + /** `pixels` is width * height tightly packed RGBA8 texels, top row first. */ + Texture(Device& device, u32 width, u32 height, std::span pixels); + ~Texture(); + + Texture(const Texture&) = delete; + Texture& operator=(const Texture&) = delete; + + const Image& getImage() const { return *this->image; } + + u32 getWidth() const { return this->image->getExtent().width; } + u32 getHeight() const { return this->image->getExtent().height; } + + vk::DescriptorImageInfo descriptorInfo() const; + + private: + std::unique_ptr image; +}; + +} // namespace sophia diff --git a/engine/src/scene/scene.cpp b/engine/src/scene/scene.cpp index 375424b..1dfb9db 100644 --- a/engine/src/scene/scene.cpp +++ b/engine/src/scene/scene.cpp @@ -1,16 +1,26 @@ #include +#include #include #include namespace sophia { -Scene::Scene() { - entt::entity entity = this->registry.create(); - - auto& a = this->registry.emplace(entity); - auto& b = this->registry.get(entity); -} +Scene::Scene() {} Scene::~Scene() { log::trace("destroying sophia::Scene"); } +Entity Scene::createEntity(const std::string& name) { + Entity entity{this->registry.create(), this}; + entity.addComponent(name.empty() ? "Entity" : name); + entity.addComponent(); + return entity; +} + +void Scene::destroyEntity(Entity entity) { + if (this->activeCamera == entity) { + this->activeCamera = entt::null; + } + this->registry.destroy(entity); +} + } // namespace sophia diff --git a/engine/src/shaders/sprite.frag b/engine/src/shaders/sprite.frag new file mode 100644 index 0000000..4f0972a --- /dev/null +++ b/engine/src/shaders/sprite.frag @@ -0,0 +1,12 @@ +#version 450 + +layout (location = 0) in vec4 fragColor; +layout (location = 1) in vec2 fragUv; + +layout (location = 0) out vec4 outColor; + +layout (set = 0, binding = 0) uniform sampler2D spriteTexture; + +void main() { + outColor = texture(spriteTexture, fragUv) * fragColor; +} diff --git a/engine/src/shaders/sprite.frag.spv.h b/engine/src/shaders/sprite.frag.spv.h new file mode 100644 index 0000000..be6a06c --- /dev/null +++ b/engine/src/shaders/sprite.frag.spv.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +namespace sophia::shaders { + +inline constexpr std::array sprite_frag_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000018, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x00000011, 0x00000015, + 0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, + 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, + 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, + 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, + 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, + 0x4374756f, 0x726f6c6f, 0x00000000, 0x00060005, 0x0000000d, 0x69727073, + 0x65546574, 0x72757478, 0x00000065, 0x00040005, 0x00000011, 0x67617266, + 0x00007655, 0x00050005, 0x00000015, 0x67617266, 0x6f6c6f43, 0x00000072, + 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d, + 0x00000021, 0x00000000, 0x00040047, 0x0000000d, 0x00000022, 0x00000000, + 0x00040047, 0x00000011, 0x0000001e, 0x00000001, 0x00040047, 0x00000015, + 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, + 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00090019, 0x0000000a, + 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, + 0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c, + 0x00000000, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000000, + 0x00040017, 0x0000000f, 0x00000006, 0x00000002, 0x00040020, 0x00000010, + 0x00000001, 0x0000000f, 0x0004003b, 0x00000010, 0x00000011, 0x00000001, + 0x00040020, 0x00000014, 0x00000001, 0x00000007, 0x0004003b, 0x00000014, + 0x00000015, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e, + 0x0000000d, 0x0004003d, 0x0000000f, 0x00000012, 0x00000011, 0x00050057, + 0x00000007, 0x00000013, 0x0000000e, 0x00000012, 0x0004003d, 0x00000007, + 0x00000016, 0x00000015, 0x00050085, 0x00000007, 0x00000017, 0x00000013, + 0x00000016, 0x0003003e, 0x00000009, 0x00000017, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/sprite.vert b/engine/src/shaders/sprite.vert new file mode 100644 index 0000000..d217501 --- /dev/null +++ b/engine/src/shaders/sprite.vert @@ -0,0 +1,31 @@ +#version 450 + +// Per instance; layout must match SpriteRenderSystem::Instance. +layout (location = 0) in mat4 model; // Maps the unit quad to world space. +layout (location = 4) in vec4 color; +layout (location = 5) in vec4 uvRect; + +layout (location = 0) out vec4 fragColor; +layout (location = 1) out vec2 fragUv; + +layout (push_constant) uniform Push { + mat4 viewProjection; +} push; + +// Two triangles covering the unit quad (0,0)-(1,1). +const vec2 CORNERS[6] = vec2[]( + vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), + vec2(1.0, 1.0), vec2(0.0, 1.0), vec2(0.0, 0.0) +); + +void main() { + vec2 corner = CORNERS[gl_VertexIndex]; + + gl_Position = push.viewProjection * model * vec4(corner, 0.0, 1.0); + // Draw order comes from the CPU-side sort, so flatten depth; sprites should + // never be clipped by the camera's near/far planes. + gl_Position.z = 0.0; + + fragColor = color; + fragUv = mix(uvRect.xy, uvRect.zw, corner); +} diff --git a/engine/src/shaders/sprite.vert.spv.h b/engine/src/shaders/sprite.vert.spv.h new file mode 100644 index 0000000..081edfa --- /dev/null +++ b/engine/src/shaders/sprite.vert.spv.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +namespace sophia::shaders { + +inline constexpr std::array sprite_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000045, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000c000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000016, 0x00000021, 0x0000002b, + 0x00000038, 0x0000003a, 0x0000003d, 0x0000003e, 0x00030003, 0x00000002, + 0x000001c2, 0x000a0004, 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, + 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, 0x00006576, 0x00080004, + 0x475f4c47, 0x4c474f4f, 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, + 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, + 0x00000009, 0x6e726f63, 0x00007265, 0x00060005, 0x00000016, 0x565f6c67, + 0x65747265, 0x646e4978, 0x00007865, 0x00050005, 0x00000019, 0x65646e69, + 0x6c626178, 0x00000065, 0x00060005, 0x0000001f, 0x505f6c67, 0x65567265, + 0x78657472, 0x00000000, 0x00060006, 0x0000001f, 0x00000000, 0x505f6c67, + 0x7469736f, 0x006e6f69, 0x00070006, 0x0000001f, 0x00000001, 0x505f6c67, + 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000001f, 0x00000002, + 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000001f, + 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, + 0x00000021, 0x00000000, 0x00040005, 0x00000024, 0x68737550, 0x00000000, + 0x00070006, 0x00000024, 0x00000000, 0x77656976, 0x6a6f7250, 0x69746365, + 0x00006e6f, 0x00040005, 0x00000026, 0x68737570, 0x00000000, 0x00040005, + 0x0000002b, 0x65646f6d, 0x0000006c, 0x00050005, 0x00000038, 0x67617266, + 0x6f6c6f43, 0x00000072, 0x00040005, 0x0000003a, 0x6f6c6f63, 0x00000072, + 0x00040005, 0x0000003d, 0x67617266, 0x00007655, 0x00040005, 0x0000003e, + 0x65527675, 0x00007463, 0x00040047, 0x00000016, 0x0000000b, 0x0000002a, + 0x00030047, 0x0000001f, 0x00000002, 0x00050048, 0x0000001f, 0x00000000, + 0x0000000b, 0x00000000, 0x00050048, 0x0000001f, 0x00000001, 0x0000000b, + 0x00000001, 0x00050048, 0x0000001f, 0x00000002, 0x0000000b, 0x00000003, + 0x00050048, 0x0000001f, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, + 0x00000024, 0x00000002, 0x00040048, 0x00000024, 0x00000000, 0x00000005, + 0x00050048, 0x00000024, 0x00000000, 0x00000007, 0x00000010, 0x00050048, + 0x00000024, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000002b, + 0x0000001e, 0x00000000, 0x00040047, 0x00000038, 0x0000001e, 0x00000000, + 0x00040047, 0x0000003a, 0x0000001e, 0x00000004, 0x00040047, 0x0000003d, + 0x0000001e, 0x00000001, 0x00040047, 0x0000003e, 0x0000001e, 0x00000005, + 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000002, + 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x0000000a, + 0x00000020, 0x00000000, 0x0004002b, 0x0000000a, 0x0000000b, 0x00000006, + 0x0004001c, 0x0000000c, 0x00000007, 0x0000000b, 0x0004002b, 0x00000006, + 0x0000000d, 0x00000000, 0x0005002c, 0x00000007, 0x0000000e, 0x0000000d, + 0x0000000d, 0x0004002b, 0x00000006, 0x0000000f, 0x3f800000, 0x0005002c, + 0x00000007, 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, 0x00000007, + 0x00000011, 0x0000000f, 0x0000000f, 0x0005002c, 0x00000007, 0x00000012, + 0x0000000d, 0x0000000f, 0x0009002c, 0x0000000c, 0x00000013, 0x0000000e, + 0x00000010, 0x00000011, 0x00000011, 0x00000012, 0x0000000e, 0x00040015, + 0x00000014, 0x00000020, 0x00000001, 0x00040020, 0x00000015, 0x00000001, + 0x00000014, 0x0004003b, 0x00000015, 0x00000016, 0x00000001, 0x00040020, + 0x00000018, 0x00000007, 0x0000000c, 0x00040017, 0x0000001c, 0x00000006, + 0x00000004, 0x0004002b, 0x0000000a, 0x0000001d, 0x00000001, 0x0004001c, + 0x0000001e, 0x00000006, 0x0000001d, 0x0006001e, 0x0000001f, 0x0000001c, + 0x00000006, 0x0000001e, 0x0000001e, 0x00040020, 0x00000020, 0x00000003, + 0x0000001f, 0x0004003b, 0x00000020, 0x00000021, 0x00000003, 0x0004002b, + 0x00000014, 0x00000022, 0x00000000, 0x00040018, 0x00000023, 0x0000001c, + 0x00000004, 0x0003001e, 0x00000024, 0x00000023, 0x00040020, 0x00000025, + 0x00000009, 0x00000024, 0x0004003b, 0x00000025, 0x00000026, 0x00000009, + 0x00040020, 0x00000027, 0x00000009, 0x00000023, 0x00040020, 0x0000002a, + 0x00000001, 0x00000023, 0x0004003b, 0x0000002a, 0x0000002b, 0x00000001, + 0x00040020, 0x00000033, 0x00000003, 0x0000001c, 0x0004002b, 0x0000000a, + 0x00000035, 0x00000002, 0x00040020, 0x00000036, 0x00000003, 0x00000006, + 0x0004003b, 0x00000033, 0x00000038, 0x00000003, 0x00040020, 0x00000039, + 0x00000001, 0x0000001c, 0x0004003b, 0x00000039, 0x0000003a, 0x00000001, + 0x00040020, 0x0000003c, 0x00000003, 0x00000007, 0x0004003b, 0x0000003c, + 0x0000003d, 0x00000003, 0x0004003b, 0x00000039, 0x0000003e, 0x00000001, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, + 0x00000005, 0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003b, + 0x00000018, 0x00000019, 0x00000007, 0x0004003d, 0x00000014, 0x00000017, + 0x00000016, 0x0003003e, 0x00000019, 0x00000013, 0x00050041, 0x00000008, + 0x0000001a, 0x00000019, 0x00000017, 0x0004003d, 0x00000007, 0x0000001b, + 0x0000001a, 0x0003003e, 0x00000009, 0x0000001b, 0x00050041, 0x00000027, + 0x00000028, 0x00000026, 0x00000022, 0x0004003d, 0x00000023, 0x00000029, + 0x00000028, 0x0004003d, 0x00000023, 0x0000002c, 0x0000002b, 0x00050092, + 0x00000023, 0x0000002d, 0x00000029, 0x0000002c, 0x0004003d, 0x00000007, + 0x0000002e, 0x00000009, 0x00050051, 0x00000006, 0x0000002f, 0x0000002e, + 0x00000000, 0x00050051, 0x00000006, 0x00000030, 0x0000002e, 0x00000001, + 0x00070050, 0x0000001c, 0x00000031, 0x0000002f, 0x00000030, 0x0000000d, + 0x0000000f, 0x00050091, 0x0000001c, 0x00000032, 0x0000002d, 0x00000031, + 0x00050041, 0x00000033, 0x00000034, 0x00000021, 0x00000022, 0x0003003e, + 0x00000034, 0x00000032, 0x00060041, 0x00000036, 0x00000037, 0x00000021, + 0x00000022, 0x00000035, 0x0003003e, 0x00000037, 0x0000000d, 0x0004003d, + 0x0000001c, 0x0000003b, 0x0000003a, 0x0003003e, 0x00000038, 0x0000003b, + 0x0004003d, 0x0000001c, 0x0000003f, 0x0000003e, 0x0007004f, 0x00000007, + 0x00000040, 0x0000003f, 0x0000003f, 0x00000000, 0x00000001, 0x0004003d, + 0x0000001c, 0x00000041, 0x0000003e, 0x0007004f, 0x00000007, 0x00000042, + 0x00000041, 0x00000041, 0x00000002, 0x00000003, 0x0004003d, 0x00000007, + 0x00000043, 0x00000009, 0x0008000c, 0x00000007, 0x00000044, 0x00000001, + 0x0000002e, 0x00000040, 0x00000042, 0x00000043, 0x0003003e, 0x0000003d, + 0x00000044, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index 8dcaa57..a3d9536 100644 --- a/examples/boids_2d/main.cpp +++ b/examples/boids_2d/main.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -7,8 +9,12 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -16,6 +22,24 @@ struct Boid { glm::vec2 position; glm::vec2 velocity; + sophia::Entity entity; +}; + +// Shows a render target filling the whole panel. +class ViewportPanel : public sophia::Panel { + public: + ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target) + : Panel{{.name = "Boids", + .windowFlags = ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse}}, + ui{ui}, + target{target} {} + + void onUpdate(const sophia::FrameInfo&) override { ui.image(target); } + + private: + sophia::UI& ui; + sophia::RenderTargetHandle target; }; constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25; @@ -81,28 +105,53 @@ class Boids2D : public sophia::Application { boid.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED; } - // Boid shape in local space, nose pointing along +x; each boid is drawn - // as an instance of this one mesh with a per-boid transform. - std::vector vertices{ - {{BOID_LENGTH * 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}}, - {{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, - {{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, - }; - boidMesh = resources().create(std::move(vertices)); + boidTexture = createBoidTexture(); + + sceneHandle = scenes().create(); + sophia::Scene& scene = scenes().get(sceneHandle); + + cameraEntity = scene.createEntity("Camera"); + cameraEntity.addComponent(); + + std::uniform_real_distribution tint{0.0f, 1.0f}; + for (Boid& boid : boids) { + boid.entity = scene.createEntity("Boid"); + boid.entity.addComponent(sophia::SpriteComponent{ + .texture = boidTexture, + .color = glm::vec4{glm::mix(glm::vec3{0.55f, 0.8f, 1.0f}, + glm::vec3{1.0f, 0.85f, 0.55f}, tint(rng)), + 1.0f}, + .size = {BOID_LENGTH, BOID_WIDTH}}); + } + + target = resources().create(); + views().create({.scene = sceneHandle, + .target = target, + .camera = cameraEntity, + .clearColor = {0.01f, 0.01f, 0.02f, 1.0f}}); + + panels().emplace(ui(), target); + panels().setDefaultLayout({.center = {"Boids"}}); } void onUpdate(const sophia::FrameInfo& frameInfo) override { float scrollDistance = SCROLL_SPEED * static_cast(frameInfo.deltaTime); - if (scrollLeft) camera.position.x -= scrollDistance; - if (scrollRight) camera.position.x += scrollDistance; - if (scrollUp) camera.position.y -= scrollDistance; - if (scrollDown) camera.position.y += scrollDistance; + glm::vec3& cameraPosition = + cameraEntity.getComponent().position; + if (scrollLeft) cameraPosition.x -= scrollDistance; + if (scrollRight) cameraPosition.x += scrollDistance; + if (scrollUp) cameraPosition.y -= scrollDistance; + if (scrollDown) cameraPosition.y += scrollDistance; + + // One world unit per pixel, whatever size the viewport is. + cameraEntity.getComponent().ortho.height = + resources().getExtent(target).y; updateBoids(static_cast(frameInfo.deltaTime)); - for (const Boid& boid : boids) { - drawBoid(boid); + for (Boid& boid : boids) { + syncTransform(boid); } } @@ -212,17 +261,40 @@ class Boids2D : public sophia::Application { if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT; } - void drawBoid(const Boid& boid) { + void syncTransform(Boid& boid) { glm::vec2 direction = boid.velocity; float speed = glm::length(direction); direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f}; float angle = std::atan2(direction.y, direction.x); - glm::mat4 transform = - glm::translate(glm::mat4{1.f}, glm::vec3(boid.position, 0.0f)) * - glm::rotate(glm::mat4{1.f}, angle, glm::vec3(0.0f, 0.0f, 1.0f)); + auto& transform = boid.entity.getComponent(); + transform.position = glm::vec3(boid.position, 0.0f); + transform.rotation = glm::angleAxis(angle, glm::vec3(0.0f, 0.0f, 1.0f)); + } - drawMesh(boidMesh, transform); + // Boid shape, nose pointing along +u, as an anti-aliased white triangle on + // a transparent background; each boid's sprite tints it. + sophia::TextureHandle createBoidTexture() { + constexpr uint32_t WIDTH = 64; + constexpr uint32_t HEIGHT = 40; + + std::vector pixels(WIDTH * HEIGHT * 4); + for (uint32_t y = 0; y < HEIGHT; ++y) { + for (uint32_t x = 0; x < WIDTH; ++x) { + float u = (static_cast(x) + 0.5f) / WIDTH; + float v = (static_cast(y) + 0.5f) / HEIGHT; + // Distance inside the triangle's edge, in pixels. + float inside = (0.5f * (1.0f - u) - std::abs(v - 0.5f)) * HEIGHT; + float alpha = std::clamp(inside + 0.5f, 0.0f, 1.0f); + + uint8_t* pixel = &pixels[(y * WIDTH + x) * 4]; + pixel[0] = pixel[1] = pixel[2] = 255; + pixel[3] = static_cast(alpha * 255.0f); + } + } + + return resources().create(WIDTH, HEIGHT, + std::move(pixels)); } void setScrollKeyHeld(sophia::KeyCode keycode, bool held) { @@ -235,7 +307,10 @@ class Boids2D : public sophia::Application { std::vector boids; std::vector> grid{ static_cast(GRID_WIDTH * GRID_HEIGHT)}; - sophia::MeshHandle boidMesh{}; + sophia::TextureHandle boidTexture{}; + sophia::SceneHandle sceneHandle{}; + sophia::RenderTargetHandle target{}; + sophia::Entity cameraEntity; bool scrollLeft = false; bool scrollRight = false; bool scrollUp = false;