Added skeletal animations and other fixes
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -38,3 +38,6 @@
|
||||
[submodule "destrum/third_party/tinyexr"]
|
||||
path = destrum/third_party/tinyexr
|
||||
url = https://github.com/syoyo/tinyexr.git
|
||||
[submodule "destrum/third_party/assimp"]
|
||||
path = destrum/third_party/assimp
|
||||
url = https://github.com/assimp/assimp.git
|
||||
|
||||
@@ -22,4 +22,4 @@ add_custom_target(CookAssets)
|
||||
add_dependencies(CookAssets
|
||||
_internal_cook_game_assets
|
||||
_internal_cook_engine_assets
|
||||
)
|
||||
)
|
||||
|
||||
2
TheChef
2
TheChef
Submodule TheChef updated: faaf7fa120...3a7b137165
@@ -4,6 +4,7 @@ set(SRC_FILES
|
||||
"src/App.cpp"
|
||||
"src/Event.cpp"
|
||||
|
||||
"src/Components/Animator.cpp"
|
||||
"src/Components/MeshRendererComponent.cpp"
|
||||
"src/Components/Rotator.cpp"
|
||||
"src/Components/Spinner.cpp"
|
||||
@@ -11,6 +12,8 @@ set(SRC_FILES
|
||||
|
||||
"src/Graphics/BindlessSetManager.cpp"
|
||||
"src/Graphics/Camera.cpp"
|
||||
"src/Graphics/ComputePipeline.cpp"
|
||||
"src/Graphics/Frustum.cpp"
|
||||
"src/Graphics/GfxDevice.cpp"
|
||||
"src/Graphics/ImageCache.cpp"
|
||||
"src/Graphics/ImageLoader.cpp"
|
||||
@@ -29,6 +32,7 @@ set(SRC_FILES
|
||||
|
||||
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
||||
"src/Graphics/Pipelines/SkyboxPipeline.cpp"
|
||||
"src/Graphics/Pipelines/SkinningPipeline.cpp"
|
||||
|
||||
"src/Input/InputManager.cpp"
|
||||
|
||||
@@ -69,6 +73,7 @@ target_link_libraries(destrum
|
||||
spdlog::spdlog
|
||||
stb::image
|
||||
tinygltf
|
||||
assimp
|
||||
|
||||
PRIVATE
|
||||
freetype::freetype
|
||||
@@ -105,14 +110,14 @@ target_compile_definitions(destrum
|
||||
|
||||
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
||||
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
||||
set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
|
||||
|
||||
add_custom_command(TARGET destrum POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets"
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||
VERBATIM
|
||||
)
|
||||
#set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
|
||||
#
|
||||
#add_custom_command(TARGET destrum POST_BUILD
|
||||
# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||
# COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets"
|
||||
# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||
# VERBATIM
|
||||
#)
|
||||
|
||||
add_custom_target(_internal_clean_engine_assets
|
||||
COMMAND TheChef
|
||||
@@ -127,3 +132,17 @@ add_custom_target(_internal_cook_engine_assets ALL
|
||||
--output "${ASSETS_RUNTIME_DIR}"
|
||||
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
BIN
destrum/assets_src/char.fbx
Normal file
Binary file not shown.
BIN
destrum/assets_src/char.jpg
Normal file
BIN
destrum/assets_src/char.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
destrum/assets_src/char2.fbx
Normal file
BIN
destrum/assets_src/char2.fbx
Normal file
Binary file not shown.
@@ -14,6 +14,10 @@ vec4 sampleTexture2DNearest(uint texID, vec2 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) {
|
||||
return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
struct MaterialData {
|
||||
vec4 baseColor;
|
||||
vec4 metallicRoughnessEmissive;
|
||||
uint textureFilteringMode;
|
||||
uint diffuseTex;
|
||||
uint normalTex;
|
||||
uint metallicRoughnessTex;
|
||||
|
||||
@@ -17,8 +17,9 @@ layout (location = 0) out vec4 outFragColor;
|
||||
void main()
|
||||
{
|
||||
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;
|
||||
|
||||
|
||||
|
||||
60
destrum/assets_src/shaders/skinning.comp
Normal file
60
destrum/assets_src/shaders/skinning.comp
Normal 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;
|
||||
}
|
||||
70
destrum/include/destrum/Components/Animator.h
Normal file
70
destrum/include/destrum/Components/Animator.h
Normal 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
|
||||
@@ -21,6 +21,8 @@ public:
|
||||
private:
|
||||
MeshID meshID{NULL_MESH_ID};
|
||||
MaterialID materialID{NULL_MATERIAL_ID};
|
||||
|
||||
std::unique_ptr<SkinnedMesh> m_skinnedMesh;
|
||||
};
|
||||
|
||||
#endif //MESHRENDERERCOMPONENT_H
|
||||
|
||||
@@ -96,6 +96,13 @@ public:
|
||||
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};
|
||||
|
||||
void Translate(const glm::vec3& translation) {
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
#ifndef 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
|
||||
|
||||
@@ -1,4 +1,80 @@
|
||||
#ifndef 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
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "Util.h"
|
||||
|
||||
class MeshCache;
|
||||
|
||||
namespace {
|
||||
using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,21 @@
|
||||
#include <string>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
struct MaterialData {
|
||||
glm::vec4 baseColor;
|
||||
glm::vec4 metalRoughnessEmissive;
|
||||
std::uint32_t diffuseTex;
|
||||
std::uint32_t normalTex;
|
||||
std::uint32_t metallicRoughnessTex;
|
||||
std::uint32_t emissiveTex;
|
||||
struct alignas(16) MaterialData {
|
||||
alignas(16) glm::vec4 baseColor;
|
||||
alignas(16) glm::vec4 metalRoughnessEmissive;
|
||||
uint32_t textureFilteringMode;
|
||||
uint32_t diffuseTex;
|
||||
uint32_t normalTex;
|
||||
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 {
|
||||
@@ -19,6 +27,8 @@ struct Material {
|
||||
float roughnessFactor{0.7f};
|
||||
float emissiveFactor{0.f};
|
||||
|
||||
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
||||
|
||||
ImageID diffuseTexture{NULL_IMAGE_ID};
|
||||
// ImageId normalMapTexture{NULL_IMAGE_ID};
|
||||
// ImageId metallicRoughnessTexture{NULL_IMAGE_ID};
|
||||
|
||||
@@ -18,13 +18,14 @@ public:
|
||||
void cleanup(GfxDevice& gfxDevice);
|
||||
|
||||
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;
|
||||
MaterialID getPlaceholderMaterialId() const;
|
||||
[[nodiscard]] MaterialID getFreeMaterialId() const;
|
||||
[[nodiscard]] MaterialID getPlaceholderMaterialId() const;
|
||||
|
||||
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||
[[nodiscard]] const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||
[[nodiscard]] VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||
|
||||
|
||||
Material& getMaterialMutable(MaterialID id);
|
||||
@@ -33,7 +34,7 @@ public:
|
||||
private:
|
||||
std::vector<Material> materials;
|
||||
|
||||
static const auto MAX_MATERIALS = 1000;
|
||||
static constexpr auto MAX_MATERIALS = 1000;
|
||||
GPUBuffer materialDataBuffer;
|
||||
|
||||
// material which is used for meshes without materials
|
||||
|
||||
@@ -13,11 +13,13 @@ public:
|
||||
|
||||
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
||||
const GPUMesh& getMesh(MeshID id) const;
|
||||
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||
|
||||
private:
|
||||
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
|
||||
|
||||
std::vector<GPUMesh> meshes;
|
||||
std::vector<CPUMesh> cpuMeshes;
|
||||
};
|
||||
|
||||
#endif //MESHCACHE_H
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
#include <destrum/Graphics/ids.h>
|
||||
|
||||
#include <destrum/Graphics/Frustum.h>
|
||||
|
||||
|
||||
struct MeshDrawCommand {
|
||||
MeshID meshId;
|
||||
glm::mat4 transformMatrix;
|
||||
|
||||
// for frustum culling
|
||||
// math::Sphere worldBoundingSphere;
|
||||
Sphere worldBoundingSphere;
|
||||
|
||||
// If set - mesh will be drawn with overrideMaterialId
|
||||
// instead of whatever material the mesh has
|
||||
@@ -20,8 +22,8 @@ struct MeshDrawCommand {
|
||||
bool castShadow{true};
|
||||
|
||||
// skinned meshes only
|
||||
// const SkinnedMesh* skinnedMesh{nullptr};
|
||||
// std::uint32_t jointMatricesStartIndex;
|
||||
const SkinnedMesh* skinnedMesh{nullptr};
|
||||
std::uint32_t jointMatricesStartIndex;
|
||||
};
|
||||
|
||||
#endif //MESHDRAWCOMMAND_H
|
||||
|
||||
@@ -1,4 +1,58 @@
|
||||
#ifndef 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
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
||||
#include "Pipelines/SkyboxPipeline.h"
|
||||
|
||||
class GameRenderer {
|
||||
@@ -35,6 +36,11 @@ public:
|
||||
void cleanup(VkDevice device);
|
||||
|
||||
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;
|
||||
|
||||
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) {
|
||||
@@ -47,6 +53,13 @@ public:
|
||||
void setSkyboxTexture(ImageID skyboxImageId);
|
||||
|
||||
void flushMaterialUpdates(GfxDevice& gfxDevice);
|
||||
[[nodiscard]] MeshCache& GetMeshCache() const {
|
||||
return meshCache;
|
||||
}
|
||||
|
||||
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const {
|
||||
return *skinningPipeline;
|
||||
}
|
||||
|
||||
private:
|
||||
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
||||
@@ -91,6 +104,8 @@ private:
|
||||
|
||||
std::unique_ptr<MeshPipeline> meshPipeline;
|
||||
std::unique_ptr<SkyboxPipeline> skyboxPipeline;
|
||||
|
||||
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
||||
};
|
||||
|
||||
#endif //RENDERER_H
|
||||
|
||||
30
destrum/include/destrum/Graphics/Resources/AnimationClip.h
Normal file
30
destrum/include/destrum/Graphics/Resources/AnimationClip.h
Normal 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
|
||||
@@ -1,4 +1,40 @@
|
||||
#ifndef 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
|
||||
|
||||
@@ -2,33 +2,48 @@
|
||||
#define MESH_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/vec2.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/vec4.hpp>
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
#include <destrum/Graphics/Resources/Buffer.h>
|
||||
#include <destrum/Graphics/Frustum.h>
|
||||
#include <destrum/Graphics/Skeleton.h>
|
||||
|
||||
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
|
||||
struct CPUMesh {
|
||||
std::vector<std::uint32_t> indices;
|
||||
|
||||
struct Vertex {
|
||||
glm::vec3 position;
|
||||
float uv_x{};
|
||||
float uv_x{};
|
||||
glm::vec3 normal;
|
||||
float uv_y{};
|
||||
float uv_y{};
|
||||
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;
|
||||
|
||||
glm::vec3 minPos;
|
||||
glm::vec3 maxPos;
|
||||
|
||||
std::optional<Skeleton> skeleton; // present only for skinned meshes
|
||||
};
|
||||
|
||||
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
|
||||
|
||||
struct GPUMesh {
|
||||
GPUBuffer vertexBuffer;
|
||||
@@ -40,78 +55,64 @@ struct GPUMesh {
|
||||
// AABB
|
||||
glm::vec3 minPos;
|
||||
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)
|
||||
// =======================
|
||||
{{-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}, 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)
|
||||
// =======================
|
||||
{{ 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}, 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)
|
||||
// =======================
|
||||
{{ 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}, 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)
|
||||
// =======================
|
||||
{{-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}, 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)
|
||||
// =======================
|
||||
{{-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}, 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)
|
||||
// =======================
|
||||
{{-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}, 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 = {
|
||||
// Front (+Z)
|
||||
0, 2, 1, 0, 3, 2,
|
||||
|
||||
// Back (-Z)
|
||||
4, 6, 5, 4, 7, 6,
|
||||
|
||||
// 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
|
||||
inline std::vector<std::uint32_t> indices = {
|
||||
0, 2, 1, 0, 3, 2, // Front
|
||||
4, 6, 5, 4, 7, 6, // Back
|
||||
8,10, 9, 8,11,10, // Right
|
||||
12,14,13, 12,15,14, // Left
|
||||
16,18,17, 16,19,18, // Top
|
||||
20,22,21, 20,23,22, // Bottom
|
||||
};
|
||||
|
||||
} // namespace CubeMesh
|
||||
|
||||
#endif //MESH_H
|
||||
#endif // MESH_H
|
||||
35
destrum/include/destrum/Graphics/SkeletalAnimation.h
Normal file
35
destrum/include/destrum/Graphics/SkeletalAnimation.h
Normal 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
|
||||
55
destrum/include/destrum/Graphics/Skeleton.h
Normal file
55
destrum/include/destrum/Graphics/Skeleton.h
Normal 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
|
||||
@@ -16,4 +16,8 @@ constexpr MaterialID NULL_MATERIAL_ID = std::numeric_limits<std::uint32_t>::max(
|
||||
using BindlessID = std::uint32_t;
|
||||
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
|
||||
|
||||
@@ -1,4 +1,86 @@
|
||||
#ifndef 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
|
||||
|
||||
@@ -1,498 +1,521 @@
|
||||
#ifndef MODELLOADER_H
|
||||
#define MODELLOADER_H
|
||||
|
||||
// CPUMesh loader with tinygltf
|
||||
// - Loads first scene (or default scene), iterates nodes, extracts mesh primitives.
|
||||
// - Handles POSITION/NORMAL/TANGENT/TEXCOORD_0 and indices.
|
||||
// - Computes minPos/maxPos.
|
||||
// - Can return per-primitive meshes, or merged-per-gltf-mesh meshes.
|
||||
// - IMPORTANT FIX: Applies node transforms (TRS / matrix) so models don't appear flipped/rotated.
|
||||
// CPUMesh loader using Assimp
|
||||
// - Loads first scene, iterates nodes recursively, extracts mesh primitives.
|
||||
// - Handles POSITION / NORMAL / TANGENT / TEXCOORD_0 and indices.
|
||||
// - Handles bones/skinning data and skeletal animations.
|
||||
// - Computes minPos / maxPos (in world space).
|
||||
// - 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
|
||||
// if included in more than one translation unit. Best practice: put the TINYGLTF_IMPLEMENTATION
|
||||
// define in exactly one .cpp. I keep it here because your original file did, but you may want
|
||||
// to move it.
|
||||
// Link against: assimp (e.g. -lassimp)
|
||||
// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …).
|
||||
|
||||
#define TINYGLTF_IMPLEMENTATION
|
||||
#define TINYGLTF_NO_STB_IMAGE_WRITE
|
||||
#include <tiny_gltf.h>
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp> // translate/scale
|
||||
#include <glm/gtc/quaternion.hpp> // quat
|
||||
#include <glm/gtx/quaternion.hpp> // mat4_cast
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <destrum/Graphics/Resources/Mesh.h>
|
||||
#include <destrum/Graphics/Skeleton.h>
|
||||
#include <destrum/Graphics/SkeletalAnimation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
namespace ModelLoader {
|
||||
|
||||
// -------------------- helpers --------------------
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static size_t ComponentSizeInBytes(int componentType) {
|
||||
switch (componentType) {
|
||||
case TINYGLTF_COMPONENT_TYPE_BYTE: return 1;
|
||||
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");
|
||||
}
|
||||
}
|
||||
// Convert an Assimp row-major 4×4 matrix to GLM column-major.
|
||||
static glm::mat4 ToGLM(const aiMatrix4x4& m) {
|
||||
return glm::transpose(glm::make_mat4(&m.a1));
|
||||
}
|
||||
|
||||
static int TypeNumComponents(int type) {
|
||||
switch (type) {
|
||||
case TINYGLTF_TYPE_SCALAR: return 1;
|
||||
case TINYGLTF_TYPE_VEC2: return 2;
|
||||
case TINYGLTF_TYPE_VEC3: return 3;
|
||||
case TINYGLTF_TYPE_VEC4: return 4;
|
||||
case TINYGLTF_TYPE_MAT2: return 4;
|
||||
case TINYGLTF_TYPE_MAT3: return 9;
|
||||
case TINYGLTF_TYPE_MAT4: return 16;
|
||||
default: throw std::runtime_error("Unknown glTF type");
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Returns pointer to the first element, plus stride in bytes.
|
||||
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);
|
||||
// ─── Post-process flags ───────────────────────────────────────────────────────
|
||||
|
||||
const size_t componentSize = ComponentSizeInBytes(accessor.componentType);
|
||||
const int numComps = TypeNumComponents(accessor.type);
|
||||
const size_t packedStride = componentSize * size_t(numComps);
|
||||
static constexpr unsigned int kImportFlags =
|
||||
aiProcess_Triangulate |
|
||||
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);
|
||||
// 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");
|
||||
}
|
||||
// ─── Primitive extraction ─────────────────────────────────────────────────────
|
||||
|
||||
const std::uint8_t* ptr = buf.data.data() + start;
|
||||
return {ptr, stride};
|
||||
}
|
||||
// Build one CPUMesh from a single aiMesh, applying the given world transform.
|
||||
// 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>
|
||||
static T ReadAs(const std::uint8_t* p) {
|
||||
T v{};
|
||||
std::memcpy(&v, p, sizeof(T));
|
||||
return v;
|
||||
}
|
||||
const size_t vertexCount = mesh->mNumVertices;
|
||||
out.vertices.resize(vertexCount);
|
||||
|
||||
static glm::vec3 ReadVec3Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
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};
|
||||
}
|
||||
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
||||
const glm::mat3 tanMat = glm::mat3(world);
|
||||
|
||||
static glm::vec2 ReadVec2Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
const float x = ReadAs<float>(p + 0);
|
||||
const float y = ReadAs<float>(p + 4);
|
||||
return glm::vec2{x, y};
|
||||
}
|
||||
glm::vec3 mn{ std::numeric_limits<float>::infinity()};
|
||||
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
||||
|
||||
static glm::vec4 ReadVec4Float(const std::uint8_t* base, size_t stride, size_t i) {
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
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};
|
||||
}
|
||||
for (size_t i = 0; i < vertexCount; ++i) {
|
||||
CPUMesh::Vertex v{};
|
||||
|
||||
static std::uint32_t ReadIndexAsU32(const tinygltf::Model& model, const tinygltf::Accessor& accessor, size_t i) {
|
||||
auto [base, stride] = GetAccessorDataPtrAndStride(model, accessor);
|
||||
const std::uint8_t* p = base + i * stride;
|
||||
// Position (required)
|
||||
const aiVector3D& ap = mesh->mVertices[i];
|
||||
v.position = glm::vec3(world * glm::vec4(ap.x, ap.y, ap.z, 1.0f));
|
||||
UpdateBounds(mn, mx, v.position);
|
||||
|
||||
switch (accessor.componentType) {
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
|
||||
return static_cast<std::uint32_t>(ReadAs<std::uint8_t>(p));
|
||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
|
||||
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);
|
||||
}
|
||||
// Normal
|
||||
if (mesh->HasNormals()) {
|
||||
const aiVector3D& an = mesh->mNormals[i];
|
||||
v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z));
|
||||
} else {
|
||||
out.indices.resize(vertexCount);
|
||||
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
|
||||
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
// If your renderer expects opposite winding vs glTF, you can flip triangle order.
|
||||
// Keep what you had:
|
||||
// for (size_t i = 0; i + 2 < out.indices.size(); i += 3) {
|
||||
// std::swap(out.indices[i + 1], out.indices[i + 2]);
|
||||
// }
|
||||
|
||||
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;
|
||||
// UV channel 0
|
||||
if (mesh->HasTextureCoords(0)) {
|
||||
v.uv_x = mesh->mTextureCoords[0][i].x;
|
||||
v.uv_y = mesh->mTextureCoords[0][i].y;
|
||||
} else {
|
||||
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)
|
||||
);
|
||||
v.uv_x = 0.0f;
|
||||
v.uv_y = 0.0f;
|
||||
}
|
||||
|
||||
// Tangent + bitangent sign (stored in w as +1/-1 handedness)
|
||||
if (mesh->HasTangentsAndBitangents()) {
|
||||
const aiVector3D& at = mesh->mTangents[i];
|
||||
const aiVector3D& ab = mesh->mBitangents[i];
|
||||
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(at.x, at.y, at.z));
|
||||
glm::vec3 b3 = glm::normalize(tanMat * glm::vec3(ab.x, ab.y, ab.z));
|
||||
glm::vec3 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 {
|
||||
int nodeIdx;
|
||||
glm::mat4 parentWorld;
|
||||
};
|
||||
// Pass 2: walk the node tree in depth-first order (parent before child),
|
||||
// registering only nodes that correspond to actual bones.
|
||||
// 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
|
||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
||||
tinygltf::TinyGLTF loader;
|
||||
tinygltf::Model model;
|
||||
std::string err, warn;
|
||||
struct StackItem { const aiNode* node; int parentIdx; };
|
||||
std::vector<StackItem> stack{{ scene->mRootNode, -1 }};
|
||||
|
||||
bool ok = false;
|
||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
||||
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
||||
while (!stack.empty()) {
|
||||
auto [node, parentIdx] = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
||||
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
||||
std::string name(node->mName.C_Str());
|
||||
int myIdx = parentIdx; // default: pass parent through to children
|
||||
|
||||
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");
|
||||
if (boneOffsets.count(name)) {
|
||||
const JointId id = nextId++;
|
||||
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;
|
||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
||||
// Push children in reverse so left-most child is processed first
|
||||
for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c)
|
||||
stack.push_back({ node->mChildren[c], myIdx });
|
||||
}
|
||||
|
||||
std::vector<NodeStackItem> stack;
|
||||
stack.reserve(scene.nodes.size());
|
||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
||||
buildParentIndex(skeleton);
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
while (!stack.empty()) {
|
||||
NodeStackItem it = stack.back();
|
||||
stack.pop_back();
|
||||
// ─── Skinning data ────────────────────────────────────────────────────────────
|
||||
|
||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
||||
const glm::mat4 local = NodeLocalMatrix(node);
|
||||
const glm::mat4 world = it.parentWorld * local;
|
||||
// Populate cpuMesh.skinningData from the matching aiMesh.
|
||||
// Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized.
|
||||
static void LoadSkinningData(CPUMesh& cpuMesh,
|
||||
const aiMesh* aiMesh,
|
||||
const Skeleton& skeleton) {
|
||||
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;
|
||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
||||
std::vector<int> weightCount(cpuMesh.vertices.size(), 0);
|
||||
|
||||
for (size_t p = 0; p < mesh.primitives.size(); ++p) {
|
||||
const tinygltf::Primitive& prim = mesh.primitives[p];
|
||||
CPUMesh cpu = LoadPrimitiveIntoCPUMesh(
|
||||
model,
|
||||
prim,
|
||||
mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name,
|
||||
world
|
||||
);
|
||||
cpu.name += "_prim" + std::to_string(p);
|
||||
result.push_back(std::move(cpu));
|
||||
for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) {
|
||||
const aiBone* bone = aiMesh->mBones[b];
|
||||
std::string boneName(bone->mName.C_Str());
|
||||
|
||||
// Find which joint index this bone maps to
|
||||
std::uint32_t jointIdx = 0;
|
||||
bool found = false;
|
||||
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||
if (skeleton.jointNames[j] == boneName) {
|
||||
jointIdx = static_cast<std::uint32_t>(j);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) continue;
|
||||
|
||||
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.
|
||||
// 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;
|
||||
if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this
|
||||
|
||||
bool ok = false;
|
||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
||||
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");
|
||||
cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx;
|
||||
cpuMesh.skinningData[vertIdx].weights[slot] = weight;
|
||||
++weightCount[vertIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CPUMesh> result;
|
||||
// ─── Animation loading ────────────────────────────────────────────────────────
|
||||
|
||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
||||
std::vector<NodeStackItem> stack;
|
||||
stack.reserve(scene.nodes.size());
|
||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
||||
static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
|
||||
const Skeleton& skeleton) {
|
||||
std::vector<SkeletalAnimation> result;
|
||||
if (!scene->HasAnimations()) return result;
|
||||
|
||||
while (!stack.empty()) {
|
||||
NodeStackItem it = stack.back();
|
||||
stack.pop_back();
|
||||
for (unsigned int a = 0; a < scene->mNumAnimations; ++a) {
|
||||
const aiAnimation* aiAnim = scene->mAnimations[a];
|
||||
const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0;
|
||||
|
||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
||||
const glm::mat4 local = NodeLocalMatrix(node);
|
||||
const glm::mat4 world = it.parentWorld * local;
|
||||
SkeletalAnimation anim;
|
||||
anim.name = aiAnim->mName.C_Str();
|
||||
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;
|
||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
||||
|
||||
CPUMesh merged{};
|
||||
merged.name = mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name;
|
||||
merged.minPos = glm::vec3(std::numeric_limits<float>::infinity());
|
||||
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
||||
|
||||
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);
|
||||
// Map bone name to joint index
|
||||
std::uint32_t jointIdx = 0;
|
||||
bool found = false;
|
||||
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||
if (skeleton.jointNames[j] == boneName) {
|
||||
jointIdx = static_cast<std::uint32_t>(j);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) continue;
|
||||
|
||||
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
|
||||
|
||||
#endif // MODELLOADER_H
|
||||
#endif // MODELLOADER_H
|
||||
189
destrum/src/Components/Animator.cpp
Normal file
189
destrum/src/Components/Animator.cpp
Normal 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;
|
||||
}
|
||||
@@ -1,19 +1,56 @@
|
||||
#include <destrum/Components/MeshRendererComponent.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
|
||||
#include "destrum/Components/Animator.h"
|
||||
#include "destrum/ObjectModel/GameObject.h"
|
||||
#include "destrum/Util/GameState.h"
|
||||
|
||||
|
||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Start() {
|
||||
Component::Start();
|
||||
if (auto* animator = GetGameObject()->GetComponent<Animator>()) {
|
||||
const auto& gfxDevice = GameState::GetInstance().Gfx();
|
||||
const auto& mesh = GameState::GetInstance().Renderer().GetMeshCache().getMesh(meshID);
|
||||
|
||||
m_skinnedMesh = std::make_unique<SkinnedMesh>();
|
||||
m_skinnedMesh->skinnedVertexBuffer = gfxDevice.createBuffer(
|
||||
mesh.numVertices * sizeof(CPUMesh::Vertex),
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
|
||||
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Update() {
|
||||
}
|
||||
|
||||
void MeshRendererComponent::Render(const RenderContext& ctx) {
|
||||
if (meshID != NULL_MESH_ID && materialID != NULL_MATERIAL_ID) {
|
||||
if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return;
|
||||
|
||||
if (auto* animator = GetGameObject()->GetComponent<Animator>(); animator && m_skinnedMesh) {
|
||||
const auto& mesh = ctx.renderer.GetMeshCache().getCPUMesh(meshID);
|
||||
const auto skeleton = GetGameObject()->GetComponent<Animator>()->getSkeleton();
|
||||
std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex();
|
||||
|
||||
const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices(
|
||||
ctx.renderer.getSkinningPipeline(),
|
||||
*skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh)
|
||||
frameIdx
|
||||
);
|
||||
|
||||
ctx.renderer.drawSkinnedMesh(
|
||||
meshID,
|
||||
GetTransform().GetWorldMatrix(),
|
||||
materialID,
|
||||
m_skinnedMesh.get(),
|
||||
jointMatricesStartIndex
|
||||
);
|
||||
} else {
|
||||
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +98,13 @@ void OrbitAndSpin::Update()
|
||||
// GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
|
||||
|
||||
// material color
|
||||
auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
|
||||
mat.baseColor = glm::vec3(
|
||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
|
||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
|
||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
|
||||
);
|
||||
GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
|
||||
// auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
|
||||
// mat.baseColor = glm::vec3(
|
||||
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
|
||||
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
|
||||
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
|
||||
// );
|
||||
// GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
|
||||
}
|
||||
|
||||
void OrbitAndSpin::Start() {
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
static const std::uint32_t maxBindlessResources = 16536;
|
||||
static const std::uint32_t maxSamplers = 32;
|
||||
constexpr std::uint32_t maxBindlessResources = 16536;
|
||||
constexpr std::uint32_t maxSamplers = 32;
|
||||
|
||||
static const std::uint32_t texturesBinding = 0;
|
||||
static const std::uint32_t samplersBinding = 1;
|
||||
constexpr std::uint32_t texturesBinding = 0;
|
||||
constexpr std::uint32_t samplersBinding = 1;
|
||||
}
|
||||
|
||||
void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
|
||||
@@ -26,7 +26,7 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
|
||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT,
|
||||
.maxSets = 10,
|
||||
.poolSizeCount = (std::uint32_t)poolSizesBindless.size(),
|
||||
.poolSizeCount = static_cast<std::uint32_t>(poolSizesBindless.size()),
|
||||
.pPoolSizes = poolSizesBindless.data(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -129,18 +129,36 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync)
|
||||
auto& mainCommandBuffer = frames[i].commandBuffer;
|
||||
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
|
||||
}
|
||||
//
|
||||
// { // create white texture
|
||||
// std::uint32_t pixel = 0xFFFFFFFF;
|
||||
// whiteImageId = createImage(
|
||||
// {
|
||||
// .format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||
// .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
// .extent = VkExtent3D{1, 1, 1},
|
||||
// },
|
||||
// "white texture",
|
||||
// &pixel);
|
||||
// }
|
||||
|
||||
{ // create white texture
|
||||
std::uint32_t pixel = 0xFFFFFFFF;
|
||||
whiteImageId = createImage(
|
||||
{
|
||||
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
.extent = VkExtent3D{1, 1, 1},
|
||||
},
|
||||
"white texture",
|
||||
&pixel);
|
||||
}
|
||||
|
||||
{ // create error texture (black/magenta checker)
|
||||
constexpr auto black = 0xFF000000;
|
||||
constexpr auto magenta = 0xFFFF00FF;
|
||||
|
||||
std::array<std::uint32_t, 4> pixels{black, magenta, magenta, black};
|
||||
errorImageId = createImage(
|
||||
{
|
||||
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.extent = VkExtent3D{2, 2, 1},
|
||||
},
|
||||
"error texture",
|
||||
pixels.data());
|
||||
imageCache.setErrorImageId(errorImageId);
|
||||
}
|
||||
|
||||
GameState::GetInstance().SetGfxDevice(this);
|
||||
}
|
||||
|
||||
@@ -495,25 +513,14 @@ GPUImage GfxDevice::createImageRaw(
|
||||
return image;
|
||||
}
|
||||
|
||||
GPUImage GfxDevice::loadImageFromFileRaw(
|
||||
const std::filesystem::path& path,
|
||||
VkImageUsageFlags usage,
|
||||
bool mipMap,
|
||||
TextureIntent intent) const {
|
||||
// 1) Decode file to CPU pixels (stb for png/jpg/hdr, tinyexr for exr)
|
||||
// IMPORTANT: util::loadImage should fill:
|
||||
// - width/height/channels(=4)
|
||||
// - hdr + (pixels OR hdrPixels)
|
||||
// - vkFormat (RGBA8_SRGB or RGBA8_UNORM or RGBA32F)
|
||||
// - byteSize (exact bytes to upload)
|
||||
GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const {
|
||||
const auto data = util::loadImage(path, intent);
|
||||
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 ||
|
||||
(data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
|
||||
|
||||
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
|
||||
spdlog::error("Failed to load image '{}'", path.string());
|
||||
return getImage(errorImageId);
|
||||
}
|
||||
|
||||
// 2) Create GPU image using the format the loader chose (matches CPU memory layout)
|
||||
auto image = createImageRaw({
|
||||
.format = data.vkFormat,
|
||||
.usage = usage |
|
||||
@@ -527,15 +534,10 @@ GPUImage GfxDevice::loadImageFromFileRaw(
|
||||
.mipMap = mipMap,
|
||||
});
|
||||
|
||||
// 3) Upload *exactly* byteSize bytes from the correct pointer
|
||||
const void* src = data.hdr
|
||||
? static_cast<const void*>(data.hdrPixels)
|
||||
: static_cast<const void*>(data.pixels);
|
||||
const void* src = data.hdr ? static_cast<const void*>(data.hdrPixels) : static_cast<const void*>(data.pixels);
|
||||
|
||||
// Use the "sized" upload to avoid BytesPerTexel mismatches
|
||||
uploadImageDataSized(image, src, data.byteSize, 0);
|
||||
|
||||
// 4) Debug label
|
||||
image.debugName = path.string();
|
||||
vkutil::addDebugLabel(device, image.image, path.string().c_str());
|
||||
|
||||
|
||||
@@ -2,21 +2,12 @@
|
||||
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
|
||||
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice)
|
||||
{}
|
||||
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) {
|
||||
}
|
||||
|
||||
ImageID ImageCache::loadImageFromFile(
|
||||
const std::filesystem::path& path,
|
||||
VkImageUsageFlags usage,
|
||||
bool mipMap,
|
||||
TextureIntent intent)
|
||||
{
|
||||
for (const auto& [id, info] : loadedImagesInfo) {
|
||||
if (info.path == path &&
|
||||
info.intent == intent &&
|
||||
info.usage == usage &&
|
||||
info.mipMap == mipMap)
|
||||
{
|
||||
ImageID ImageCache::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) {
|
||||
for (const auto& [id, info]: loadedImagesInfo) {
|
||||
if (info.path == path && info.intent == intent && info.usage == usage && info.mipMap == mipMap) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -41,13 +32,11 @@ ImageID ImageCache::loadImageFromFile(
|
||||
return id;
|
||||
}
|
||||
|
||||
ImageID ImageCache::addImage(GPUImage image)
|
||||
{
|
||||
ImageID ImageCache::addImage(GPUImage image) {
|
||||
return addImage(getFreeImageId(), std::move(image));
|
||||
}
|
||||
|
||||
ImageID ImageCache::addImage(ImageID id, GPUImage image)
|
||||
{
|
||||
ImageID ImageCache::addImage(ImageID id, GPUImage image) {
|
||||
image.setBindlessId(static_cast<std::uint32_t>(id));
|
||||
if (id != images.size()) {
|
||||
images[id] = std::move(image); // replacing existing image
|
||||
@@ -59,20 +48,17 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image)
|
||||
return id;
|
||||
}
|
||||
|
||||
const GPUImage& ImageCache::getImage(ImageID id) const
|
||||
{
|
||||
const GPUImage& ImageCache::getImage(ImageID id) const {
|
||||
assert(id != NULL_IMAGE_ID && id < images.size());
|
||||
return images.at(id);
|
||||
}
|
||||
|
||||
ImageID ImageCache::getFreeImageId() const
|
||||
{
|
||||
ImageID ImageCache::getFreeImageId() const {
|
||||
return images.size();
|
||||
}
|
||||
|
||||
void ImageCache::destroyImages()
|
||||
{
|
||||
for (const auto& image : images) {
|
||||
void ImageCache::destroyImages() {
|
||||
for (const auto& image: images) {
|
||||
gfxDevice.destroyImage(image);
|
||||
}
|
||||
images.clear();
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace util
|
||||
{
|
||||
ImageData data;
|
||||
|
||||
|
||||
|
||||
// ---------- EXR ----------
|
||||
if (isExrExt(p)) {
|
||||
float* out = nullptr;
|
||||
|
||||
@@ -24,7 +24,7 @@ void MaterialCache::init(GfxDevice& gfxDevice)
|
||||
&normal);
|
||||
}
|
||||
|
||||
Material placeholderMaterial{.name = "PLACEHOLDER_MATERIAL"};
|
||||
Material placeholderMaterial{.diffuseTexture = defaultNormalMapTextureID, .name = "PLACEHOLDER_MATERIAL"};
|
||||
placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial);
|
||||
}
|
||||
|
||||
@@ -41,13 +41,14 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
|
||||
};
|
||||
|
||||
// store on GPU
|
||||
MaterialData* data = (MaterialData*)materialDataBuffer.info.pMappedData;
|
||||
auto whiteTextureID = gfxDevice.getWhiteTextureID();
|
||||
auto id = getFreeMaterialId();
|
||||
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||
const auto whiteTextureID = gfxDevice.getWhiteTextureID();
|
||||
const auto id = getFreeMaterialId();
|
||||
assert(id < MAX_MATERIALS);
|
||||
data[id] = MaterialData{
|
||||
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
||||
.metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f},
|
||||
.textureFilteringMode = static_cast<std::uint32_t>(material.textureFilteringMode),
|
||||
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
||||
.normalTex = whiteTextureID,
|
||||
.metallicRoughnessTex = whiteTextureID,
|
||||
@@ -60,6 +61,15 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
|
||||
return id;
|
||||
}
|
||||
|
||||
MaterialID MaterialCache::addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID) {
|
||||
Material material{};
|
||||
material.name = "idk";
|
||||
material.diffuseTexture = textureID;
|
||||
material.metallicFactor = 0.0f;
|
||||
material.roughnessFactor = 1.0f;
|
||||
return addMaterial(gfxDevice, material);
|
||||
}
|
||||
|
||||
const Material& MaterialCache::getMaterial(MaterialID id) const
|
||||
{
|
||||
return materials.at(id);
|
||||
@@ -100,7 +110,7 @@ void MaterialCache::updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id)
|
||||
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
||||
.metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f),
|
||||
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
||||
.normalTex = whiteTextureID, // if you have this field
|
||||
.normalTex = whiteTextureID,
|
||||
.metallicRoughnessTex = whiteTextureID,
|
||||
.emissiveTex = whiteTextureID,
|
||||
};
|
||||
|
||||
@@ -3,25 +3,30 @@
|
||||
#include <destrum/Graphics/Resources/Mesh.h>
|
||||
#include <destrum/Graphics/GfxDevice.h>
|
||||
#include <destrum/Graphics/Util.h>
|
||||
|
||||
#include <destrum/Util/MathUtils.h>
|
||||
// #include <destrum/Math/Util.h>
|
||||
|
||||
MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
|
||||
{
|
||||
auto gpuMesh = GPUMesh{
|
||||
.numVertices = (std::uint32_t)cpuMesh.vertices.size(),
|
||||
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
|
||||
.minPos = cpuMesh.minPos,
|
||||
.maxPos = cpuMesh.maxPos,
|
||||
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
|
||||
.minPos = cpuMesh.minPos,
|
||||
.maxPos = cpuMesh.maxPos,
|
||||
.hasSkeleton = cpuMesh.skeleton.has_value(),
|
||||
};
|
||||
|
||||
std::vector<glm::vec3> positions(cpuMesh.vertices.size());
|
||||
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i) {
|
||||
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i)
|
||||
positions[i] = cpuMesh.vertices[i].position;
|
||||
}
|
||||
gpuMesh.boundingSphere = calculateBoundingSphere(positions);
|
||||
|
||||
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
|
||||
|
||||
const auto id = meshes.size();
|
||||
meshes.push_back(std::move(gpuMesh));
|
||||
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -70,6 +75,33 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
|
||||
const auto idxBufferName = cpuMesh.name + " (idx)";
|
||||
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
|
||||
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str());
|
||||
|
||||
if (gpuMesh.hasSkeleton) {
|
||||
// create skinning data buffer
|
||||
const auto skinningDataSize = cpuMesh.vertices.size() * sizeof(CPUMesh::SkinningData);
|
||||
gpuMesh.skinningDataBuffer = gfxDevice.createBuffer(
|
||||
skinningDataSize,
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
|
||||
|
||||
const auto staging =
|
||||
gfxDevice.createBuffer(skinningDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
|
||||
// copy data
|
||||
void* data = staging.info.pMappedData;
|
||||
memcpy(data, cpuMesh.skinningData.data(), skinningDataSize);
|
||||
|
||||
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
|
||||
const auto vertexCopy = VkBufferCopy{
|
||||
.srcOffset = 0,
|
||||
.dstOffset = 0,
|
||||
.size = skinningDataSize,
|
||||
};
|
||||
vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy);
|
||||
});
|
||||
|
||||
gfxDevice.destroyBuffer(staging);
|
||||
}
|
||||
}
|
||||
|
||||
const GPUMesh& MeshCache::getMesh(MeshID id) const
|
||||
@@ -77,6 +109,11 @@ const GPUMesh& MeshCache::getMesh(MeshID id) const
|
||||
return meshes.at(id);
|
||||
}
|
||||
|
||||
const CPUMesh& MeshCache::getCPUMesh(MeshID id) const
|
||||
{
|
||||
return cpuMeshes.at(id);
|
||||
}
|
||||
|
||||
void MeshCache::cleanup(const GfxDevice& gfxDevice)
|
||||
{
|
||||
for (const auto& mesh : meshes) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
|
||||
#include "destrum/Graphics/Frustum.h"
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
|
||||
}
|
||||
|
||||
@@ -68,6 +71,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
||||
m_pipeline->bind(cmd);
|
||||
gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout);
|
||||
|
||||
int ActualDrawCalls = 0;
|
||||
|
||||
const auto viewport = VkViewport{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
@@ -86,16 +91,17 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
||||
|
||||
auto prevMeshId = NULL_MESH_ID;
|
||||
|
||||
// const auto frustum = edge::createFrustumFromCamera(camera);
|
||||
const auto frustum = edge::createFrustumFromCamera(camera);
|
||||
|
||||
for (const auto& dcIdx : drawCommands) {
|
||||
// const auto& dc = drawCommands[dcIdx];
|
||||
const auto& dc = dcIdx;
|
||||
|
||||
// if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
|
||||
// continue;
|
||||
// continue;
|
||||
// }
|
||||
|
||||
ActualDrawCalls++;
|
||||
|
||||
const auto& mesh = meshCache.getMesh(dc.meshId);
|
||||
if (dc.meshId != prevMeshId) {
|
||||
prevMeshId = dc.meshId;
|
||||
@@ -106,7 +112,7 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
||||
const auto pushConstants = PushConstants{
|
||||
.transform = dc.transformMatrix,
|
||||
.sceneDataBuffer = sceneDataBuffer.address,
|
||||
.vertexBuffer = mesh.vertexBuffer.address,
|
||||
.vertexBuffer = dc.skinnedMesh != nullptr ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address,
|
||||
.materialId = dc.materialId,
|
||||
};
|
||||
vkCmdPushConstants(
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
|
||||
skyboxPipeline = std::make_unique<SkyboxPipeline>();
|
||||
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
||||
|
||||
skinningPipeline = std::make_unique<SkinningPipeline>();
|
||||
skinningPipeline->init(gfxDevice);
|
||||
|
||||
|
||||
GameState::GetInstance().SetRenderer(this);
|
||||
}
|
||||
@@ -30,6 +33,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
|
||||
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
|
||||
flushMaterialUpdates(gfxDevice);
|
||||
meshDrawCommands.clear();
|
||||
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
|
||||
}
|
||||
|
||||
void GameRenderer::endDrawing() {
|
||||
@@ -37,6 +41,13 @@ void GameRenderer::endDrawing() {
|
||||
}
|
||||
|
||||
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) {
|
||||
|
||||
for (const auto& dc : meshDrawCommands) {
|
||||
if (!dc.skinnedMesh) {
|
||||
continue;
|
||||
}
|
||||
skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc);
|
||||
}
|
||||
const auto gpuSceneData = GPUSceneData{
|
||||
.view = sceneData.camera.GetViewMatrix(),
|
||||
.proj = sceneData.camera.GetProjectionMatrix(),
|
||||
@@ -107,7 +118,7 @@ void GameRenderer::cleanup(VkDevice device) {
|
||||
|
||||
void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) {
|
||||
const auto& mesh = meshCache.getMesh(id);
|
||||
// const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
||||
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
||||
assert(materialId != NULL_MATERIAL_ID);
|
||||
|
||||
meshDrawCommands.push_back(MeshDrawCommand{
|
||||
@@ -117,6 +128,25 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma
|
||||
});
|
||||
}
|
||||
|
||||
void GameRenderer::drawSkinnedMesh(MeshID id,
|
||||
const glm::mat4& transform,
|
||||
MaterialID materialId,
|
||||
SkinnedMesh* skinnedMesh,
|
||||
std::size_t jointMatricesStartIndex) {
|
||||
const auto& mesh = meshCache.getMesh(id);
|
||||
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
||||
assert(materialId != NULL_MATERIAL_ID);
|
||||
assert(skinnedMesh != nullptr);
|
||||
|
||||
meshDrawCommands.push_back(MeshDrawCommand{
|
||||
.meshId = id,
|
||||
.transformMatrix = transform,
|
||||
.materialId = materialId,
|
||||
.skinnedMesh = skinnedMesh,
|
||||
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex),
|
||||
});
|
||||
}
|
||||
|
||||
const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
|
||||
return gfx_device.getImage(drawImageId);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,16 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c
|
||||
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
|
||||
}
|
||||
|
||||
void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) {
|
||||
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
|
||||
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
|
||||
.objectType = VK_OBJECT_TYPE_PIPELINE,
|
||||
.objectHandle = (std::uint64_t)pipeline,
|
||||
.pObjectName = label,
|
||||
};
|
||||
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
|
||||
}
|
||||
|
||||
void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
|
||||
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
|
||||
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
|
||||
|
||||
8
destrum/third_party/CMakeLists.txt
vendored
8
destrum/third_party/CMakeLists.txt
vendored
@@ -1,4 +1,5 @@
|
||||
# glm
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
add_subdirectory(glm)
|
||||
|
||||
# stb
|
||||
@@ -56,3 +57,10 @@ add_subdirectory(tinygltf)
|
||||
|
||||
add_subdirectory(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
1
destrum/third_party/assimp
vendored
Submodule
Submodule destrum/third_party/assimp added at e13e0b5b7d
@@ -22,14 +22,26 @@ target_include_directories(lightkeeper PRIVATE "${CMAKE_CURRENT_LIST_DIR}/includ
|
||||
|
||||
target_link_libraries(lightkeeper PRIVATE destrum::destrum)
|
||||
|
||||
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_BINARY_DIR}/assets/game")
|
||||
#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_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
|
||||
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}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/assets"
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -47,5 +59,4 @@ add_custom_target(_internal_cook_game_assets ALL
|
||||
DEPENDS TheChef
|
||||
)
|
||||
|
||||
|
||||
|
||||
destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
BIN
lightkeeper/assets_src/plane.glb
Normal file
BIN
lightkeeper/assets_src/plane.glb
Normal file
Binary file not shown.
@@ -11,6 +11,7 @@
|
||||
#include "destrum/Components/OrbitAndSpin.h"
|
||||
#include "destrum/ObjectModel/GameObject.h"
|
||||
#include "destrum/Util/ModelLoader.h"
|
||||
#include "destrum/Components/Animator.h"
|
||||
|
||||
LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) {
|
||||
}
|
||||
@@ -52,19 +53,19 @@ void LightKeeper::customInit() {
|
||||
const float orbitRadius = 5.0f;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
||||
|
||||
auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
||||
childMeshComp->SetMeshID(testMeshID);
|
||||
childMeshComp->SetMaterialID(testMaterialID);
|
||||
|
||||
childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
||||
|
||||
// Add orbit + self spin
|
||||
auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
||||
orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
||||
|
||||
scene.Add(childCube);
|
||||
// auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
||||
//
|
||||
// auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
||||
// childMeshComp->SetMeshID(testMeshID);
|
||||
// childMeshComp->SetMaterialID(testMaterialID);
|
||||
//
|
||||
// childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
||||
//
|
||||
// // Add orbit + self spin
|
||||
// auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
||||
// orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
||||
//
|
||||
// scene.Add(childCube);
|
||||
}
|
||||
testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec
|
||||
//rotate 180 around X axis
|
||||
@@ -94,6 +95,68 @@ void LightKeeper::customInit() {
|
||||
skyboxCubemap->CreateCubeMap();
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user