2D scene rendering + image loading #15

Merged
burkkyy merged 18 commits from issue-14/2d-scene-support-spritecomponent-2d-render-system into main 2026-09-30 15:56:33 -07:00
26 changed files with 1101 additions and 158 deletions
Showing only changes of commit a21a842b5b - Show all commits
+58 -3
View File
@@ -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)
+120 -15
View File
@@ -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();
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);
drawAddComponent();
}
if (ImGui::CollapsingHeader("Mesh Renderer",
void InspectorPanel::drawTag() {
auto& tag = selected.getComponent<sophia::TagComponent>();
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", &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)) {
ImGui::Text("Mesh: %s", meshName.c_str());
ImGui::Text("Material: %s", materialName.c_str());
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
return;
}
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%s", scriptName.c_str());
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
#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;
};
+20 -20
View File
@@ -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();
});
}
+8 -20
View File
@@ -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;
};
+21
View File
@@ -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;
+7
View File
@@ -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
+2
View File
@@ -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
+6 -1
View File
@@ -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; }
+7 -4
View File
@@ -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);
}
});
}
+1
View File
@@ -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());
}
+80 -52
View File
@@ -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);
auto& camera = sceneView.camera;
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
"camera does not have camera component!");
auto& registry = scene.registry;
auto view = registry.view<MeshComponent, TransformComponent>();
for (auto entity : view) {
auto& transform = view.get<TransformComponent>(entity);
log::info(transform);
void SceneRenderer::beginFrame(u32 frameIndex) {
this->spriteRenderSystem.beginFrame(frameIndex);
}
// 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
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
const SceneView& sceneView) {
const Scene& scene = this->sceneRegistry.get(sceneView.scene);
const entt::registry& registry = scene.registry;
// We now have
/*
M = mesh
T = transform
entt::entity camera = sceneView.camera;
SOPHIA_ASSERT(registry.all_of<CameraComponent>(camera),
"camera does not have camera component!");
Work Batch
|- M0
| |- T0
| \- T2
|
|- M1
| |- T1
| \- T5
|
\- M2
|- T3
\- T4
vk::Extent2D extent =
this->resources.renderTargetPool.get(sceneView.target).getExtent();
float aspect =
static_cast<float>(extent.width) / static_cast<float>(extent.height);
*/
// The view matrix ignores the camera's scale; zoom is the projection's job.
glm::mat4 cameraWorld{1.f};
if (const auto* transform = registry.try_get<TransformComponent>(camera)) {
cameraWorld = glm::translate(glm::mat4{1.f}, transform->position) *
glm::mat4_cast(transform->rotation);
}
glm::mat4 viewProjection =
registry.get<CameraComponent>(camera).getProjection(aspect) *
glm::inverse(cameraWorld);
/*
for batch in Work Batch:
batch.mesh.bind()
renderSprites(commandBuffer, scene, viewProjection);
for transform in batch.transforms:
commandBuffer.pushConstants(transform)
batch.mesh.draw(commandBuffer)
// TODO: meshes. Group MeshComponent entities by mesh and draw each group
// (instanced), as sprites are grouped by texture.
}
NOTE: Use instanced rendering?
*/
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}});
}
// 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};
});
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);
if (this->spriteBatches.empty() ||
this->spriteBatches.back().texture != item.texture) {
this->spriteBatches.push_back({item.texture, index, 0});
}
this->spriteBatches.back().instanceCount++;
}
this->spriteRenderSystem.render(commandBuffer, viewProjection,
this->spriteInstances, this->spriteBatches);
}
} // namespace sophia
+29 -3
View File
@@ -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
+11 -2
View File
@@ -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
+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/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
+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 -20
View File
@@ -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;