diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 89d7e29..31a89a6 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -10,6 +10,10 @@ #include #include +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include + namespace sophia { class Application { @@ -40,6 +44,13 @@ class Application { Camera2D camera; std::vector args; + // Creates a mesh that persists across frames, for callers that want to + // draw the same geometry many times with different transforms (e.g. + // instancing) instead of resubmitting vertices every frame via Canvas. + MeshHandle createMesh(std::vector vertices, + std::vector indices); + void drawMesh(MeshHandle handle, const glm::mat4& transform); + private: friend class Canvas; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp new file mode 100644 index 0000000..61b9ac3 --- /dev/null +++ b/engine/include/sophia/components.hpp @@ -0,0 +1,35 @@ +#pragma once + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +#include +#include + +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 diff --git a/engine/include/sophia/types.hpp b/engine/include/sophia/types.hpp index 83fc851..f121532 100644 --- a/engine/include/sophia/types.hpp +++ b/engine/include/sophia/types.hpp @@ -17,4 +17,6 @@ using s64 = int64_t; using f32 = float; using f64 = double; +using MeshHandle = u32; + } // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 85275c7..3fee77b 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -183,7 +183,16 @@ class Application::Impl { commandBuffer.setViewport(0, viewport); commandBuffer.setScissor(0, scissor); - modelRenderSystem.render(commandBuffer, this->meshes, frameInfo); + std::vector instances; + instances.reserve(this->meshes.size() + this->drawCalls.size()); + for (const std::unique_ptr& 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(); commandBuffer.endRendering(); @@ -224,6 +233,25 @@ class Application::Impl { this->meshes.clear(); } + MeshHandle createMesh(std::vector vertices, + std::vector 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); + + this->persistentMeshes.push_back( + std::make_unique(this->device, builder)); + return static_cast(this->persistentMeshes.size() - 1); + } + + void drawMesh(MeshHandle handle, const glm::mat4& transform) { + this->drawCalls.push_back( + {this->persistentMeshes[handle].get(), transform}); + } + private: Application& application; const Application::Config config; @@ -232,6 +260,8 @@ class Application::Impl { RenderContext renderContext; std::vector> meshes; + std::vector> persistentMeshes; + std::vector drawCalls; }; void Application::setCommandLineArgs(int argc, const char* const* argv) { @@ -254,4 +284,13 @@ void Application::pushMesh(std::vector vertices, void Application::clearMeshes() { impl->clearMeshes(); } +MeshHandle Application::createMesh(std::vector vertices, + std::vector indices) { + return impl->createMesh(std::move(vertices), std::move(indices)); +} + +void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { + impl->drawMesh(handle, transform); +} + } // namespace sophia diff --git a/engine/src/assets/asset_manager.cpp b/engine/src/assets/asset_manager.cpp new file mode 100644 index 0000000..7ec4d74 --- /dev/null +++ b/engine/src/assets/asset_manager.cpp @@ -0,0 +1,24 @@ +#include "assets/asset_manager.hpp" + +#include "mesh.hpp" + +namespace sophia { + +AssetManager::AssetManager(Device& device) : device{device}, meshPool{device} {} + +template <> +MeshHandle AssetManager::create(const Mesh::Builder& builder) { + return meshPool.create(builder); +} + +template <> +const Mesh& AssetManager::get(MeshHandle handle) const { + return meshPool.get(handle); +} + +template <> +void AssetManager::release(MeshHandle handle) { + meshPool.release(handle); +} + +} // namespace sophia diff --git a/engine/src/assets/asset_manager.hpp b/engine/src/assets/asset_manager.hpp new file mode 100644 index 0000000..91abc30 --- /dev/null +++ b/engine/src/assets/asset_manager.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "device.hpp" +#include "pools/mesh_pool.hpp" + +namespace sophia { + +class AssetManager { + public: + explicit AssetManager(Device& device); + ~AssetManager() = default; + + AssetManager(const AssetManager&) = delete; + AssetManager& operator=(const AssetManager&) = delete; + + template + using HandleFor = typename T::HandleType; + + template + HandleFor load(const std::string& path); + + template + HandleFor create(const typename T::Builder& builder); + + template + const T& get(HandleFor handle) const; + + template + void release(HandleFor handle); + + private: + Device& device; + + MeshPool meshPool; + // TexturePool texturePool; + // MaterialPool materialPool; +}; + +} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.cpp b/engine/src/assets/pools/mesh_pool.cpp new file mode 100644 index 0000000..c7c4010 --- /dev/null +++ b/engine/src/assets/pools/mesh_pool.cpp @@ -0,0 +1,41 @@ +#include "assets/pools/mesh_pool.hpp" + +#include + +namespace sophia { + +MeshPool::MeshPool(Device& device) : device{device} {} + +MeshPool::~MeshPool() = default; + +MeshHandle MeshPool::create(const Mesh::Builder& builder) { + auto mesh = std::make_unique(device, builder); + + if (!freeList.empty()) { + MeshHandle handle = freeList.back(); + freeList.pop_back(); + meshes[handle] = std::move(mesh); + return handle; + } + + meshes.push_back(std::move(mesh)); + return static_cast(meshes.size() - 1); +} + +const Mesh& MeshPool::get(MeshHandle handle) const { + SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); + SOPHIA_ASSERT(meshes[handle] != nullptr, + "MeshHandle refers to a released mesh:", handle); + return *meshes[handle]; +} + +void MeshPool::release(MeshHandle handle) { + SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); + SOPHIA_ASSERT(meshes[handle] != nullptr, + "MeshHandle already released:", handle); + + meshes[handle].reset(); + freeList.push_back(handle); +} + +} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.hpp b/engine/src/assets/pools/mesh_pool.hpp new file mode 100644 index 0000000..09845e5 --- /dev/null +++ b/engine/src/assets/pools/mesh_pool.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#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: + Device& device; + + std::vector> meshes; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/scene.cpp b/engine/src/scene.cpp new file mode 100644 index 0000000..865868f --- /dev/null +++ b/engine/src/scene.cpp @@ -0,0 +1,17 @@ +#include "scene.hpp" + +#include +#include + +namespace sophia { + +Scene::Scene() { + entt::entity entity = this->registry.create(); + + auto& a = this->registry.emplace(entity); + auto& b = this->registry.get(entity); +} + +Scene::~Scene() { log::trace("destroying sophia::Scene"); } + +} // namespace sophia diff --git a/engine/src/scene.hpp b/engine/src/scene.hpp new file mode 100644 index 0000000..ed40054 --- /dev/null +++ b/engine/src/scene.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace sophia { + +class Scene { + public: + Scene(); + ~Scene(); + + private: + entt::registry registry; +}; + +} // namespace sophia diff --git a/engine/src/systems/model_render_system.cpp b/engine/src/systems/model_render_system.cpp index b6b7e8a..699602c 100644 --- a/engine/src/systems/model_render_system.cpp +++ b/engine/src/systems/model_render_system.cpp @@ -1,6 +1,6 @@ #include "model_render_system.hpp" -#include +#include #include "shaders/model_render_system.frag.spv.h" #include "shaders/model_render_system.vert.spv.h" @@ -12,8 +12,8 @@ ModelRenderSystem::ModelRenderSystem(Device& device, : device{device}, renderContext{renderContext} { createPipelineLayout(); - assert(this->pipelineLayout.get() != nullptr && - "Cannot create pipeline before pipeline layout"); + SOPHIA_ASSERT(this->pipelineLayout.get() != nullptr, + "Cannot create pipeline before pipeline layout"); auto vertexShader = Shader::Builder() .fromSPIRV(shaders::model_render_system_vert_spv) @@ -42,23 +42,23 @@ ModelRenderSystem::~ModelRenderSystem() { } void ModelRenderSystem::render(vk::CommandBuffer commandBuffer, - std::span> meshes, + std::span instances, const FrameInfo& frameInfo) { glm::mat4 viewProjection = frameInfo.camera.getViewProjection(); this->graphicsPipeline->bind(commandBuffer); - for (const std::unique_ptr& 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); } } diff --git a/engine/src/systems/model_render_system.hpp b/engine/src/systems/model_render_system.hpp index 4d0e4be..93d70b8 100644 --- a/engine/src/systems/model_render_system.hpp +++ b/engine/src/systems/model_render_system.hpp @@ -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> meshes, + std::span instances, const FrameInfo& frameInfo); private: diff --git a/examples/boids_2d/CMakeLists.txt b/examples/boids_2d/CMakeLists.txt new file mode 100644 index 0000000..dce21a0 --- /dev/null +++ b/examples/boids_2d/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(boids_2d main.cpp) + +target_link_libraries(boids_2d PRIVATE ${PROJECT_NAME}) diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp new file mode 100644 index 0000000..3d89b12 --- /dev/null +++ b/examples/boids_2d/main.cpp @@ -0,0 +1,248 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +#include + +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(2.0f * WORLD_HALF_WIDTH / CELL_SIZE) + 1; + static constexpr int GRID_HEIGHT = + static_cast(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(std::stoul(args[1])); + } + boids.resize(numberOfBoids); + + sophia::EventSystem::get().addListener( + [this](sophia::KeyPressedEvent& e) { + setScrollKeyHeld(e.getKeyCode(), true); + }); + sophia::EventSystem::get().addListener( + [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 posX{-WORLD_HALF_WIDTH, + WORLD_HALF_WIDTH}; + std::uniform_real_distribution posY{-WORLD_HALF_HEIGHT, + WORLD_HALF_HEIGHT}; + std::uniform_real_distribution angle{0.0f, glm::two_pi()}; + + 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 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 = createMesh(std::move(vertices), {}); + } + + void onUpdate(const sophia::FrameInfo& frameInfo) override { + float scrollDistance = + SCROLL_SPEED * static_cast(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(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((position.x + WORLD_HALF_WIDTH) / CELL_SIZE); + int cy = static_cast((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& 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 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(neighborCount); + acceleration += + steerTowards(boids[i], averageVelocity) * ALIGNMENT_WEIGHT; + + averagePosition /= static_cast(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 boids; + std::vector> grid{ + static_cast(GRID_WIDTH * GRID_HEIGHT)}; + sophia::MeshHandle boidMesh{}; + bool scrollLeft = false; + bool scrollRight = false; + bool scrollUp = false; + bool scrollDown = false; +}; + +SOPHIA_MAIN(Boids2D)