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

3
.gitmodules vendored
View File

@@ -38,3 +38,6 @@
[submodule "destrum/third_party/tinyexr"] [submodule "destrum/third_party/tinyexr"]
path = destrum/third_party/tinyexr path = destrum/third_party/tinyexr
url = https://github.com/syoyo/tinyexr.git url = https://github.com/syoyo/tinyexr.git
[submodule "destrum/third_party/assimp"]
path = destrum/third_party/assimp
url = https://github.com/assimp/assimp.git

View File

@@ -22,4 +22,4 @@ add_custom_target(CookAssets)
add_dependencies(CookAssets add_dependencies(CookAssets
_internal_cook_game_assets _internal_cook_game_assets
_internal_cook_engine_assets _internal_cook_engine_assets
) )

Submodule TheChef updated: faaf7fa120...3a7b137165

View File

@@ -4,6 +4,7 @@ set(SRC_FILES
"src/App.cpp" "src/App.cpp"
"src/Event.cpp" "src/Event.cpp"
"src/Components/Animator.cpp"
"src/Components/MeshRendererComponent.cpp" "src/Components/MeshRendererComponent.cpp"
"src/Components/Rotator.cpp" "src/Components/Rotator.cpp"
"src/Components/Spinner.cpp" "src/Components/Spinner.cpp"
@@ -11,6 +12,8 @@ set(SRC_FILES
"src/Graphics/BindlessSetManager.cpp" "src/Graphics/BindlessSetManager.cpp"
"src/Graphics/Camera.cpp" "src/Graphics/Camera.cpp"
"src/Graphics/ComputePipeline.cpp"
"src/Graphics/Frustum.cpp"
"src/Graphics/GfxDevice.cpp" "src/Graphics/GfxDevice.cpp"
"src/Graphics/ImageCache.cpp" "src/Graphics/ImageCache.cpp"
"src/Graphics/ImageLoader.cpp" "src/Graphics/ImageLoader.cpp"
@@ -29,6 +32,7 @@ set(SRC_FILES
"src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/MeshPipeline.cpp"
"src/Graphics/Pipelines/SkyboxPipeline.cpp" "src/Graphics/Pipelines/SkyboxPipeline.cpp"
"src/Graphics/Pipelines/SkinningPipeline.cpp"
"src/Input/InputManager.cpp" "src/Input/InputManager.cpp"
@@ -69,6 +73,7 @@ target_link_libraries(destrum
spdlog::spdlog spdlog::spdlog
stb::image stb::image
tinygltf tinygltf
assimp
PRIVATE PRIVATE
freetype::freetype freetype::freetype
@@ -105,14 +110,14 @@ target_compile_definitions(destrum
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine") #set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
#
add_custom_command(TARGET destrum POST_BUILD #add_custom_command(TARGET destrum POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}" # COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets" # COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets"
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}" # COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}"
VERBATIM # VERBATIM
) #)
add_custom_target(_internal_clean_engine_assets add_custom_target(_internal_clean_engine_assets
COMMAND TheChef COMMAND TheChef
@@ -127,3 +132,17 @@ add_custom_target(_internal_cook_engine_assets ALL
--output "${ASSETS_RUNTIME_DIR}" --output "${ASSETS_RUNTIME_DIR}"
DEPENDS TheChef DEPENDS TheChef
) )
function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
# This resolves to destrum's own directory at function DEFINITION time
set(_engine_src "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_src")
set(_engine_runtime "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_runtime")
set(_output_engine "${GAME_OUTPUT_DIR}/assets/engine")
add_custom_command(TARGET ${GAME_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${GAME_OUTPUT_DIR}/assets"
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_output_engine}"
COMMAND ${CMAKE_COMMAND} -E create_symlink "${_engine_runtime}" "${_output_engine}"
VERBATIM
)
endfunction()

BIN
destrum/assets_src/char.fbx Normal file

Binary file not shown.

BIN
destrum/assets_src/char.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

View File

@@ -14,6 +14,10 @@ vec4 sampleTexture2DNearest(uint texID, vec2 uv) {
return texture(nonuniformEXT(sampler2D(textures[texID], samplers[NEAREST_SAMPLER_ID])), uv); return texture(nonuniformEXT(sampler2D(textures[texID], samplers[NEAREST_SAMPLER_ID])), uv);
} }
vec4 sampleTexture2D(uint texID, uint samplerID, vec2 uv) {
return texture(nonuniformEXT(sampler2D(textures[texID], samplers[samplerID])), uv);
}
vec4 sampleTexture2DMSNearest(uint texID, ivec2 p, int s) { vec4 sampleTexture2DMSNearest(uint texID, ivec2 p, int s) {
return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s); return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s);
} }

View File

@@ -6,6 +6,7 @@
struct MaterialData { struct MaterialData {
vec4 baseColor; vec4 baseColor;
vec4 metallicRoughnessEmissive; vec4 metallicRoughnessEmissive;
uint textureFilteringMode;
uint diffuseTex; uint diffuseTex;
uint normalTex; uint normalTex;
uint metallicRoughnessTex; uint metallicRoughnessTex;

View File

@@ -17,8 +17,9 @@ layout (location = 0) out vec4 outFragColor;
void main() void main()
{ {
MaterialData material = pcs.sceneData.materials.data[pcs.materialID]; MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode;
vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV) * material.baseColor; vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
outFragColor = diffuse; outFragColor = diffuse;

View File

@@ -0,0 +1,60 @@
#version 460
#extension GL_GOOGLE_include_directive : require
#extension GL_EXT_buffer_reference : require
#include "vertex.glsl"
struct SkinningDataType {
ivec4 jointIds;
vec4 weights;
};
layout (buffer_reference, std430) readonly buffer SkinningData {
SkinningDataType data[];
};
layout (buffer_reference, std430) readonly buffer JointMatrices {
mat4 matrices[];
};
layout (push_constant) uniform constants
{
JointMatrices jointMatrices;
uint jointMatricesStartIndex;
uint numVertices;
VertexBuffer inputBuffer;
SkinningData skinningData;
VertexBuffer outputBuffer;
} pcs;
layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
mat4 getJointMatrix(int jointId) {
if (jointId < 0) return mat4(1.0);
return pcs.jointMatrices.matrices[pcs.jointMatricesStartIndex + jointId];
}
void main()
{
uint index = gl_GlobalInvocationID.x;
if (index >= pcs.numVertices) {
return;
}
SkinningDataType sd = pcs.skinningData.data[index];
mat4 skinMatrix =
sd.weights.x * getJointMatrix(sd.jointIds.x) +
sd.weights.y * getJointMatrix(sd.jointIds.y) +
sd.weights.z * getJointMatrix(sd.jointIds.z) +
sd.weights.w * getJointMatrix(sd.jointIds.w);
Vertex v = pcs.inputBuffer.vertices[index];
v.position = vec3(skinMatrix * vec4(v.position, 1.0));
mat3 skinMat3 = mat3(skinMatrix);
v.normal = skinMat3 * v.normal;
v.tangent.xyz = skinMat3 * v.tangent.xyz; // don't transform tangent.w
pcs.outputBuffer.vertices[index] = v;
}

View File

@@ -0,0 +1,70 @@
#ifndef ANIMATOR_H
#define ANIMATOR_H
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/mat4x4.hpp>
#include <destrum/ObjectModel/Component.h>
#include <destrum/Graphics/Resources/Mesh.h>
#include <destrum/Graphics/SkeletalAnimation.h>
class SkinningPipeline;
class Animator : public Component {
public:
explicit Animator(GameObject& parent);
// Component interface
void Update() override;
void ImGuiInspector() override;
// Animation control
void addClip(std::shared_ptr<SkeletalAnimation> clip);
void play(const std::string& name, float blendTime = 0.f);
void stop();
bool isPlaying() const { return m_current.clip != nullptr; }
const std::string& currentClipName() const { return m_currentClipName; }
float currentTime() const { return m_current.time; }
// Called during draw command building
std::size_t uploadJointMatrices(SkinningPipeline& pipeline,
const Skeleton& skeleton,
std::size_t frameIndex);
Skeleton* getSkeleton() {
return &m_skeleton;
}
void setSkeleton(Skeleton skeleton) {
m_skeleton = std::move(skeleton);
}
private:
struct PlaybackState {
SkeletalAnimation* clip = nullptr;
float time = 0.f;
float speed = 1.f;
};
Skeleton m_skeleton{};
PlaybackState m_current;
PlaybackState m_previous;
float m_blendT = 0.f;
float m_blendDuration = 0.f;
std::string m_currentClipName;
std::unordered_map<std::string, std::shared_ptr<SkeletalAnimation>> m_clips;
std::vector<glm::mat4> computeJointMatrices(const Skeleton& skeleton);
glm::vec3 sampleTranslation(const SkeletalAnimation::Track& track, float t);
glm::quat sampleRotation (const SkeletalAnimation::Track& track, float t);
glm::vec3 sampleScale (const SkeletalAnimation::Track& track, float t);
};
#endif // ANIMATOR_H

View File

@@ -21,6 +21,8 @@ public:
private: private:
MeshID meshID{NULL_MESH_ID}; MeshID meshID{NULL_MESH_ID};
MaterialID materialID{NULL_MATERIAL_ID}; MaterialID materialID{NULL_MATERIAL_ID};
std::unique_ptr<SkinnedMesh> m_skinnedMesh;
}; };
#endif //MESHRENDERERCOMPONENT_H #endif //MESHRENDERERCOMPONENT_H

View File

@@ -96,6 +96,13 @@ public:
this->CalculateProjectionMatrix(); this->CalculateProjectionMatrix();
} }
float GetZNear() const { return m_zNear; }
void SetZNear(float zNear) { m_zNear = zNear; }
float GetZFar() const { return m_zFar; }
void SetZFar(float zFar) { m_zFar = zFar; }
float GetFOVY() const { return fovAngle; }
float GetAspectRatio() const { return m_aspectRatio; }
glm::vec3 m_position{2.f, 0, 0}; glm::vec3 m_position{2.f, 0, 0};
void Translate(const glm::vec3& translation) { void Translate(const glm::vec3& translation) {

View File

@@ -1,4 +1,48 @@
#ifndef COMPUTEPIPELINE_H #ifndef COMPUTEPIPELINE_H
#define COMPUTEPIPELINE_H #define COMPUTEPIPELINE_H
#include <string>
#include <vector>
#include <destrum/Graphics/GfxDevice.h>
struct ComputePipelineConfigInfo {
std::string name;
// Optional specialization constants (can be nullptr if unused)
VkSpecializationInfo* specializationInfo{nullptr};
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
};
class ComputePipeline {
public:
ComputePipeline(GfxDevice& device,
const std::string& compPath,
const ComputePipelineConfigInfo& configInfo);
~ComputePipeline();
ComputePipeline(const ComputePipeline& other) = delete;
ComputePipeline(ComputePipeline&& other) noexcept = delete;
ComputePipeline& operator=(const ComputePipeline& other) = delete;
ComputePipeline& operator=(ComputePipeline&& other) noexcept = delete;
void bind(VkCommandBuffer buffer) const;
static void DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo);
private:
static std::vector<char> readFile(const std::string& filename);
void CreateComputePipeline(const std::string& compPath,
const ComputePipelineConfigInfo& configInfo);
void CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const;
GfxDevice& m_device;
VkPipeline m_computePipeline{VK_NULL_HANDLE};
VkShaderModule m_compShaderModule{VK_NULL_HANDLE};
};
#endif //COMPUTEPIPELINE_H #endif //COMPUTEPIPELINE_H

View File

@@ -1,4 +1,80 @@
#ifndef FRUSTUM_H #ifndef FRUSTUM_H
#define FRUSTUM_H #define FRUSTUM_H
#include <glm/glm.hpp>
#include "Camera.h"
struct Frustum {
struct Plane {
Plane() = default;
Plane(const glm::vec3& p1, const glm::vec3& norm) :
normal(glm::normalize(norm)), distance(glm::dot(normal, p1))
{}
glm::vec3 normal{0.f, 1.f, 0.f};
// distance from the origin to the nearest point in the plane
float distance{0.f};
float getSignedDistanceToPlane(const glm::vec3& point) const
{
return glm::dot(normal, point) - distance;
}
};
const Plane& getPlane(int i) const
{
switch (i) {
case 0:
return farFace;
case 1:
return nearFace;
case 2:
return leftFace;
case 3:
return rightFace;
case 4:
return topFace;
case 5:
return bottomFace;
default:
assert(false);
return nearFace;
}
}
Plane farFace;
Plane nearFace;
Plane leftFace;
Plane rightFace;
Plane topFace;
Plane bottomFace;
};
struct Sphere {
glm::vec3 center{};
float radius{};
};
struct AABB {
glm::vec3 min;
glm::vec3 max;
glm::vec3 calculateSize() const { return glm::abs(max - min); }
};
namespace edge
{
std::array<glm::vec3, 8> calculateFrustumCornersWorldSpace(const Camera& camera);
Frustum createFrustumFromCamera(const Camera& camera);
bool isInFrustum(const Frustum& frustum, const Sphere& s);
bool isInFrustum(const Frustum& frustum, const AABB& aabb);
Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton);
}
#endif //FRUSTUM_H #endif //FRUSTUM_H

View File

@@ -26,6 +26,8 @@
#include "Util.h" #include "Util.h"
class MeshCache;
namespace { namespace {
using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>; using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>;
} }

View File

@@ -4,13 +4,21 @@
#include <string> #include <string>
#include <glm/glm.hpp> #include <glm/glm.hpp>
struct MaterialData { struct alignas(16) MaterialData {
glm::vec4 baseColor; alignas(16) glm::vec4 baseColor;
glm::vec4 metalRoughnessEmissive; alignas(16) glm::vec4 metalRoughnessEmissive;
std::uint32_t diffuseTex; uint32_t textureFilteringMode;
std::uint32_t normalTex; uint32_t diffuseTex;
std::uint32_t metallicRoughnessTex; uint32_t normalTex;
std::uint32_t emissiveTex; uint32_t metallicRoughnessTex;
uint32_t emissiveTex;
uint32_t _pad0, _pad1, _pad2; // explicit to 64 bytes
};
enum class TextureFilteringMode : std::uint32_t {
Nearest,
Linear,
Anisotropic,
}; };
struct Material { struct Material {
@@ -19,6 +27,8 @@ struct Material {
float roughnessFactor{0.7f}; float roughnessFactor{0.7f};
float emissiveFactor{0.f}; float emissiveFactor{0.f};
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
ImageID diffuseTexture{NULL_IMAGE_ID}; ImageID diffuseTexture{NULL_IMAGE_ID};
// ImageId normalMapTexture{NULL_IMAGE_ID}; // ImageId normalMapTexture{NULL_IMAGE_ID};
// ImageId metallicRoughnessTexture{NULL_IMAGE_ID}; // ImageId metallicRoughnessTexture{NULL_IMAGE_ID};

View File

@@ -18,13 +18,14 @@ public:
void cleanup(GfxDevice& gfxDevice); void cleanup(GfxDevice& gfxDevice);
MaterialID addMaterial(GfxDevice& gfxDevice, Material material); MaterialID addMaterial(GfxDevice& gfxDevice, Material material);
const Material& getMaterial(MaterialID id) const; MaterialID addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID);
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
MaterialID getFreeMaterialId() const; [[nodiscard]] MaterialID getFreeMaterialId() const;
MaterialID getPlaceholderMaterialId() const; [[nodiscard]] MaterialID getPlaceholderMaterialId() const;
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; } [[nodiscard]] const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; } [[nodiscard]] VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
Material& getMaterialMutable(MaterialID id); Material& getMaterialMutable(MaterialID id);
@@ -33,7 +34,7 @@ public:
private: private:
std::vector<Material> materials; std::vector<Material> materials;
static const auto MAX_MATERIALS = 1000; static constexpr auto MAX_MATERIALS = 1000;
GPUBuffer materialDataBuffer; GPUBuffer materialDataBuffer;
// material which is used for meshes without materials // material which is used for meshes without materials

View File

@@ -13,11 +13,13 @@ public:
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh); MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
const GPUMesh& getMesh(MeshID id) const; const GPUMesh& getMesh(MeshID id) const;
const CPUMesh& getCPUMesh(MeshID id) const;
private: private:
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const; void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
std::vector<GPUMesh> meshes; std::vector<GPUMesh> meshes;
std::vector<CPUMesh> cpuMeshes;
}; };
#endif //MESHCACHE_H #endif //MESHCACHE_H

View File

@@ -5,13 +5,15 @@
#include <destrum/Graphics/ids.h> #include <destrum/Graphics/ids.h>
#include <destrum/Graphics/Frustum.h>
struct MeshDrawCommand { struct MeshDrawCommand {
MeshID meshId; MeshID meshId;
glm::mat4 transformMatrix; glm::mat4 transformMatrix;
// for frustum culling // for frustum culling
// math::Sphere worldBoundingSphere; Sphere worldBoundingSphere;
// If set - mesh will be drawn with overrideMaterialId // If set - mesh will be drawn with overrideMaterialId
// instead of whatever material the mesh has // instead of whatever material the mesh has
@@ -20,8 +22,8 @@ struct MeshDrawCommand {
bool castShadow{true}; bool castShadow{true};
// skinned meshes only // skinned meshes only
// const SkinnedMesh* skinnedMesh{nullptr}; const SkinnedMesh* skinnedMesh{nullptr};
// std::uint32_t jointMatricesStartIndex; std::uint32_t jointMatricesStartIndex;
}; };
#endif //MESHDRAWCOMMAND_H #endif //MESHDRAWCOMMAND_H

View File

@@ -1,4 +1,58 @@
#ifndef SKINNINGPIPELINE_H #ifndef SKINNINGPIPELINE_H
#define SKINNINGPIPELINE_H #define SKINNINGPIPELINE_H
#include <vulkan/vulkan.h>
#include <array>
#include <span>
#include <glm/glm.hpp>
#include <destrum/Graphics/ComputePipeline.h>
#include <destrum/Graphics/Resources/AppendableBuffer.h>
#include "destrum/Graphics/Resources/NBuffer.h"
struct MeshDrawCommand;
class MeshCache;
class GfxDevice;
class SkinningPipeline final {
public:
void init(GfxDevice& gfxDevice);
void cleanup(GfxDevice& gfxDevice);
void doSkinning(
VkCommandBuffer cmd,
std::size_t frameIndex,
const MeshCache& meshCache,
const MeshDrawCommand& dc);
void beginDrawing(std::size_t frameIndex);
std::size_t appendJointMatrices(
std::span<const glm::mat4> jointMatrices,
std::size_t frameIndex);
private:
VkPipelineLayout m_pipelineLayout;
std::unique_ptr<ComputePipeline> skinningPipeline;
struct PushConstants {
VkDeviceAddress jointMatricesBuffer;
std::uint32_t jointMatricesStartIndex;
std::uint32_t numVertices;
VkDeviceAddress inputBuffer;
VkDeviceAddress skinningData;
VkDeviceAddress outputBuffer;
};
static constexpr std::size_t MAX_JOINT_MATRICES = 5000;
struct PerFrameData {
AppendableBuffer<glm::mat4> jointMatricesBuffer;
};
std::array<PerFrameData, FRAMES_IN_FLIGHT> framesData;
// NBuffer framesData;
PerFrameData& getCurrentFrameData(std::size_t frameIndex);
};
#endif //SKINNINGPIPELINE_H #endif //SKINNINGPIPELINE_H

View File

@@ -13,6 +13,7 @@
#include <memory> #include <memory>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
#include "Pipelines/SkyboxPipeline.h" #include "Pipelines/SkyboxPipeline.h"
class GameRenderer { class GameRenderer {
@@ -35,6 +36,11 @@ public:
void cleanup(VkDevice device); void cleanup(VkDevice device);
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
void drawSkinnedMesh(MeshID id,
const glm::mat4& transform,
MaterialID materialId,
SkinnedMesh* skinnedMesh,
std::size_t jointMatricesStartIndex);
const GPUImage& getDrawImage(const GfxDevice& gfx_device) const; const GPUImage& getDrawImage(const GfxDevice& gfx_device) const;
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) {
@@ -47,6 +53,13 @@ public:
void setSkyboxTexture(ImageID skyboxImageId); void setSkyboxTexture(ImageID skyboxImageId);
void flushMaterialUpdates(GfxDevice& gfxDevice); void flushMaterialUpdates(GfxDevice& gfxDevice);
[[nodiscard]] MeshCache& GetMeshCache() const {
return meshCache;
}
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const {
return *skinningPipeline;
}
private: private:
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
@@ -91,6 +104,8 @@ private:
std::unique_ptr<MeshPipeline> meshPipeline; std::unique_ptr<MeshPipeline> meshPipeline;
std::unique_ptr<SkyboxPipeline> skyboxPipeline; std::unique_ptr<SkyboxPipeline> skyboxPipeline;
std::unique_ptr<SkinningPipeline> skinningPipeline;
}; };
#endif //RENDERER_H #endif //RENDERER_H

View File

@@ -0,0 +1,30 @@
#ifndef ANIMATIONCLIP_H
#define ANIMATIONCLIP_H
#include <cstdint>
#include <string>
#include <vector>
#include <glm/glm.hpp>
struct JointKeyframes {
std::uint32_t jointIndex; // matches your skeleton joint order
std::vector<float> positionTimes;
std::vector<glm::vec3> positions;
std::vector<float> rotationTimes;
std::vector<glm::quat> rotations;
std::vector<float> scaleTimes;
std::vector<glm::vec3> scales;
};
struct AnimationClip {
std::string name;
float duration; // seconds
float ticksPerSecond;
std::vector<JointKeyframes> channels; // one per animated joint
};
#endif //ANIMATIONCLIP_H

View File

@@ -1,4 +1,40 @@
#ifndef APPENDABLEBUFFER_H #ifndef APPENDABLEBUFFER_H
#define APPENDABLEBUFFER_H #define APPENDABLEBUFFER_H
#include <cassert>
#include <cstring> // memcpy
#include <span>
#include <vulkan/vulkan.h>
#include <destrum/Graphics/Resources/Buffer.h>
template<typename T>
struct AppendableBuffer {
void append(std::span<const T> elements)
{
assert(size + elements.size() <= capacity);
auto arr = (T*)buffer.info.pMappedData;
std::memcpy((void*)&arr[size], elements.data(), elements.size() * sizeof(T));
size += elements.size();
}
void append(const T& element)
{
assert(size + 1 <= capacity);
auto arr = (T*)buffer.info.pMappedData;
std::memcpy((void*)&arr[size], &element, sizeof(T));
++size;
}
void clear() { size = 0; }
VkBuffer getVkBuffer() const { return buffer.buffer; }
GPUBuffer buffer;
std::size_t capacity{};
std::size_t size{0};
};
#endif //APPENDABLEBUFFER_H #endif //APPENDABLEBUFFER_H

View File

@@ -2,33 +2,48 @@
#define MESH_H #define MESH_H
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include <glm/vec3.hpp> #include <glm/vec3.hpp>
#include <glm/vec4.hpp> #include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <destrum/Graphics/Resources/Buffer.h> #include <destrum/Graphics/Resources/Buffer.h>
#include <destrum/Graphics/Frustum.h>
#include <destrum/Graphics/Skeleton.h>
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
struct CPUMesh { struct CPUMesh {
std::vector<std::uint32_t> indices;
struct Vertex { struct Vertex {
glm::vec3 position; glm::vec3 position;
float uv_x{}; float uv_x{};
glm::vec3 normal; glm::vec3 normal;
float uv_y{}; float uv_y{};
glm::vec4 tangent; glm::vec4 tangent;
}; };
std::vector<Vertex> vertices;
struct SkinningData {
glm::vec<4, std::uint32_t> jointIds;
glm::vec4 weights;
};
std::vector<std::uint32_t> indices;
std::vector<Vertex> vertices;
std::vector<SkinningData> skinningData; // empty if no skeleton
std::string name; std::string name;
glm::vec3 minPos; glm::vec3 minPos;
glm::vec3 maxPos; glm::vec3 maxPos;
std::optional<Skeleton> skeleton; // present only for skinned meshes
}; };
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
struct GPUMesh { struct GPUMesh {
GPUBuffer vertexBuffer; GPUBuffer vertexBuffer;
@@ -40,78 +55,64 @@ struct GPUMesh {
// AABB // AABB
glm::vec3 minPos; glm::vec3 minPos;
glm::vec3 maxPos; glm::vec3 maxPos;
// math::Sphere boundingSphere; Sphere boundingSphere;
bool hasSkeleton{false};
GPUBuffer skinningDataBuffer; // valid only when hasSkeleton == true
}; };
static std::vector<CPUMesh::Vertex> vertices = { // ─── Skinned output buffer (one per entity, not per mesh asset) ───────────────
// =======================
struct SkinnedMesh {
GPUBuffer skinnedVertexBuffer;
};
// ─── Cube geometry (dev / test asset) ─────────────────────────────────────────
namespace CubeMesh {
inline std::vector<CPUMesh::Vertex> vertices = {
// +Z (Front) // +Z (Front)
// =======================
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}}, {{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
// =======================
// -Z (Back) // -Z (Back)
// =======================
{{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}}, {{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
{{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}}, {{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
{{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}}, {{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
{{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}}, {{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
// =======================
// +X (Right) // +X (Right)
// =======================
{{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}}, {{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}}, {{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}}, {{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
{{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}}, {{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
// =======================
// -X (Left) // -X (Left)
// =======================
{{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}}, {{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
{{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}}, {{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
{{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}}, {{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
{{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}}, {{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
// =======================
// +Y (Top) // +Y (Top)
// =======================
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}}, {{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}}, {{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
{{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
// =======================
// -Y (Bottom) // -Y (Bottom)
// =======================
{{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}}, {{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}}, {{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}}, {{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
}; };
static std::vector<uint32_t> indices = { inline std::vector<std::uint32_t> indices = {
// Front (+Z) 0, 2, 1, 0, 3, 2, // Front
0, 2, 1, 0, 3, 2, 4, 6, 5, 4, 7, 6, // Back
8,10, 9, 8,11,10, // Right
// Back (-Z) 12,14,13, 12,15,14, // Left
4, 6, 5, 4, 7, 6, 16,18,17, 16,19,18, // Top
20,22,21, 20,23,22, // Bottom
// Right (+X)
8,10, 9, 8,11,10,
// Left (-X)
12,14,13, 12,15,14,
// Top (+Y)
16,18,17, 16,19,18,
// Bottom (-Y)
20,22,21, 20,23,22
}; };
} // namespace CubeMesh
#endif //MESH_H #endif // MESH_H

View File

@@ -0,0 +1,35 @@
#ifndef SKELETALANIMATION_H
#define SKELETALANIMATION_H
#include <map>
#include <string>
#include <vector>
#include <glm/gtc/quaternion.hpp>
#include <glm/vec3.hpp>
struct SkeletalAnimation {
struct Keyframe {
float time;
glm::vec3 translation;
glm::quat rotation;
glm::vec3 scale;
};
struct Track {
std::uint32_t jointIndex;
std::vector<Keyframe> keyframes; // sorted by time
};
std::vector<Track> tracks; // one per animated joint (not all joints need a track)
float duration{0.f};
bool looped{true};
std::string name;
int startFrame{0};
std::map<int, std::vector<std::string>> events;
const std::vector<std::string>& getEventsForFrame(int frame) const;
};
#endif // SKELETALANIMATION_H

View File

@@ -0,0 +1,55 @@
#ifndef SKELETON_H
#define SKELETON_H
#include <string>
#include <vector>
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp>
#include <destrum/Graphics/ids.h>
struct Joint {
JointId id{NULL_JOINT_ID};
glm::vec3 localTranslation{0.f, 0.f, 0.f};
glm::quat localRotation{glm::identity<glm::quat>()};
glm::vec3 localScale{1.f, 1.f, 1.f};
};
struct Skeleton {
struct JointNode {
JointId id{NULL_JOINT_ID};
std::vector<JointId> children;
};
std::vector<JointNode> hierarchy;
std::vector<glm::mat4> inverseBindMatrices;
std::vector<Joint> joints;
std::vector<std::string> jointNames;
std::vector<int> parentIndex; // -1 = root, built once after loading
};
inline void buildParentIndex(Skeleton& skeleton) {
const std::size_t numJoints = skeleton.joints.size();
skeleton.parentIndex.assign(numJoints, -1);
// Build id -> slot map so we don't assume hierarchy[i] == joints[i]
std::unordered_map<JointId, std::size_t> idToSlot;
idToSlot.reserve(numJoints);
for (std::size_t j = 0; j < numJoints; ++j)
idToSlot[skeleton.joints[j].id] = j;
for (const auto& node : skeleton.hierarchy) {
auto parentIt = idToSlot.find(node.id);
if (parentIt == idToSlot.end()) continue;
for (const JointId childId : node.children) {
auto childIt = idToSlot.find(childId);
if (childIt != idToSlot.end())
skeleton.parentIndex[childIt->second] = static_cast<int>(parentIt->second);
}
}
}
#endif // SKELETON_H

View File

@@ -16,4 +16,8 @@ constexpr MaterialID NULL_MATERIAL_ID = std::numeric_limits<std::uint32_t>::max(
using BindlessID = std::uint32_t; using BindlessID = std::uint32_t;
constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits<std::uint32_t>::max(); constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits<std::uint32_t>::max();
using JointId = std::uint16_t;
static const JointId NULL_JOINT_ID = std::numeric_limits<JointId>::max();
static const JointId ROOT_JOINT_ID = 0;
#endif //IDS_H #endif //IDS_H

View File

@@ -1,4 +1,86 @@
#ifndef MATHUTILS_H #ifndef MATHUTILS_H
#define MATHUTILS_H #define MATHUTILS_H
#include <destrum/Graphics/Frustum.h>
#include <span>
#include <array>
#include <glm/glm.hpp>
#include <cassert>
#include "glm/gtx/norm.hpp"
inline Sphere calculateBoundingSphere(std::span<glm::vec3> positions)
{
assert(!positions.empty());
auto calculateInitialSphere = [](const std::span<glm::vec3>& positions) -> Sphere {
constexpr int dirCount = 13;
static const std::array<glm::vec3, dirCount> direction = {{
{1.f, 0.f, 0.f},
{0.f, 1.f, 0.f},
{0.f, 0.f, 1.f},
{1.f, 1.f, 0.f},
{1.f, 0.f, 1.f},
{0.f, 1.f, 1.f},
{1.f, -1.f, 0.f},
{1.f, 0.f, -1.f},
{0.f, 1.f, -1.f},
{1.f, 1.f, 1.f},
{1.f, -1.f, 1.f},
{1.f, 1.f, -1.f},
{1.f, -1.f, -1.f},
}};
std::array<float, dirCount> dmin{};
std::array<float, dirCount> dmax{};
std::array<std::size_t, dirCount> imin{};
std::array<std::size_t, dirCount> imax{};
// Find min and max dot products for each direction and record vertex indices.
for (int j = 0; j < dirCount; ++j) {
const auto& u = direction[j];
dmin[j] = glm::dot(u, positions[0]);
dmax[j] = dmin[j];
for (std::size_t i = 1; i < positions.size(); ++i) {
const auto d = glm::dot(u, positions[i]);
if (d < dmin[j]) {
dmin[j] = d;
imin[j] = i;
} else if (d > dmax[j]) {
dmax[j] = d;
imax[j] = i;
}
};
}
// Find direction for which vertices at min and max extents are furthest apart.
float d2 = glm::length2(positions[imax[0]] - positions[imin[0]]);
int k = 0;
for (int j = 1; j < dirCount; j++) {
const auto m2 = glm::length2(positions[imax[j]] - positions[imin[j]]);
if (m2 > d2) {
d2 = m2;
k = j;
}
}
const auto center = (positions[imin[k]] + positions[imax[k]]) * 0.5f;
float radius = sqrt(d2) * 0.5f;
return {center, radius};
};
// Determine initial center and radius.
auto s = calculateInitialSphere(positions);
// Make pass through vertices and adjust sphere as necessary.
for (std::size_t i = 0; i < positions.size(); i++) {
const auto pv = positions[i] - s.center;
float m2 = glm::length2(pv);
if (m2 > s.radius * s.radius) {
auto q = s.center - (pv * (s.radius / std::sqrt(m2)));
s.center = (q + positions[i]) * 0.5f;
s.radius = glm::length(q - s.center);
}
}
return s;
}
#endif //MATHUTILS_H #endif //MATHUTILS_H

View File

@@ -1,498 +1,521 @@
#ifndef MODELLOADER_H #ifndef MODELLOADER_H
#define MODELLOADER_H #define MODELLOADER_H
// CPUMesh loader with tinygltf // CPUMesh loader using Assimp
// - Loads first scene (or default scene), iterates nodes, extracts mesh primitives. // - Loads first scene, iterates nodes recursively, extracts mesh primitives.
// - Handles POSITION/NORMAL/TANGENT/TEXCOORD_0 and indices. // - Handles POSITION / NORMAL / TANGENT / TEXCOORD_0 and indices.
// - Computes minPos/maxPos. // - Handles bones/skinning data and skeletal animations.
// - Can return per-primitive meshes, or merged-per-gltf-mesh meshes. // - Computes minPos / maxPos (in world space).
// - IMPORTANT FIX: Applies node transforms (TRS / matrix) so models don't appear flipped/rotated. // - Can return per-primitive meshes (PerPrimitive) or one merged mesh per
// Assimp mesh-node (MergedPerMesh).
// - LoadSkinnedModel loads meshes + skeleton + animations in one call.
// - For skinned meshes, vertex positions are kept in local/bind-pose space
// (identity world transform) so the skinning shader can apply joint matrices
// correctly at runtime.
// //
// NOTE: Defining TINYGLTF_IMPLEMENTATION in a header can cause ODR / multiple-definition issues // Link against: assimp (e.g. -lassimp)
// if included in more than one translation unit. Best practice: put the TINYGLTF_IMPLEMENTATION // Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …).
// define in exactly one .cpp. I keep it here because your original file did, but you may want
// to move it.
#define TINYGLTF_IMPLEMENTATION #include <assimp/Importer.hpp>
#define TINYGLTF_NO_STB_IMAGE_WRITE #include <assimp/scene.h>
#include <tiny_gltf.h> #include <assimp/postprocess.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> // translate/scale #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp> // quat #include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp> // mat4_cast #include <glm/gtc/quaternion.hpp>
#include <destrum/Graphics/Resources/Mesh.h> #include <destrum/Graphics/Resources/Mesh.h>
#include <destrum/Graphics/Skeleton.h>
#include <destrum/Graphics/SkeletalAnimation.h>
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <limits> #include <limits>
#include <optional> #include <stdexcept>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <stdexcept>
#include <iostream>
#include <algorithm>
namespace ModelLoader { namespace ModelLoader {
// -------------------- helpers -------------------- // ─── Helpers ──────────────────────────────────────────────────────────────────
static size_t ComponentSizeInBytes(int componentType) { // Convert an Assimp row-major 4×4 matrix to GLM column-major.
switch (componentType) { static glm::mat4 ToGLM(const aiMatrix4x4& m) {
case TINYGLTF_COMPONENT_TYPE_BYTE: return 1; return glm::transpose(glm::make_mat4(&m.a1));
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: return 1; }
case TINYGLTF_COMPONENT_TYPE_SHORT: return 2;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: return 2;
case TINYGLTF_COMPONENT_TYPE_INT: return 4;
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: return 4;
case TINYGLTF_COMPONENT_TYPE_FLOAT: return 4;
case TINYGLTF_COMPONENT_TYPE_DOUBLE: return 8;
default: throw std::runtime_error("Unknown glTF component type");
}
}
static int TypeNumComponents(int type) { static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
switch (type) { mn.x = std::min(mn.x, p.x);
case TINYGLTF_TYPE_SCALAR: return 1; mn.y = std::min(mn.y, p.y);
case TINYGLTF_TYPE_VEC2: return 2; mn.z = std::min(mn.z, p.z);
case TINYGLTF_TYPE_VEC3: return 3; mx.x = std::max(mx.x, p.x);
case TINYGLTF_TYPE_VEC4: return 4; mx.y = std::max(mx.y, p.y);
case TINYGLTF_TYPE_MAT2: return 4; mx.z = std::max(mx.z, p.z);
case TINYGLTF_TYPE_MAT3: return 9; }
case TINYGLTF_TYPE_MAT4: return 16;
default: throw std::runtime_error("Unknown glTF type");
}
}
// Returns pointer to the first element, plus stride in bytes. // ─── Post-process flags ───────────────────────────────────────────────────────
static std::pair<const std::uint8_t*, size_t>
GetAccessorDataPtrAndStride(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
if (accessor.bufferView < 0) {
throw std::runtime_error("Accessor has no bufferView");
}
const tinygltf::BufferView& bv = model.bufferViews.at(accessor.bufferView);
const tinygltf::Buffer& buf = model.buffers.at(bv.buffer);
const size_t componentSize = ComponentSizeInBytes(accessor.componentType); static constexpr unsigned int kImportFlags =
const int numComps = TypeNumComponents(accessor.type); aiProcess_Triangulate |
const size_t packedStride = componentSize * size_t(numComps); aiProcess_CalcTangentSpace |
aiProcess_JoinIdenticalVertices |
aiProcess_GenSmoothNormals |
aiProcess_FlipUVs |
aiProcess_LimitBoneWeights; // guarantees <= 4 weights per vertex
size_t stride = bv.byteStride ? size_t(bv.byteStride) : packedStride; static const aiScene* LoadScene(Assimp::Importer& importer, const std::string& path) {
const aiScene* scene = importer.ReadFile(path, kImportFlags);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode)
throw std::runtime_error(std::string("Assimp failed to load: ") + importer.GetErrorString());
return scene;
}
const size_t start = size_t(bv.byteOffset) + size_t(accessor.byteOffset); // ─── Primitive extraction ─────────────────────────────────────────────────────
// Conservative bounds check; don't hard-fail on weird stride but catches obvious issues.
if (start > buf.data.size()) {
throw std::runtime_error("Accessor start is out of buffer bounds");
}
const std::uint8_t* ptr = buf.data.data() + start; // Build one CPUMesh from a single aiMesh, applying the given world transform.
return {ptr, stride}; // 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) {
CPUMesh out{};
out.name = name;
template <typename T> const size_t vertexCount = mesh->mNumVertices;
static T ReadAs(const std::uint8_t* p) { out.vertices.resize(vertexCount);
T v{};
std::memcpy(&v, p, sizeof(T));
return v;
}
static glm::vec3 ReadVec3Float(const std::uint8_t* base, size_t stride, size_t i) { const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
const std::uint8_t* p = base + i * stride; const glm::mat3 tanMat = glm::mat3(world);
const float x = ReadAs<float>(p + 0);
const float y = ReadAs<float>(p + 4);
const float z = ReadAs<float>(p + 8);
return glm::vec3{x, y, z};
}
static glm::vec2 ReadVec2Float(const std::uint8_t* base, size_t stride, size_t i) { glm::vec3 mn{ std::numeric_limits<float>::infinity()};
const std::uint8_t* p = base + i * stride; glm::vec3 mx{-std::numeric_limits<float>::infinity()};
const float x = ReadAs<float>(p + 0);
const float y = ReadAs<float>(p + 4);
return glm::vec2{x, y};
}
static glm::vec4 ReadVec4Float(const std::uint8_t* base, size_t stride, size_t i) { for (size_t i = 0; i < vertexCount; ++i) {
const std::uint8_t* p = base + i * stride; CPUMesh::Vertex v{};
const float x = ReadAs<float>(p + 0);
const float y = ReadAs<float>(p + 4);
const float z = ReadAs<float>(p + 8);
const float w = ReadAs<float>(p + 12);
return glm::vec4{x, y, z, w};
}
static std::uint32_t ReadIndexAsU32(const tinygltf::Model& model, const tinygltf::Accessor& accessor, size_t i) { // Position (required)
auto [base, stride] = GetAccessorDataPtrAndStride(model, accessor); const aiVector3D& ap = mesh->mVertices[i];
const std::uint8_t* p = base + i * stride; v.position = glm::vec3(world * glm::vec4(ap.x, ap.y, ap.z, 1.0f));
UpdateBounds(mn, mx, v.position);
switch (accessor.componentType) { // Normal
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: if (mesh->HasNormals()) {
return static_cast<std::uint32_t>(ReadAs<std::uint8_t>(p)); const aiVector3D& an = mesh->mNormals[i];
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z));
return static_cast<std::uint32_t>(ReadAs<std::uint16_t>(p));
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
return static_cast<std::uint32_t>(ReadAs<std::uint32_t>(p));
default:
throw std::runtime_error("Unsupported index componentType (expected u8/u16/u32)");
}
}
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);
mn.z = std::min(mn.z, p.z);
mx.x = std::max(mx.x, p.x);
mx.y = std::max(mx.y, p.y);
mx.z = std::max(mx.z, p.z);
}
// Build a node local matrix from either node.matrix or TRS.
// glTF stores rotation as quaternion [x,y,z,w].
static glm::mat4 NodeLocalMatrix(const tinygltf::Node& node) {
if (node.matrix.size() == 16) {
glm::mat4 m(1.0f);
// glTF is column-major; GLM is column-major -> fill columns.
for (int c = 0; c < 4; ++c)
for (int r = 0; r < 4; ++r)
m[c][r] = static_cast<float>(node.matrix[c * 4 + r]);
return m;
}
glm::vec3 t(0.0f);
if (node.translation.size() == 3) {
t = glm::vec3(
static_cast<float>(node.translation[0]),
static_cast<float>(node.translation[1]),
static_cast<float>(node.translation[2])
);
}
glm::quat q(1.0f, 0.0f, 0.0f, 0.0f); // w,x,y,z
if (node.rotation.size() == 4) {
q = glm::quat(
static_cast<float>(node.rotation[3]), // w
static_cast<float>(node.rotation[0]), // x
static_cast<float>(node.rotation[1]), // y
static_cast<float>(node.rotation[2]) // z
);
}
glm::vec3 s(1.0f);
if (node.scale.size() == 3) {
s = glm::vec3(
static_cast<float>(node.scale[0]),
static_cast<float>(node.scale[1]),
static_cast<float>(node.scale[2])
);
}
const glm::mat4 T = glm::translate(glm::mat4(1.0f), t);
const glm::mat4 R = glm::toMat4(q);
const glm::mat4 S = glm::scale(glm::mat4(1.0f), s);
return T * R * S;
}
// -------------------- primitive extraction --------------------
static CPUMesh LoadPrimitiveIntoCPUMesh(const tinygltf::Model& model,
const tinygltf::Primitive& prim,
const std::string& nameForMesh,
const glm::mat4& world) {
CPUMesh out{};
out.name = nameForMesh;
// POSITION is required
auto itPos = prim.attributes.find("POSITION");
if (itPos == prim.attributes.end()) {
throw std::runtime_error("Primitive has no POSITION attribute");
}
const tinygltf::Accessor& accPos = model.accessors.at(itPos->second);
if (accPos.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accPos.type != TINYGLTF_TYPE_VEC3) {
throw std::runtime_error("POSITION must be VEC3 float for this loader");
}
const size_t vertexCount = size_t(accPos.count);
// Optional attributes
const tinygltf::Accessor* accNormal = nullptr;
const tinygltf::Accessor* accTangent = nullptr;
const tinygltf::Accessor* accUV0 = nullptr;
if (auto it = prim.attributes.find("NORMAL"); it != prim.attributes.end()) {
accNormal = &model.accessors.at(it->second);
if (accNormal->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accNormal->type != TINYGLTF_TYPE_VEC3)
accNormal = nullptr;
}
if (auto it = prim.attributes.find("TANGENT"); it != prim.attributes.end()) {
accTangent = &model.accessors.at(it->second);
if (accTangent->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accTangent->type != TINYGLTF_TYPE_VEC4)
accTangent = nullptr;
}
if (auto it = prim.attributes.find("TEXCOORD_0"); it != prim.attributes.end()) {
accUV0 = &model.accessors.at(it->second);
if (accUV0->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accUV0->type != TINYGLTF_TYPE_VEC2)
accUV0 = nullptr;
}
// Prepare pointers/strides
auto [posBase, posStride] = GetAccessorDataPtrAndStride(model, accPos);
const std::uint8_t* nrmBase = nullptr;
size_t nrmStride = 0;
const std::uint8_t* tanBase = nullptr;
size_t tanStride = 0;
const std::uint8_t* uvBase = nullptr;
size_t uvStride = 0;
if (accNormal) {
auto p = GetAccessorDataPtrAndStride(model, *accNormal);
nrmBase = p.first;
nrmStride = p.second;
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr;
}
if (accTangent) {
auto p = GetAccessorDataPtrAndStride(model, *accTangent);
tanBase = p.first;
tanStride = p.second;
if (size_t(accTangent->count) != vertexCount) accTangent = nullptr;
}
if (accUV0) {
auto p = GetAccessorDataPtrAndStride(model, *accUV0);
uvBase = p.first;
uvStride = p.second;
if (size_t(accUV0->count) != vertexCount) accUV0 = nullptr;
}
// Allocate vertices
out.vertices.resize(vertexCount);
// Bounds init (in world space, because we transform positions)
glm::vec3 mn{std::numeric_limits<float>::infinity()};
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
// Normal matrix
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
const glm::mat3 tanMat = glm::mat3(world);
for (size_t i = 0; i < vertexCount; ++i) {
CPUMesh::Vertex v{};
// Position
const glm::vec3 pLocal = ReadVec3Float(posBase, posStride, i);
v.position = glm::vec3(world * glm::vec4(pLocal, 1.0f));
UpdateBounds(mn, mx, v.position);
// Normal
if (accNormal) {
const glm::vec3 nLocal = ReadVec3Float(nrmBase, nrmStride, i);
v.normal = glm::normalize(nrmMat * nLocal);
} else {
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
}
// UV
if (accUV0) {
glm::vec2 uv = ReadVec2Float(uvBase, uvStride, i);
v.uv_x = uv.x;
v.uv_y = uv.y;
} else {
v.uv_x = 0.0f;
v.uv_y = 0.0f;
}
// Tangent
if (accTangent) {
glm::vec4 t = ReadVec4Float(tanBase, tanStride, i);
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(t));
v.tangent = glm::vec4(t3, t.w); // keep handedness in w
} else {
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
}
out.vertices[i] = v;
}
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
// Indices
if (prim.indices >= 0) {
const tinygltf::Accessor& accIdx = model.accessors.at(prim.indices);
if (accIdx.type != TINYGLTF_TYPE_SCALAR) {
throw std::runtime_error("Indices accessor must be SCALAR");
}
out.indices.resize(size_t(accIdx.count));
for (size_t i = 0; i < size_t(accIdx.count); ++i) {
out.indices[i] = ReadIndexAsU32(model, accIdx, i);
}
} else { } else {
out.indices.resize(vertexCount); v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
} }
// If your renderer expects opposite winding vs glTF, you can flip triangle order. // UV channel 0
// Keep what you had: if (mesh->HasTextureCoords(0)) {
// for (size_t i = 0; i + 2 < out.indices.size(); i += 3) { v.uv_x = mesh->mTextureCoords[0][i].x;
// std::swap(out.indices[i + 1], out.indices[i + 2]); v.uv_y = mesh->mTextureCoords[0][i].y;
// }
return out;
}
// Merge "src" into "dst" (offset indices)
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
const std::uint32_t baseVertex = static_cast<std::uint32_t>(dst.vertices.size());
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
dst.indices.reserve(dst.indices.size() + src.indices.size());
for (std::uint32_t idx : src.indices) {
dst.indices.push_back(baseVertex + idx);
}
if (dst.vertices.size() == src.vertices.size()) {
dst.minPos = src.minPos;
dst.maxPos = src.maxPos;
} else { } else {
dst.minPos = glm::vec3( v.uv_x = 0.0f;
std::min(dst.minPos.x, src.minPos.x), v.uv_y = 0.0f;
std::min(dst.minPos.y, src.minPos.y), }
std::min(dst.minPos.z, src.minPos.z)
); // Tangent + bitangent sign (stored in w as +1/-1 handedness)
dst.maxPos = glm::vec3( if (mesh->HasTangentsAndBitangents()) {
std::max(dst.maxPos.x, src.maxPos.x), const aiVector3D& at = mesh->mTangents[i];
std::max(dst.maxPos.y, src.maxPos.y), const aiVector3D& ab = mesh->mBitangents[i];
std::max(dst.maxPos.z, src.maxPos.z) 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 n3 = v.normal;
float sign = (glm::dot(glm::cross(n3, t3), b3) < 0.0f) ? -1.0f : 1.0f;
v.tangent = glm::vec4(t3, sign);
} else {
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
}
out.vertices[i] = v;
}
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
// Indices
if (mesh->HasFaces()) {
out.indices.reserve(size_t(mesh->mNumFaces) * 3);
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
const aiFace& face = mesh->mFaces[f];
for (unsigned int k = 0; k < face.mNumIndices; ++k)
out.indices.push_back(static_cast<std::uint32_t>(face.mIndices[k]));
}
} else {
out.indices.resize(vertexCount);
for (size_t i = 0; i < vertexCount; ++i)
out.indices[i] = static_cast<std::uint32_t>(i);
}
return out;
}
// Merge src into dst, offsetting src indices by the current dst vertex count.
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
const std::uint32_t base = static_cast<std::uint32_t>(dst.vertices.size());
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
dst.indices.reserve(dst.indices.size() + src.indices.size());
for (std::uint32_t idx : src.indices)
dst.indices.push_back(base + idx);
dst.minPos = glm::vec3(
std::min(dst.minPos.x, src.minPos.x),
std::min(dst.minPos.y, src.minPos.y),
std::min(dst.minPos.z, src.minPos.z)
);
dst.maxPos = glm::vec3(
std::max(dst.maxPos.x, src.maxPos.x),
std::max(dst.maxPos.y, src.maxPos.y),
std::max(dst.maxPos.z, src.maxPos.z)
);
}
// ─── Skeleton loading ─────────────────────────────────────────────────────────
static Skeleton LoadSkeleton(const aiScene* scene) {
Skeleton skeleton;
// Pass 1: collect all bone names and their inverse bind matrices
// from every mesh in the scene.
std::unordered_map<std::string, glm::mat4> boneOffsets;
for (unsigned int m = 0; m < scene->mNumMeshes; ++m) {
const aiMesh* mesh = scene->mMeshes[m];
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
const aiBone* bone = mesh->mBones[b];
std::string boneName(bone->mName.C_Str());
if (!boneOffsets.count(boneName))
boneOffsets[boneName] = ToGLM(bone->mOffsetMatrix);
} }
} }
// -------------------- public API -------------------- if (boneOffsets.empty()) return skeleton; // static mesh, no skeleton
struct NodeStackItem { // Pass 2: walk the node tree in depth-first order (parent before child),
int nodeIdx; // registering only nodes that correspond to actual bones.
glm::mat4 parentWorld; // This guarantees joints are ordered parent-before-child, which is required
}; // by Animator::computeJointMatrices.
JointId nextId = 0;
// Option A: return one CPUMesh per *primitive* encountered in the scene struct StackItem { const aiNode* node; int parentIdx; };
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) { std::vector<StackItem> stack{{ scene->mRootNode, -1 }};
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool ok = false; while (!stack.empty()) {
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb"); auto [node, parentIdx] = stack.back();
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path); stack.pop_back();
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n"; std::string name(node->mName.C_Str());
if (!ok) throw std::runtime_error("Failed to load glTF: " + err); int myIdx = parentIdx; // default: pass parent through to children
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0; if (boneOffsets.count(name)) {
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) { const JointId id = nextId++;
throw std::runtime_error("glTF has no valid scene"); myIdx = static_cast<int>(id);
Joint joint{};
joint.id = id;
skeleton.joints.push_back(joint);
skeleton.jointNames.push_back(name);
skeleton.inverseBindMatrices.push_back(boneOffsets[name]);
// Grow hierarchy to accommodate this id
while (skeleton.hierarchy.size() <= id)
skeleton.hierarchy.push_back({});
skeleton.hierarchy[id].id = id; // record the node's own id
if (parentIdx >= 0)
skeleton.hierarchy[parentIdx].children.push_back(id);
} }
std::vector<CPUMesh> result; // Push children in reverse so left-most child is processed first
const tinygltf::Scene& scene = model.scenes.at(sceneIndex); for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c)
stack.push_back({ node->mChildren[c], myIdx });
}
std::vector<NodeStackItem> stack; buildParentIndex(skeleton);
stack.reserve(scene.nodes.size()); return skeleton;
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)}); }
while (!stack.empty()) { // ─── Skinning data ────────────────────────────────────────────────────────────
NodeStackItem it = stack.back();
stack.pop_back();
const tinygltf::Node& node = model.nodes.at(it.nodeIdx); // Populate cpuMesh.skinningData from the matching aiMesh.
const glm::mat4 local = NodeLocalMatrix(node); // Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized.
const glm::mat4 world = it.parentWorld * local; static void LoadSkinningData(CPUMesh& cpuMesh,
const aiMesh* aiMesh,
const Skeleton& skeleton) {
if (!aiMesh->HasBones() || skeleton.joints.empty()) return;
for (int child : node.children) stack.push_back({child, world}); cpuMesh.skinningData.assign(cpuMesh.vertices.size(),
CPUMesh::SkinningData{ {0, 0, 0, 0}, {0.f, 0.f, 0.f, 0.f} });
if (node.mesh < 0) continue; std::vector<int> weightCount(cpuMesh.vertices.size(), 0);
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
for (size_t p = 0; p < mesh.primitives.size(); ++p) { for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) {
const tinygltf::Primitive& prim = mesh.primitives[p]; const aiBone* bone = aiMesh->mBones[b];
CPUMesh cpu = LoadPrimitiveIntoCPUMesh( std::string boneName(bone->mName.C_Str());
model,
prim, // Find which joint index this bone maps to
mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name, std::uint32_t jointIdx = 0;
world bool found = false;
); for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
cpu.name += "_prim" + std::to_string(p); if (skeleton.jointNames[j] == boneName) {
result.push_back(std::move(cpu)); jointIdx = static_cast<std::uint32_t>(j);
found = true;
break;
} }
} }
if (!found) continue;
return result; 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];
// Option B: return one CPUMesh per *glTF mesh instance*, merging all primitives of that mesh into one CPUMesh. if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this
// Note: If the same glTF mesh is instanced by multiple nodes, you'll get one merged CPUMesh PER NODE instance.
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
tinygltf::TinyGLTF loader;
tinygltf::Model model;
std::string err, warn;
bool ok = false; cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx;
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb"); cpuMesh.skinningData[vertIdx].weights[slot] = weight;
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path); ++weightCount[vertIdx];
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
throw std::runtime_error("glTF has no valid scene");
} }
}
}
std::vector<CPUMesh> result; // ─── Animation loading ────────────────────────────────────────────────────────
const tinygltf::Scene& scene = model.scenes.at(sceneIndex); static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
std::vector<NodeStackItem> stack; const Skeleton& skeleton) {
stack.reserve(scene.nodes.size()); std::vector<SkeletalAnimation> result;
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)}); if (!scene->HasAnimations()) return result;
while (!stack.empty()) { for (unsigned int a = 0; a < scene->mNumAnimations; ++a) {
NodeStackItem it = stack.back(); const aiAnimation* aiAnim = scene->mAnimations[a];
stack.pop_back(); const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0;
const tinygltf::Node& node = model.nodes.at(it.nodeIdx); SkeletalAnimation anim;
const glm::mat4 local = NodeLocalMatrix(node); anim.name = aiAnim->mName.C_Str();
const glm::mat4 world = it.parentWorld * local; anim.duration = static_cast<float>(aiAnim->mDuration / tps);
anim.looped = true;
for (int child : node.children) stack.push_back({child, world}); for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) {
const aiNodeAnim* ch = aiAnim->mChannels[c];
std::string boneName(ch->mNodeName.C_Str());
if (node.mesh < 0) continue; // Map bone name to joint index
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh); std::uint32_t jointIdx = 0;
bool found = false;
CPUMesh merged{}; for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
merged.name = mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name; if (skeleton.jointNames[j] == boneName) {
merged.minPos = glm::vec3(std::numeric_limits<float>::infinity()); jointIdx = static_cast<std::uint32_t>(j);
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity()); found = true;
break;
bool any = false;
for (const tinygltf::Primitive& prim : mesh.primitives) {
CPUMesh part = LoadPrimitiveIntoCPUMesh(model, prim, merged.name, world);
if (!any) {
merged = std::move(part);
any = true;
} else {
AppendMesh(merged, part);
} }
} }
if (!found) continue;
if (any) result.push_back(std::move(merged)); SkeletalAnimation::Track track;
track.jointIndex = jointIdx;
// FIX: 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;
// 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;
for (unsigned int k = 0; k < numKeys; ++k) {
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 };
}
track.keyframes.push_back(kf);
}
anim.tracks.push_back(std::move(track));
} }
return result; result.push_back(std::move(anim));
} }
return result;
}
// ─── Public API: static meshes ────────────────────────────────────────────────
// Option A: one CPUMesh per aiMesh reference encountered in the node tree.
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
Assimp::Importer importer;
const aiScene* scene = LoadScene(importer, path);
std::vector<CPUMesh> result;
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
while (!stack.empty()) {
auto [node, parentWorld] = stack.back();
stack.pop_back();
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
for (unsigned int c = 0; c < node->mNumChildren; ++c)
stack.push_back({ node->mChildren[c], world });
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
std::string name = mesh->mName.length > 0
? std::string(mesh->mName.C_Str())
: ("mesh_" + std::to_string(node->mMeshes[m]));
name += "_prim" + std::to_string(m);
result.push_back(LoadAiMeshIntoCPUMesh(mesh, name, world));
}
}
return result;
}
// Option B: one CPUMesh per node, merging all aiMesh references of that node.
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
Assimp::Importer importer;
const aiScene* scene = LoadScene(importer, path);
std::vector<CPUMesh> result;
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
while (!stack.empty()) {
auto [node, parentWorld] = stack.back();
stack.pop_back();
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
for (unsigned int c = 0; c < node->mNumChildren; ++c)
stack.push_back({ node->mChildren[c], world });
if (node->mNumMeshes == 0) continue;
std::string mergedName = node->mName.length > 0
? std::string(node->mName.C_Str())
: std::string(scene->mMeshes[node->mMeshes[0]]->mName.C_Str());
CPUMesh merged{};
merged.name = mergedName;
merged.minPos = glm::vec3( std::numeric_limits<float>::infinity());
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
bool any = false;
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
CPUMesh part = LoadAiMeshIntoCPUMesh(mesh, mergedName, world);
if (!any) { merged = std::move(part); any = true; }
else AppendMesh(merged, part);
}
if (any) result.push_back(std::move(merged));
}
return result;
}
// ─── Public API: skinned model ────────────────────────────────────────────────
struct SkinnedModel {
std::vector<CPUMesh> meshes;
Skeleton skeleton;
std::vector<SkeletalAnimation> animations;
};
// Loads meshes + skeleton + animations in a single Assimp pass.
// Each CPUMesh has skinningData and skeleton populated.
//
// IMPORTANT: skinned mesh vertices are loaded with an identity world transform
// so they remain in bind-pose space. The skinning shader applies joint matrices
// at runtime — baking the node transform into positions would break that.
//
// Usage:
// auto model = ModelLoader::LoadSkinnedModel("character.glb");
// MeshId id = meshCache.uploadMesh(model.meshes[0]);
// auto* anim = go.AddComponent<Animator>();
// for (auto& clip : model.animations)
// anim->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
// anim->play("Walk");
static SkinnedModel LoadSkinnedModel(const std::string& path) {
Assimp::Importer importer;
const aiScene* scene = LoadScene(importer, path);
SkinnedModel model;
model.skeleton = LoadSkeleton(scene);
model.animations = LoadAnimations(scene, model.skeleton);
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.f) }};
while (!stack.empty()) {
auto [node, parentWorld] = stack.back();
stack.pop_back();
// We still traverse with world transforms so non-skinned siblings
// work correctly, but skinned meshes are loaded with identity below.
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
for (unsigned int c = 0; c < node->mNumChildren; ++c)
stack.push_back({ node->mChildren[c], world });
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
const aiMesh* aiMesh = scene->mMeshes[node->mMeshes[m]];
std::string name = aiMesh->mName.length > 0
? std::string(aiMesh->mName.C_Str())
: ("mesh_" + std::to_string(node->mMeshes[m]));
// FIX: pass identity matrix for skinned meshes so vertices stay in
// bind-pose space. The inverse bind matrices and joint transforms
// are already expressed in that space — baking the node world
// transform into positions would offset everything incorrectly.
const bool isSkinned = aiMesh->HasBones() && !model.skeleton.joints.empty();
const glm::mat4 meshWorld = isSkinned ? glm::mat4{1.f} : world;
CPUMesh cpu = LoadAiMeshIntoCPUMesh(aiMesh, name, meshWorld);
LoadSkinningData(cpu, aiMesh, model.skeleton);
cpu.skeleton = model.skeleton;
model.meshes.push_back(std::move(cpu));
}
}
return model;
}
} // namespace ModelLoader } // namespace ModelLoader
#endif // MODELLOADER_H #endif // MODELLOADER_H

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/Components/MeshRendererComponent.h>
#include <destrum/ObjectModel/Transform.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") { MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
} }
void MeshRendererComponent::Start() { void MeshRendererComponent::Start() {
Component::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::Update() {
} }
void MeshRendererComponent::Render(const RenderContext& ctx) { 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); ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
} }
} }

View File

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

View File

@@ -7,11 +7,11 @@
namespace namespace
{ {
static const std::uint32_t maxBindlessResources = 16536; constexpr std::uint32_t maxBindlessResources = 16536;
static const std::uint32_t maxSamplers = 32; constexpr std::uint32_t maxSamplers = 32;
static const std::uint32_t texturesBinding = 0; constexpr std::uint32_t texturesBinding = 0;
static const std::uint32_t samplersBinding = 1; constexpr std::uint32_t samplersBinding = 1;
} }
void BindlessSetManager::init(VkDevice device, float maxAnisotropy) 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, .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT, .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT,
.maxSets = 10, .maxSets = 10,
.poolSizeCount = (std::uint32_t)poolSizesBindless.size(), .poolSizeCount = static_cast<std::uint32_t>(poolSizesBindless.size()),
.pPoolSizes = poolSizesBindless.data(), .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; auto& mainCommandBuffer = frames[i].commandBuffer;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
} }
//
// { // create white texture { // create white texture
// std::uint32_t pixel = 0xFFFFFFFF; std::uint32_t pixel = 0xFFFFFFFF;
// whiteImageId = createImage( whiteImageId = createImage(
// { {
// .format = VK_FORMAT_R8G8B8A8_UNORM, .format = VK_FORMAT_R8G8B8A8_UNORM,
// .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
// .extent = VkExtent3D{1, 1, 1}, .extent = VkExtent3D{1, 1, 1},
// }, },
// "white texture", "white texture",
// &pixel); &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); GameState::GetInstance().SetGfxDevice(this);
} }
@@ -495,25 +513,14 @@ GPUImage GfxDevice::createImageRaw(
return image; return image;
} }
GPUImage GfxDevice::loadImageFromFileRaw( GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const {
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)
const auto data = util::loadImage(path, intent); 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()); spdlog::error("Failed to load image '{}'", path.string());
return getImage(errorImageId); return getImage(errorImageId);
} }
// 2) Create GPU image using the format the loader chose (matches CPU memory layout)
auto image = createImageRaw({ auto image = createImageRaw({
.format = data.vkFormat, .format = data.vkFormat,
.usage = usage | .usage = usage |
@@ -527,15 +534,10 @@ GPUImage GfxDevice::loadImageFromFileRaw(
.mipMap = mipMap, .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); uploadImageDataSized(image, src, data.byteSize, 0);
// 4) Debug label
image.debugName = path.string(); image.debugName = path.string();
vkutil::addDebugLabel(device, image.image, path.string().c_str()); vkutil::addDebugLabel(device, image.image, path.string().c_str());

View File

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

View File

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

View File

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

View File

@@ -3,25 +3,30 @@
#include <destrum/Graphics/Resources/Mesh.h> #include <destrum/Graphics/Resources/Mesh.h>
#include <destrum/Graphics/GfxDevice.h> #include <destrum/Graphics/GfxDevice.h>
#include <destrum/Graphics/Util.h> #include <destrum/Graphics/Util.h>
#include <destrum/Util/MathUtils.h>
// #include <destrum/Math/Util.h> // #include <destrum/Math/Util.h>
MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh) MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
{ {
auto gpuMesh = GPUMesh{ auto gpuMesh = GPUMesh{
.numVertices = (std::uint32_t)cpuMesh.vertices.size(), .numVertices = (std::uint32_t)cpuMesh.vertices.size(),
.numIndices = (std::uint32_t)cpuMesh.indices.size(), .numIndices = (std::uint32_t)cpuMesh.indices.size(),
.minPos = cpuMesh.minPos, .minPos = cpuMesh.minPos,
.maxPos = cpuMesh.maxPos, .maxPos = cpuMesh.maxPos,
.hasSkeleton = cpuMesh.skeleton.has_value(),
}; };
std::vector<glm::vec3> positions(cpuMesh.vertices.size()); 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; positions[i] = cpuMesh.vertices[i].position;
} gpuMesh.boundingSphere = calculateBoundingSphere(positions);
uploadMesh(gfxDevice, cpuMesh, gpuMesh); uploadMesh(gfxDevice, cpuMesh, gpuMesh);
const auto id = meshes.size(); const auto id = meshes.size();
meshes.push_back(std::move(gpuMesh)); meshes.push_back(std::move(gpuMesh));
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
return id; return id;
} }
@@ -70,6 +75,33 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
const auto idxBufferName = cpuMesh.name + " (idx)"; const auto idxBufferName = cpuMesh.name + " (idx)";
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str()); vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.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 const GPUMesh& MeshCache::getMesh(MeshID id) const
@@ -77,6 +109,11 @@ const GPUMesh& MeshCache::getMesh(MeshID id) const
return meshes.at(id); return meshes.at(id);
} }
const CPUMesh& MeshCache::getCPUMesh(MeshID id) const
{
return cpuMeshes.at(id);
}
void MeshCache::cleanup(const GfxDevice& gfxDevice) void MeshCache::cleanup(const GfxDevice& gfxDevice)
{ {
for (const auto& mesh : meshes) { for (const auto& mesh : meshes) {

View File

@@ -1,6 +1,9 @@
#include <destrum/Graphics/Pipelines/MeshPipeline.h> #include <destrum/Graphics/Pipelines/MeshPipeline.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include "destrum/Graphics/Frustum.h"
#include "spdlog/spdlog.h"
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} { MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
} }
@@ -68,6 +71,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
m_pipeline->bind(cmd); m_pipeline->bind(cmd);
gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout); gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout);
int ActualDrawCalls = 0;
const auto viewport = VkViewport{ const auto viewport = VkViewport{
.x = 0, .x = 0,
.y = 0, .y = 0,
@@ -86,16 +91,17 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
auto prevMeshId = NULL_MESH_ID; auto prevMeshId = NULL_MESH_ID;
// const auto frustum = edge::createFrustumFromCamera(camera); const auto frustum = edge::createFrustumFromCamera(camera);
for (const auto& dcIdx : drawCommands) { for (const auto& dcIdx : drawCommands) {
// const auto& dc = drawCommands[dcIdx];
const auto& dc = dcIdx; const auto& dc = dcIdx;
// if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) { // if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
// continue; // continue;
// } // }
ActualDrawCalls++;
const auto& mesh = meshCache.getMesh(dc.meshId); const auto& mesh = meshCache.getMesh(dc.meshId);
if (dc.meshId != prevMeshId) { if (dc.meshId != prevMeshId) {
prevMeshId = dc.meshId; prevMeshId = dc.meshId;
@@ -106,7 +112,7 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
const auto pushConstants = PushConstants{ const auto pushConstants = PushConstants{
.transform = dc.transformMatrix, .transform = dc.transformMatrix,
.sceneDataBuffer = sceneDataBuffer.address, .sceneDataBuffer = sceneDataBuffer.address,
.vertexBuffer = mesh.vertexBuffer.address, .vertexBuffer = dc.skinnedMesh != nullptr ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address,
.materialId = dc.materialId, .materialId = dc.materialId,
}; };
vkCmdPushConstants( 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 = std::make_unique<SkyboxPipeline>();
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat); skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
skinningPipeline = std::make_unique<SkinningPipeline>();
skinningPipeline->init(gfxDevice);
GameState::GetInstance().SetRenderer(this); GameState::GetInstance().SetRenderer(this);
} }
@@ -30,6 +33,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) { void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
flushMaterialUpdates(gfxDevice); flushMaterialUpdates(gfxDevice);
meshDrawCommands.clear(); meshDrawCommands.clear();
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
} }
void GameRenderer::endDrawing() { void GameRenderer::endDrawing() {
@@ -37,6 +41,13 @@ void GameRenderer::endDrawing() {
} }
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) { 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{ const auto gpuSceneData = GPUSceneData{
.view = sceneData.camera.GetViewMatrix(), .view = sceneData.camera.GetViewMatrix(),
.proj = sceneData.camera.GetProjectionMatrix(), .proj = sceneData.camera.GetProjectionMatrix(),
@@ -107,7 +118,7 @@ void GameRenderer::cleanup(VkDevice device) {
void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) { void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) {
const auto& mesh = meshCache.getMesh(id); 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); assert(materialId != NULL_MATERIAL_ID);
meshDrawCommands.push_back(MeshDrawCommand{ 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 { const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
return gfx_device.getImage(drawImageId); return gfx_device.getImage(drawImageId);
} }

View File

@@ -122,6 +122,16 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c
vkSetDebugUtilsObjectNameEXT(device, &nameInfo); 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) { void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,

View File

@@ -1,4 +1,5 @@
# glm # glm
set(BUILD_SHARED_LIBS OFF)
add_subdirectory(glm) add_subdirectory(glm)
# stb # stb
@@ -56,3 +57,10 @@ add_subdirectory(tinygltf)
add_subdirectory(tinyexr) add_subdirectory(tinyexr)
target_include_directories(tinyexr PUBLIC "${CMAKE_CURRENT_LIST_DIR}/tinyexr") target_include_directories(tinyexr PUBLIC "${CMAKE_CURRENT_LIST_DIR}/tinyexr")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE)
set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(assimp)

1
destrum/third_party/assimp vendored Submodule

View File

@@ -22,14 +22,26 @@ target_include_directories(lightkeeper PRIVATE "${CMAKE_CURRENT_LIST_DIR}/includ
target_link_libraries(lightkeeper PRIVATE destrum::destrum) target_link_libraries(lightkeeper PRIVATE destrum::destrum)
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src") #set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime") #set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game") #set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game")
#
#add_custom_command(TARGET lightkeeper POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}"
# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}"
# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}"
# VERBATIM
#)
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets/game")
add_custom_command(TARGET lightkeeper POST_BUILD add_custom_command(TARGET lightkeeper POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/assets"
COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}"
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}" COMMAND ${CMAKE_COMMAND} -E rm -rf "${CMAKE_CURRENT_BINARY_DIR}/assets/game"
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/assets/game"
VERBATIM VERBATIM
) )
@@ -47,5 +59,4 @@ add_custom_target(_internal_cook_game_assets ALL
DEPENDS TheChef DEPENDS TheChef
) )
destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}")

Binary file not shown.

View File

@@ -11,6 +11,7 @@
#include "destrum/Components/OrbitAndSpin.h" #include "destrum/Components/OrbitAndSpin.h"
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelLoader.h" #include "destrum/Util/ModelLoader.h"
#include "destrum/Components/Animator.h"
LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) { LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) {
} }
@@ -52,19 +53,19 @@ void LightKeeper::customInit() {
const float orbitRadius = 5.0f; const float orbitRadius = 5.0f;
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i)); // auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
//
auto childMeshComp = childCube->AddComponent<MeshRendererComponent>(); // auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
childMeshComp->SetMeshID(testMeshID); // childMeshComp->SetMeshID(testMeshID);
childMeshComp->SetMaterialID(testMaterialID); // childMeshComp->SetMaterialID(testMaterialID);
//
childCube->GetTransform().SetWorldScale(glm::vec3(0.1f)); // childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
//
// Add orbit + self spin // // Add orbit + self spin
auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f)); // auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
orbit->Randomize(1337u + (uint32_t)i); // stable random per index // orbit->Randomize(1337u + (uint32_t)i); // stable random per index
//
scene.Add(childCube); // scene.Add(childCube);
} }
testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec
//rotate 180 around X axis //rotate 180 around X axis
@@ -94,6 +95,68 @@ void LightKeeper::customInit() {
skyboxCubemap->CreateCubeMap(); skyboxCubemap->CreateCubeMap();
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID()); renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
const auto planeObj = std::make_shared<GameObject>("GroundPlane");
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
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<GameObject>("Character");
auto charModel = ModelLoader::LoadSkinnedModel(
AssetFS::GetInstance().GetFullPath("engine://char.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<MeshRendererComponent>();
charMeshComp->SetMeshID(charMeshID);
charMeshComp->SetMaterialID(charMaterialID);
const auto animator = CharObj->AddComponent<Animator>();
animator->setSkeleton(std::move(charModel.skeleton));
for (auto& clip : charModel.animations) {
animator->addClip(std::make_shared<SkeletalAnimation>(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(charModel.animations[0].name);
// animator->play("Armature|main");
animator->play("Armature|mixamo.com");
// 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) { void LightKeeper::customUpdate(float dt) {