diff --git a/destrum/assets_src/char2.fbx b/destrum/assets_src/char2.fbx index 5647086..c9f094d 100644 Binary files a/destrum/assets_src/char2.fbx and b/destrum/assets_src/char2.fbx differ diff --git a/destrum/assets_src/textures/T_T.png b/destrum/assets_src/textures/T_T.png new file mode 100644 index 0000000..4592130 Binary files /dev/null and b/destrum/assets_src/textures/T_T.png differ diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index 389c547..4e4f843 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -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{}; + }; diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index b2996f4..852efa3 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.h @@ -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); diff --git a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h index b7e1011..f00b300 100644 --- a/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h +++ b/destrum/include/destrum/Graphics/Pipelines/MeshPipeline.h @@ -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 diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 5aff843..5a7ba85 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.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); diff --git a/destrum/include/destrum/Util/ModelLoader.h b/destrum/include/destrum/Util/ModelLoader.h index bdab3cc..b959747 100644 --- a/destrum/include/destrum/Util/ModelLoader.h +++ b/destrum/include/destrum/Util/ModelLoader.h @@ -34,11 +34,13 @@ #include #include +#include #include #include #include #include #include +#include #include 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::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& 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 +BuildJointIndexMap(const Skeleton& skeleton) { + std::unordered_map map; + map.reserve(skeleton.jointNames.size()); + + for (std::size_t i = 0; i < skeleton.jointNames.size(); ++i) + map[skeleton.jointNames[i]] = static_cast(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::infinity()}; glm::vec3 mx{-std::numeric_limits::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(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 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(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::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((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((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& 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& 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& 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 LoadAnimations(const aiScene* scene, - const Skeleton& skeleton) { + const Skeleton& skeleton) { std::vector result; - if (!scene->HasAnimations()) return result; + if (!scene->HasAnimations() || skeleton.joints.empty()) return result; + + const auto jointIndexByName = BuildJointIndexMap(skeleton); + + std::unordered_map 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(aiAnim->mDuration / tps); + anim.name = aiAnim->mName.length > 0 + ? std::string(aiAnim->mName.C_Str()) + : ("Animation_" + std::to_string(a)); + anim.duration = static_cast(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(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 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(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(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(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 \ No newline at end of file +#endif // MODELLOADER_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index fc6442f..e895608 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -66,6 +67,8 @@ void App::run() { InputManager::GetInstance().BeginFrame(); camera.Update(dt); + + SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { @@ -76,8 +79,11 @@ void App::run() { switch (event.window.event) { case SDL_WINDOWEVENT_SIZE_CHANGED: case SDL_WINDOWEVENT_RESIZED: - m_params.windowSize = { event.window.data1, event.window.data2 }; + { + resizePending = true; + lastResizeTime = std::chrono::steady_clock::now(); break; + } } } if (InputManager::GetInstance().ProcessEvent(event)) { @@ -104,15 +110,33 @@ void App::run() { const float alpha = accumulator / fixedDt; - if (!gfxDevice.needsSwapchainRecreate()) { - customDraw(); + if (gfxDevice.needsSwapchainRecreate() || resizePending) { + auto now = std::chrono::steady_clock::now(); + + if (resizePending && + now - lastResizeTime < std::chrono::milliseconds(100)) { + continue; + } + + int w = 0; + int h = 0; + SDL_Vulkan_GetDrawableSize(window, &w, &h); + + if (w == 0 || h == 0) { + continue; + } + + spdlog::info("Recreating swapchain to size: {}x{}", w, h); + + gfxDevice.recreateSwapchain(w, h); + onWindowResize(w, h); + + resizePending = false; + continue; } - if (gfxDevice.needsSwapchainRecreate()) { - spdlog::info("Recreating swapchain to size: {}x{}", m_params.windowSize.x, m_params.windowSize.y); - gfxDevice.recreateSwapchain(m_params.windowSize.x, m_params.windowSize.y); - onWindowResize(m_params.windowSize.x, m_params.windowSize.y); - } + customDraw(); + if (frameLimit) { auto sleepTime = Time::GetInstance().SleepDuration(); diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index 9bd0d80..1f29409 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -45,7 +45,8 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) .imageCubeArray = VK_TRUE, .geometryShader = VK_TRUE, // for im3d .depthClamp = VK_TRUE, - .samplerAnisotropy = VK_TRUE, + .fillModeNonSolid = VK_TRUE, + .samplerAnisotropy = VK_TRUE }; constexpr auto features12 = VkPhysicalDeviceVulkan12Features{ @@ -63,11 +64,13 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) .dynamicRendering = true, }; + physicalDevice = vkb::PhysicalDeviceSelector{instance} .set_minimum_version(1, 3) .set_required_features(deviceFeatures) .set_required_features_12(features12) .set_required_features_13(features13) + .add_required_extension(VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME) .set_surface(surface) .prefer_gpu_device_type(vkb::PreferredDeviceType::discrete) .select() diff --git a/destrum/src/Graphics/Pipeline.cpp b/destrum/src/Graphics/Pipeline.cpp index 45c0dd4..356ce90 100644 --- a/destrum/src/Graphics/Pipeline.cpp +++ b/destrum/src/Graphics/Pipeline.cpp @@ -89,7 +89,7 @@ void Pipeline::DefaultPipelineConfigInfo(PipelineConfigInfo& configInfo) { configInfo.depthStencilInfo.front = {}; // Optional configInfo.depthStencilInfo.back = {}; // Optional - configInfo.dynamicStateEnables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + configInfo.dynamicStateEnables = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_POLYGON_MODE_EXT}; configInfo.dynamicStateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; configInfo.dynamicStateInfo.pDynamicStates = configInfo.dynamicStateEnables.data(); configInfo.dynamicStateInfo.dynamicStateCount = static_cast(configInfo.dynamicStateEnables.size()); diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index ca8c552..bf256c7 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -89,6 +89,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd, }; vkCmdSetScissor(cmd, 0, 1, &scissor); + vkCmdSetPolygonModeEXT(cmd, m_renderWireframe ? VK_POLYGON_MODE_LINE : VK_POLYGON_MODE_FILL); + auto prevMeshId = NULL_MESH_ID; const auto frustum = edge::createFrustumFromCamera(camera); diff --git a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp index c36cd73..2a0b22c 100644 --- a/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp @@ -81,6 +81,8 @@ void SkyboxPipeline::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camer skyboxRotation = glm::rotate(skyboxRotation, rotationSpeed * static_cast(Time::GetInstance().DeltaTime()), glm::vec3(0.f, 1.f, 0.f)); pipeline->bind(cmd); + vkCmdSetPolygonModeEXT(cmd, VK_POLYGON_MODE_FILL); + gfxDevice.bindBindlessDescSet(cmd, pipelineLayout); const glm::mat3 r = glm::mat3(skyboxRotation); diff --git a/destrum/src/Graphics/Swapchain.cpp b/destrum/src/Graphics/Swapchain.cpp index 73085c9..19fc22b 100644 --- a/destrum/src/Graphics/Swapchain.cpp +++ b/destrum/src/Graphics/Swapchain.cpp @@ -65,40 +65,72 @@ void Swapchain::createSwapchain(GfxDevice* gfxDevice, VkFormat format, std::uint extent = m_swapchain.extent; } -void Swapchain::recreateSwapchain(const GfxDevice& gfxDevice, VkFormat format, std::uint32_t width, std::uint32_t height, bool vSync) { - assert(m_swapchain); +void Swapchain::recreateSwapchain( + const GfxDevice& gfxDevice, + VkFormat format, + std::uint32_t width, + std::uint32_t height, + bool vSync) +{ + if (width == 0 || height == 0) { + dirty = true; + return; + } + + VkDevice device = gfxDevice.getDevice(); + + vkDeviceWaitIdle(device); + + auto oldSwapchain = m_swapchain; + + auto res = vkb::SwapchainBuilder{gfxDevice.getVkbDevice()} + .set_old_swapchain(oldSwapchain) + .set_desired_format(VkSurfaceFormatKHR{ + .format = format, + .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, + }) + .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT) + .set_desired_present_mode( + vSync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) + .set_desired_extent(width, height) + .build(); - assert(format == VK_FORMAT_B8G8R8A8_SRGB && "TODO: test other formats"); - auto res = vkb::SwapchainBuilder{gfxDevice.getDevice()} - .set_old_swapchain(m_swapchain) - .set_desired_format(VkSurfaceFormatKHR{ - .format = format, - .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, - }) - .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT) - .set_desired_present_mode( - vSync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) - .set_desired_extent(width, height) - .build(); if (!res.has_value()) { throw std::runtime_error(std::format( "failed to create swapchain: error = {}, vk result = {}", res.full_error().type.message(), string_VkResult(res.full_error().vk_result))); } - vkb::destroy_swapchain(m_swapchain); - for (auto imageView: imageViews) { - vkDestroyImageView(m_gfxDevice->getDevice(), imageView, nullptr); + for (auto sem : imageRenderSemaphores) { + vkDestroySemaphore(device, sem, nullptr); } + imageRenderSemaphores.clear(); + + for (auto imageView : imageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + imageViews.clear(); + + vkb::destroy_swapchain(oldSwapchain); m_swapchain = res.value(); images = m_swapchain.get_images().value(); imageViews = m_swapchain.get_image_views().value(); - dirty = false; + VkSemaphoreCreateInfo sci{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + + imageRenderSemaphores.resize(images.size()); + + for (auto& sem : imageRenderSemaphores) { + VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &sem)); + } + extent = m_swapchain.extent; + dirty = false; } void Swapchain::cleanup() { @@ -126,6 +158,12 @@ void Swapchain::resetFences(int index) const { VK_CHECK(vkResetFences(m_gfxDevice->getDevice(), 1, &frame.renderFence)); } +struct SwapchainAcquireResult { + VkResult result = VK_SUCCESS; + VkImage image = VK_NULL_HANDLE; + uint32_t imageIndex = 0; +}; + std::pair Swapchain::acquireNextImage(int index) { std::uint32_t swapchainImageIndex{}; const auto result = vkAcquireNextImageKHR( diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index b203a02..33fd4cb 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -25,143 +25,147 @@ void LightKeeper::customInit() { const float aspectRatio = static_cast(m_params.renderSize.x) / static_cast(m_params.renderSize.y); camera.setAspectRatio(aspectRatio); - // - // testMesh.name = "Test Mesh"; - // auto list_of_models = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://kitty.glb").generic_string()); - // testMesh = list_of_models[0]; - // testMeshID = meshCache.addMesh(gfxDevice, testMesh); - // spdlog::info("TestMesh uploaded with id: {}", testMeshID); - // - // auto testimgID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://kitty.png")); - // spdlog::info("Test image loaded with id: {}", testimgID); - // testMaterialID = materialCache.addMaterial(gfxDevice, { - // .baseColor = glm::vec3(1.f), - // .diffuseTexture = testimgID, - // }); - // spdlog::info("Test material created with id: {}", testMaterialID); - // - // camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); - // + + testMesh.name = "Test Mesh"; + auto list_of_models = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://kitty.glb").generic_string()); + testMesh = list_of_models[0]; + testMeshID = meshCache.addMesh(gfxDevice, testMesh); + spdlog::info("TestMesh uploaded with id: {}", testMeshID); + + auto testimgID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://kitty.png")); + spdlog::info("Test image loaded with id: {}", testimgID); + testMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .diffuseTexture = testimgID, + }); + spdlog::info("Test material created with id: {}", testMaterialID); + + camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); + auto& scene = SceneManager::GetInstance().CreateScene("Main"); + + // auto testCube = std::make_shared("TestCube"); + // auto meshComp = testCube->AddComponent(); + // meshComp->SetMeshID(testMeshID); + // meshComp->SetMaterialID(testMaterialID); + const int count = 100; + const float radius = 5.0f; + + const float orbitRadius = 5.0f; + + for (int i = 0; i < count; ++i) { + // auto childCube = std::make_shared(fmt::format("ChildCube{}", i)); + // + // auto childMeshComp = childCube->AddComponent(); + // childMeshComp->SetMeshID(testMeshID); + // childMeshComp->SetMaterialID(testMaterialID); + // + // childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); + // + // // Add orbit + self spin + // auto orbit = childCube->AddComponent(orbitRadius, glm::vec3(0.0f)); + // orbit->Randomize(1337u + (uint32_t)i); // stable random per index + // + // scene.Add(childCube); + } + // testCube->AddComponent(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec + //rotate 180 around X axis + // testCube->GetTransform().SetLocalRotation(glm::quat(glm::vec3(glm::radians(180.0f), 0.0f, 0.0f))); // - // // auto testCube = std::make_shared("TestCube"); - // // auto meshComp = testCube->AddComponent(); - // // meshComp->SetMeshID(testMeshID); - // // meshComp->SetMaterialID(testMaterialID); - // const int count = 100; - // const float radius = 5.0f; + auto globeRoot = std::make_shared("GlobeRoot"); + globeRoot->GetTransform().SetWorldPosition(glm::vec3(0.0f)); + globeRoot->AddComponent(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec + scene.Add(globeRoot); + + + + // scene.Add(testCube); + + // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg"); + // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg"); + const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr"); // - // const float orbitRadius = 5.0f; + // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png"); // - // for (int i = 0; i < count; ++i) { - // // auto childCube = std::make_shared(fmt::format("ChildCube{}", i)); - // // - // // auto childMeshComp = childCube->AddComponent(); - // // childMeshComp->SetMeshID(testMeshID); - // // childMeshComp->SetMaterialID(testMaterialID); - // // - // // childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); - // // - // // // Add orbit + self spin - // // auto orbit = childCube->AddComponent(orbitRadius, glm::vec3(0.0f)); - // // orbit->Randomize(1337u + (uint32_t)i); // stable random per index - // // - // // scene.Add(childCube); - // } - // // testCube->AddComponent(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec - // //rotate 180 around X axis - // // testCube->GetTransform().SetLocalRotation(glm::quat(glm::vec3(glm::radians(180.0f), 0.0f, 0.0f))); - // // - // auto globeRoot = std::make_shared("GlobeRoot"); - // globeRoot->GetTransform().SetWorldPosition(glm::vec3(0.0f)); - // globeRoot->AddComponent(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec - // scene.Add(globeRoot); + const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert"); + const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag"); // + skyboxCubemap = std::make_unique(); + skyboxCubemap->LoadCubeMap(skyboxID.generic_string()); + skyboxCubemap->InitCubemapPipeline(vertShaderPath.generic_string(), fragShaderPath.generic_string()); + skyboxCubemap->CreateCubeMap(); + + renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); + // - // - // // scene.Add(testCube); - // - // // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg"); - // // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg"); - // const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr"); - // // - // // const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png"); - // // - // const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert"); - // const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag"); - // // - // skyboxCubemap = std::make_unique(); - // skyboxCubemap->LoadCubeMap(skyboxID.generic_string()); - // skyboxCubemap->InitCubemapPipeline(vertShaderPath.generic_string(), fragShaderPath.generic_string()); - // skyboxCubemap->CreateCubeMap(); - // - // renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); - // - // // - // const auto planeObj = std::make_shared("GroundPlane"); - // const auto planeMeshComp = planeObj->AddComponent(); - // const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string()); - // const auto planeMeshID = meshCache.addMesh(gfxDevice, planeModel[0]); - // - // const auto planeTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://grass.png")); - // const auto planeMaterialID = materialCache.addMaterial(gfxDevice, { - // .baseColor = glm::vec3(1.f), - // .textureFilteringMode = TextureFilteringMode::Nearest, - // .diffuseTexture = planeTextureID, - // .name = "GroundPlaneMaterial", - // }); - // planeMeshComp->SetMeshID(planeMeshID); - // planeMeshComp->SetMaterialID(planeMaterialID); - // planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); - // planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); - // scene.Add(planeObj); - // - // - // // At the bottom of customInit(), replace the incomplete CharObj block: - // - // const auto CharObj = std::make_shared("Character"); - // - // auto charModel = ModelLoader::LoadSkinnedModel( - // AssetFS::GetInstance().GetFullPath("engine://char2.fbx").generic_string() - // ); - // - // const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); - // - // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); - // const auto charMaterialID = materialCache.addMaterial(gfxDevice, { - // .baseColor = glm::vec3(1.f), - // .diffuseTexture = charTextureID, - // .name = "CharacterMaterial", - // }); - // - // const auto charMeshComp = CharObj->AddComponent(); - // charMeshComp->SetMeshID(charMeshID); - // charMeshComp->SetMaterialID(charMaterialID); - // - // - // const auto animator = CharObj->AddComponent(); - // animator->setSkeleton(std::move(charModel.skeleton)); - // for (auto& clip : charModel.animations) { - // animator->addClip(std::make_shared(std::move(clip))); - // } - // - // for (const auto& clip : charModel.animations) - // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); - // - // if (!charModel.animations.empty()) - // // animator->play("Armature|Armature|mixamo.com"); - // // // animator->play(charModel.animations[0].name); - // animator->play("Armature|Armature|Armature|main"); - // // or: animator->play("Run", 0.2f); // 0.2s cross-fade - // - // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); - // // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); - // scene.Add(CharObj); + const auto planeObj = std::make_shared("GroundPlane"); + const auto planeMeshComp = planeObj->AddComponent(); + const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string()); + const auto planeMeshID = meshCache.addMesh(gfxDevice, planeModel[0]); + + const auto planeTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://grass.png")); + const auto planeMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .textureFilteringMode = TextureFilteringMode::Nearest, + .diffuseTexture = planeTextureID, + .name = "GroundPlaneMaterial", + }); + planeMeshComp->SetMeshID(planeMeshID); + planeMeshComp->SetMaterialID(planeMaterialID); + planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f)); + planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); + scene.Add(planeObj); + + + // At the bottom of customInit(), replace the incomplete CharObj block: + + const auto CharObj = std::make_shared("Character"); + + auto charModel = ModelLoader::LoadSkinnedModel( + AssetFS::GetInstance().GetFullPath("engine://char2.fbx").generic_string() + ); + + const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]); + + const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://textures/T_T.png")); + const auto charMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .diffuseTexture = charTextureID, + .name = "CharacterMaterial", + }); + + const auto charMeshComp = CharObj->AddComponent(); + charMeshComp->SetMeshID(charMeshID); + charMeshComp->SetMaterialID(charMaterialID); + + + const auto animator = CharObj->AddComponent(); + animator->setSkeleton(std::move(charModel.skeleton)); + for (auto& clip : charModel.animations) { + animator->addClip(std::make_shared(std::move(clip))); + } + + for (const auto& clip : charModel.animations) + spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + + if (!charModel.animations.empty()) + // animator->play("Armature|Armature|mixamo.com"); + // // animator->play(charModel.animations[0].name); + animator->play("Armature|Armature|Armature|main"); + // or: animator->play("Run", 0.2f); // 0.2s cross-fade + + CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); + // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); + scene.Add(CharObj); } void LightKeeper::customUpdate(float dt) { camera.Update(dt); SceneManager::GetInstance().Update(); + + if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_1)) { + renderer.setRenderWireframe(!renderer.getRenderWireframe()); + } } void LightKeeper::customDraw() {