Compare commits
10 Commits
9aef3f57fc
...
8824f64225
| Author | SHA1 | Date | |
|---|---|---|---|
| 8824f64225 | |||
| a6b98e0613 | |||
| b16507c61d | |||
| 1229f5c017 | |||
| 34faf8b61e | |||
| c97db37b31 | |||
| 05fe3c2ee8 | |||
| 12a1e6eca9 | |||
| 6e2c6c0fc5 | |||
| 4190ff847b |
@@ -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?"
|
||||
@@ -3,13 +3,20 @@
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sophia/asset_registry.hpp>
|
||||
#include <sophia/camera_2d.hpp>
|
||||
#include <sophia/canvas.hpp>
|
||||
#include <sophia/frame_info.hpp>
|
||||
#include <sophia/types.hpp>
|
||||
#include <sophia/ui/debug_ui.hpp>
|
||||
#include <sophia/ui/panel_registry.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define GLM_FORCE_RADIANS
|
||||
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace sophia {
|
||||
|
||||
class Application {
|
||||
@@ -23,7 +30,7 @@ class Application {
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
Application(const Config& config);
|
||||
explicit Application(const Config& config);
|
||||
virtual ~Application();
|
||||
|
||||
void run();
|
||||
@@ -36,10 +43,17 @@ class Application {
|
||||
virtual void onRender() {}
|
||||
virtual void onDestroy() {}
|
||||
|
||||
AssetRegistry& assets() { return this->assetRegistry; }
|
||||
PanelRegistry& panels() { return this->panelRegistry; }
|
||||
|
||||
DebugUI& debugUI() { return this->_debugUI; }
|
||||
|
||||
Canvas canvas;
|
||||
Camera2D camera;
|
||||
std::vector<std::string> args;
|
||||
|
||||
void drawMesh(MeshHandle handle, const glm::mat4& transform);
|
||||
|
||||
private:
|
||||
friend class Canvas;
|
||||
|
||||
@@ -48,6 +62,11 @@ class Application {
|
||||
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
|
||||
AssetRegistry assetRegistry;
|
||||
PanelRegistry panelRegistry;
|
||||
|
||||
DebugUI _debugUI;
|
||||
};
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
@@ -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
|
||||
@@ -33,7 +33,7 @@ class Canvas {
|
||||
private:
|
||||
friend class Application;
|
||||
|
||||
Canvas(Application* application);
|
||||
explicit Canvas(Application* application);
|
||||
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -10,10 +10,13 @@
|
||||
|
||||
#define GLM_FORCE_RADIANS
|
||||
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||
#include <imgui.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "buffer.hpp"
|
||||
#include "device.hpp"
|
||||
#include "imgui_renderer.hpp"
|
||||
#include "mesh.hpp"
|
||||
#include "pipelines/graphics_pipeline.hpp"
|
||||
#include "render_context.hpp"
|
||||
@@ -49,72 +52,10 @@ class Application::Impl {
|
||||
device{&window},
|
||||
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() {
|
||||
ModelRenderSystem modelRenderSystem{device, renderContext};
|
||||
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
|
||||
renderContext.getImageCount()};
|
||||
|
||||
this->application.onCreate();
|
||||
|
||||
@@ -139,7 +80,6 @@ class Application::Impl {
|
||||
static_cast<float>(extent.height)),
|
||||
this->application.camera};
|
||||
this->application.onUpdate(frameInfo);
|
||||
this->application.onRender();
|
||||
this->application.camera.setProjection(
|
||||
-frameInfo.currentFramebufferExtent.x / 2.f,
|
||||
frameInfo.currentFramebufferExtent.x / 2.f,
|
||||
@@ -151,6 +91,11 @@ class Application::Impl {
|
||||
continue;
|
||||
}
|
||||
|
||||
imguiRenderer.beginFrame();
|
||||
|
||||
this->application.panelRegistry.update(frameInfo);
|
||||
this->application.onRender();
|
||||
|
||||
renderContext.transitionCurrentImage(
|
||||
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
|
||||
|
||||
@@ -183,7 +128,18 @@ class Application::Impl {
|
||||
commandBuffer.setViewport(0, viewport);
|
||||
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();
|
||||
|
||||
@@ -224,6 +180,12 @@ class Application::Impl {
|
||||
this->meshes.clear();
|
||||
}
|
||||
|
||||
void drawMesh(const Mesh& mesh, const glm::mat4& transform) {
|
||||
this->drawCalls.push_back({&mesh, transform});
|
||||
}
|
||||
|
||||
Device& getDevice() { return device; }
|
||||
|
||||
private:
|
||||
Application& application;
|
||||
const Application::Config config;
|
||||
@@ -232,6 +194,7 @@ class Application::Impl {
|
||||
RenderContext renderContext;
|
||||
|
||||
std::vector<std::unique_ptr<Mesh>> meshes;
|
||||
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
|
||||
};
|
||||
|
||||
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)
|
||||
: impl{std::make_unique<Impl>(*this, config)},
|
||||
canvas{this},
|
||||
args{std::move(commandLineArgs)} {}
|
||||
args{std::move(commandLineArgs)},
|
||||
assetRegistry{impl->getDevice()},
|
||||
_debugUI{this->panelRegistry} {}
|
||||
|
||||
Application::~Application() = default;
|
||||
|
||||
@@ -254,4 +219,8 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices,
|
||||
|
||||
void Application::clearMeshes() { impl->clearMeshes(); }
|
||||
|
||||
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
|
||||
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform);
|
||||
}
|
||||
|
||||
} // namespace sophia
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -36,7 +36,7 @@ Mesh::Mesh(Device& device, const Builder& builder) : device{device} {
|
||||
createIndexBuffers(builder.indices);
|
||||
}
|
||||
|
||||
Mesh::~Mesh() {}
|
||||
Mesh::~Mesh() = default;
|
||||
|
||||
void Mesh::bind(vk::CommandBuffer commandBuffer) const {
|
||||
vk::Buffer buffers[] = {this->vertexBuffer->getBuffer()};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <sophia/asset_registry.hpp>
|
||||
#include <sophia/types.hpp>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -142,8 +142,6 @@ void RenderContext::recreateSwapchain() {
|
||||
// Old fences referred to images owned by the now-replaced swapchain;
|
||||
// there's nothing valid left to wait on.
|
||||
this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{});
|
||||
|
||||
log::trace("recreated swapchain");
|
||||
}
|
||||
|
||||
void RenderContext::createFrames() {
|
||||
|
||||
@@ -45,6 +45,9 @@ class RenderContext {
|
||||
vk::Format getImageFormat() const {
|
||||
return this->swapchain->getImageFormat();
|
||||
}
|
||||
u32 getImageCount() const {
|
||||
return static_cast<u32>(this->swapchain->imageCount());
|
||||
}
|
||||
|
||||
private:
|
||||
void recreateSwapchain();
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace sophia {
|
||||
|
||||
class Scene {
|
||||
public:
|
||||
Scene();
|
||||
~Scene();
|
||||
|
||||
private:
|
||||
entt::registry registry;
|
||||
};
|
||||
|
||||
} // namespace sophia
|
||||
@@ -186,7 +186,7 @@ vk::PresentModeKHR Swapchain::choosePresentMode(
|
||||
bestMode = availablePresentMode;
|
||||
}
|
||||
}
|
||||
log::warning("Present mode MAILBOX not available.");
|
||||
log::trace("Present mode MAILBOX not available.");
|
||||
|
||||
if (bestMode == vk::PresentModeKHR::eImmediate) {
|
||||
log::info("Selected present mode: IMMEDIATE (no vsync)");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#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.vert.spv.h"
|
||||
@@ -12,7 +12,7 @@ ModelRenderSystem::ModelRenderSystem(Device& device,
|
||||
: device{device}, renderContext{renderContext} {
|
||||
createPipelineLayout();
|
||||
|
||||
assert(this->pipelineLayout.get() != nullptr &&
|
||||
SOPHIA_ASSERT(this->pipelineLayout.get() != nullptr,
|
||||
"Cannot create pipeline before pipeline layout");
|
||||
|
||||
auto vertexShader = Shader::Builder()
|
||||
@@ -42,23 +42,23 @@ ModelRenderSystem::~ModelRenderSystem() {
|
||||
}
|
||||
|
||||
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
|
||||
std::span<const std::unique_ptr<Mesh>> meshes,
|
||||
std::span<const MeshInstance> instances,
|
||||
const FrameInfo& frameInfo) {
|
||||
glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
|
||||
|
||||
this->graphicsPipeline->bind(commandBuffer);
|
||||
|
||||
for (const std::unique_ptr<Mesh>& mesh : meshes) {
|
||||
for (const MeshInstance& instance : instances) {
|
||||
PushConstantData push{};
|
||||
push.modelMatrix = viewProjection;
|
||||
push.transform = viewProjection * instance.transform;
|
||||
|
||||
commandBuffer.pushConstants(
|
||||
this->pipelineLayout.get(),
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
|
||||
0, sizeof(PushConstantData), &push);
|
||||
|
||||
mesh->bind(commandBuffer);
|
||||
mesh->draw(commandBuffer);
|
||||
instance.mesh->bind(commandBuffer);
|
||||
instance.mesh->draw(commandBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,15 @@ namespace sophia {
|
||||
|
||||
class ModelRenderSystem {
|
||||
public:
|
||||
// Layout must match the shader's `Push` block in model_render_system.vert.
|
||||
struct PushConstantData {
|
||||
glm::mat4 modelMatrix{1.f};
|
||||
glm::mat4 normalMatrix{1.f};
|
||||
glm::mat4 transform{1.f};
|
||||
glm::vec4 color{1.f};
|
||||
};
|
||||
|
||||
struct MeshInstance {
|
||||
const Mesh* mesh;
|
||||
glm::mat4 transform{1.f};
|
||||
};
|
||||
|
||||
ModelRenderSystem(const ModelRenderSystem&) = delete;
|
||||
@@ -25,7 +31,7 @@ class ModelRenderSystem {
|
||||
~ModelRenderSystem();
|
||||
|
||||
void render(vk::CommandBuffer commandBuffer,
|
||||
std::span<const std::unique_ptr<Mesh>> meshes,
|
||||
std::span<const MeshInstance> instances,
|
||||
const FrameInfo& frameInfo);
|
||||
|
||||
private:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
add_executable(boids_2d main.cpp)
|
||||
|
||||
target_link_libraries(boids_2d PRIVATE ${PROJECT_NAME})
|
||||
@@ -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
@@ -1,200 +1,17 @@
|
||||
#include <cmath>
|
||||
#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>
|
||||
|
||||
struct Boid {
|
||||
glm::vec2 position;
|
||||
glm::vec2 velocity;
|
||||
};
|
||||
|
||||
constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25;
|
||||
|
||||
class Testbed : 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 = 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);
|
||||
});
|
||||
}
|
||||
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
|
||||
|
||||
void onCreate() override {
|
||||
std::cout << "Testbed 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;
|
||||
}
|
||||
debugUI().show();
|
||||
}
|
||||
|
||||
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;
|
||||
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
||||
};
|
||||
|
||||
SOPHIA_MAIN(Testbed)
|
||||
|
||||
Reference in New Issue
Block a user