Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 571472a204 | |||
| a21a842b5b | |||
| 5644782a1f | |||
| b44fe94d5c |
@@ -1,5 +1,7 @@
|
||||
# sophia
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
COMING SOON
|
||||
|
||||
+58
-3
@@ -1,5 +1,7 @@
|
||||
#include <iostream>
|
||||
#include <sophia/application.hpp>
|
||||
#include <sophia/components.hpp>
|
||||
#include <sophia/entity.hpp>
|
||||
|
||||
#include "panels/asset_browser_panel.hpp"
|
||||
#include "panels/inspector_panel.hpp"
|
||||
@@ -15,12 +17,19 @@ class Editor : public sophia::Application {
|
||||
|
||||
debugUI().show();
|
||||
|
||||
panels().emplace<SceneHierarchyPanel>();
|
||||
panels().emplace<InspectorPanel>();
|
||||
textures.emplace_back("Checker", createCheckerTexture());
|
||||
|
||||
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>();
|
||||
sophia::RenderTargetHandle sceneTarget =
|
||||
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().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<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)
|
||||
|
||||
@@ -3,30 +3,135 @@
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <sophia/components.hpp>
|
||||
|
||||
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<sophia::TagComponent>();
|
||||
|
||||
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<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", °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<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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <sophia/entity.hpp>
|
||||
#include <sophia/frame_info.hpp>
|
||||
#include <sophia/handles.hpp>
|
||||
#include <sophia/ui/panel.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
void SceneHierarchyPanel::drawNode(const Node& node) {
|
||||
if (node.children.empty()) {
|
||||
#include <sophia/components.hpp>
|
||||
|
||||
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_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<sophia::TagComponent>().tag;
|
||||
ImGui::PushID(static_cast<int>(static_cast<sophia::u32>(entity)));
|
||||
ImGui::TreeNodeEx(name.c_str(), flags);
|
||||
if (ImGui::IsItemClicked()) {
|
||||
selected = node.name;
|
||||
}
|
||||
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);
|
||||
selected = entity;
|
||||
}
|
||||
ImGui::PopID();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <sophia/entity.hpp>
|
||||
#include <sophia/frame_info.hpp>
|
||||
#include <sophia/scene.hpp>
|
||||
#include <sophia/ui/panel.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<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;
|
||||
sophia::Scene& scene;
|
||||
sophia::Entity& selected;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -59,4 +59,11 @@ class Entity {
|
||||
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
|
||||
|
||||
@@ -38,11 +38,13 @@ class Panel;
|
||||
class RenderTarget;
|
||||
class Scene;
|
||||
struct SceneView;
|
||||
class Texture;
|
||||
|
||||
using MeshHandle = Handle<Mesh>;
|
||||
using PanelHandle = Handle<Panel>;
|
||||
using RenderTargetHandle = Handle<RenderTarget>;
|
||||
using SceneHandle = Handle<Scene>;
|
||||
using SceneViewHandle = Handle<SceneView>;
|
||||
using TextureHandle = Handle<Texture>;
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
@@ -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 <typename T>
|
||||
void release(Handle<T> handle);
|
||||
@@ -45,16 +47,25 @@ class ResourceRegistryRef {
|
||||
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
|
||||
u32 width = 1, u32 height = 1);
|
||||
|
||||
TextureHandle createResource(std::type_identity<Texture>, u32 width,
|
||||
u32 height, std::vector<u8> pixels);
|
||||
|
||||
ResourceRegistry* registry;
|
||||
};
|
||||
|
||||
template <>
|
||||
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
|
||||
|
||||
template <>
|
||||
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
|
||||
|
||||
template <>
|
||||
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
|
||||
|
||||
template <>
|
||||
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
|
||||
|
||||
template <>
|
||||
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
@@ -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 <typename Fn>
|
||||
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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<u32>(this->swapchain->imageCount());
|
||||
}
|
||||
|
||||
@@ -1,74 +1,102 @@
|
||||
#include "rendering/scene_renderer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sophia/components.hpp>
|
||||
#include <sophia/util/assert.hpp>
|
||||
#include <tuple>
|
||||
|
||||
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<CameraComponent>(),
|
||||
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<CameraComponent>(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<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) {
|
||||
auto& transform = view.get<TransformComponent>(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<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
|
||||
// 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<u32>(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
|
||||
|
||||
@@ -2,28 +2,54 @@
|
||||
|
||||
#include <sophia/scene_registry.hpp>
|
||||
#include <sophia/scene_view.hpp>
|
||||
#include <vector>
|
||||
|
||||
#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<SpriteDrawItem> spriteDrawItems;
|
||||
std::vector<SpriteRenderSystem::Instance> spriteInstances;
|
||||
std::vector<SpriteRenderSystem::Batch> spriteBatches;
|
||||
};
|
||||
|
||||
} // 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
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <sophia/handles.hpp>
|
||||
#include <sophia/util/pool.hpp>
|
||||
|
||||
#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<u8, 4> WHITE{0xFF, 0xFF, 0xFF, 0xFF};
|
||||
whiteTexture = texturePool.create(device, 1, 1, WHITE);
|
||||
}
|
||||
|
||||
Device& device;
|
||||
Pool<Mesh> meshPool{"Mesh"};
|
||||
RenderTargetPool renderTargetPool;
|
||||
// TexturePool textures;
|
||||
Pool<Texture> 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
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include <stb_image.h>
|
||||
|
||||
#include <sophia/resource_registry_ref.hpp>
|
||||
#include <sophia/util/assert.hpp>
|
||||
#include <sophia/util/logger.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "resources/resource_registry.hpp"
|
||||
|
||||
@@ -14,6 +18,26 @@ MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) {
|
||||
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 <>
|
||||
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
|
||||
registry->meshPool.release(handle);
|
||||
@@ -24,6 +48,15 @@ void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle 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(
|
||||
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
|
||||
std::vector<u32> indices) {
|
||||
@@ -42,6 +75,13 @@ RenderTargetHandle ResourceRegistryRef::createResource(
|
||||
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,
|
||||
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<float>(texture.getWidth()),
|
||||
static_cast<float>(texture.getHeight())};
|
||||
}
|
||||
|
||||
bool ResourceRegistryRef::isAlive(TextureHandle handle) const {
|
||||
return registry->texturePool.isAlive(handle);
|
||||
}
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,16 +1,26 @@
|
||||
#include <sophia/components.hpp>
|
||||
#include <sophia/entity.hpp>
|
||||
#include <sophia/scene.hpp>
|
||||
#include <sophia/util/logger.hpp>
|
||||
|
||||
namespace sophia {
|
||||
|
||||
Scene::Scene() {
|
||||
entt::entity entity = this->registry.create();
|
||||
|
||||
auto& a = this->registry.emplace<TransformComponent>(entity);
|
||||
auto& b = this->registry.get<TransformComponent>(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<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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending(
|
||||
bool enabled) {
|
||||
this->alphaBlending = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
||||
if (this->vertexShader == nullptr || this->fragmentShader == nullptr) {
|
||||
log::fatal(
|
||||
@@ -128,7 +134,8 @@ std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
||||
this->device, *this->vertexShader, *this->fragmentShader,
|
||||
this->pipelineLayout, this->colorAttachmentFormats,
|
||||
this->depthAttachmentFormat, this->stencilAttachmentFormat,
|
||||
this->bindingDescriptions, this->attributeDescriptions));
|
||||
this->bindingDescriptions, this->attributeDescriptions,
|
||||
this->alphaBlending));
|
||||
}
|
||||
|
||||
GraphicsPipeline::GraphicsPipeline(
|
||||
@@ -137,10 +144,21 @@ GraphicsPipeline::GraphicsPipeline(
|
||||
std::vector<vk::Format> colorAttachmentFormats,
|
||||
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions)
|
||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
|
||||
bool alphaBlending)
|
||||
: Pipeline(device, vk::PipelineBindPoint::eGraphics) {
|
||||
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.attributeDescriptions = std::move(attributeDescriptions);
|
||||
this->config.pipelineLayout = pipelineLayout;
|
||||
|
||||
@@ -47,7 +47,8 @@ class GraphicsPipeline : public Pipeline {
|
||||
std::vector<vk::Format> colorAttachmentFormats,
|
||||
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
|
||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
|
||||
bool alphaBlending);
|
||||
|
||||
void setDefaultGraphicsPipelineConfig();
|
||||
|
||||
@@ -69,6 +70,8 @@ class GraphicsPipeline::Builder {
|
||||
Builder& setVertexInput(
|
||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||
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;
|
||||
|
||||
@@ -83,6 +86,7 @@ class GraphicsPipeline::Builder {
|
||||
vk::Format stencilAttachmentFormat = vk::Format::eUndefined;
|
||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{};
|
||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
|
||||
bool alphaBlending = false;
|
||||
};
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
+95
-20
@@ -1,3 +1,5 @@
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -7,8 +9,12 @@
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <sophia/application.hpp>
|
||||
#include <sophia/components.hpp>
|
||||
#include <sophia/entity.hpp>
|
||||
#include <sophia/event.hpp>
|
||||
#include <sophia/key_event.hpp>
|
||||
#include <sophia/scene_view.hpp>
|
||||
#include <sophia/ui/panel.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -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<sophia::CanvasVertex> 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<sophia::Mesh>(std::move(vertices));
|
||||
boidTexture = createBoidTexture();
|
||||
|
||||
sceneHandle = scenes().create();
|
||||
sophia::Scene& scene = scenes().get(sceneHandle);
|
||||
|
||||
cameraEntity = scene.createEntity("Camera");
|
||||
cameraEntity.addComponent<sophia::CameraComponent>();
|
||||
|
||||
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 {
|
||||
float scrollDistance =
|
||||
SCROLL_SPEED * static_cast<float>(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<sophia::TransformComponent>().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<sophia::CameraComponent>().ortho.height =
|
||||
resources().getExtent(target).y;
|
||||
|
||||
updateBoids(static_cast<float>(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<sophia::TransformComponent>();
|
||||
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<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) {
|
||||
@@ -235,7 +307,10 @@ class Boids2D : public sophia::Application {
|
||||
std::vector<Boid> boids;
|
||||
std::vector<std::vector<uint32_t>> grid{
|
||||
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 scrollRight = false;
|
||||
bool scrollUp = false;
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user