Compare commits
10 Commits
| 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 <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
|
||||||
|
|||||||
@@ -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:
|
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;
|
||||||
|
|||||||
@@ -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_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
|
||||||
|
|||||||
@@ -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);
|
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,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
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)");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 <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)
|
||||||
|
|||||||
Reference in New Issue
Block a user