Compare commits

...

10 Commits

Author SHA1 Message Date
burkkyy 8824f64225 Merge pull request 'Huge changes' (#3) from dev into main
Reviewed-on: #3
2026-09-15 02:36:50 -07:00
burkkyy a6b98e0613 Added console concept 2026-09-15 02:36:23 -07:00
burkkyy b16507c61d application updated 2026-09-15 02:28:28 -07:00
burkkyy 1229f5c017 test bed testing debugUI 2026-09-15 02:24:44 -07:00
burkkyy 34faf8b61e glossary 2026-09-15 02:24:17 -07:00
burkkyy c97db37b31 mesh pool 2026-09-15 02:23:41 -07:00
burkkyy 05fe3c2ee8 changes for ImGui 2026-09-15 02:23:22 -07:00
burkkyy 12a1e6eca9 AssetRegistry and small changes 2026-09-15 02:20:38 -07:00
burkkyy 6e2c6c0fc5 2d boids example done 2026-09-13 00:52:08 -07:00
burkkyy 4190ff847b asset_manager concept 2026-09-09 11:36:07 -07:00
36 changed files with 1757 additions and 270 deletions
+329
View File
@@ -0,0 +1,329 @@
# Glossary
## AssetRegistry
An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out
lightweight Handles instead of pointers or ownership.
Internally it's a collection of per-type pools, each responsible for one
asset type:
```
AssetRegistry
├── MeshPool → MeshHandle
├── TexturePool → TextureHandle (planned)
└── MaterialPool → MaterialHandle (planned)
```
A Handle is a small (index, generation) pair. The index locates the slot in
the pool; the generation is bumped when that slot is released, so a Handle
kept around after release() is detected as stale instead of resolving to
someone else's asset (or freed memory).
Typical lifecycle:
```
load<Mesh>(path) / loadMesh(...) → MeshHandle
│
get<Mesh>(handle) → const Mesh&
│
release<Mesh>(handle)
```
Think: "Where do I get or release a GPU asset by handle, without owning its
lifetime directly?"
## Buffer
A Buffer is a linear region of GPU memory used to store arbitrary data.
Unlike an Image, which is designed for multidimensional image data, a buffer is essentially:
```
[ byte byte byte byte byte byte ... ]
```
Buffers commonly contain:
Vertex data
Index data
Uniform data
Storage data
Indirect draw commands
For example:
```
Vertex Buffer
┌─────────────────────────────────┐
│ Vertex │ Vertex │ Vertex │ ... │
└─────────────────────────────────┘
```
In Vulkan, VkBuffer / vk::Buffer is a fundamental resource.
Think: "A linear chunk of GPU-accessible data."
## Camera
A Camera defines how the scene is viewed and projected onto the screen.
It generally contains:
Position
Orientation
Projection parameters
View matrix
Projection matrix
For example:
```
Camera
├── Position
├── Rotation
├── Projection
│ ├── Perspective
│ └── Orthographic
├── View Matrix
└── Projection Matrix
```
Think: "From where and how am I looking at the scene?"
## Canvas
A Canvas is a coordinate/rendering space onto which things are drawn.
For UI, for example:
```
Canvas
┌──────────────────────────────┐
│ │
│ ┌──────────────┐ │
│ │ Panel │ │
│ └──────────────┘ │
│ │
│ Button │
│ │
└──────────────────────────────┘
```
A canvas establishes things such as:
Dimensions
Coordinate system
Origin
Scaling
Potentially clipping
In a UI system, a Canvas is essentially:
The space in which UI elements are laid out and rendered.
## Frame
Frame is context-dependent, but in a renderer it usually means one iteration of rendering the scene to produce one displayed image.
## Layer
A Layer is a logical ordering/grouping of content.
For example:
Layer 0: Background
Layer 1: World
Layer 2: Characters
Layer 3: Effects
Layer 4: UI
Higher layers can be rendered over lower layers.
In a 2D engine, layers might determine draw order:
```
Background
↓
Tiles
↓
Player
↓
Particles
↓
UI
```
However, Layer is very engine-specific. In a 3D renderer it could instead represent a rendering category, visibility mask, or render-pass grouping.
## Material
A Material describes how a surface should be rendered.
It usually combines:
Shader/pipeline configuration
Textures
Colors
Numerical parameters
For example:
```
Material
├── Shader
├── Albedo Texture
├── Normal Texture
├── Roughness
├── Metallic
└── Base Color
```
Then:
```
Mesh + Material + Transform
↓
Renderer
↓
GPU
```
A useful conceptual distinction is:
Mesh = what the object is shaped like.
Material = how that shape should look.
## Mesh
A Mesh is the geometric data used to describe the shape of an object.
Typically contains:
Vertex positions
Normals
Texture coordinates (UVs)
Tangents
Indices
For example, a cube mesh contains the vertices and triangles necessary to describe a cube.
```
Mesh
├── Vertices
├── Indices
└── Vertex attributes
├── Position
├── Normal
├── UV
└── Tangent
```
Think: "What shape is this object?"
## Panel
A Panel is a rectangular UI region/container.
For example:
```
┌──────────────────────────────┐
│ Toolbar │
├───────────┬──────────────────┤
│ │ │
│ Scene │ Viewport │
│ │ │
├───────────┼──────────────────┤
│ Inspector │ Console │
└───────────┴──────────────────┘
```
Each of those regions could be a panel.
A panel generally has:
Position
Size
Child elements
UI contents
Potentially its own rendering/context
This is primarily a UI concept, rather than a GPU concept.
## PanelRegistry
A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
PanelHandles instead of pointers or ownership.
Internally it's a generation-checked slot pool, the same shape AssetRegistry's
per-type pools use:
```
PanelRegistry
└── Slot { Panel, generation } → PanelHandle
```
Typical lifecycle:
```
add(panel) / emplace<T>(...) → PanelHandle
│
get(handle) / get<T>(handle) → Panel& / T&
│
remove(handle)
```
Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds.
Think: "Where do I register or look up a UI panel by handle, without owning
its lifetime directly?"
## Sprite
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
For example:
```
Texture Atlas
┌─────────────────────┐
│ Player │ Enemy │
│ │ │
├────────┼────────────┤
│ Tree │ Rock │
└─────────────────────┘
```
A sprite might reference only the Player region of that texture.
A useful engine abstraction might be:
```cpp
Sprite {
Texture* texture;
Rect uv;
}
```
Think: "Which 2D picture/region am I drawing?"
## Texture
A Texture is image data intended to be used by a shader.
It can represent:
Color/albedo
Normal maps
Roughness
Metallic values
Masks
Height maps
etc.
A texture doesn't necessarily have to be displayed directly.
Texture → Shader → Material/Object
Think: "What image/data does the shader sample?"
+20 -1
View File
@@ -3,13 +3,20 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp> #include <sophia/canvas.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia { namespace sophia {
class Application { class Application {
@@ -23,7 +30,7 @@ class Application {
Application(const Application&) = delete; Application(const Application&) = delete;
Application& operator=(const Application&) = delete; Application& operator=(const Application&) = delete;
Application(const Config& config); explicit Application(const Config& config);
virtual ~Application(); virtual ~Application();
void run(); void run();
@@ -36,10 +43,17 @@ class Application {
virtual void onRender() {} virtual void onRender() {}
virtual void onDestroy() {} virtual void onDestroy() {}
AssetRegistry& assets() { return this->assetRegistry; }
PanelRegistry& panels() { return this->panelRegistry; }
DebugUI& debugUI() { return this->_debugUI; }
Canvas canvas; Canvas canvas;
Camera2D camera; Camera2D camera;
std::vector<std::string> args; std::vector<std::string> args;
void drawMesh(MeshHandle handle, const glm::mat4& transform);
private: private:
friend class Canvas; friend class Canvas;
@@ -48,6 +62,11 @@ class Application {
class Impl; class Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry;
PanelRegistry panelRegistry;
DebugUI _debugUI;
}; };
} // namespace sophia } // namespace sophia
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <memory>
#include <sophia/handles.hpp>
#include <string>
#include <vector>
#include "sophia/canvas.hpp"
#include "sophia/types.hpp"
namespace sophia {
class Device;
class Mesh;
template <typename T>
struct AssetTraits;
template <>
struct AssetTraits<Mesh> {
using Handle = MeshHandle;
};
class AssetRegistry {
public:
~AssetRegistry();
AssetRegistry(const AssetRegistry&) = delete;
AssetRegistry& operator=(const AssetRegistry&) = delete;
template <typename T>
typename AssetTraits<T>::Handle load(const std::string& path);
/**
* TEMPORARY: Remove once load<Mesh> can read real model files from disk.
*/
MeshHandle loadMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices);
template <typename T>
void release(typename AssetTraits<T>::Handle handle);
private:
friend class Application;
explicit AssetRegistry(Device& device);
template <typename T>
const T& get(typename AssetTraits<T>::Handle handle) const;
class Impl;
std::unique_ptr<Impl> impl;
};
template <>
MeshHandle AssetRegistry::load<Mesh>(const std::string& path);
template <>
void AssetRegistry::release<Mesh>(MeshHandle handle);
template <>
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const;
} // namespace sophia
+1 -1
View File
@@ -33,7 +33,7 @@ class Canvas {
private: private:
friend class Application; friend class Application;
Canvas(Application* application); explicit Canvas(Application* application);
class Impl; class Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <sophia/asset_registry.hpp>
#include <string>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
namespace sophia {
struct TagComponent {
std::string tag; // entt::hashed_string ?
};
struct TransformComponent {
glm::vec3 position{0.f, 0.f, 0.f};
glm::quat rotation{1.f, 0.f, 0.f, 0.f};
glm::vec3 scale{1.f, 1.f, 1.f};
glm::mat4 mat4() const {
return glm::translate(glm::mat4{1.f}, position) * glm::mat4_cast(rotation) *
glm::scale(glm::mat4{1.f}, scale);
}
glm::mat3 normalMatrix() const {
return glm::mat3{glm::transpose(glm::inverse(mat4()))};
}
};
struct MeshComponent {
MeshHandle handle;
};
} // namespace sophia
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <compare>
#include "sophia/types.hpp"
namespace sophia {
template <typename Tag>
struct Handle {
static constexpr u32 INVALID_INDEX = (1u << 20) - 1;
u32 index : 20 = INVALID_INDEX;
u32 generation : 12 = 1;
[[nodiscard]] static constexpr Handle invalid() {
return Handle{INVALID_INDEX, 0};
}
[[nodiscard]] constexpr bool isValid() const {
return index != INVALID_INDEX;
}
[[nodiscard]] auto operator<=>(const Handle&) const = default;
};
struct MeshTag {};
struct PanelTag {};
using MeshHandle = Handle<MeshTag>;
using PanelHandle = Handle<PanelTag>;
} // namespace sophia
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <sophia/handles.hpp>
#include <sophia/ui/panel_registry.hpp>
namespace sophia {
class DebugUI {
public:
DebugUI(const DebugUI&) = delete;
DebugUI& operator=(const DebugUI&) = delete;
explicit DebugUI(PanelRegistry& registry);
~DebugUI() = default;
void show();
void hide();
void showPanelRegistry(bool show = true);
void showFrameStats(bool show = true);
void showConsole(bool show = true);
private:
PanelRegistry& registry;
PanelHandle panelRegistryHandle;
PanelHandle frameStatsHandle;
PanelHandle consoleHandle;
};
} // namespace sophia
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <string>
#include <utility>
namespace sophia {
class Panel {
public:
Panel(const Panel&) = delete;
Panel& operator=(const Panel&) = delete;
Panel(Panel&&) noexcept = default;
Panel& operator=(Panel&&) noexcept = default;
explicit Panel(std::string name = {}) : name{std::move(name)} {}
virtual ~Panel() = default;
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
const std::string& getName() const { return name; }
bool isVisible() const { return this->visible; }
void setVisible(bool visible) { this->visible = visible; }
void toggleVisible() { this->visible = !this->visible; }
protected:
bool visible = true;
private:
std::string name;
};
} // namespace sophia
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <memory>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/util/assert.hpp>
#include <string_view>
#include <typeinfo>
#include <utility>
#include <vector>
namespace sophia {
class PanelRegistry {
public:
PanelRegistry(const PanelRegistry&) = delete;
PanelRegistry& operator=(const PanelRegistry&) = delete;
PanelRegistry() = default;
~PanelRegistry() = default;
PanelHandle add(std::unique_ptr<Panel> panel);
template <typename T, typename... Args>
PanelHandle emplace(Args&&... args) {
return add(std::make_unique<T>(std::forward<Args>(args)...));
}
const Panel& get(PanelHandle handle) const;
Panel& get(PanelHandle handle) {
return const_cast<Panel&>(std::as_const(*this).get(handle));
}
template <typename T>
const T& get(PanelHandle handle) const {
const Panel& panel = get(handle);
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
"PanelHandle does not refer to a", typeid(T).name());
return static_cast<const T&>(panel);
}
template <typename T>
T& get(PanelHandle handle) {
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
}
void remove(PanelHandle handle);
bool isValid(PanelHandle handle) const;
template <typename T>
const T* find() const {
for (const Slot& slot : slots) {
if (slot.panel) {
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) {
return panel;
}
}
}
return nullptr;
}
template <typename T>
T* find() {
return const_cast<T*>(std::as_const(*this).template find<T>());
}
const Panel* find(std::string_view name) const {
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->getName() == name) {
return slot.panel.get();
}
}
return nullptr;
}
Panel* find(std::string_view name) {
return const_cast<Panel*>(std::as_const(*this).find(name));
}
u32 size() const;
u32 visibleCount() const;
std::vector<std::string_view> names() const {
std::vector<std::string_view> result;
result.reserve(slots.size());
for (const Slot& slot : slots) {
if (slot.panel) {
result.push_back(slot.panel->getName());
}
}
return result;
}
void update(const FrameInfo& frameInfo);
private:
struct Slot {
std::unique_ptr<Panel> panel;
u32 generation = 0;
};
std::vector<Slot> slots;
std::vector<u32> freeList;
};
} // namespace sophia
+36 -67
View File
@@ -10,10 +10,13 @@
#define GLM_FORCE_RADIANS #define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE #define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <imgui.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "buffer.hpp" #include "buffer.hpp"
#include "device.hpp" #include "device.hpp"
#include "imgui_renderer.hpp"
#include "mesh.hpp" #include "mesh.hpp"
#include "pipelines/graphics_pipeline.hpp" #include "pipelines/graphics_pipeline.hpp"
#include "render_context.hpp" #include "render_context.hpp"
@@ -49,72 +52,10 @@ class Application::Impl {
device{&window}, device{&window},
renderContext{window, device} {} renderContext{window, device} {}
/*void run(const std::string& vertShaderPath,
const std::string& fragShaderPath) {
log::info("Running Render Triangle Test...");
RenderContext renderContext{window, device};
auto vertexShader = Shader::Builder()
.fromGLSL(vertShaderPath)
.setStage(ShaderStage::VERTEX)
.build();
auto fragmentShader = Shader::Builder()
.fromGLSL(fragShaderPath)
.setStage(ShaderStage::FRAGMENT)
.build();
vk::PushConstantRange pushConstantRange{};
pushConstantRange.stageFlags =
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstants);
vk::PipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
vk::UniquePipelineLayout pipelineLayout =
device.get()->createPipelineLayoutUnique(pipelineLayoutInfo);
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{
{0, sizeof(Vertex), vk::VertexInputRate::eVertex}};
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{
{0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)},
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}};
std::unique_ptr<sophia::GraphicsPipeline> graphicsPipeline =
GraphicsPipeline::Builder(device)
.setVertexShader(*vertexShader)
.setFragmentShader(*fragmentShader)
.setPipelineLayout(pipelineLayout.get())
.setColorAttachmentFormats({renderContext.getImageFormat()})
.setVertexInput(bindingDescriptions, attributeDescriptions)
.build();
// Oversized triangle covering the full screen so the fragment shader runs
// for every pixel.
std::vector<Vertex> vertices{
{{-1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}},
{{3.0f, -1.0f}, {0.0f, 1.0f, 0.0f}},
{{-1.0f, 3.0f}, {0.0f, 0.0f, 1.0f}},
};
Buffer vertexBuffer{device, sizeof(Vertex),
static_cast<u32>(vertices.size()),
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent};
vertexBuffer.map();
vertexBuffer.writeToBuffer(vertices.data(),
sizeof(Vertex) * vertices.size());
vertexBuffer.unmap();
}*/
void run() { void run() {
ModelRenderSystem modelRenderSystem{device, renderContext}; ModelRenderSystem modelRenderSystem{device, renderContext};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount()};
this->application.onCreate(); this->application.onCreate();
@@ -139,7 +80,6 @@ class Application::Impl {
static_cast<float>(extent.height)), static_cast<float>(extent.height)),
this->application.camera}; this->application.camera};
this->application.onUpdate(frameInfo); this->application.onUpdate(frameInfo);
this->application.onRender();
this->application.camera.setProjection( this->application.camera.setProjection(
-frameInfo.currentFramebufferExtent.x / 2.f, -frameInfo.currentFramebufferExtent.x / 2.f,
frameInfo.currentFramebufferExtent.x / 2.f, frameInfo.currentFramebufferExtent.x / 2.f,
@@ -151,6 +91,11 @@ class Application::Impl {
continue; continue;
} }
imguiRenderer.beginFrame();
this->application.panelRegistry.update(frameInfo);
this->application.onRender();
renderContext.transitionCurrentImage( renderContext.transitionCurrentImage(
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
@@ -183,7 +128,18 @@ class Application::Impl {
commandBuffer.setViewport(0, viewport); commandBuffer.setViewport(0, viewport);
commandBuffer.setScissor(0, scissor); commandBuffer.setScissor(0, scissor);
modelRenderSystem.render(commandBuffer, this->meshes, frameInfo); std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size());
for (const std::unique_ptr<Mesh>& mesh : this->meshes) {
instances.push_back({mesh.get(), glm::mat4{1.f}});
}
instances.insert(instances.end(), this->drawCalls.begin(),
this->drawCalls.end());
modelRenderSystem.render(commandBuffer, instances, frameInfo);
this->drawCalls.clear();
imguiRenderer.endFrame(commandBuffer);
commandBuffer.endRendering(); commandBuffer.endRendering();
@@ -224,6 +180,12 @@ class Application::Impl {
this->meshes.clear(); this->meshes.clear();
} }
void drawMesh(const Mesh& mesh, const glm::mat4& transform) {
this->drawCalls.push_back({&mesh, transform});
}
Device& getDevice() { return device; }
private: private:
Application& application; Application& application;
const Application::Config config; const Application::Config config;
@@ -232,6 +194,7 @@ class Application::Impl {
RenderContext renderContext; RenderContext renderContext;
std::vector<std::unique_ptr<Mesh>> meshes; std::vector<std::unique_ptr<Mesh>> meshes;
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
}; };
void Application::setCommandLineArgs(int argc, const char* const* argv) { void Application::setCommandLineArgs(int argc, const char* const* argv) {
@@ -241,7 +204,9 @@ void Application::setCommandLineArgs(int argc, const char* const* argv) {
Application::Application(const Config& config) Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*this, config)}, : impl{std::make_unique<Impl>(*this, config)},
canvas{this}, canvas{this},
args{std::move(commandLineArgs)} {} args{std::move(commandLineArgs)},
assetRegistry{impl->getDevice()},
_debugUI{this->panelRegistry} {}
Application::~Application() = default; Application::~Application() = default;
@@ -254,4 +219,8 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices,
void Application::clearMeshes() { impl->clearMeshes(); } void Application::clearMeshes() { impl->clearMeshes(); }
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform);
}
} // namespace sophia } // namespace sophia
+60
View File
@@ -0,0 +1,60 @@
#include <sophia/asset_registry.hpp>
#include <sophia/util/assert.hpp>
#include "mesh.hpp"
#include "pools/mesh_pool.hpp"
namespace sophia {
class AssetRegistry::Impl {
public:
explicit Impl(Device& device) : meshPool{device} {}
MeshPool meshPool;
// TexturePool texturePool;
// MaterialPool materialPool;
};
AssetRegistry::AssetRegistry(Device& device)
: impl{std::make_unique<Impl>(device)} {}
AssetRegistry::~AssetRegistry() = default;
template <>
MeshHandle AssetRegistry::load<Mesh>(const std::string& path) {
(void)path;
SOPHIA_ASSERT(false,
"Mesh::load(path) is not implemented yet - no model file "
"format is supported; build one via Mesh::Builder for now");
return MeshHandle{};
}
template <typename T>
typename AssetTraits<T>::Handle load(const std::string&) {
static_assert(sizeof(T) == 0,
"load<T> is not implemented for this asset type");
}
template <>
void AssetRegistry::release<Mesh>(MeshHandle handle) {
impl->meshPool.release(handle);
}
template <>
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const {
return impl->meshPool.get(handle);
}
MeshHandle AssetRegistry::loadMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices) {
Mesh::Builder builder{};
builder.vertices.reserve(vertices.size());
for (const CanvasVertex& vertex : vertices) {
builder.vertices.push_back({vertex.position, vertex.color});
}
builder.indices = std::move(indices);
return impl->meshPool.create(builder);
}
} // namespace sophia
+58
View File
@@ -0,0 +1,58 @@
#include "assets/pools/mesh_pool.hpp"
#include <sophia/util/assert.hpp>
namespace sophia {
namespace {
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
} // namespace
MeshPool::MeshPool(Device& device) : device{device} {}
MeshPool::~MeshPool() = default;
MeshHandle MeshPool::create(const Mesh::Builder& builder) {
auto mesh = std::make_unique<Mesh>(device, builder);
if (!freeList.empty()) {
u32 index = freeList.back();
freeList.pop_back();
slots[index].mesh = std::move(mesh);
return MeshHandle{index, slots[index].generation};
}
slots.push_back(Slot{std::move(mesh), 0});
return MeshHandle{static_cast<u32>(slots.size() - 1), 0};
}
const Mesh& MeshPool::get(MeshHandle handle) const {
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"MeshHandle index out of range:", handle.index);
const Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale MeshHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.mesh != nullptr,
"MeshHandle refers to a released mesh:", handle.index);
return *slot.mesh;
}
void MeshPool::release(MeshHandle handle) {
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"MeshHandle index out of range:", handle.index);
Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale MeshHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.mesh != nullptr,
"MeshHandle already released:", handle.index);
slot.mesh.reset();
slot.generation = (slot.generation + 1) & GENERATION_MASK;
freeList.push_back(handle.index);
}
} // namespace sophia
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <memory>
#include <vector>
#include "device.hpp"
#include "mesh.hpp"
namespace sophia {
class MeshPool {
public:
explicit MeshPool(Device& device);
~MeshPool();
MeshPool(const MeshPool&) = delete;
MeshPool& operator=(const MeshPool&) = delete;
MeshHandle create(const Mesh::Builder& builder);
const Mesh& get(MeshHandle handle) const;
void release(MeshHandle handle);
private:
struct Slot {
std::unique_ptr<Mesh> mesh;
u32 generation = 0;
};
Device& device;
std::vector<Slot> slots;
std::vector<u32> freeList;
};
} // namespace sophia
+85
View File
@@ -0,0 +1,85 @@
#include "imgui_renderer.hpp"
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>
namespace sophia {
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat,
u32 minImageCount) {
// 1. Construct ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true);
ImGui_ImplVulkan_InitInfo initInfo = {};
device.populateImGuiInitInfo(initInfo);
initInfo.DescriptorPoolSize =
IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5;
initInfo.MinImageCount = minImageCount;
initInfo.ImageCount = minImageCount;
VkFormat rawColorAttachmentFormat =
static_cast<VkFormat>(colorAttachmentFormat);
initInfo.UseDynamicRendering = true;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount =
1;
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo
.pColorAttachmentFormats = &rawColorAttachmentFormat;
ImGui_ImplVulkan_Init(&initInfo);
// 2. Enable dark theme
ImGui::StyleColorsDark();
// 3. Enable docking / keyboard
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
/**
* When viewports are enabled we tweak WindowRounding/WindowBg so
* platform windows can look identical to regular ones.
*/
ImGuiStyle& style = ImGui::GetStyle();
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
}
ImGuiRenderer::~ImGuiRenderer() {
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
}
void ImGuiRenderer::beginFrame() {
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(),
ImGuiDockNodeFlags_PassthruCentralNode);
}
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
ImGui::Render();
ImDrawData* mainDrawData = ImGui::GetDrawData();
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer);
}
} // namespace sophia
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <sophia/types.hpp>
#include "device.hpp"
#include "window.hpp"
namespace sophia {
class ImGuiRenderer {
public:
ImGuiRenderer(const ImGuiRenderer&) = delete;
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, u32 minImageCount);
~ImGuiRenderer();
void beginFrame();
void endFrame(vk::CommandBuffer commandBuffer);
};
} // namespace sophia
+1 -1
View File
@@ -36,7 +36,7 @@ Mesh::Mesh(Device& device, const Builder& builder) : device{device} {
createIndexBuffers(builder.indices); createIndexBuffers(builder.indices);
} }
Mesh::~Mesh() {} Mesh::~Mesh() = default;
void Mesh::bind(vk::CommandBuffer commandBuffer) const { void Mesh::bind(vk::CommandBuffer commandBuffer) const {
vk::Buffer buffers[] = {this->vertexBuffer->getBuffer()}; vk::Buffer buffers[] = {this->vertexBuffer->getBuffer()};
+1
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <vector> #include <vector>
-2
View File
@@ -142,8 +142,6 @@ void RenderContext::recreateSwapchain() {
// Old fences referred to images owned by the now-replaced swapchain; // Old fences referred to images owned by the now-replaced swapchain;
// there's nothing valid left to wait on. // there's nothing valid left to wait on.
this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{}); this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{});
log::trace("recreated swapchain");
} }
void RenderContext::createFrames() { void RenderContext::createFrames() {
+3
View File
@@ -45,6 +45,9 @@ class RenderContext {
vk::Format getImageFormat() const { vk::Format getImageFormat() const {
return this->swapchain->getImageFormat(); return this->swapchain->getImageFormat();
} }
u32 getImageCount() const {
return static_cast<u32>(this->swapchain->imageCount());
}
private: private:
void recreateSwapchain(); void recreateSwapchain();
+17
View File
@@ -0,0 +1,17 @@
#include "scene.hpp"
#include <sophia/components.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() { log::trace("destroying sophia::Scene"); }
} // namespace sophia
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <entt/entt.hpp>
namespace sophia {
class Scene {
public:
Scene();
~Scene();
private:
entt::registry registry;
};
} // namespace sophia
+1 -1
View File
@@ -186,7 +186,7 @@ vk::PresentModeKHR Swapchain::choosePresentMode(
bestMode = availablePresentMode; bestMode = availablePresentMode;
} }
} }
log::warning("Present mode MAILBOX not available."); log::trace("Present mode MAILBOX not available.");
if (bestMode == vk::PresentModeKHR::eImmediate) { if (bestMode == vk::PresentModeKHR::eImmediate) {
log::info("Selected present mode: IMMEDIATE (no vsync)"); log::info("Selected present mode: IMMEDIATE (no vsync)");
+7 -7
View File
@@ -1,6 +1,6 @@
#include "model_render_system.hpp" #include "model_render_system.hpp"
#include <cassert> #include <sophia/util/assert.hpp>
#include "shaders/model_render_system.frag.spv.h" #include "shaders/model_render_system.frag.spv.h"
#include "shaders/model_render_system.vert.spv.h" #include "shaders/model_render_system.vert.spv.h"
@@ -12,7 +12,7 @@ ModelRenderSystem::ModelRenderSystem(Device& device,
: device{device}, renderContext{renderContext} { : device{device}, renderContext{renderContext} {
createPipelineLayout(); createPipelineLayout();
assert(this->pipelineLayout.get() != nullptr && SOPHIA_ASSERT(this->pipelineLayout.get() != nullptr,
"Cannot create pipeline before pipeline layout"); "Cannot create pipeline before pipeline layout");
auto vertexShader = Shader::Builder() auto vertexShader = Shader::Builder()
@@ -42,23 +42,23 @@ ModelRenderSystem::~ModelRenderSystem() {
} }
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer, void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
std::span<const std::unique_ptr<Mesh>> meshes, std::span<const MeshInstance> instances,
const FrameInfo& frameInfo) { const FrameInfo& frameInfo) {
glm::mat4 viewProjection = frameInfo.camera.getViewProjection(); glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
this->graphicsPipeline->bind(commandBuffer); this->graphicsPipeline->bind(commandBuffer);
for (const std::unique_ptr<Mesh>& mesh : meshes) { for (const MeshInstance& instance : instances) {
PushConstantData push{}; PushConstantData push{};
push.modelMatrix = viewProjection; push.transform = viewProjection * instance.transform;
commandBuffer.pushConstants( commandBuffer.pushConstants(
this->pipelineLayout.get(), this->pipelineLayout.get(),
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
0, sizeof(PushConstantData), &push); 0, sizeof(PushConstantData), &push);
mesh->bind(commandBuffer); instance.mesh->bind(commandBuffer);
mesh->draw(commandBuffer); instance.mesh->draw(commandBuffer);
} }
} }
+9 -3
View File
@@ -13,9 +13,15 @@ namespace sophia {
class ModelRenderSystem { class ModelRenderSystem {
public: public:
// Layout must match the shader's `Push` block in model_render_system.vert.
struct PushConstantData { struct PushConstantData {
glm::mat4 modelMatrix{1.f}; glm::mat4 transform{1.f};
glm::mat4 normalMatrix{1.f}; glm::vec4 color{1.f};
};
struct MeshInstance {
const Mesh* mesh;
glm::mat4 transform{1.f};
}; };
ModelRenderSystem(const ModelRenderSystem&) = delete; ModelRenderSystem(const ModelRenderSystem&) = delete;
@@ -25,7 +31,7 @@ class ModelRenderSystem {
~ModelRenderSystem(); ~ModelRenderSystem();
void render(vk::CommandBuffer commandBuffer, void render(vk::CommandBuffer commandBuffer,
std::span<const std::unique_ptr<Mesh>> meshes, std::span<const MeshInstance> instances,
const FrameInfo& frameInfo); const FrameInfo& frameInfo);
private: private:
+47
View File
@@ -0,0 +1,47 @@
#include <sophia/ui/debug_ui.hpp>
#include "panels/console_panel.hpp"
#include "panels/frame_stats_panel.hpp"
#include "panels/panel_registry_panel.hpp"
namespace sophia {
DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} {
// #ifndef NDEBUG
this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry);
this->frameStatsHandle = registry.emplace<FrameStatsPanel>();
this->consoleHandle = registry.emplace<ConsolePanel>();
// #endif // !NDEBUG
}
void DebugUI::show() {
showPanelRegistry(true);
showFrameStats(true);
showConsole(true);
}
void DebugUI::hide() {
showPanelRegistry(false);
showFrameStats(false);
showConsole(false);
}
void DebugUI::showPanelRegistry(bool show) {
if (registry.isValid(panelRegistryHandle)) {
registry.get(panelRegistryHandle).setVisible(show);
}
}
void DebugUI::showFrameStats(bool show) {
if (registry.isValid(frameStatsHandle)) {
registry.get(frameStatsHandle).setVisible(show);
}
}
void DebugUI::showConsole(bool show) {
if (registry.isValid(consoleHandle)) {
registry.get(consoleHandle).setVisible(show);
}
}
} // namespace sophia
+88
View File
@@ -0,0 +1,88 @@
#include <sophia/ui/panel_registry.hpp>
#include <sophia/util/assert.hpp>
namespace sophia {
namespace {
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
} // namespace
PanelHandle PanelRegistry::add(std::unique_ptr<Panel> panel) {
if (!freeList.empty()) {
u32 index = freeList.back();
freeList.pop_back();
slots[index].panel = std::move(panel);
return PanelHandle{index, slots[index].generation};
}
slots.push_back(Slot{std::move(panel), 0});
return PanelHandle{static_cast<u32>(slots.size() - 1), 0};
}
const Panel& PanelRegistry::get(PanelHandle handle) const {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
const Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle refers to a removed panel:", handle.index);
return *slot.panel;
}
void PanelRegistry::remove(PanelHandle handle) {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle already removed:", handle.index);
slot.panel.reset();
slot.generation = (slot.generation + 1) & GENERATION_MASK;
freeList.push_back(handle.index);
}
bool PanelRegistry::isValid(PanelHandle handle) const {
if (!handle.isValid() || handle.index >= slots.size()) {
return false;
}
const Slot& slot = slots[handle.index];
return slot.generation == handle.generation && slot.panel != nullptr;
}
u32 PanelRegistry::size() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel) {
count++;
}
}
return count;
}
u32 PanelRegistry::visibleCount() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
count++;
}
}
return count;
}
void PanelRegistry::update(const FrameInfo& frameInfo) {
for (auto& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
slot.panel->onUpdate(frameInfo);
}
}
}
} // namespace sophia
+136
View File
@@ -0,0 +1,136 @@
#include "console_panel.hpp"
#include <algorithm>
namespace sophia {
namespace {
constexpr ImVec4 COLOR_DEFAULT = ImVec4(0.85f, 0.85f, 0.85f, 1.0f);
constexpr ImVec4 COLOR_COMMAND = ImVec4(1.0f, 0.78f, 0.37f, 1.0f);
constexpr ImVec4 COLOR_ERROR = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
constexpr ImVec4 COLOR_INFO = ImVec4(0.4f, 0.7f, 1.0f, 1.0f);
} // namespace
void ConsolePanel::registerDefaultCommands() {
log("Console ready. Type 'help' for a list of commands.");
}
void ConsolePanel::log(const std::string& message) {
entries.push_back(Entry{message, COLOR_DEFAULT});
scrollToBottom = true;
}
void ConsolePanel::clear() { entries.clear(); }
void ConsolePanel::execute(const std::string& command) {
entries.push_back(Entry{"> " + command, COLOR_COMMAND});
scrollToBottom = true;
historyPos = -1;
history.erase(std::remove(history.begin(), history.end(), command),
history.end());
history.push_back(command);
if (command == "help") {
entries.push_back(
Entry{"Available commands: help, clear, history", COLOR_INFO});
} else if (command == "clear") {
clear();
} else if (command == "history") {
for (const std::string& past : history) {
entries.push_back(Entry{past, COLOR_DEFAULT});
}
} else if (commandCallback) {
commandCallback(*this, command);
} else {
entries.push_back(Entry{"Unknown command: '" + command + "'", COLOR_ERROR});
}
}
int ConsolePanel::textEditCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag != ImGuiInputTextFlags_CallbackHistory) {
return 0;
}
const int previousPos = historyPos;
if (data->EventKey == ImGuiKey_UpArrow) {
if (historyPos == -1) {
historyPos = static_cast<int>(history.size()) - 1;
} else if (historyPos > 0) {
historyPos--;
}
} else if (data->EventKey == ImGuiKey_DownArrow) {
if (historyPos != -1 && ++historyPos >= static_cast<int>(history.size())) {
historyPos = -1;
}
}
if (previousPos != historyPos) {
const std::string& historyStr =
historyPos >= 0 ? history[static_cast<size_t>(historyPos)] : "";
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, historyStr.c_str());
}
return 0;
}
void ConsolePanel::onUpdate(const FrameInfo&) {
ImGui::Begin("Console");
if (ImGui::SmallButton("Clear")) {
clear();
}
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &autoScroll);
ImGui::Separator();
const float footerHeight =
ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
ImGui::BeginChild("ScrollRegion", ImVec2(0, -footerHeight), false,
ImGuiWindowFlags_HorizontalScrollbar);
for (const Entry& entry : entries) {
ImGui::PushStyleColor(ImGuiCol_Text, entry.color);
ImGui::TextUnformatted(entry.text.c_str());
ImGui::PopStyleColor();
}
if (scrollToBottom ||
(autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
ImGui::SetScrollHereY(1.0f);
}
scrollToBottom = false;
ImGui::EndChild();
ImGui::Separator();
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackHistory;
bool reclaimFocus = false;
if (ImGui::InputText(
"Input", inputBuffer, sizeof(inputBuffer), inputFlags,
[](ImGuiInputTextCallbackData* data) -> int {
auto* console = static_cast<ConsolePanel*>(data->UserData);
return console->textEditCallback(data);
},
this)) {
std::string command{inputBuffer};
if (!command.empty()) {
execute(command);
}
inputBuffer[0] = '\0';
reclaimFocus = true;
}
ImGui::SetItemDefaultFocus();
if (reclaimFocus) {
ImGui::SetKeyboardFocusHere(-1);
}
ImGui::End();
}
} // namespace sophia
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <imgui.h>
#include <functional>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
namespace sophia {
class ConsolePanel : public Panel {
public:
using CommandCallback =
std::function<void(ConsolePanel&, const std::string&)>;
ConsolePanel() : Panel{"Console"} { registerDefaultCommands(); }
void onUpdate(const FrameInfo& frameInfo) override;
void log(const std::string& message);
void clear();
void setCommandCallback(CommandCallback callback) {
this->commandCallback = std::move(callback);
}
private:
struct Entry {
std::string text;
ImVec4 color;
};
void registerDefaultCommands();
void execute(const std::string& command);
int textEditCallback(struct ImGuiInputTextCallbackData* data);
std::vector<Entry> entries;
std::vector<std::string> history;
int historyPos = -1;
char inputBuffer[256] = {};
bool autoScroll = true;
bool scrollToBottom = false;
CommandCallback commandCallback;
};
} // namespace sophia
@@ -0,0 +1,57 @@
#include "ecs_inspector_panel.hpp"
#include <imgui.h>
#include <sophia/components.hpp>
namespace sophia {
void EcsInspectorPanel::onUpdate(const FrameInfo&) {
ImGui::Begin("ECS Inspector");
entityScratch.clear();
for (entt::entity entity : registry.storage<entt::entity>()) {
entityScratch.push_back(entity);
}
ImGui::BeginChild("EntityList", ImVec2(0, 200), true);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(entityScratch.size()));
while (clipper.Step()) {
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
entt::entity entity = entityScratch[static_cast<size_t>(i)];
const TagComponent* tag = registry.try_get<TagComponent>(entity);
std::string label = tag ? tag->tag : "Entity";
ImGui::PushID(static_cast<int>(entt::to_integral(entity)));
if (ImGui::Selectable(label.c_str(), selected == entity)) {
selected = entity;
}
ImGui::PopID();
}
}
ImGui::EndChild();
ImGui::Separator();
if (registry.valid(selected)) {
if (auto* transform = registry.try_get<TransformComponent>(selected)) {
ImGui::DragFloat3("Position", &transform->position.x, 0.1f);
ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f);
ImGui::Text("Rotation: %.2f, %.2f, %.2f, %.2f", transform->rotation.w,
transform->rotation.x, transform->rotation.y,
transform->rotation.z);
}
if (auto* mesh = registry.try_get<MeshComponent>(selected)) {
ImGui::Text("Mesh: index=%u generation=%u", mesh->handle.index,
mesh->handle.generation);
}
} else {
ImGui::TextDisabled("No entity selected");
}
ImGui::End();
}
} // namespace sophia
@@ -0,0 +1,23 @@
#pragma once
#include <entt/entt.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <vector>
namespace sophia {
class EcsInspectorPanel : public Panel {
public:
explicit EcsInspectorPanel(entt::registry& registry)
: Panel{"ECS Inspector"}, registry{registry} {}
void onUpdate(const FrameInfo& frameInfo) override;
private:
entt::registry& registry;
entt::entity selected = entt::null;
std::vector<entt::entity> entityScratch;
};
} // namespace sophia
@@ -0,0 +1,44 @@
#pragma once
#include <imgui.h>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
namespace sophia {
class FrameStatsPanel : public Panel {
public:
FrameStatsPanel() : Panel{"Frame Stats"} {}
void onUpdate(const FrameInfo& frameInfo) override {
frameCount++;
accumulatedTime += frameInfo.deltaTime;
if (frameInfo.elapsedTime >= nextRefreshTime) {
displayedFps = static_cast<double>(frameCount) / accumulatedTime;
displayedFrameTimeMs =
(accumulatedTime / static_cast<double>(frameCount)) * 1000.0;
frameCount = 0;
accumulatedTime = 0.0;
nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL;
}
ImGui::Begin("Frame Stats");
ImGui::Text("FPS: %.1f", displayedFps);
ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs);
ImGui::End();
}
private:
static constexpr double REFRESH_INTERVAL = 1.0;
double accumulatedTime = 0.0;
u32 frameCount = 0;
double nextRefreshTime = 0.0;
double displayedFps = 0.0;
double displayedFrameTimeMs = 0.0;
};
} // namespace sophia
@@ -0,0 +1,28 @@
#include "panel_registry_panel.hpp"
#include <imgui.h>
namespace sophia {
void PanelRegistryPanel::onUpdate(const FrameInfo&) {
ImGui::Begin("PanelRegistry");
ImGui::Text("%u panels (%u visible)", registry.size(),
registry.visibleCount());
ImGui::Separator();
for (std::string_view name : registry.names()) {
Panel* panel = registry.find(name);
if (!panel || panel == this) {
continue;
}
bool visible = panel->isVisible();
if (ImGui::Checkbox(name.data(), &visible)) {
panel->setVisible(visible);
}
}
ImGui::End();
}
} // namespace sophia
@@ -0,0 +1,20 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/panel_registry.hpp>
namespace sophia {
class PanelRegistryPanel : public Panel {
public:
explicit PanelRegistryPanel(PanelRegistry& registry)
: Panel{"PanelRegistry"}, registry{registry} {}
void onUpdate(const FrameInfo& frameInfo) override;
private:
PanelRegistry& registry;
};
} // namespace sophia
+3
View File
@@ -0,0 +1,3 @@
add_executable(boids_2d main.cpp)
target_link_libraries(boids_2d PRIVATE ${PROJECT_NAME})
+248
View File
@@ -0,0 +1,248 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <random>
#include <sophia/application.hpp>
#include <sophia/event.hpp>
#include <sophia/key_event.hpp>
#include <stdexcept>
#include <string>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
struct Boid {
glm::vec2 position;
glm::vec2 velocity;
};
constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25;
class Boids2D : public sophia::Application {
public:
static constexpr float SCROLL_SPEED = 200.0f;
// World is a fixed box independent of the camera; boids wrap at its edges.
static constexpr float WORLD_HALF_WIDTH = 1000.0f;
static constexpr float WORLD_HALF_HEIGHT = 1000.0f;
static constexpr float MAX_SPEED = 120.0f;
static constexpr float MAX_FORCE = 200.0f;
static constexpr float PERCEPTION_RADIUS = 150.0f;
static constexpr float SEPARATION_RADIUS = 40.0f;
static constexpr float ALIGNMENT_WEIGHT = 1.0f;
static constexpr float COHESION_WEIGHT = 1.0f;
static constexpr float SEPARATION_WEIGHT = 1.5f;
static constexpr float BOID_LENGTH = 20.0f;
static constexpr float BOID_WIDTH = 12.0f;
// Neighbor lookups use a uniform grid instead of an O(n^2) scan. Cell size
// equals PERCEPTION_RADIUS so that a boid's neighbors can only ever fall in
// its own cell or the 8 cells surrounding it.
static constexpr float CELL_SIZE = PERCEPTION_RADIUS;
static constexpr int GRID_WIDTH =
static_cast<int>(2.0f * WORLD_HALF_WIDTH / CELL_SIZE) + 1;
static constexpr int GRID_HEIGHT =
static_cast<int>(2.0f * WORLD_HALF_HEIGHT / CELL_SIZE) + 1;
Boids2D() : Application{{.width = 750, .height = 1000, .name = "Boids2D"}} {
uint32_t numberOfBoids = DEFAULT_NUMBER_OF_BOIDS;
if (args.size() > 1) {
numberOfBoids = static_cast<uint32_t>(std::stoul(args[1]));
}
boids.resize(numberOfBoids);
sophia::EventSystem::get().addListener<sophia::KeyPressedEvent>(
[this](sophia::KeyPressedEvent& e) {
setScrollKeyHeld(e.getKeyCode(), true);
});
sophia::EventSystem::get().addListener<sophia::KeyReleasedEvent>(
[this](sophia::KeyReleasedEvent& e) {
setScrollKeyHeld(e.getKeyCode(), false);
});
}
void onCreate() override {
std::cout << "Boids2D created" << std::endl;
std::mt19937 rng{std::random_device{}()};
std::uniform_real_distribution<float> posX{-WORLD_HALF_WIDTH,
WORLD_HALF_WIDTH};
std::uniform_real_distribution<float> posY{-WORLD_HALF_HEIGHT,
WORLD_HALF_HEIGHT};
std::uniform_real_distribution<float> angle{0.0f, glm::two_pi<float>()};
for (Boid& boid : boids) {
boid.position = {posX(rng), posY(rng)};
float theta = angle(rng);
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 = assets().loadMesh(std::move(vertices), {});
}
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;
updateBoids(static_cast<float>(frameInfo.deltaTime));
for (const Boid& boid : boids) {
drawBoid(boid);
}
}
private:
// Maps a world position to its grid cell, clamped to the grid bounds.
glm::ivec2 cellOf(glm::vec2 position) const {
int cx = static_cast<int>((position.x + WORLD_HALF_WIDTH) / CELL_SIZE);
int cy = static_cast<int>((position.y + WORLD_HALF_HEIGHT) / CELL_SIZE);
return {std::clamp(cx, 0, GRID_WIDTH - 1),
std::clamp(cy, 0, GRID_HEIGHT - 1)};
}
void rebuildGrid() {
for (std::vector<uint32_t>& cell : grid) cell.clear();
for (uint32_t i = 0; i < boids.size(); ++i) {
glm::ivec2 cell = cellOf(boids[i].position);
grid[cell.y * GRID_WIDTH + cell.x].push_back(i);
}
}
void updateBoids(float deltaTime) {
rebuildGrid();
std::vector<glm::vec2> accelerations(boids.size());
for (size_t i = 0; i < boids.size(); ++i) {
glm::vec2 separation{0.0f};
glm::vec2 averageVelocity{0.0f};
glm::vec2 averagePosition{0.0f};
int neighborCount = 0;
glm::ivec2 cell = cellOf(boids[i].position);
for (int cy = std::max(cell.y - 1, 0);
cy <= std::min(cell.y + 1, GRID_HEIGHT - 1); ++cy) {
for (int cx = std::max(cell.x - 1, 0);
cx <= std::min(cell.x + 1, GRID_WIDTH - 1); ++cx) {
for (uint32_t j : grid[cy * GRID_WIDTH + cx]) {
if (j == i) continue;
glm::vec2 offset = boids[i].position - boids[j].position;
float dist = glm::length(offset);
if (dist >= PERCEPTION_RADIUS) continue;
averageVelocity += boids[j].velocity;
averagePosition += boids[j].position;
++neighborCount;
if (dist > 0.0f && dist < SEPARATION_RADIUS) {
separation += offset / (dist * dist);
}
}
}
}
glm::vec2 acceleration =
steerTowards(boids[i], separation) * SEPARATION_WEIGHT;
if (neighborCount > 0) {
averageVelocity /= static_cast<float>(neighborCount);
acceleration +=
steerTowards(boids[i], averageVelocity) * ALIGNMENT_WEIGHT;
averagePosition /= static_cast<float>(neighborCount);
glm::vec2 towardsGroup = averagePosition - boids[i].position;
acceleration += steerTowards(boids[i], towardsGroup) * COHESION_WEIGHT;
}
accelerations[i] = acceleration;
}
for (size_t i = 0; i < boids.size(); ++i) {
Boid& boid = boids[i];
boid.velocity += accelerations[i] * deltaTime;
float speed = glm::length(boid.velocity);
if (speed > MAX_SPEED) {
boid.velocity = boid.velocity / speed * MAX_SPEED;
}
boid.position += boid.velocity * deltaTime;
wrapPosition(boid.position);
}
}
// Steers boid towards desiredDirection at up to MAX_SPEED, clamped to
// MAX_FORCE.
glm::vec2 steerTowards(const Boid& boid, glm::vec2 desiredDirection) {
float directionLength = glm::length(desiredDirection);
if (directionLength < 1e-5f) return glm::vec2{0.0f};
glm::vec2 desired = desiredDirection / directionLength * MAX_SPEED;
glm::vec2 steer = desired - boid.velocity;
float steerLength = glm::length(steer);
if (steerLength > MAX_FORCE) {
steer = steer / steerLength * MAX_FORCE;
}
return steer;
}
void wrapPosition(glm::vec2& position) {
if (position.x > WORLD_HALF_WIDTH) position.x = -WORLD_HALF_WIDTH;
if (position.x < -WORLD_HALF_WIDTH) position.x = WORLD_HALF_WIDTH;
if (position.y > WORLD_HALF_HEIGHT) position.y = -WORLD_HALF_HEIGHT;
if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT;
}
void drawBoid(const Boid& boid) {
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));
drawMesh(boidMesh, transform);
}
void setScrollKeyHeld(sophia::KeyCode keycode, bool held) {
if (keycode == sophia::Key::Left) scrollLeft = held;
if (keycode == sophia::Key::Right) scrollRight = held;
if (keycode == sophia::Key::Up) scrollUp = held;
if (keycode == sophia::Key::Down) scrollDown = held;
}
std::vector<Boid> boids;
std::vector<std::vector<uint32_t>> grid{
static_cast<size_t>(GRID_WIDTH * GRID_HEIGHT)};
sophia::MeshHandle boidMesh{};
bool scrollLeft = false;
bool scrollRight = false;
bool scrollUp = false;
bool scrollDown = false;
};
SOPHIA_MAIN(Boids2D)
+3 -186
View File
@@ -1,200 +1,17 @@
#include <cmath>
#include <iostream> #include <iostream>
#include <random>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/event.hpp>
#include <sophia/key_event.hpp>
#include <stdexcept>
#include <string>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
struct Boid {
glm::vec2 position;
glm::vec2 velocity;
};
constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25;
class Testbed : public sophia::Application { class Testbed : public sophia::Application {
public: public:
static constexpr float SCROLL_SPEED = 200.0f; Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
// World is a fixed box independent of the camera; boids wrap at its edges.
static constexpr float WORLD_HALF_WIDTH = 375.0f;
static constexpr float WORLD_HALF_HEIGHT = 500.0f;
static constexpr float MAX_SPEED = 120.0f;
static constexpr float MAX_FORCE = 200.0f;
static constexpr float PERCEPTION_RADIUS = 150.0f;
static constexpr float SEPARATION_RADIUS = 40.0f;
static constexpr float ALIGNMENT_WEIGHT = 1.0f;
static constexpr float COHESION_WEIGHT = 1.0f;
static constexpr float SEPARATION_WEIGHT = 1.5f;
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {
uint32_t numberOfBoids = DEFAULT_NUMBER_OF_BOIDS;
if (args.size() > 1) {
numberOfBoids = static_cast<uint32_t>(std::stoul(args[1]));
}
boids.resize(numberOfBoids);
sophia::EventSystem::get().addListener<sophia::KeyPressedEvent>(
[this](sophia::KeyPressedEvent& e) {
setScrollKeyHeld(e.getKeyCode(), true);
});
sophia::EventSystem::get().addListener<sophia::KeyReleasedEvent>(
[this](sophia::KeyReleasedEvent& e) {
setScrollKeyHeld(e.getKeyCode(), false);
});
}
void onCreate() override { void onCreate() override {
std::cout << "Testbed created" << std::endl; std::cout << "Testbed created" << std::endl;
std::mt19937 rng{std::random_device{}()}; debugUI().show();
std::uniform_real_distribution<float> posX{-WORLD_HALF_WIDTH,
WORLD_HALF_WIDTH};
std::uniform_real_distribution<float> posY{-WORLD_HALF_HEIGHT,
WORLD_HALF_HEIGHT};
std::uniform_real_distribution<float> angle{0.0f, glm::two_pi<float>()};
for (Boid& boid : boids) {
boid.position = {posX(rng), posY(rng)};
float theta = angle(rng);
boid.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED;
}
} }
void onUpdate(const sophia::FrameInfo& frameInfo) override { 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;
updateBoids(static_cast<float>(frameInfo.deltaTime));
canvas.clear();
for (const Boid& boid : boids) {
drawBoid(boid);
}
}
private:
void updateBoids(float deltaTime) {
std::vector<glm::vec2> accelerations(boids.size());
for (size_t i = 0; i < boids.size(); ++i) {
glm::vec2 separation{0.0f};
glm::vec2 averageVelocity{0.0f};
glm::vec2 averagePosition{0.0f};
int neighborCount = 0;
for (size_t j = 0; j < boids.size(); ++j) {
if (i == j) continue;
glm::vec2 offset = boids[i].position - boids[j].position;
float dist = glm::length(offset);
if (dist >= PERCEPTION_RADIUS) continue;
averageVelocity += boids[j].velocity;
averagePosition += boids[j].position;
++neighborCount;
if (dist > 0.0f && dist < SEPARATION_RADIUS) {
separation += offset / (dist * dist);
}
}
glm::vec2 acceleration =
steerTowards(boids[i], separation) * SEPARATION_WEIGHT;
if (neighborCount > 0) {
averageVelocity /= static_cast<float>(neighborCount);
acceleration +=
steerTowards(boids[i], averageVelocity) * ALIGNMENT_WEIGHT;
averagePosition /= static_cast<float>(neighborCount);
glm::vec2 towardsGroup = averagePosition - boids[i].position;
acceleration += steerTowards(boids[i], towardsGroup) * COHESION_WEIGHT;
}
accelerations[i] = acceleration;
}
for (size_t i = 0; i < boids.size(); ++i) {
Boid& boid = boids[i];
boid.velocity += accelerations[i] * deltaTime;
float speed = glm::length(boid.velocity);
if (speed > MAX_SPEED) {
boid.velocity = boid.velocity / speed * MAX_SPEED;
}
boid.position += boid.velocity * deltaTime;
wrapPosition(boid.position);
}
}
// Steers boid towards desiredDirection at up to MAX_SPEED, clamped to
// MAX_FORCE.
glm::vec2 steerTowards(const Boid& boid, glm::vec2 desiredDirection) {
float directionLength = glm::length(desiredDirection);
if (directionLength < 1e-5f) return glm::vec2{0.0f};
glm::vec2 desired = desiredDirection / directionLength * MAX_SPEED;
glm::vec2 steer = desired - boid.velocity;
float steerLength = glm::length(steer);
if (steerLength > MAX_FORCE) {
steer = steer / steerLength * MAX_FORCE;
}
return steer;
}
void wrapPosition(glm::vec2& position) {
if (position.x > WORLD_HALF_WIDTH) position.x = -WORLD_HALF_WIDTH;
if (position.x < -WORLD_HALF_WIDTH) position.x = WORLD_HALF_WIDTH;
if (position.y > WORLD_HALF_HEIGHT) position.y = -WORLD_HALF_HEIGHT;
if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT;
}
void drawBoid(const Boid& boid) {
constexpr float LENGTH = 20.0f;
constexpr float WIDTH = 12.0f;
glm::vec2 direction = boid.velocity;
float speed = glm::length(direction);
direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f};
glm::vec2 perpendicular{-direction.y, direction.x};
glm::vec2 nose = boid.position + direction * (LENGTH * 0.5f);
glm::vec2 tailLeft = boid.position - direction * (LENGTH * 0.5f) +
perpendicular * (WIDTH * 0.5f);
glm::vec2 tailRight = boid.position - direction * (LENGTH * 0.5f) -
perpendicular * (WIDTH * 0.5f);
canvas.triangle(nose, tailLeft, tailRight, {1.0f, 1.0f, 1.0f});
}
void setScrollKeyHeld(sophia::KeyCode keycode, bool held) {
if (keycode == sophia::Key::Left) scrollLeft = held;
if (keycode == sophia::Key::Right) scrollRight = held;
if (keycode == sophia::Key::Up) scrollUp = held;
if (keycode == sophia::Key::Down) scrollDown = held;
}
std::vector<Boid> boids;
bool scrollLeft = false;
bool scrollRight = false;
bool scrollUp = false;
bool scrollDown = false;
}; };
SOPHIA_MAIN(Testbed) SOPHIA_MAIN(Testbed)