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
6 changed files with 64 additions and 45 deletions
Showing only changes of commit 0b09aeb5d9 - Show all commits
+17
View File
@@ -66,6 +66,14 @@ struct SpriteComponent {
// Higher draws on top. Ties are broken by the transform's position.z. // Higher draws on top. Ties are broken by the transform's position.z.
s32 sortOrder = 0; s32 sortOrder = 0;
// Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's
// rectangle around its pivot, then by `transform`.
glm::mat4 getModel(const TransformComponent& transform) const {
return transform.mat4() *
glm::translate(glm::mat4{1.f}, glm::vec3{-pivot * size, 0.f}) *
glm::scale(glm::mat4{1.f}, glm::vec3{size, 1.f});
}
}; };
struct CameraComponent { struct CameraComponent {
@@ -98,6 +106,15 @@ struct CameraComponent {
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
ortho.nearPlane, ortho.farPlane); ortho.nearPlane, ortho.farPlane);
} }
// World -> clip space for this camera placed by `transform`. The view
// ignores the transform's scale; zoom is the projection's job.
glm::mat4 getViewProjection(const TransformComponent& transform,
float aspect) const {
glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) *
glm::mat4_cast(transform.rotation);
return getProjection(aspect) * glm::inverse(world);
}
}; };
} // namespace sophia } // namespace sophia
+2
View File
@@ -19,6 +19,8 @@ class SceneRegistry {
const Scene& get(SceneHandle handle) const { return pool.get(handle); } const Scene& get(SceneHandle handle) const { return pool.get(handle); }
Scene& get(SceneHandle handle) { return pool.get(handle); } Scene& get(SceneHandle handle) { return pool.get(handle); }
const Scene& peek(SceneHandle handle) const { return pool.get(handle); }
void release(SceneHandle handle) { pool.release(handle); } void release(SceneHandle handle) { pool.release(handle); }
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); } bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
@@ -35,6 +35,10 @@ class SceneViewRegistry {
return pool.get(handle); return pool.get(handle);
} }
const SceneView& peek(SceneViewHandle handle) const {
return pool.get(handle);
}
void release(SceneViewHandle handle) { void release(SceneViewHandle handle) {
isRenderQueueDirty = true; isRenderQueueDirty = true;
pool.release(handle); pool.release(handle);
+22 -9
View File
@@ -6,6 +6,7 @@
#include <cstddef> #include <cstddef>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <map> #include <map>
#include <set>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
#include <string> #include <string>
@@ -87,11 +88,12 @@ class Application::Impl {
PanelRegistry& panelRegistry = this->application.panelRegistry; PanelRegistry& panelRegistry = this->application.panelRegistry;
SceneRegistry& sceneRegistry = this->application.sceneRegistry; SceneRegistry& sceneRegistry = this->application.sceneRegistry;
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry; SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
const SceneViewRegistry& sceneViewRegistryConst =
this->application.sceneViewRegistry;
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
// Views already warned about having no camera, so each warns once.
std::set<SceneViewHandle> viewsWithoutCamera;
auto startTime = std::chrono::high_resolution_clock::now(); auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime; auto currentTime = startTime;
u32 frameIndex = 0; u32 frameIndex = 0;
@@ -148,8 +150,8 @@ class Application::Impl {
graph.importImage(renderContext.getCurrentImage(), Access::Present); graph.importImage(renderContext.getCurrentImage(), Access::Present);
std::map<RenderTargetHandle, ImageRef> viewTargets; std::map<RenderTargetHandle, ImageRef> viewTargets;
for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) { for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
const SceneView& view = sceneViewRegistryConst.get(handle); const SceneView& view = sceneViewRegistry.peek(handle);
if (!sceneRegistry.isAlive(view.scene) || if (!sceneRegistry.isAlive(view.scene) ||
!renderTargetPool.isAlive(view.target)) { !renderTargetPool.isAlive(view.target)) {
continue; continue;
@@ -172,12 +174,23 @@ class Application::Impl {
: vk::AttachmentLoadOp::eLoad, : vk::AttachmentLoadOp::eLoad,
{color.r, color.g, color.b, color.a}); {color.r, color.g, color.b, color.a});
}, },
[&sceneRenderer, &sceneViewRegistryConst, [&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
handle](vk::CommandBuffer commandBuffer) { &viewsWithoutCamera, handle](vk::CommandBuffer commandBuffer) {
const SceneView& view = sceneViewRegistryConst.get(handle); const SceneView& view = sceneViewRegistry.peek(handle);
if (view.camera) { Entity camera =
sceneRenderer.render(commandBuffer, view); view.camera ? view.camera
: sceneRegistry.get(view.scene).getActiveCamera();
// No camera to look through: the target is just cleared.
if (!camera) {
if (viewsWithoutCamera.insert(handle).second) {
log::warning(
"scene view has no camera and its scene has no active "
"camera; it will only be cleared (see "
"Scene::createCamera)");
}
return;
} }
sceneRenderer.render(commandBuffer, view, camera);
}); });
} }
+14 -32
View File
@@ -14,19 +14,13 @@ SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
resources{resources}, resources{resources},
spriteRenderSystem{device, colorFormat, resources} {} spriteRenderSystem{device, colorFormat, resources} {}
SceneRenderer::~SceneRenderer() {}
void SceneRenderer::beginFrame(u32 frameIndex) { void SceneRenderer::beginFrame(u32 frameIndex) {
this->spriteRenderSystem.beginFrame(frameIndex); this->spriteRenderSystem.beginFrame(frameIndex);
} }
void SceneRenderer::render(vk::CommandBuffer commandBuffer, void SceneRenderer::render(vk::CommandBuffer commandBuffer,
const SceneView& sceneView) { const SceneView& sceneView, Entity camera) {
const Scene& scene = this->sceneRegistry.get(sceneView.scene); SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
const entt::registry& registry = scene.registry;
entt::entity camera = sceneView.camera;
SOPHIA_ASSERT(registry.all_of<CameraComponent>(camera),
"camera does not have camera component!"); "camera does not have camera component!");
vk::Extent2D extent = vk::Extent2D extent =
@@ -34,17 +28,12 @@ void SceneRenderer::render(vk::CommandBuffer commandBuffer,
float aspect = float aspect =
static_cast<float>(extent.width) / static_cast<float>(extent.height); static_cast<float>(extent.width) / static_cast<float>(extent.height);
// The view matrix ignores the camera's scale; zoom is the projection's job.
glm::mat4 cameraWorld{1.f};
if (const auto* transform = registry.try_get<TransformComponent>(camera)) {
cameraWorld = glm::translate(glm::mat4{1.f}, transform->position) *
glm::mat4_cast(transform->rotation);
}
glm::mat4 viewProjection = glm::mat4 viewProjection =
registry.get<CameraComponent>(camera).getProjection(aspect) * camera.getComponent<CameraComponent>().getViewProjection(
glm::inverse(cameraWorld); camera.getComponent<TransformComponent>(), aspect);
renderSprites(commandBuffer, scene, viewProjection); renderSprites(commandBuffer, this->sceneRegistry.get(sceneView.scene),
viewProjection);
// TODO: meshes. Group MeshComponent entities by mesh and draw each group // TODO: meshes. Group MeshComponent entities by mesh and draw each group
// (instanced), as sprites are grouped by texture. // (instanced), as sprites are grouped by texture.
@@ -53,8 +42,7 @@ void SceneRenderer::render(vk::CommandBuffer commandBuffer,
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer, void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
const Scene& scene, const Scene& scene,
const glm::mat4& viewProjection) { const glm::mat4& viewProjection) {
std::vector<SpriteDrawItem>& items = this->spriteDrawItems; this->spriteDrawItems.clear();
items.clear();
auto view = auto view =
scene.registry.view<const TransformComponent, const SpriteComponent>(); scene.registry.view<const TransformComponent, const SpriteComponent>();
@@ -63,28 +51,22 @@ void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
? sprite.texture ? sprite.texture
: this->resources.whiteTexture; : this->resources.whiteTexture;
// Unit quad -> sprite rectangle around its pivot -> world. this->spriteDrawItems.push_back(
glm::mat4 model = {sprite.sortOrder,
transform.mat4() * transform.position.z,
glm::translate(glm::mat4{1.f}, texture,
glm::vec3{-sprite.pivot * sprite.size, 0.f}) * {sprite.getModel(transform), sprite.color, sprite.uvRect}});
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 // Texture is the last key so that sprites whose order doesn't matter
// relative to each other still end up in as few batches as possible. // relative to each other still end up in as few batches as possible.
std::ranges::sort(items, {}, [](const SpriteDrawItem& item) { std::ranges::sort(this->spriteDrawItems, {}, [](const SpriteDrawItem& item) {
return std::tuple{item.sortOrder, item.depth, item.texture}; return std::tuple{item.sortOrder, item.depth, item.texture};
}); });
this->spriteInstances.clear(); this->spriteInstances.clear();
this->spriteBatches.clear(); this->spriteBatches.clear();
for (const SpriteDrawItem& item : items) { for (const SpriteDrawItem& item : this->spriteDrawItems) {
auto index = static_cast<u32>(this->spriteInstances.size()); auto index = static_cast<u32>(this->spriteInstances.size());
this->spriteInstances.push_back(item.instance); this->spriteInstances.push_back(item.instance);
+5 -4
View File
@@ -21,14 +21,15 @@ class SceneRenderer {
SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources, SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
Device& device, vk::Format colorFormat); Device& device, vk::Format colorFormat);
~SceneRenderer();
// Call once per frame, before any render(). // Call once per frame, before any render().
void beginFrame(u32 frameIndex); void beginFrame(u32 frameIndex);
// Records `sceneView` into `commandBuffer`. Rendering into the view's // Records `sceneView` as seen through `camera` (an entity of the view's
// target must already have begun. // scene with a CameraComponent) into `commandBuffer`. Rendering into the
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView); // view's target must already have begun.
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView,
Entity camera);
private: private:
struct SpriteDrawItem { struct SpriteDrawItem {