Fix some stuff

This commit is contained in:
2026-06-21 18:00:22 +02:00
parent b1dad89214
commit d534e57ad2
14 changed files with 518 additions and 238 deletions
+4
View File
@@ -52,6 +52,10 @@ protected:
bool frameLimit{false};
float frameTime{0.f};
float avgFPS{0.f};
bool resizePending = false;
std::chrono::steady_clock::time_point lastResizeTime{};
};
@@ -91,6 +91,8 @@ public:
VmaAllocator getAllocator() const { return allocator; }
vkb::Device getVkbDevice() const { return device; }
ImageID createImage(const vkutil::CreateImageInfo& createInfo, const std::string& debugName = "", void* pixelData = nullptr, ImageID imageId = NULL_IMAGE_ID);
ImageID createDrawImage(VkFormat format, glm::ivec2 size, const std::string& debugName = "", ImageID imageId = NULL_IMAGE_ID);
ImageID loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT, bool mipMap = false, TextureIntent intent = TextureIntent::ColorSrgb);
@@ -29,6 +29,12 @@ public:
void cleanup(VkDevice device);
void setRenderWireframe(bool wireframe) {
m_renderWireframe = wireframe;
}
bool getRenderWireframe(){ return m_renderWireframe; }
private:
VkPipelineLayout m_pipelineLayout;
@@ -43,6 +49,8 @@ private:
std::uint32_t padding;
};
bool m_renderWireframe{false};
};
#endif //MESHPIPELINE_H
@@ -61,6 +61,14 @@ public:
return *skinningPipeline;
}
void setRenderWireframe(bool wireframe) {
meshPipeline->setRenderWireframe(wireframe);
}
bool getRenderWireframe() {
return meshPipeline->getRenderWireframe();
}
private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
+269 -84
View File
@@ -34,11 +34,13 @@
#include <destrum/Graphics/SkeletalAnimation.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace ModelLoader {
@@ -50,6 +52,21 @@ static glm::mat4 ToGLM(const aiMatrix4x4& m) {
return glm::transpose(glm::make_mat4(&m.a1));
}
static glm::vec3 ToGLM(const aiVector3D& v) {
return glm::vec3(v.x, v.y, v.z);
}
static glm::quat ToGLM(const aiQuaternion& q) {
// glm::quat constructor order is (w, x, y, z).
return glm::normalize(glm::quat(q.w, q.x, q.y, q.z));
}
static glm::vec3 SafeNormalize(const glm::vec3& v, const glm::vec3& fallback) {
const float len2 = glm::dot(v, v);
if (len2 <= std::numeric_limits<float>::epsilon()) return fallback;
return v * (1.0f / std::sqrt(len2));
}
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
mn.x = std::min(mn.x, p.x);
mn.y = std::min(mn.y, p.y);
@@ -59,6 +76,47 @@ static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
mx.z = std::max(mx.z, p.z);
}
struct NodeTRS {
glm::vec3 translation{0.0f, 0.0f, 0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
};
static NodeTRS DecomposeNodeTransform(const aiNode* node) {
NodeTRS out{};
aiVector3D scaling;
aiVector3D translation;
aiQuaternion rotation;
node->mTransformation.Decompose(scaling, rotation, translation);
out.translation = ToGLM(translation);
out.rotation = ToGLM(rotation);
out.scale = ToGLM(scaling);
return out;
}
static void CollectNodeDefaults(const aiNode* node,
std::unordered_map<std::string, NodeTRS>& defaults) {
if (!node) return;
defaults[std::string(node->mName.C_Str())] = DecomposeNodeTransform(node);
for (unsigned int c = 0; c < node->mNumChildren; ++c)
CollectNodeDefaults(node->mChildren[c], defaults);
}
static std::unordered_map<std::string, std::uint32_t>
BuildJointIndexMap(const Skeleton& skeleton) {
std::unordered_map<std::string, std::uint32_t> map;
map.reserve(skeleton.jointNames.size());
for (std::size_t i = 0; i < skeleton.jointNames.size(); ++i)
map[skeleton.jointNames[i]] = static_cast<std::uint32_t>(i);
return map;
}
// ─── Post-process flags ───────────────────────────────────────────────────────
static constexpr unsigned int kImportFlags =
@@ -82,16 +140,17 @@ static const aiScene* LoadScene(Assimp::Importer& importer, const std::string& p
// For skinned meshes, pass glm::mat4{1.f} so vertices stay in bind-pose space.
// Skinning data is NOT populated here — call LoadSkinningData afterwards.
static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh,
const std::string& name,
const glm::mat4& world) {
const std::string& name,
const glm::mat4& world) {
CPUMesh out{};
out.name = name;
const size_t vertexCount = mesh->mNumVertices;
out.vertices.resize(vertexCount);
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
const glm::mat3 tanMat = glm::mat3(world);
const glm::mat3 world3 = glm::mat3(world);
const glm::mat3 nrmMat = glm::transpose(glm::inverse(world3));
const glm::mat3 tanMat = world3;
glm::vec3 mn{ std::numeric_limits<float>::infinity()};
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
@@ -107,7 +166,8 @@ static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh,
// Normal
if (mesh->HasNormals()) {
const aiVector3D& an = mesh->mNormals[i];
v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z));
v.normal = SafeNormalize(nrmMat * glm::vec3(an.x, an.y, an.z),
glm::vec3(0.0f, 1.0f, 0.0f));
} else {
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
}
@@ -125,8 +185,10 @@ static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh,
if (mesh->HasTangentsAndBitangents()) {
const aiVector3D& at = mesh->mTangents[i];
const aiVector3D& ab = mesh->mBitangents[i];
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(at.x, at.y, at.z));
glm::vec3 b3 = glm::normalize(tanMat * glm::vec3(ab.x, ab.y, ab.z));
glm::vec3 t3 = SafeNormalize(tanMat * glm::vec3(at.x, at.y, at.z),
glm::vec3(1.0f, 0.0f, 0.0f));
glm::vec3 b3 = SafeNormalize(tanMat * glm::vec3(ab.x, ab.y, ab.z),
glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 n3 = v.normal;
float sign = (glm::dot(glm::cross(n3, t3), b3) < 0.0f) ? -1.0f : 1.0f;
v.tangent = glm::vec4(t3, sign);
@@ -182,6 +244,7 @@ static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
static Skeleton LoadSkeleton(const aiScene* scene) {
Skeleton skeleton;
skeleton.rootPreTransform = glm::mat4{1.0f};
// Pass 1: collect all bone names and their inverse bind matrices
// from every mesh in the scene.
@@ -223,7 +286,7 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
stack.pop_back();
std::string name(node->mName.C_Str());
int myIdx = parentIdx;
int myIdx = parentIdx;
glm::mat4 myAccWorld = accWorld; // only used while still outside the bone hierarchy
if (boneOffsets.count(name)) {
@@ -237,7 +300,7 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
skeleton.jointNames.push_back(name);
skeleton.inverseBindMatrices.push_back(boneOffsets[name]);
// Grow hierarchy arrays to accommodate this id
// Grow hierarchy arrays to accommodate this id.
while (skeleton.hierarchy.size() <= id)
skeleton.hierarchy.push_back({});
skeleton.hierarchy[id].id = id;
@@ -251,10 +314,11 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
skeleton.rootPreTransform = accWorld;
glm::mat4& m = skeleton.rootPreTransform;
// Normalize each basis vector to remove scale
m[0] = glm::vec4(glm::normalize(glm::vec3(m[0])), 0.f);
m[1] = glm::vec4(glm::normalize(glm::vec3(m[1])), 0.f);
m[2] = glm::vec4(glm::normalize(glm::vec3(m[2])), 0.f);
// Normalize each basis vector to remove scale without creating
// NaNs if an imported file contains a degenerate transform.
m[0] = glm::vec4(SafeNormalize(glm::vec3(m[0]), glm::vec3(1.0f, 0.0f, 0.0f)), 0.f);
m[1] = glm::vec4(SafeNormalize(glm::vec3(m[1]), glm::vec3(0.0f, 1.0f, 0.0f)), 0.f);
m[2] = glm::vec4(SafeNormalize(glm::vec3(m[2]), glm::vec3(0.0f, 0.0f, 1.0f)), 0.f);
}
// Once we are inside the bone hierarchy the pre-transform is fully
@@ -265,7 +329,7 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
myAccWorld = accWorld * ToGLM(node->mTransformation);
}
// Push children in reverse so left-most child is processed first
// Push children in reverse so left-most child is processed first.
for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c)
stack.push_back({ node->mChildren[c], myIdx, myAccWorld });
}
@@ -279,119 +343,240 @@ static Skeleton LoadSkeleton(const aiScene* scene) {
// Populate cpuMesh.skinningData from the matching aiMesh.
// Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized.
static void LoadSkinningData(CPUMesh& cpuMesh,
const aiMesh* aiMesh,
const Skeleton& skeleton) {
const aiMesh* aiMesh,
const Skeleton& skeleton) {
if (!aiMesh->HasBones() || skeleton.joints.empty()) return;
cpuMesh.skinningData.assign(cpuMesh.vertices.size(),
CPUMesh::SkinningData{ {0, 0, 0, 0}, {0.f, 0.f, 0.f, 0.f} });
const auto jointIndexByName = BuildJointIndexMap(skeleton);
std::vector<int> weightCount(cpuMesh.vertices.size(), 0);
for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) {
const aiBone* bone = aiMesh->mBones[b];
std::string boneName(bone->mName.C_Str());
// Find which joint index this bone maps to
std::uint32_t jointIdx = 0;
bool found = false;
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
if (skeleton.jointNames[j] == boneName) {
jointIdx = static_cast<std::uint32_t>(j);
found = true;
break;
}
}
if (!found) continue;
const auto jointIt = jointIndexByName.find(boneName);
if (jointIt == jointIndexByName.end()) continue;
const std::uint32_t jointIdx = jointIt->second;
for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
const unsigned int vertIdx = bone->mWeights[w].mVertexId;
const float weight = bone->mWeights[w].mWeight;
const int slot = weightCount[vertIdx];
if (vertIdx >= cpuMesh.skinningData.size()) continue;
if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this
const float weight = bone->mWeights[w].mWeight;
const int slot = weightCount[vertIdx];
if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this.
cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx;
cpuMesh.skinningData[vertIdx].weights[slot] = weight;
++weightCount[vertIdx];
}
}
// Normalize weights so the shader does not accidentally shrink/explode
// vertices if the importer returned imperfect totals.
for (std::size_t i = 0; i < cpuMesh.skinningData.size(); ++i) {
auto& skin = cpuMesh.skinningData[i];
const float sum = skin.weights[0] + skin.weights[1] + skin.weights[2] + skin.weights[3];
if (sum > std::numeric_limits<float>::epsilon()) {
skin.weights[0] /= sum;
skin.weights[1] /= sum;
skin.weights[2] /= sum;
skin.weights[3] /= sum;
} else {
// Defensive fallback for malformed meshes with unweighted vertices.
skin.jointIds = {0, 0, 0, 0};
skin.weights = {1.f, 0.f, 0.f, 0.f};
}
}
}
// ─── Animation loading ────────────────────────────────────────────────────────
static glm::vec3 SampleVectorKeys(const aiVectorKey* keys,
unsigned int count,
double tick,
const glm::vec3& fallback) {
if (!keys || count == 0) return fallback;
if (count == 1 || tick <= keys[0].mTime) return ToGLM(keys[0].mValue);
for (unsigned int i = 0; i + 1 < count; ++i) {
const double t0 = keys[i].mTime;
const double t1 = keys[i + 1].mTime;
if (tick <= t1) {
const double denom = t1 - t0;
const float alpha = denom > 0.0
? static_cast<float>((tick - t0) / denom)
: 0.0f;
return glm::mix(ToGLM(keys[i].mValue), ToGLM(keys[i + 1].mValue), alpha);
}
}
return ToGLM(keys[count - 1].mValue);
}
static glm::quat SampleQuatKeys(const aiQuatKey* keys,
unsigned int count,
double tick,
const glm::quat& fallback) {
if (!keys || count == 0) return fallback;
if (count == 1 || tick <= keys[0].mTime) return ToGLM(keys[0].mValue);
for (unsigned int i = 0; i + 1 < count; ++i) {
const double t0 = keys[i].mTime;
const double t1 = keys[i + 1].mTime;
if (tick <= t1) {
const double denom = t1 - t0;
const float alpha = denom > 0.0
? static_cast<float>((tick - t0) / denom)
: 0.0f;
return glm::normalize(glm::slerp(ToGLM(keys[i].mValue),
ToGLM(keys[i + 1].mValue),
alpha));
}
}
return ToGLM(keys[count - 1].mValue);
}
static void AppendUniqueKeyTimes(std::vector<double>& times,
const aiVectorKey* keys,
unsigned int count) {
if (!keys) return;
for (unsigned int i = 0; i < count; ++i)
times.push_back(keys[i].mTime);
}
static void AppendUniqueKeyTimes(std::vector<double>& times,
const aiQuatKey* keys,
unsigned int count) {
if (!keys) return;
for (unsigned int i = 0; i < count; ++i)
times.push_back(keys[i].mTime);
}
static void SortAndUniqueKeyTimes(std::vector<double>& times) {
std::sort(times.begin(), times.end());
constexpr double epsilon = 1e-6;
times.erase(std::unique(times.begin(), times.end(),
[](double a, double b) { return std::abs(a - b) <= epsilon; }),
times.end());
}
static double GetMaxAnimationKeyTick(const aiAnimation* aiAnim) {
double maxTick = 0.0;
for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) {
const aiNodeAnim* ch = aiAnim->mChannels[c];
if (ch->mNumPositionKeys > 0)
maxTick = std::max(maxTick, ch->mPositionKeys[ch->mNumPositionKeys - 1].mTime);
if (ch->mNumRotationKeys > 0)
maxTick = std::max(maxTick, ch->mRotationKeys[ch->mNumRotationKeys - 1].mTime);
if (ch->mNumScalingKeys > 0)
maxTick = std::max(maxTick, ch->mScalingKeys[ch->mNumScalingKeys - 1].mTime);
}
return maxTick;
}
static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
const Skeleton& skeleton) {
const Skeleton& skeleton) {
std::vector<SkeletalAnimation> result;
if (!scene->HasAnimations()) return result;
if (!scene->HasAnimations() || skeleton.joints.empty()) return result;
const auto jointIndexByName = BuildJointIndexMap(skeleton);
std::unordered_map<std::string, NodeTRS> nodeDefaults;
CollectNodeDefaults(scene->mRootNode, nodeDefaults);
result.reserve(scene->mNumAnimations);
for (unsigned int a = 0; a < scene->mNumAnimations; ++a) {
const aiAnimation* aiAnim = scene->mAnimations[a];
const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0;
const double durationTicks = aiAnim->mDuration > 0.0
? aiAnim->mDuration
: GetMaxAnimationKeyTick(aiAnim);
SkeletalAnimation anim;
anim.name = aiAnim->mName.C_Str();
anim.duration = static_cast<float>(aiAnim->mDuration / tps);
anim.name = aiAnim->mName.length > 0
? std::string(aiAnim->mName.C_Str())
: ("Animation_" + std::to_string(a));
anim.duration = static_cast<float>(durationTicks / tps);
anim.looped = true;
anim.tracks.reserve(aiAnim->mNumChannels);
for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) {
const aiNodeAnim* ch = aiAnim->mChannels[c];
std::string boneName(ch->mNodeName.C_Str());
const std::string boneName(ch->mNodeName.C_Str());
// Map bone name to joint index
std::uint32_t jointIdx = 0;
bool found = false;
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
if (skeleton.jointNames[j] == boneName) {
jointIdx = static_cast<std::uint32_t>(j);
found = true;
break;
}
}
if (!found) continue;
const auto jointIt = jointIndexByName.find(boneName);
if (jointIt == jointIndexByName.end()) continue;
SkeletalAnimation::Track track;
track.jointIndex = jointIdx;
const auto defaultIt = nodeDefaults.find(boneName);
const NodeTRS defaults = defaultIt != nodeDefaults.end()
? defaultIt->second
: NodeTRS{};
// Position, rotation and scale channels can have different keyframe
// counts and independent time axes. Build the track from the position
// channel's timeline and pick the nearest rotation/scale key for each
// sample rather than assuming index alignment. This avoids corrupted
// keyframes when counts differ.
const unsigned int numPos = ch->mNumPositionKeys;
const unsigned int numRot = ch->mNumRotationKeys;
const unsigned int numSca = ch->mNumScalingKeys;
// counts and independent time axes. Build a unified timeline from
// every key time, then sample/interpolate every TRS component at
// that time.
//
// This fixes the old loader's bad assumptions:
// - rotation/scale keys might be absent;
// - scale-only tracks are valid;
// - key counts do not have to match;
// - key index k does not imply the same timestamp in each channel;
// - missing components must fall back to the node's bind/local pose,
// not zero translation or invalid array access.
std::vector<double> keyTicks;
keyTicks.reserve(ch->mNumPositionKeys + ch->mNumRotationKeys + ch->mNumScalingKeys);
// Use position keys to drive the timeline (most common case).
// If there are no position keys fall back to rotation keys.
const unsigned int numKeys = numPos > 0 ? numPos : numRot;
AppendUniqueKeyTimes(keyTicks, ch->mPositionKeys, ch->mNumPositionKeys);
AppendUniqueKeyTimes(keyTicks, ch->mRotationKeys, ch->mNumRotationKeys);
AppendUniqueKeyTimes(keyTicks, ch->mScalingKeys, ch->mNumScalingKeys);
SortAndUniqueKeyTimes(keyTicks);
for (unsigned int k = 0; k < numKeys; ++k) {
if (keyTicks.empty()) {
// Extremely defensive: an animation channel with no keys should
// not normally exist, but keep a stable bind-pose track instead
// of returning an empty/corrupt track.
keyTicks.push_back(0.0);
}
SkeletalAnimation::Track track;
track.jointIndex = jointIt->second;
track.keyframes.reserve(keyTicks.size());
for (double tick : keyTicks) {
SkeletalAnimation::Keyframe kf;
// Time comes from whichever channel drives this loop
if (numPos > 0) {
kf.time = static_cast<float>(ch->mPositionKeys[k].mTime / tps);
const auto& p = ch->mPositionKeys[k].mValue;
kf.translation = { p.x, p.y, p.z };
} else {
kf.time = static_cast<float>(ch->mRotationKeys[k].mTime / tps);
kf.translation = { 0.f, 0.f, 0.f };
}
// Nearest rotation key at this index
{
const unsigned int ri = std::min(k, numRot - 1);
const auto& r = ch->mRotationKeys[ri].mValue;
kf.rotation = glm::quat(r.w, r.x, r.y, r.z); // glm: (w,x,y,z)
}
// Nearest scale key at this index
{
const unsigned int si = std::min(k, numSca - 1);
const auto& s = ch->mScalingKeys[si].mValue;
kf.scale = { s.x, s.y, s.z };
}
kf.time = static_cast<float>(tick / tps);
kf.translation = SampleVectorKeys(ch->mPositionKeys,
ch->mNumPositionKeys,
tick,
defaults.translation);
kf.rotation = SampleQuatKeys(ch->mRotationKeys,
ch->mNumRotationKeys,
tick,
defaults.rotation);
kf.scale = SampleVectorKeys(ch->mScalingKeys,
ch->mNumScalingKeys,
tick,
defaults.scale);
track.keyframes.push_back(kf);
}
@@ -558,4 +743,4 @@ static SkinnedModel LoadSkinnedModel(const std::string& path) {
} // namespace ModelLoader
#endif // MODELLOADER_H
#endif // MODELLOADER_H