Compare commits

...

4 Commits

Author SHA1 Message Date
burkkyy 571472a204 adding screenshot of using engine editor as a showcase 2026-09-25 23:09:56 -07:00
burkkyy a21a842b5b Initial claude dump 2026-09-25 22:59:01 -07:00
burkkyy 5644782a1f Adding in simple alpha blending 2026-09-25 22:34:25 -07:00
burkkyy b44fe94d5c image view example 2026-09-25 22:33:30 -07:00
33 changed files with 1214 additions and 161 deletions
+2
View File
@@ -1,5 +1,7 @@
# sophia # sophia
![showcase](./media/showcase.png)
## Getting Started ## Getting Started
COMING SOON COMING SOON
+58 -3
View File
@@ -1,5 +1,7 @@
#include <iostream> #include <iostream>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include "panels/asset_browser_panel.hpp" #include "panels/asset_browser_panel.hpp"
#include "panels/inspector_panel.hpp" #include "panels/inspector_panel.hpp"
@@ -15,12 +17,19 @@ class Editor : public sophia::Application {
debugUI().show(); debugUI().show();
panels().emplace<SceneHierarchyPanel>(); textures.emplace_back("Checker", createCheckerTexture());
panels().emplace<InspectorPanel>();
sophia::SceneHandle sceneHandle = scenes().create();
sophia::Scene& scene = scenes().get(sceneHandle);
sophia::Entity camera = createDemoScene(scene);
panels().emplace<SceneHierarchyPanel>(scene, selected);
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({.scene = scenes().create(), .target = sceneTarget}); views().create(
{.scene = sceneHandle, .target = sceneTarget, .camera = camera});
panels().emplace<ViewportPanel>(ui(), sceneTarget); panels().emplace<ViewportPanel>(ui(), sceneTarget);
panels().setDefaultLayout( panels().setDefaultLayout(
@@ -31,6 +40,52 @@ class Editor : public sophia::Application {
} }
void onUpdate(const sophia::FrameInfo& frameInfo) override {} void onUpdate(const sophia::FrameInfo& frameInfo) override {}
private:
sophia::Entity createDemoScene(sophia::Scene& scene) {
sophia::Entity camera = scene.createEntity("Main Camera");
camera.addComponent<sophia::CameraComponent>();
scene.setActiveCamera(camera);
sophia::Entity checker = scene.createEntity("Checker");
checker.getComponent<sophia::TransformComponent>().position = {-1.5f, 0.f,
0.f};
checker.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.texture = textures[0].second, .size = {2.f, 2.f}});
sophia::Entity red = scene.createEntity("Red");
red.getComponent<sophia::TransformComponent>().position = {1.f, -0.5f, 0.f};
red.addComponent<sophia::SpriteComponent>(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<sophia::TransformComponent>().position = {1.75f, 0.25f,
0.f};
blue.addComponent<sophia::SpriteComponent>(
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<sophia::u8> 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<sophia::Texture>(SIZE, SIZE, std::move(pixels));
}
sophia::Entity selected;
InspectorPanel::TextureList textures;
}; };
SOPHIA_MAIN(Editor) SOPHIA_MAIN(Editor)
+123 -18
View File
@@ -3,30 +3,135 @@
#include <imgui.h> #include <imgui.h>
#include <cstdio> #include <cstdio>
#include <sophia/components.hpp>
void InspectorPanel::onUpdate(const sophia::FrameInfo&) { void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
char nameBuf[128]; if (!selected) {
std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str()); ImGui::TextDisabled("No entity selected");
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) { return;
name = nameBuf;
} }
ImGui::Text("Tag: %s", tag.c_str());
drawTag();
ImGui::Separator();
drawTransform();
drawCamera();
drawSprite();
ImGui::Separator(); ImGui::Separator();
drawAddComponent();
}
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { void InspectorPanel::drawTag() {
ImGui::DragFloat3("Position", &position.x, 0.1f); auto& tag = selected.getComponent<sophia::TagComponent>();
ImGui::DragFloat3("Rotation", &rotation.x, 0.1f);
ImGui::DragFloat3("Scale", &scale.x, 0.1f);
}
if (ImGui::CollapsingHeader("Mesh Renderer", char nameBuf[128];
ImGuiTreeNodeFlags_DefaultOpen)) { std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str());
ImGui::Text("Mesh: %s", meshName.c_str()); if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
ImGui::Text("Material: %s", materialName.c_str()); tag.tag = nameBuf;
} }
}
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%s", scriptName.c_str()); void InspectorPanel::drawTransform() {
if (!selected.hasComponent<sophia::TransformComponent>() ||
!ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& transform = selected.getComponent<sophia::TransformComponent>();
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", &degrees, 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<sophia::CameraComponent>() ||
!ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& camera = selected.getComponent<sophia::CameraComponent>();
int projection = static_cast<int>(camera.projection);
if (ImGui::Combo("Projection", &projection, "Orthographic\0Perspective\0")) {
camera.projection =
static_cast<sophia::CameraComponent::Projection>(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<sophia::SpriteComponent>()) {
return;
}
bool open = true;
if (!ImGui::CollapsingHeader("Sprite", &open,
ImGuiTreeNodeFlags_DefaultOpen)) {
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
return;
}
auto& sprite = selected.getComponent<sophia::SpriteComponent>();
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<sophia::SpriteComponent>();
}
}
void InspectorPanel::drawAddComponent() {
if (ImGui::Button("Add Component")) {
ImGui::OpenPopup("AddComponent");
}
if (ImGui::BeginPopup("AddComponent")) {
if (!selected.hasComponent<sophia::SpriteComponent>() &&
ImGui::MenuItem("Sprite")) {
selected.addComponent<sophia::SpriteComponent>();
}
if (!selected.hasComponent<sophia::CameraComponent>() &&
ImGui::MenuItem("Camera")) {
selected.addComponent<sophia::CameraComponent>();
}
ImGui::EndPopup();
} }
} }
+17 -11
View File
@@ -1,25 +1,31 @@
#pragma once #pragma once
#include <glm/glm.hpp> #include <sophia/entity.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <string> #include <string>
#include <utility>
#include <vector>
class InspectorPanel : public sophia::Panel { class InspectorPanel : public sophia::Panel {
public: public:
InspectorPanel() : Panel{{.name = "Inspector"}} {} // Stand-in for an asset browser: the textures a sprite can pick from.
using TextureList =
std::vector<std::pair<std::string, sophia::TextureHandle>>;
InspectorPanel(sophia::Entity& selected, const TextureList& textures)
: Panel{{.name = "Inspector"}}, selected{selected}, textures{textures} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override; void onUpdate(const sophia::FrameInfo& frameInfo) override;
private: private:
std::string name = "Player"; void drawTag();
std::string tag = "Player"; void drawTransform();
void drawCamera();
void drawSprite();
void drawAddComponent();
glm::vec3 position{0.0f, 1.0f, 0.0f}; sophia::Entity& selected;
glm::vec3 rotation{0.0f, 0.0f, 0.0f}; const TextureList& textures;
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";
}; };
+20 -20
View File
@@ -2,31 +2,31 @@
#include <imgui.h> #include <imgui.h>
void SceneHierarchyPanel::drawNode(const Node& node) { #include <sophia/components.hpp>
if (node.children.empty()) {
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
if (ImGui::Button("+ Sprite")) {
selected = scene.createEntity("Sprite");
selected.addComponent<sophia::SpriteComponent>();
}
ImGui::Separator();
// Flat for now: there is no parent/child component yet.
scene.forEachEntity([this](sophia::Entity entity) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoTreePushOnOpen |
ImGuiTreeNodeFlags_SpanAvailWidth; ImGuiTreeNodeFlags_SpanAvailWidth;
if (node.name == selected) { if (entity == selected) {
flags |= ImGuiTreeNodeFlags_Selected; flags |= ImGuiTreeNodeFlags_Selected;
} }
ImGui::TreeNodeEx(node.name.c_str(), flags);
const std::string& name = entity.getComponent<sophia::TagComponent>().tag;
ImGui::PushID(static_cast<int>(static_cast<sophia::u32>(entity)));
ImGui::TreeNodeEx(name.c_str(), flags);
if (ImGui::IsItemClicked()) { if (ImGui::IsItemClicked()) {
selected = node.name; selected = entity;
} }
return; ImGui::PopID();
} });
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);
}
} }
+8 -20
View File
@@ -1,32 +1,20 @@
#pragma once #pragma once
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/scene.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
class SceneHierarchyPanel : public sophia::Panel { class SceneHierarchyPanel : public sophia::Panel {
public: 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; void onUpdate(const sophia::FrameInfo& frameInfo) override;
private: private:
struct Node { sophia::Scene& scene;
std::string name; sophia::Entity& selected;
std::vector<Node> children;
};
void drawNode(const Node& node);
std::vector<Node> nodes = {
{"Main Camera", {}},
{"Directional Light", {}},
{"Ground", {}},
{"Player", {}},
{"Enemies", {{"Enemy_01", {}}, {"Enemy_02", {}}, {"Enemy_03", {}}}},
{"Props", {{"Crate_01", {}}, {"Crate_02", {}}}},
};
std::string selected;
}; };
+21
View File
@@ -47,6 +47,27 @@ struct MeshComponent {
MeshHandle handle; 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 CameraComponent {
struct Orthographic { struct Orthographic {
float height = 10.f; float height = 10.f;
+7
View File
@@ -59,4 +59,11 @@ class Entity {
Scene* scene = nullptr; Scene* scene = nullptr;
}; };
template <typename Fn>
void Scene::forEachEntity(Fn&& fn) {
for (auto [handle] : registry.storage<entt::entity>().each()) {
fn(Entity{handle, this});
}
}
} // namespace sophia } // namespace sophia
+2
View File
@@ -38,11 +38,13 @@ class Panel;
class RenderTarget; class RenderTarget;
class Scene; class Scene;
struct SceneView; struct SceneView;
class Texture;
using MeshHandle = Handle<Mesh>; using MeshHandle = Handle<Mesh>;
using PanelHandle = Handle<Panel>; using PanelHandle = Handle<Panel>;
using RenderTargetHandle = Handle<RenderTarget>; using RenderTargetHandle = Handle<RenderTarget>;
using SceneHandle = Handle<Scene>; using SceneHandle = Handle<Scene>;
using SceneViewHandle = Handle<SceneView>; using SceneViewHandle = Handle<SceneView>;
using TextureHandle = Handle<Texture>;
} // namespace sophia } // namespace sophia
@@ -26,8 +26,10 @@ class ResourceRegistryRef {
void requestResize(RenderTargetHandle handle, u32 width, u32 height); void requestResize(RenderTargetHandle handle, u32 width, u32 height);
glm::vec2 getExtent(RenderTargetHandle handle) const; glm::vec2 getExtent(RenderTargetHandle handle) const;
glm::vec2 getExtent(TextureHandle handle) const;
bool isAlive(RenderTargetHandle handle) const; bool isAlive(RenderTargetHandle handle) const;
bool isAlive(TextureHandle handle) const;
template <typename T> template <typename T>
void release(Handle<T> handle); void release(Handle<T> handle);
@@ -45,16 +47,25 @@ class ResourceRegistryRef {
RenderTargetHandle createResource(std::type_identity<RenderTarget>, RenderTargetHandle createResource(std::type_identity<RenderTarget>,
u32 width = 1, u32 height = 1); u32 width = 1, u32 height = 1);
TextureHandle createResource(std::type_identity<Texture>, u32 width,
u32 height, std::vector<u8> pixels);
ResourceRegistry* registry; ResourceRegistry* registry;
}; };
template <> template <>
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path); MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
template <>
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
template <> template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle); void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
template <> template <>
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle); void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
template <>
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
} // namespace sophia } // namespace sophia
+6 -1
View File
@@ -13,9 +13,14 @@ class Scene {
Scene(); 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); void destroyEntity(Entity entity);
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
template <typename Fn>
void forEachEntity(Fn&& fn);
// The camera a render request uses when it doesn't name one. Null until // The camera a render request uses when it doesn't name one. Null until
// set. // set.
entt::entity getActiveCamera() const { return activeCamera; } entt::entity getActiveCamera() const { return activeCamera; }
+7 -4
View File
@@ -65,7 +65,8 @@ class Application::Impl {
void run() { void run() {
ModelRenderSystem modelRenderSystem{device, renderContext}; ModelRenderSystem modelRenderSystem{device, renderContext};
SceneRenderer sceneRenderer{this->application.sceneRegistry, SceneRenderer sceneRenderer{this->application.sceneRegistry,
this->resourceRegistry}; this->resourceRegistry, device,
renderContext.getImageFormat()};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount(), renderContext.getImageCount(),
this->resourceRegistry}; this->resourceRegistry};
@@ -128,6 +129,7 @@ class Application::Impl {
} }
imguiRenderer.beginFrame(); imguiRenderer.beginFrame();
sceneRenderer.beginFrame(renderContext.getFrameIndex());
panelRegistry.update(frameInfo); panelRegistry.update(frameInfo);
this->application.onRender(); this->application.onRender();
@@ -170,10 +172,11 @@ 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, &sceneViewRegistry, handle](vk::CommandBuffer) { [&sceneRenderer, &sceneViewRegistryConst,
const SceneView& view = sceneViewRegistry.get(handle); handle](vk::CommandBuffer commandBuffer) {
const SceneView& view = sceneViewRegistryConst.get(handle);
if (view.camera) { if (view.camera) {
sceneRenderer.render(view); sceneRenderer.render(commandBuffer, view);
} }
}); });
} }
+1
View File
@@ -39,6 +39,7 @@ class RenderContext {
vk::Format getImageFormat() const { vk::Format getImageFormat() const {
return this->swapchain->getImageFormat(); return this->swapchain->getImageFormat();
} }
u32 getFrameIndex() const { return this->currentFrameIndex; }
u32 getImageCount() const { u32 getImageCount() const {
return static_cast<u32>(this->swapchain->imageCount()); return static_cast<u32>(this->swapchain->imageCount());
} }
+78 -50
View File
@@ -1,74 +1,102 @@
#include "rendering/scene_renderer.hpp" #include "rendering/scene_renderer.hpp"
#include <algorithm>
#include <sophia/components.hpp> #include <sophia/components.hpp>
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
#include <tuple>
namespace sophia { namespace sophia {
namespace {
struct PushConstantData {
glm::mat4 transform{1.f};
glm::vec4 color{1.f};
};
} // namespace
SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry, SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
ResourceRegistry& resources) ResourceRegistry& resources, Device& device,
: sceneRegistry{sceneRegistry}, resources{resources} {} vk::Format colorFormat)
: sceneRegistry{sceneRegistry},
resources{resources},
spriteRenderSystem{device, colorFormat, resources} {}
SceneRenderer::~SceneRenderer() {} SceneRenderer::~SceneRenderer() {}
void SceneRenderer::render(SceneView sceneView) { void SceneRenderer::beginFrame(u32 frameIndex) {
auto& handle = sceneView.scene; this->spriteRenderSystem.beginFrame(frameIndex);
auto& scene = this->sceneRegistry.get(handle); }
auto& camera = sceneView.camera; void SceneRenderer::render(vk::CommandBuffer commandBuffer,
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), 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<CameraComponent>(camera),
"camera does not have camera component!"); "camera does not have camera component!");
auto& registry = scene.registry; vk::Extent2D extent =
this->resources.renderTargetPool.get(sceneView.target).getExtent();
float aspect =
static_cast<float>(extent.width) / static_cast<float>(extent.height);
auto view = registry.view<MeshComponent, TransformComponent>(); // 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 =
registry.get<CameraComponent>(camera).getProjection(aspect) *
glm::inverse(cameraWorld);
for (auto entity : view) { renderSprites(commandBuffer, scene, viewProjection);
auto& transform = view.get<TransformComponent>(entity);
log::info(transform); // 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<SpriteDrawItem>& items = this->spriteDrawItems;
items.clear();
auto view =
scene.registry.view<const TransformComponent, const SpriteComponent>();
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 // Texture is the last key so that sprites whose order doesn't matter
// For each mesh, get all entities(ES) that have a transform and that mesh // relative to each other still end up in as few batches as possible.
// For e in ES: add e's transform component to a list std::ranges::sort(items, {}, [](const SpriteDrawItem& item) {
return std::tuple{item.sortOrder, item.depth, item.texture};
});
// We now have this->spriteInstances.clear();
/* this->spriteBatches.clear();
M = mesh for (const SpriteDrawItem& item : items) {
T = transform auto index = static_cast<u32>(this->spriteInstances.size());
this->spriteInstances.push_back(item.instance);
Work Batch if (this->spriteBatches.empty() ||
|- M0 this->spriteBatches.back().texture != item.texture) {
| |- T0 this->spriteBatches.push_back({item.texture, index, 0});
| \- T2 }
| this->spriteBatches.back().instanceCount++;
|- M1 }
| |- T1
| \- T5
|
\- M2
|- T3
\- T4
*/ this->spriteRenderSystem.render(commandBuffer, viewProjection,
this->spriteInstances, this->spriteBatches);
/*
for batch in Work Batch:
batch.mesh.bind()
for transform in batch.transforms:
commandBuffer.pushConstants(transform)
batch.mesh.draw(commandBuffer)
NOTE: Use instanced rendering?
*/
} }
} // namespace sophia } // namespace sophia
+29 -3
View File
@@ -2,28 +2,54 @@
#include <sophia/scene_registry.hpp> #include <sophia/scene_registry.hpp>
#include <sophia/scene_view.hpp> #include <sophia/scene_view.hpp>
#include <vector>
#include "rendering/sprite_render_system.hpp"
#include "resources/resource_registry.hpp" #include "resources/resource_registry.hpp"
namespace sophia { namespace sophia {
/** /**
* !TEMP to be decoupled once scene batching and rendering impl * !TEMP to be decoupled once scene batching and rendering impl
*
* Turns a scene's components into draw calls for a SceneView.
*/ */
class SceneRenderer { class SceneRenderer {
public: public:
SceneRenderer(const SceneRenderer&) = delete; SceneRenderer(const SceneRenderer&) = delete;
SceneRenderer& operator=(const SceneRenderer&) = delete; SceneRenderer& operator=(const SceneRenderer&) = delete;
SceneRenderer(const SceneRegistry& sceneRegistry, SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
ResourceRegistry& resources); Device& device, vk::Format colorFormat);
~SceneRenderer(); ~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: 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; const SceneRegistry& sceneRegistry;
ResourceRegistry& resources; ResourceRegistry& resources;
SpriteRenderSystem spriteRenderSystem;
// Reused across frames to avoid reallocating.
std::vector<SpriteDrawItem> spriteDrawItems;
std::vector<SpriteRenderSystem::Instance> spriteInstances;
std::vector<SpriteRenderSystem::Batch> spriteBatches;
}; };
} // namespace sophia } // namespace sophia
@@ -0,0 +1,194 @@
#include "rendering/sprite_render_system.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#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<const Instance> instances,
std::span<const Batch> 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<u32>(instances.size()), available);
buffer.writeToBuffer(const_cast<Instance*>(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<u32>(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<vk::VertexInputBindingDescription> bindings{
{0, sizeof(Instance), vk::VertexInputRate::eInstance}};
std::vector<vk::VertexInputAttributeDescription> 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<u32>(offsetof(Instance, model) +
column * sizeof(glm::vec4))});
}
attributes.push_back({4, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, color))});
attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(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>& buffer : this->instanceBuffers) {
buffer = std::make_unique<Buffer>(
this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME,
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
buffer->map();
}
}
} // namespace sophia
@@ -0,0 +1,94 @@
#pragma once
#include <array>
#include <memory>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <span>
#include <unordered_map>
#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<const Instance> instances,
std::span<const Batch> 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<DescriptorSetLayout> setLayout;
std::unique_ptr<DescriptorPool> 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<u32, CachedDescriptorSet> descriptorSets;
vk::UniquePipelineLayout pipelineLayout;
std::unique_ptr<GraphicsPipeline> graphicsPipeline;
std::array<std::unique_ptr<Buffer>, RenderContext::MAX_FRAMES_IN_FLIGHT>
instanceBuffers;
u32 frameIndex = 0;
u32 instanceCursor = 0;
};
} // namespace sophia
+11 -2
View File
@@ -1,10 +1,12 @@
#pragma once #pragma once
#include <array>
#include <sophia/handles.hpp> #include <sophia/handles.hpp>
#include <sophia/util/pool.hpp> #include <sophia/util/pool.hpp>
#include "resources/mesh.hpp" #include "resources/mesh.hpp"
#include "resources/pools/render_target_pool.hpp" #include "resources/pools/render_target_pool.hpp"
#include "resources/texture.hpp"
#include "vulkan/device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
@@ -15,13 +17,20 @@ class ResourceRegistry {
ResourceRegistry& operator=(const ResourceRegistry&) = delete; ResourceRegistry& operator=(const ResourceRegistry&) = delete;
ResourceRegistry(Device& device, vk::Format renderTargetFormat) ResourceRegistry(Device& device, vk::Format renderTargetFormat)
: device{device}, renderTargetPool{device, renderTargetFormat} {} : device{device}, renderTargetPool{device, renderTargetFormat} {
constexpr std::array<u8, 4> WHITE{0xFF, 0xFF, 0xFF, 0xFF};
whiteTexture = texturePool.create(device, 1, 1, WHITE);
}
Device& device; Device& device;
Pool<Mesh> meshPool{"Mesh"}; Pool<Mesh> meshPool{"Mesh"};
RenderTargetPool renderTargetPool; RenderTargetPool renderTargetPool;
// TexturePool textures; Pool<Texture> texturePool{"Texture"};
// MaterialPool materials; // 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 } // namespace sophia
@@ -1,5 +1,9 @@
#include <stb_image.h>
#include <sophia/resource_registry_ref.hpp> #include <sophia/resource_registry_ref.hpp>
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include "resources/resource_registry.hpp" #include "resources/resource_registry.hpp"
@@ -14,6 +18,26 @@ MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) {
return MeshHandle{}; return MeshHandle{};
} }
template <>
TextureHandle ResourceRegistryRef::load<Texture>(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<size_t>(width) * static_cast<size_t>(height) * 4;
TextureHandle handle = registry->texturePool.create(
registry->device, static_cast<u32>(width), static_cast<u32>(height),
std::span<const u8>{pixels, size});
stbi_image_free(pixels);
return handle;
}
template <> template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) { void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
registry->meshPool.release(handle); registry->meshPool.release(handle);
@@ -24,6 +48,15 @@ void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle) {
registry->renderTargetPool.release(handle); registry->renderTargetPool.release(handle);
} }
template <>
void ResourceRegistryRef::release<Texture>(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( MeshHandle ResourceRegistryRef::createResource(
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices, std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
std::vector<u32> indices) { std::vector<u32> indices) {
@@ -42,6 +75,13 @@ RenderTargetHandle ResourceRegistryRef::createResource(
return registry->renderTargetPool.create(width, height); return registry->renderTargetPool.create(width, height);
} }
TextureHandle ResourceRegistryRef::createResource(std::type_identity<Texture>,
u32 width, u32 height,
std::vector<u8> pixels) {
return registry->texturePool.create(registry->device, width, height,
std::span<const u8>{pixels});
}
void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width, void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width,
u32 height) { u32 height) {
registry->renderTargetPool.get(handle).requestResize(width, height); registry->renderTargetPool.get(handle).requestResize(width, height);
@@ -56,4 +96,14 @@ bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const {
return registry->renderTargetPool.isAlive(handle); return registry->renderTargetPool.isAlive(handle);
} }
glm::vec2 ResourceRegistryRef::getExtent(TextureHandle handle) const {
const Texture& texture = registry->texturePool.get(handle);
return {static_cast<float>(texture.getWidth()),
static_cast<float>(texture.getHeight())};
}
bool ResourceRegistryRef::isAlive(TextureHandle handle) const {
return registry->texturePool.isAlive(handle);
}
} // namespace sophia } // namespace sophia
+32
View File
@@ -0,0 +1,32 @@
#include "resources/texture.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
namespace sophia {
Texture::Texture(Device& device, u32 width, u32 height,
std::span<const u8> pixels) {
SOPHIA_ASSERT(width > 0 && height > 0, "texture must not be empty");
SOPHIA_ASSERT(pixels.size() == static_cast<size_t>(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
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <sophia/types.hpp>
#include <span>
#include <vulkan/vulkan.hpp>
#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<const u8> 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> image;
};
} // namespace sophia
+16 -6
View File
@@ -1,16 +1,26 @@
#include <sophia/components.hpp> #include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/scene.hpp> #include <sophia/scene.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
namespace sophia { namespace sophia {
Scene::Scene() { Scene::Scene() {}
entt::entity entity = this->registry.create();
auto& a = this->registry.emplace<TransformComponent>(entity);
auto& b = this->registry.get<TransformComponent>(entity);
}
Scene::~Scene() { log::trace("destroying sophia::Scene"); } Scene::~Scene() { log::trace("destroying sophia::Scene"); }
Entity Scene::createEntity(const std::string& name) {
Entity entity{this->registry.create(), this};
entity.addComponent<TagComponent>(name.empty() ? "Entity" : name);
entity.addComponent<TransformComponent>();
return entity;
}
void Scene::destroyEntity(Entity entity) {
if (this->activeCamera == entity) {
this->activeCamera = entt::null;
}
this->registry.destroy(entity);
}
} // namespace sophia } // namespace sophia
+12
View File
@@ -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;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 186> 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
+31
View File
@@ -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);
}
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 519> 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
@@ -95,6 +95,12 @@ GraphicsPipeline::Builder& GraphicsPipeline::Builder::setVertexInput(
return *this; return *this;
} }
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending(
bool enabled) {
this->alphaBlending = enabled;
return *this;
}
std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const { std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
if (this->vertexShader == nullptr || this->fragmentShader == nullptr) { if (this->vertexShader == nullptr || this->fragmentShader == nullptr) {
log::fatal( log::fatal(
@@ -128,7 +134,8 @@ std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
this->device, *this->vertexShader, *this->fragmentShader, this->device, *this->vertexShader, *this->fragmentShader,
this->pipelineLayout, this->colorAttachmentFormats, this->pipelineLayout, this->colorAttachmentFormats,
this->depthAttachmentFormat, this->stencilAttachmentFormat, this->depthAttachmentFormat, this->stencilAttachmentFormat,
this->bindingDescriptions, this->attributeDescriptions)); this->bindingDescriptions, this->attributeDescriptions,
this->alphaBlending));
} }
GraphicsPipeline::GraphicsPipeline( GraphicsPipeline::GraphicsPipeline(
@@ -137,10 +144,21 @@ GraphicsPipeline::GraphicsPipeline(
std::vector<vk::Format> colorAttachmentFormats, std::vector<vk::Format> colorAttachmentFormats,
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat, vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
std::vector<vk::VertexInputBindingDescription> bindingDescriptions, std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions) std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
bool alphaBlending)
: Pipeline(device, vk::PipelineBindPoint::eGraphics) { : Pipeline(device, vk::PipelineBindPoint::eGraphics) {
setDefaultGraphicsPipelineConfig(); setDefaultGraphicsPipelineConfig();
if (alphaBlending) {
vk::PipelineColorBlendAttachmentState& blend =
this->config.colorBlendAttachment;
blend.blendEnable = vk::True;
blend.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
blend.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
blend.srcAlphaBlendFactor = vk::BlendFactor::eOne;
blend.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
}
this->config.bindingDescriptions = std::move(bindingDescriptions); this->config.bindingDescriptions = std::move(bindingDescriptions);
this->config.attributeDescriptions = std::move(attributeDescriptions); this->config.attributeDescriptions = std::move(attributeDescriptions);
this->config.pipelineLayout = pipelineLayout; this->config.pipelineLayout = pipelineLayout;
@@ -47,7 +47,8 @@ class GraphicsPipeline : public Pipeline {
std::vector<vk::Format> colorAttachmentFormats, std::vector<vk::Format> colorAttachmentFormats,
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat, vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
std::vector<vk::VertexInputBindingDescription> bindingDescriptions, std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions); std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
bool alphaBlending);
void setDefaultGraphicsPipelineConfig(); void setDefaultGraphicsPipelineConfig();
@@ -69,6 +70,8 @@ class GraphicsPipeline::Builder {
Builder& setVertexInput( Builder& setVertexInput(
std::vector<vk::VertexInputBindingDescription> bindingDescriptions, std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions); std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
// Straight (non-premultiplied) "over" blending on every color attachment.
Builder& setAlphaBlending(bool enabled = true);
std::unique_ptr<GraphicsPipeline> build() const; std::unique_ptr<GraphicsPipeline> build() const;
@@ -83,6 +86,7 @@ class GraphicsPipeline::Builder {
vk::Format stencilAttachmentFormat = vk::Format::eUndefined; vk::Format stencilAttachmentFormat = vk::Format::eUndefined;
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{}; std::vector<vk::VertexInputBindingDescription> bindingDescriptions{};
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{}; std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
bool alphaBlending = false;
}; };
} // namespace sophia } // namespace sophia
+95 -20
View File
@@ -1,3 +1,5 @@
#include <imgui.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -7,8 +9,12 @@
#include <iostream> #include <iostream>
#include <random> #include <random>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/event.hpp> #include <sophia/event.hpp>
#include <sophia/key_event.hpp> #include <sophia/key_event.hpp>
#include <sophia/scene_view.hpp>
#include <sophia/ui/panel.hpp>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -16,6 +22,24 @@
struct Boid { struct Boid {
glm::vec2 position; glm::vec2 position;
glm::vec2 velocity; 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; 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.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED;
} }
// Boid shape in local space, nose pointing along +x; each boid is drawn boidTexture = createBoidTexture();
// as an instance of this one mesh with a per-boid transform.
std::vector<sophia::CanvasVertex> vertices{ sceneHandle = scenes().create();
{{BOID_LENGTH * 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}}, sophia::Scene& scene = scenes().get(sceneHandle);
{{-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}}, cameraEntity = scene.createEntity("Camera");
}; cameraEntity.addComponent<sophia::CameraComponent>();
boidMesh = resources().create<sophia::Mesh>(std::move(vertices));
std::uniform_real_distribution<float> tint{0.0f, 1.0f};
for (Boid& boid : boids) {
boid.entity = scene.createEntity("Boid");
boid.entity.addComponent<sophia::SpriteComponent>(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<sophia::RenderTarget>();
views().create({.scene = sceneHandle,
.target = target,
.camera = cameraEntity,
.clearColor = {0.01f, 0.01f, 0.02f, 1.0f}});
panels().emplace<ViewportPanel>(ui(), target);
panels().setDefaultLayout({.center = {"Boids"}});
} }
void onUpdate(const sophia::FrameInfo& frameInfo) override { void onUpdate(const sophia::FrameInfo& frameInfo) override {
float scrollDistance = float scrollDistance =
SCROLL_SPEED * static_cast<float>(frameInfo.deltaTime); SCROLL_SPEED * static_cast<float>(frameInfo.deltaTime);
if (scrollLeft) camera.position.x -= scrollDistance; glm::vec3& cameraPosition =
if (scrollRight) camera.position.x += scrollDistance; cameraEntity.getComponent<sophia::TransformComponent>().position;
if (scrollUp) camera.position.y -= scrollDistance; if (scrollLeft) cameraPosition.x -= scrollDistance;
if (scrollDown) camera.position.y += 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<sophia::CameraComponent>().ortho.height =
resources().getExtent(target).y;
updateBoids(static_cast<float>(frameInfo.deltaTime)); updateBoids(static_cast<float>(frameInfo.deltaTime));
for (const Boid& boid : boids) { for (Boid& boid : boids) {
drawBoid(boid); syncTransform(boid);
} }
} }
@@ -212,17 +261,40 @@ class Boids2D : public sophia::Application {
if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT; 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; glm::vec2 direction = boid.velocity;
float speed = glm::length(direction); float speed = glm::length(direction);
direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f}; direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f};
float angle = std::atan2(direction.y, direction.x); float angle = std::atan2(direction.y, direction.x);
glm::mat4 transform = auto& transform = boid.entity.getComponent<sophia::TransformComponent>();
glm::translate(glm::mat4{1.f}, glm::vec3(boid.position, 0.0f)) * transform.position = glm::vec3(boid.position, 0.0f);
glm::rotate(glm::mat4{1.f}, angle, glm::vec3(0.0f, 0.0f, 1.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<uint8_t> pixels(WIDTH * HEIGHT * 4);
for (uint32_t y = 0; y < HEIGHT; ++y) {
for (uint32_t x = 0; x < WIDTH; ++x) {
float u = (static_cast<float>(x) + 0.5f) / WIDTH;
float v = (static_cast<float>(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<uint8_t>(alpha * 255.0f);
}
}
return resources().create<sophia::Texture>(WIDTH, HEIGHT,
std::move(pixels));
} }
void setScrollKeyHeld(sophia::KeyCode keycode, bool held) { void setScrollKeyHeld(sophia::KeyCode keycode, bool held) {
@@ -235,7 +307,10 @@ class Boids2D : public sophia::Application {
std::vector<Boid> boids; std::vector<Boid> boids;
std::vector<std::vector<uint32_t>> grid{ std::vector<std::vector<uint32_t>> grid{
static_cast<size_t>(GRID_WIDTH * GRID_HEIGHT)}; static_cast<size_t>(GRID_WIDTH * GRID_HEIGHT)};
sophia::MeshHandle boidMesh{}; sophia::TextureHandle boidTexture{};
sophia::SceneHandle sceneHandle{};
sophia::RenderTargetHandle target{};
sophia::Entity cameraEntity;
bool scrollLeft = false; bool scrollLeft = false;
bool scrollRight = false; bool scrollRight = false;
bool scrollUp = false; bool scrollUp = false;
+7
View File
@@ -0,0 +1,7 @@
add_executable(image_viewer main.cpp)
target_link_libraries(image_viewer PRIVATE ${PROJECT_NAME})
target_compile_definitions(image_viewer PRIVATE
IMAGE_VIEWER_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+79
View File
@@ -0,0 +1,79 @@
#include <imgui.h>
#include <algorithm>
#include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/scene_view.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
// Shows a render target filling the whole panel.
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Image",
.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;
};
// Displays an image file (anything stb_image reads: JPEG, PNG, BMP, ...) as a
// sprite in a 2D scene, scaled to fit the viewport.
// image_viewer [path]
class ImageViewer : public sophia::Application {
public:
ImageViewer()
: Application{{.width = 1000, .height = 750, .name = "ImageViewer"}} {}
void onCreate() override {
std::string path =
args.size() > 1 ? args[1] : IMAGE_VIEWER_ASSET_DIR "/dog.jpeg";
sophia::SceneHandle sceneHandle = scenes().create();
sophia::Scene& scene = scenes().get(sceneHandle);
camera = scene.createEntity("Camera");
camera.addComponent<sophia::CameraComponent>();
sophia::TextureHandle texture = resources().load<sophia::Texture>(path);
imageSize = resources().getExtent(texture);
// One world unit per image pixel; the camera zooms to fit.
sophia::Entity image = scene.createEntity("Image");
image.addComponent<sophia::SpriteComponent>(
sophia::SpriteComponent{.texture = texture, .size = imageSize});
target = resources().create<sophia::RenderTarget>();
views().create({.scene = sceneHandle,
.target = target,
.camera = camera,
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
panels().emplace<ViewportPanel>(ui(), target);
// panels().setDefaultLayout({.center = {"Image"}});
}
void onUpdate(const sophia::FrameInfo&) override {
// Fit the whole image: show enough height that neither dimension of the
// image overflows the viewport.
glm::vec2 viewport = resources().getExtent(target);
float viewportAspect = viewport.x / viewport.y;
camera.getComponent<sophia::CameraComponent>().ortho.height =
std::max(imageSize.y, imageSize.x / viewportAspect);
}
private:
sophia::Entity camera;
sophia::RenderTargetHandle target{};
glm::vec2 imageSize{1.f};
};
SOPHIA_MAIN(ImageViewer)
Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB