Added skeletal animations and other fixes

This commit is contained in:
2026-03-16 12:57:53 +01:00
parent 2cc18b3329
commit 28c6703892
51 changed files with 1964 additions and 602 deletions

View File

@@ -0,0 +1,189 @@
#include <destrum/Components/Animator.h>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
#include <destrum/ObjectModel/GameObject.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include "spdlog/spdlog.h"
Animator::Animator(GameObject& parent)
: Component(parent, "Animator") {}
// ─── Component interface ──────────────────────────────────────────────────────
void Animator::Update() {
if (!m_current.clip) return;
// Time delta comes from your engine's time system
// const float dt = Time::GetDeltaTime();
const float dt = 0.016f; // ~60 FPS
m_current.time += dt * m_current.speed;
if (m_current.clip->looped)
m_current.time = std::fmod(m_current.time, m_current.clip->duration);
else
m_current.time = std::min(m_current.time, m_current.clip->duration);
if (m_previous.clip) {
m_previous.time += dt * m_previous.speed;
if (m_previous.clip->looped)
m_previous.time = std::fmod(m_previous.time, m_previous.clip->duration);
m_blendT += dt / m_blendDuration;
if (m_blendT >= 1.f) {
m_blendT = 1.f;
m_previous = {};
}
}
spdlog::info("Playing '{}': time = {:.2f}s, blend = {:.2f}",
m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str(),
m_current.time,
m_blendT);
}
void Animator::ImGuiInspector() {
// ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str());
// ImGui::Text("Time: %.2f", m_current.time);
// ImGui::Text("Blend: %.2f", m_blendT);
//
// if (!isPlaying()) {
// ImGui::BeginDisabled();
// ImGui::Button("Stop");
// ImGui::EndDisabled();
// } else if (ImGui::Button("Stop")) {
// stop();
// }
//
// ImGui::Separator();
// ImGui::Text("Clips:");
// for (auto& [name, _] : m_clips) {
// if (ImGui::Selectable(name.c_str(), name == m_currentClipName))
// play(name);
// }
}
// ─── Animation control ────────────────────────────────────────────────────────
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
m_clips[clip->name] = std::move(clip);
}
void Animator::play(const std::string& name, float blendTime) {
auto it = m_clips.find(name);
if (it == m_clips.end()) return;
if (m_current.clip && blendTime > 0.f) {
m_previous = m_current;
m_blendT = 0.f;
m_blendDuration = blendTime;
} else {
m_previous = {};
m_blendT = 1.f;
}
m_current = { it->second.get(), 0.f, 1.f };
m_currentClipName = name;
}
void Animator::stop() {
m_current = {};
m_previous = {};
m_currentClipName = {};
}
// ─── Joint matrix upload ──────────────────────────────────────────────────────
std::size_t Animator::uploadJointMatrices(SkinningPipeline& pipeline,
const Skeleton& skeleton,
std::size_t frameIndex) {
auto matrices = computeJointMatrices(skeleton);
return pipeline.appendJointMatrices(matrices, frameIndex);
}
// ─── Private: pose evaluation ─────────────────────────────────────────────────
std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton) {
const std::size_t numJoints = skeleton.joints.size();
std::vector<glm::mat4> global(numJoints, glm::mat4{1.f});
std::vector<glm::mat4> result(numJoints, glm::mat4{1.f});
auto findTrack = [](const SkeletalAnimation* clip,
std::uint32_t idx) -> const SkeletalAnimation::Track* {
if (!clip) return nullptr;
for (auto& t : clip->tracks)
if (t.jointIndex == idx) return &t;
return nullptr;
};
for (std::uint32_t i = 0; i < numJoints; ++i) {
glm::vec3 tr = {0.f, 0.f, 0.f};
glm::quat rot = glm::identity<glm::quat>();
glm::vec3 sc = {1.f, 1.f, 1.f};
if (const auto* track = findTrack(m_current.clip, i)) {
tr = sampleTranslation(*track, m_current.time);
rot = sampleRotation (*track, m_current.time);
sc = sampleScale (*track, m_current.time);
}
if (m_previous.clip && m_blendT < 1.f) {
if (const auto* prev = findTrack(m_previous.clip, i)) {
tr = glm::mix (sampleTranslation(*prev, m_previous.time), tr, m_blendT);
rot = glm::slerp(sampleRotation (*prev, m_previous.time), rot, m_blendT);
sc = glm::mix (sampleScale (*prev, m_previous.time), sc, m_blendT);
}
}
glm::mat4 local = glm::translate(glm::mat4{1.f}, tr)
* glm::mat4_cast(rot)
* glm::scale(glm::mat4{1.f}, sc);
const int parent = skeleton.parentIndex[i];
global[i] = (parent < 0) ? local : global[parent] * local;
result[i] = global[i] * skeleton.inverseBindMatrices[i];
}
return result;
}
// ─── Keyframe sampling ────────────────────────────────────────────────────────
glm::vec3 Animator::sampleTranslation(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].translation;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::mix(kf[i].translation, kf[i + 1].translation, f);
}
}
return kf.back().translation;
}
glm::quat Animator::sampleRotation(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].rotation;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::slerp(kf[i].rotation, kf[i + 1].rotation, f);
}
}
return kf.back().rotation;
}
glm::vec3 Animator::sampleScale(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].scale;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::mix(kf[i].scale, kf[i + 1].scale, f);
}
}
return kf.back().scale;
}

View File

@@ -1,19 +1,56 @@
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/ObjectModel/Transform.h>
#include "destrum/Components/Animator.h"
#include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/GameState.h"
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
}
void MeshRendererComponent::Start() {
Component::Start();
if (auto* animator = GetGameObject()->GetComponent<Animator>()) {
const auto& gfxDevice = GameState::GetInstance().Gfx();
const auto& mesh = GameState::GetInstance().Renderer().GetMeshCache().getMesh(meshID);
m_skinnedMesh = std::make_unique<SkinnedMesh>();
m_skinnedMesh->skinnedVertexBuffer = gfxDevice.createBuffer(
mesh.numVertices * sizeof(CPUMesh::Vertex),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
);
}
}
void MeshRendererComponent::Update() {
}
void MeshRendererComponent::Render(const RenderContext& ctx) {
if (meshID != NULL_MESH_ID && materialID != NULL_MATERIAL_ID) {
if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return;
if (auto* animator = GetGameObject()->GetComponent<Animator>(); animator && m_skinnedMesh) {
const auto& mesh = ctx.renderer.GetMeshCache().getCPUMesh(meshID);
const auto skeleton = GetGameObject()->GetComponent<Animator>()->getSkeleton();
std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex();
const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices(
ctx.renderer.getSkinningPipeline(),
*skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh)
frameIdx
);
ctx.renderer.drawSkinnedMesh(
meshID,
GetTransform().GetWorldMatrix(),
materialID,
m_skinnedMesh.get(),
jointMatricesStartIndex
);
} else {
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
}
}

View File

@@ -98,13 +98,13 @@ void OrbitAndSpin::Update()
// GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
// material color
auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
mat.baseColor = glm::vec3(
0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
);
GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
// auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
// mat.baseColor = glm::vec3(
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
// );
// GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
}
void OrbitAndSpin::Start() {

View File

@@ -7,11 +7,11 @@
namespace
{
static const std::uint32_t maxBindlessResources = 16536;
static const std::uint32_t maxSamplers = 32;
constexpr std::uint32_t maxBindlessResources = 16536;
constexpr std::uint32_t maxSamplers = 32;
static const std::uint32_t texturesBinding = 0;
static const std::uint32_t samplersBinding = 1;
constexpr std::uint32_t texturesBinding = 0;
constexpr std::uint32_t samplersBinding = 1;
}
void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
@@ -26,7 +26,7 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT,
.maxSets = 10,
.poolSizeCount = (std::uint32_t)poolSizesBindless.size(),
.poolSizeCount = static_cast<std::uint32_t>(poolSizesBindless.size()),
.pPoolSizes = poolSizesBindless.data(),
};

View File

@@ -0,0 +1,105 @@
#include <destrum/Graphics/ComputePipeline.h>
#include <cassert>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "destrum/Util/GameState.h"
#include "spdlog/spdlog.h"
ComputePipeline::ComputePipeline(GfxDevice& device,
const std::string& compPath,
const ComputePipelineConfigInfo& configInfo)
: m_device(device) {
CreateComputePipeline(compPath, configInfo);
}
ComputePipeline::~ComputePipeline() {
if (m_compShaderModule != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_device.getDevice(), m_compShaderModule, nullptr);
}
if (m_computePipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device.getDevice(), m_computePipeline, nullptr);
}
}
void ComputePipeline::bind(VkCommandBuffer buffer) const {
vkCmdBindPipeline(buffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_computePipeline);
}
void ComputePipeline::DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo) {
configInfo.name = "DefaultComputePipelineConfigInfo";
configInfo.specializationInfo = nullptr;
configInfo.pipelineLayout = VK_NULL_HANDLE;
}
std::vector<char> ComputePipeline::readFile(const std::string& filename) {
std::ifstream file(filename, std::ios::ate | std::ios::binary);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
const size_t fileSize = static_cast<size_t>(file.tellg());
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), static_cast<std::streamsize>(fileSize));
file.close();
return buffer;
}
void ComputePipeline::CreateComputePipeline(const std::string& compPath,
const ComputePipelineConfigInfo& configInfo) {
assert(configInfo.pipelineLayout != VK_NULL_HANDLE && "no pipelineLayout provided in configInfo");
assert(!compPath.empty() && "Compute shader path is empty");
const std::string compFileName = std::filesystem::path(compPath).filename().string();
auto compCode = readFile(compPath);
spdlog::debug("Compute shader code size: {}", compCode.size());
spdlog::debug("Compute shader file: {}", compFileName);
CreateShaderModule(compCode, &m_compShaderModule);
VkPipelineShaderStageCreateInfo compStageInfo{};
compStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
compStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT;
compStageInfo.module = m_compShaderModule;
compStageInfo.pName = "main";
compStageInfo.pSpecializationInfo = configInfo.specializationInfo;
VkComputePipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
pipelineInfo.stage = compStageInfo;
pipelineInfo.layout = configInfo.pipelineLayout;
pipelineInfo.basePipelineIndex = -1;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
if (vkCreateComputePipelines(m_device.getDevice(),
VK_NULL_HANDLE,
1,
&pipelineInfo,
nullptr,
&m_computePipeline) != VK_SUCCESS) {
throw std::runtime_error("Can't make compute pipeline!");
}
if (!configInfo.name.empty()) {
vkutil::addDebugLabel(GameState::GetInstance().Gfx().getDevice(), m_computePipeline, configInfo.name.c_str());
}
}
void ComputePipeline::CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const {
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
if (vkCreateShaderModule(m_device.getDevice(), &createInfo, nullptr, shaderModule) != VK_SUCCESS) {
throw std::runtime_error("Failed to create shader module!");
}
}

View File

@@ -0,0 +1,128 @@
#include <destrum/Graphics/Frustum.h>
Frustum edge::createFrustumFromCamera(const Camera& camera) {
// TODO: write a non-horrible version of this, lol
Frustum frustum;
// if (camera.isOrthographic()) {
// const auto points = calculateFrustumCornersWorldSpace(camera);
//
// /*
// 5────────6
// ╱┊ ╱│
//
// 1──┼─────2 │
// │ ┊ (C) │ │ Y ╿ . Z
// │ 4┈┈┈┈┈│┈┈7 │
// │ X │
// │╱ │╱ ╾────┼
// 0--------3
//
// */
// // from bottom-left and moving CW...
// static const std::array<int, 4> near{0, 1, 2, 3};
// static const std::array<int, 4> far{7, 6, 5, 4};
// static const std::array<int, 4> left{4, 5, 1, 0};
// static const std::array<int, 4> right{3, 2, 6, 7};
// static const std::array<int, 4> bottom{4, 0, 3, 7};
// static const std::array<int, 4> top{5, 6, 2, 1};
//
// frustum.nearFace = {findCenter(points, near), findNormal(points, near)};
// frustum.farFace = {findCenter(points, far), findNormal(points, far)};
// frustum.leftFace = {findCenter(points, left), findNormal(points, left)};
// frustum.rightFace = {findCenter(points, right), findNormal(points, right)};
// frustum.bottomFace = {findCenter(points, bottom), findNormal(points, bottom)};
// frustum.topFace = {findCenter(points, top), findNormal(points, top)};
// } else {
const auto camPos = camera.GetPosition();
const auto camFront = camera.GetForward();
const auto camUp = camera.GetUp();
const auto camRight = camera.GetRight();
const auto zNear = camera.GetZNear();
const auto zFar = camera.GetZFar();
const auto halfVSide = zFar * tanf(camera.GetFOVY() * .5f);
const auto halfHSide = halfVSide * camera.GetAspectRatio();
const auto frontMultFar = zFar * camFront;
frustum.nearFace = {camPos + zNear * camFront, camFront};
frustum.farFace = {camPos + frontMultFar, -camFront};
frustum.leftFace = {camPos, glm::cross(camUp, frontMultFar + camRight * halfHSide)};
frustum.rightFace = {camPos, glm::cross(frontMultFar - camRight * halfHSide, camUp)};
frustum.bottomFace = {camPos, glm::cross(frontMultFar + camUp * halfVSide, camRight)};
frustum.topFace = {camPos, glm::cross(camRight, frontMultFar - camUp * halfVSide)};
// }
return frustum;
}
bool isOnOrForwardPlane(const Frustum::Plane& plane, const Sphere& sphere)
{
return plane.getSignedDistanceToPlane(sphere.center) > -sphere.radius;
}
bool edge::isInFrustum(const Frustum& frustum, const Sphere& s) {
return (
isOnOrForwardPlane(frustum.farFace, s) && isOnOrForwardPlane(frustum.nearFace, s) &&
isOnOrForwardPlane(frustum.leftFace, s) && isOnOrForwardPlane(frustum.rightFace, s) &&
isOnOrForwardPlane(frustum.topFace, s) && isOnOrForwardPlane(frustum.bottomFace, s));
}
bool edge::isInFrustum(const Frustum& frustum, const AABB& aabb) {
glm::vec3 vmin, vmax;
bool ret = true;
for (int i = 0; i < 6; ++i) {
const auto& plane = frustum.getPlane(i);
// X axis
if (plane.normal.x < 0) {
vmin.x = aabb.min.x;
vmax.x = aabb.max.x;
} else {
vmin.x = aabb.max.x;
vmax.x = aabb.min.x;
}
// Y axis
if (plane.normal.y < 0) {
vmin.y = aabb.min.y;
vmax.y = aabb.max.y;
} else {
vmin.y = aabb.max.y;
vmax.y = aabb.min.y;
}
// Z axis
if (plane.normal.z < 0) {
vmin.z = aabb.min.z;
vmax.z = aabb.max.z;
} else {
vmin.z = aabb.max.z;
vmax.z = aabb.min.z;
}
if (plane.getSignedDistanceToPlane(vmin) < 0) {
return false;
}
if (plane.getSignedDistanceToPlane(vmax) <= 0) {
ret = true;
}
}
return ret;
}
glm::vec3 getTransformScale(const glm::mat4& transform)
{
float sx = glm::length(glm::vec3{transform[0][0], transform[0][1], transform[0][2]});
float sy = glm::length(glm::vec3{transform[1][0], transform[1][1], transform[1][2]});
float sz = glm::length(glm::vec3{transform[2][0], transform[2][1], transform[2][2]});
return {sx, sy, sz};
}
Sphere edge::calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton) {
const auto scale = getTransformScale(transform);
float maxScale = std::max({scale.x, scale.y, scale.z});
if (hasSkeleton) {
maxScale = 5.f; // ignore scale for skeleton meshes (TODO: fix)
// setting scale to 1.f causes prolems with frustum culling
}
auto sphereWorld = s;
sphereWorld.radius *= maxScale;
sphereWorld.center = glm::vec3(transform * glm::vec4(sphereWorld.center, 1.f));
return sphereWorld;
}

View File

@@ -129,18 +129,36 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync)
auto& mainCommandBuffer = frames[i].commandBuffer;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
}
//
// { // create white texture
// std::uint32_t pixel = 0xFFFFFFFF;
// whiteImageId = createImage(
// {
// .format = VK_FORMAT_R8G8B8A8_UNORM,
// .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
// .extent = VkExtent3D{1, 1, 1},
// },
// "white texture",
// &pixel);
// }
{ // create white texture
std::uint32_t pixel = 0xFFFFFFFF;
whiteImageId = createImage(
{
.format = VK_FORMAT_R8G8B8A8_UNORM,
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
.extent = VkExtent3D{1, 1, 1},
},
"white texture",
&pixel);
}
{ // create error texture (black/magenta checker)
constexpr auto black = 0xFF000000;
constexpr auto magenta = 0xFFFF00FF;
std::array<std::uint32_t, 4> pixels{black, magenta, magenta, black};
errorImageId = createImage(
{
.format = VK_FORMAT_R8G8B8A8_UNORM,
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.extent = VkExtent3D{2, 2, 1},
},
"error texture",
pixels.data());
imageCache.setErrorImageId(errorImageId);
}
GameState::GetInstance().SetGfxDevice(this);
}
@@ -495,25 +513,14 @@ GPUImage GfxDevice::createImageRaw(
return image;
}
GPUImage GfxDevice::loadImageFromFileRaw(
const std::filesystem::path& path,
VkImageUsageFlags usage,
bool mipMap,
TextureIntent intent) const {
// 1) Decode file to CPU pixels (stb for png/jpg/hdr, tinyexr for exr)
// IMPORTANT: util::loadImage should fill:
// - width/height/channels(=4)
// - hdr + (pixels OR hdrPixels)
// - vkFormat (RGBA8_SRGB or RGBA8_UNORM or RGBA32F)
// - byteSize (exact bytes to upload)
GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const {
const auto data = util::loadImage(path, intent);
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 ||
(data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
spdlog::error("Failed to load image '{}'", path.string());
return getImage(errorImageId);
}
// 2) Create GPU image using the format the loader chose (matches CPU memory layout)
auto image = createImageRaw({
.format = data.vkFormat,
.usage = usage |
@@ -527,15 +534,10 @@ GPUImage GfxDevice::loadImageFromFileRaw(
.mipMap = mipMap,
});
// 3) Upload *exactly* byteSize bytes from the correct pointer
const void* src = data.hdr
? static_cast<const void*>(data.hdrPixels)
: static_cast<const void*>(data.pixels);
const void* src = data.hdr ? static_cast<const void*>(data.hdrPixels) : static_cast<const void*>(data.pixels);
// Use the "sized" upload to avoid BytesPerTexel mismatches
uploadImageDataSized(image, src, data.byteSize, 0);
// 4) Debug label
image.debugName = path.string();
vkutil::addDebugLabel(device, image.image, path.string().c_str());

View File

@@ -2,21 +2,12 @@
#include <destrum/Graphics/GfxDevice.h>
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice)
{}
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) {
}
ImageID ImageCache::loadImageFromFile(
const std::filesystem::path& path,
VkImageUsageFlags usage,
bool mipMap,
TextureIntent intent)
{
for (const auto& [id, info] : loadedImagesInfo) {
if (info.path == path &&
info.intent == intent &&
info.usage == usage &&
info.mipMap == mipMap)
{
ImageID ImageCache::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) {
for (const auto& [id, info]: loadedImagesInfo) {
if (info.path == path && info.intent == intent && info.usage == usage && info.mipMap == mipMap) {
return id;
}
}
@@ -41,13 +32,11 @@ ImageID ImageCache::loadImageFromFile(
return id;
}
ImageID ImageCache::addImage(GPUImage image)
{
ImageID ImageCache::addImage(GPUImage image) {
return addImage(getFreeImageId(), std::move(image));
}
ImageID ImageCache::addImage(ImageID id, GPUImage image)
{
ImageID ImageCache::addImage(ImageID id, GPUImage image) {
image.setBindlessId(static_cast<std::uint32_t>(id));
if (id != images.size()) {
images[id] = std::move(image); // replacing existing image
@@ -59,20 +48,17 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image)
return id;
}
const GPUImage& ImageCache::getImage(ImageID id) const
{
const GPUImage& ImageCache::getImage(ImageID id) const {
assert(id != NULL_IMAGE_ID && id < images.size());
return images.at(id);
}
ImageID ImageCache::getFreeImageId() const
{
ImageID ImageCache::getFreeImageId() const {
return images.size();
}
void ImageCache::destroyImages()
{
for (const auto& image : images) {
void ImageCache::destroyImages() {
for (const auto& image: images) {
gfxDevice.destroyImage(image);
}
images.clear();

View File

@@ -37,6 +37,8 @@ namespace util
{
ImageData data;
// ---------- EXR ----------
if (isExrExt(p)) {
float* out = nullptr;

View File

@@ -24,7 +24,7 @@ void MaterialCache::init(GfxDevice& gfxDevice)
&normal);
}
Material placeholderMaterial{.name = "PLACEHOLDER_MATERIAL"};
Material placeholderMaterial{.diffuseTexture = defaultNormalMapTextureID, .name = "PLACEHOLDER_MATERIAL"};
placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial);
}
@@ -41,13 +41,14 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
};
// store on GPU
MaterialData* data = (MaterialData*)materialDataBuffer.info.pMappedData;
auto whiteTextureID = gfxDevice.getWhiteTextureID();
auto id = getFreeMaterialId();
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
const auto whiteTextureID = gfxDevice.getWhiteTextureID();
const auto id = getFreeMaterialId();
assert(id < MAX_MATERIALS);
data[id] = MaterialData{
.baseColor = glm::vec4(material.baseColor, 1.0f),
.metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f},
.textureFilteringMode = static_cast<std::uint32_t>(material.textureFilteringMode),
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
.normalTex = whiteTextureID,
.metallicRoughnessTex = whiteTextureID,
@@ -60,6 +61,15 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
return id;
}
MaterialID MaterialCache::addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID) {
Material material{};
material.name = "idk";
material.diffuseTexture = textureID;
material.metallicFactor = 0.0f;
material.roughnessFactor = 1.0f;
return addMaterial(gfxDevice, material);
}
const Material& MaterialCache::getMaterial(MaterialID id) const
{
return materials.at(id);
@@ -100,7 +110,7 @@ void MaterialCache::updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id)
.baseColor = glm::vec4(material.baseColor, 1.0f),
.metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f),
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
.normalTex = whiteTextureID, // if you have this field
.normalTex = whiteTextureID,
.metallicRoughnessTex = whiteTextureID,
.emissiveTex = whiteTextureID,
};

View File

@@ -3,25 +3,30 @@
#include <destrum/Graphics/Resources/Mesh.h>
#include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Util.h>
#include <destrum/Util/MathUtils.h>
// #include <destrum/Math/Util.h>
MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
{
auto gpuMesh = GPUMesh{
.numVertices = (std::uint32_t)cpuMesh.vertices.size(),
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
.minPos = cpuMesh.minPos,
.maxPos = cpuMesh.maxPos,
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
.minPos = cpuMesh.minPos,
.maxPos = cpuMesh.maxPos,
.hasSkeleton = cpuMesh.skeleton.has_value(),
};
std::vector<glm::vec3> positions(cpuMesh.vertices.size());
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i) {
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i)
positions[i] = cpuMesh.vertices[i].position;
}
gpuMesh.boundingSphere = calculateBoundingSphere(positions);
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
const auto id = meshes.size();
meshes.push_back(std::move(gpuMesh));
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
return id;
}
@@ -70,6 +75,33 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
const auto idxBufferName = cpuMesh.name + " (idx)";
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str());
if (gpuMesh.hasSkeleton) {
// create skinning data buffer
const auto skinningDataSize = cpuMesh.vertices.size() * sizeof(CPUMesh::SkinningData);
gpuMesh.skinningDataBuffer = gfxDevice.createBuffer(
skinningDataSize,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
const auto staging =
gfxDevice.createBuffer(skinningDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
// copy data
void* data = staging.info.pMappedData;
memcpy(data, cpuMesh.skinningData.data(), skinningDataSize);
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
const auto vertexCopy = VkBufferCopy{
.srcOffset = 0,
.dstOffset = 0,
.size = skinningDataSize,
};
vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy);
});
gfxDevice.destroyBuffer(staging);
}
}
const GPUMesh& MeshCache::getMesh(MeshID id) const
@@ -77,6 +109,11 @@ const GPUMesh& MeshCache::getMesh(MeshID id) const
return meshes.at(id);
}
const CPUMesh& MeshCache::getCPUMesh(MeshID id) const
{
return cpuMeshes.at(id);
}
void MeshCache::cleanup(const GfxDevice& gfxDevice)
{
for (const auto& mesh : meshes) {

View File

@@ -1,6 +1,9 @@
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
#include <destrum/FS/AssetFS.h>
#include "destrum/Graphics/Frustum.h"
#include "spdlog/spdlog.h"
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
}
@@ -68,6 +71,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
m_pipeline->bind(cmd);
gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout);
int ActualDrawCalls = 0;
const auto viewport = VkViewport{
.x = 0,
.y = 0,
@@ -86,16 +91,17 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
auto prevMeshId = NULL_MESH_ID;
// const auto frustum = edge::createFrustumFromCamera(camera);
const auto frustum = edge::createFrustumFromCamera(camera);
for (const auto& dcIdx : drawCommands) {
// const auto& dc = drawCommands[dcIdx];
const auto& dc = dcIdx;
// if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
// continue;
// continue;
// }
ActualDrawCalls++;
const auto& mesh = meshCache.getMesh(dc.meshId);
if (dc.meshId != prevMeshId) {
prevMeshId = dc.meshId;
@@ -106,7 +112,7 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
const auto pushConstants = PushConstants{
.transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer = mesh.vertexBuffer.address,
.vertexBuffer = dc.skinnedMesh != nullptr ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address,
.materialId = dc.materialId,
};
vkCmdPushConstants(

View File

@@ -0,0 +1,98 @@
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
#include "destrum/FS/AssetFS.h"
#include "destrum/Graphics/MeshCache.h"
#include "destrum/Graphics/MeshDrawCommand.h"
void SkinningPipeline::init(GfxDevice& gfxDevice) {
const auto& device = gfxDevice.getDevice();
const auto pushConstant = VkPushConstantRange{
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
.offset = 0,
.size = sizeof(PushConstants),
};
const auto pushConstants = std::array{pushConstant};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = pushConstants.data();
if (vkCreatePipelineLayout(gfxDevice.getDevice().device, &pipelineLayoutInfo, nullptr, &m_pipelineLayout) != VK_SUCCESS) {
throw std::runtime_error("Could not make pipleine layout");
}
ComputePipelineConfigInfo pipelineConfig{};
pipelineConfig.name = "skinning compute pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
const auto SkinningShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/skinning.comp");
skinningPipeline = std::make_unique<ComputePipeline>(
gfxDevice,
SkinningShaderPath.generic_string(),
pipelineConfig
);
for (std::size_t i = 0; i < FRAMES_IN_FLIGHT; ++i) {
auto& jointMatricesBuffer = framesData[i].jointMatricesBuffer;
jointMatricesBuffer.capacity = MAX_JOINT_MATRICES;
jointMatricesBuffer.buffer = gfxDevice.createBuffer(
MAX_JOINT_MATRICES * sizeof(glm::mat4),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
}
}
void SkinningPipeline::cleanup(GfxDevice& gfxDevice) {
for (auto& frame : framesData) {
gfxDevice.destroyBuffer(frame.jointMatricesBuffer.buffer);
}
vkDestroyPipelineLayout(gfxDevice.getDevice().device, m_pipelineLayout, nullptr);
skinningPipeline.reset();
}
void SkinningPipeline::doSkinning(VkCommandBuffer cmd, std::size_t frameIndex, const MeshCache& meshCache, const MeshDrawCommand& dc) {
skinningPipeline->bind(cmd);
const auto& mesh = meshCache.getMesh(dc.meshId);
assert(mesh.hasSkeleton);
assert(dc.skinnedMesh);
const auto cs = PushConstants{
.jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer.buffer.address,
.jointMatricesStartIndex = static_cast<std::uint32_t>(dc.jointMatricesStartIndex), // explicit cast
.numVertices = mesh.numVertices,
.inputBuffer = mesh.vertexBuffer.address,
.skinningData = mesh.skinningDataBuffer.address,
.outputBuffer = dc.skinnedMesh->skinnedVertexBuffer.address,
};
vkCmdPushConstants(cmd, m_pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), &cs);
static const auto workgroupSize = 256;
// const auto groupSizeX = (std::uint32_t)std::ceil(mesh.numVertices / (float)workgroupSize);
const auto groupSizeX = static_cast<std::uint32_t>(
std::ceil(mesh.numVertices / (float)workgroupSize));
vkCmdDispatch(cmd, groupSizeX, 1, 1);
}
void SkinningPipeline::beginDrawing(std::size_t frameIndex) {
getCurrentFrameData(frameIndex).jointMatricesBuffer.clear();
}
std::size_t SkinningPipeline::appendJointMatrices(std::span<const glm::mat4> jointMatrices, std::size_t frameIndex) {
auto& jointMatricesBuffer = getCurrentFrameData(frameIndex).jointMatricesBuffer;
const auto startIndex = jointMatricesBuffer.size;
jointMatricesBuffer.append(jointMatrices);
return startIndex;
}
SkinningPipeline::PerFrameData& SkinningPipeline::getCurrentFrameData(std::size_t frameIndex) {
return framesData[frameIndex % FRAMES_IN_FLIGHT];
}

View File

@@ -23,6 +23,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
skyboxPipeline = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice);
GameState::GetInstance().SetRenderer(this);
}
@@ -30,6 +33,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
flushMaterialUpdates(gfxDevice);
meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
}
void GameRenderer::endDrawing() {
@@ -37,6 +41,13 @@ void GameRenderer::endDrawing() {
}
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) {
for (const auto& dc : meshDrawCommands) {
if (!dc.skinnedMesh) {
continue;
}
skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc);
}
const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(),
.proj = sceneData.camera.GetProjectionMatrix(),
@@ -107,7 +118,7 @@ void GameRenderer::cleanup(VkDevice device) {
void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) {
const auto& mesh = meshCache.getMesh(id);
// const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
assert(materialId != NULL_MATERIAL_ID);
meshDrawCommands.push_back(MeshDrawCommand{
@@ -117,6 +128,25 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma
});
}
void GameRenderer::drawSkinnedMesh(MeshID id,
const glm::mat4& transform,
MaterialID materialId,
SkinnedMesh* skinnedMesh,
std::size_t jointMatricesStartIndex) {
const auto& mesh = meshCache.getMesh(id);
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
assert(materialId != NULL_MATERIAL_ID);
assert(skinnedMesh != nullptr);
meshDrawCommands.push_back(MeshDrawCommand{
.meshId = id,
.transformMatrix = transform,
.materialId = materialId,
.skinnedMesh = skinnedMesh,
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex),
});
}
const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
return gfx_device.getImage(drawImageId);
}

View File

@@ -122,6 +122,16 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_PIPELINE,
.objectHandle = (std::uint64_t)pipeline,
.pObjectName = label,
};
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
}
void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,