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"]
|
[submodule "destrum/third_party/tinyexr"]
|
||||||
path = destrum/third_party/tinyexr
|
path = destrum/third_party/tinyexr
|
||||||
url = https://github.com/syoyo/tinyexr.git
|
url = https://github.com/syoyo/tinyexr.git
|
||||||
|
[submodule "destrum/third_party/assimp"]
|
||||||
|
path = destrum/third_party/assimp
|
||||||
|
url = https://github.com/assimp/assimp.git
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ add_custom_target(CookAssets)
|
|||||||
add_dependencies(CookAssets
|
add_dependencies(CookAssets
|
||||||
_internal_cook_game_assets
|
_internal_cook_game_assets
|
||||||
_internal_cook_engine_assets
|
_internal_cook_engine_assets
|
||||||
)
|
)
|
||||||
|
|||||||
2
TheChef
2
TheChef
Submodule TheChef updated: faaf7fa120...3a7b137165
@@ -4,6 +4,7 @@ set(SRC_FILES
|
|||||||
"src/App.cpp"
|
"src/App.cpp"
|
||||||
"src/Event.cpp"
|
"src/Event.cpp"
|
||||||
|
|
||||||
|
"src/Components/Animator.cpp"
|
||||||
"src/Components/MeshRendererComponent.cpp"
|
"src/Components/MeshRendererComponent.cpp"
|
||||||
"src/Components/Rotator.cpp"
|
"src/Components/Rotator.cpp"
|
||||||
"src/Components/Spinner.cpp"
|
"src/Components/Spinner.cpp"
|
||||||
@@ -11,6 +12,8 @@ set(SRC_FILES
|
|||||||
|
|
||||||
"src/Graphics/BindlessSetManager.cpp"
|
"src/Graphics/BindlessSetManager.cpp"
|
||||||
"src/Graphics/Camera.cpp"
|
"src/Graphics/Camera.cpp"
|
||||||
|
"src/Graphics/ComputePipeline.cpp"
|
||||||
|
"src/Graphics/Frustum.cpp"
|
||||||
"src/Graphics/GfxDevice.cpp"
|
"src/Graphics/GfxDevice.cpp"
|
||||||
"src/Graphics/ImageCache.cpp"
|
"src/Graphics/ImageCache.cpp"
|
||||||
"src/Graphics/ImageLoader.cpp"
|
"src/Graphics/ImageLoader.cpp"
|
||||||
@@ -29,6 +32,7 @@ set(SRC_FILES
|
|||||||
|
|
||||||
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
||||||
"src/Graphics/Pipelines/SkyboxPipeline.cpp"
|
"src/Graphics/Pipelines/SkyboxPipeline.cpp"
|
||||||
|
"src/Graphics/Pipelines/SkinningPipeline.cpp"
|
||||||
|
|
||||||
"src/Input/InputManager.cpp"
|
"src/Input/InputManager.cpp"
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ target_link_libraries(destrum
|
|||||||
spdlog::spdlog
|
spdlog::spdlog
|
||||||
stb::image
|
stb::image
|
||||||
tinygltf
|
tinygltf
|
||||||
|
assimp
|
||||||
|
|
||||||
PRIVATE
|
PRIVATE
|
||||||
freetype::freetype
|
freetype::freetype
|
||||||
@@ -105,14 +110,14 @@ target_compile_definitions(destrum
|
|||||||
|
|
||||||
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
||||||
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
||||||
set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
|
#set(OUTPUT_ENGINE_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/engine")
|
||||||
|
#
|
||||||
add_custom_command(TARGET destrum POST_BUILD
|
#add_custom_command(TARGET destrum POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}"
|
# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets"
|
# COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/assets"
|
||||||
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}"
|
# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_ENGINE_ASSETS_DIR}"
|
||||||
VERBATIM
|
# VERBATIM
|
||||||
)
|
#)
|
||||||
|
|
||||||
add_custom_target(_internal_clean_engine_assets
|
add_custom_target(_internal_clean_engine_assets
|
||||||
COMMAND TheChef
|
COMMAND TheChef
|
||||||
@@ -127,3 +132,17 @@ add_custom_target(_internal_cook_engine_assets ALL
|
|||||||
--output "${ASSETS_RUNTIME_DIR}"
|
--output "${ASSETS_RUNTIME_DIR}"
|
||||||
DEPENDS TheChef
|
DEPENDS TheChef
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
|
||||||
|
# This resolves to destrum's own directory at function DEFINITION time
|
||||||
|
set(_engine_src "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_src")
|
||||||
|
set(_engine_runtime "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/assets_runtime")
|
||||||
|
set(_output_engine "${GAME_OUTPUT_DIR}/assets/engine")
|
||||||
|
|
||||||
|
add_custom_command(TARGET ${GAME_TARGET} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${GAME_OUTPUT_DIR}/assets"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_output_engine}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "${_engine_runtime}" "${_output_engine}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|||||||
BIN
destrum/assets_src/char.fbx
Normal file
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);
|
return texture(nonuniformEXT(sampler2D(textures[texID], samplers[NEAREST_SAMPLER_ID])), uv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec4 sampleTexture2D(uint texID, uint samplerID, vec2 uv) {
|
||||||
|
return texture(nonuniformEXT(sampler2D(textures[texID], samplers[samplerID])), uv);
|
||||||
|
}
|
||||||
|
|
||||||
vec4 sampleTexture2DMSNearest(uint texID, ivec2 p, int s) {
|
vec4 sampleTexture2DMSNearest(uint texID, ivec2 p, int s) {
|
||||||
return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s);
|
return texelFetch(nonuniformEXT(sampler2DMS(texturesMS[texID], samplers[NEAREST_SAMPLER_ID])), p, s);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
struct MaterialData {
|
struct MaterialData {
|
||||||
vec4 baseColor;
|
vec4 baseColor;
|
||||||
vec4 metallicRoughnessEmissive;
|
vec4 metallicRoughnessEmissive;
|
||||||
|
uint textureFilteringMode;
|
||||||
uint diffuseTex;
|
uint diffuseTex;
|
||||||
uint normalTex;
|
uint normalTex;
|
||||||
uint metallicRoughnessTex;
|
uint metallicRoughnessTex;
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ layout (location = 0) out vec4 outFragColor;
|
|||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
|
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
|
||||||
|
uint samplerID = pcs.sceneData.materials.data[pcs.materialID].textureFilteringMode;
|
||||||
|
|
||||||
vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV) * material.baseColor;
|
vec4 diffuse = sampleTexture2D(material.diffuseTex, samplerID, inUV) * material.baseColor;
|
||||||
outFragColor = diffuse;
|
outFragColor = diffuse;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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:
|
private:
|
||||||
MeshID meshID{NULL_MESH_ID};
|
MeshID meshID{NULL_MESH_ID};
|
||||||
MaterialID materialID{NULL_MATERIAL_ID};
|
MaterialID materialID{NULL_MATERIAL_ID};
|
||||||
|
|
||||||
|
std::unique_ptr<SkinnedMesh> m_skinnedMesh;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //MESHRENDERERCOMPONENT_H
|
#endif //MESHRENDERERCOMPONENT_H
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ public:
|
|||||||
this->CalculateProjectionMatrix();
|
this->CalculateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float GetZNear() const { return m_zNear; }
|
||||||
|
void SetZNear(float zNear) { m_zNear = zNear; }
|
||||||
|
float GetZFar() const { return m_zFar; }
|
||||||
|
void SetZFar(float zFar) { m_zFar = zFar; }
|
||||||
|
float GetFOVY() const { return fovAngle; }
|
||||||
|
float GetAspectRatio() const { return m_aspectRatio; }
|
||||||
|
|
||||||
glm::vec3 m_position{2.f, 0, 0};
|
glm::vec3 m_position{2.f, 0, 0};
|
||||||
|
|
||||||
void Translate(const glm::vec3& translation) {
|
void Translate(const glm::vec3& translation) {
|
||||||
|
|||||||
@@ -1,4 +1,48 @@
|
|||||||
#ifndef COMPUTEPIPELINE_H
|
#ifndef COMPUTEPIPELINE_H
|
||||||
#define COMPUTEPIPELINE_H
|
#define COMPUTEPIPELINE_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
|
|
||||||
|
struct ComputePipelineConfigInfo {
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
// Optional specialization constants (can be nullptr if unused)
|
||||||
|
VkSpecializationInfo* specializationInfo{nullptr};
|
||||||
|
|
||||||
|
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ComputePipeline {
|
||||||
|
public:
|
||||||
|
ComputePipeline(GfxDevice& device,
|
||||||
|
const std::string& compPath,
|
||||||
|
const ComputePipelineConfigInfo& configInfo);
|
||||||
|
~ComputePipeline();
|
||||||
|
|
||||||
|
ComputePipeline(const ComputePipeline& other) = delete;
|
||||||
|
ComputePipeline(ComputePipeline&& other) noexcept = delete;
|
||||||
|
ComputePipeline& operator=(const ComputePipeline& other) = delete;
|
||||||
|
ComputePipeline& operator=(ComputePipeline&& other) noexcept = delete;
|
||||||
|
|
||||||
|
void bind(VkCommandBuffer buffer) const;
|
||||||
|
|
||||||
|
static void DefaultPipelineConfigInfo(ComputePipelineConfigInfo& configInfo);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::vector<char> readFile(const std::string& filename);
|
||||||
|
|
||||||
|
void CreateComputePipeline(const std::string& compPath,
|
||||||
|
const ComputePipelineConfigInfo& configInfo);
|
||||||
|
|
||||||
|
void CreateShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule) const;
|
||||||
|
|
||||||
|
GfxDevice& m_device;
|
||||||
|
|
||||||
|
VkPipeline m_computePipeline{VK_NULL_HANDLE};
|
||||||
|
VkShaderModule m_compShaderModule{VK_NULL_HANDLE};
|
||||||
|
};
|
||||||
|
|
||||||
#endif //COMPUTEPIPELINE_H
|
#endif //COMPUTEPIPELINE_H
|
||||||
|
|||||||
@@ -1,4 +1,80 @@
|
|||||||
#ifndef FRUSTUM_H
|
#ifndef FRUSTUM_H
|
||||||
#define FRUSTUM_H
|
#define FRUSTUM_H
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "Camera.h"
|
||||||
|
|
||||||
|
struct Frustum {
|
||||||
|
struct Plane {
|
||||||
|
Plane() = default;
|
||||||
|
Plane(const glm::vec3& p1, const glm::vec3& norm) :
|
||||||
|
normal(glm::normalize(norm)), distance(glm::dot(normal, p1))
|
||||||
|
{}
|
||||||
|
|
||||||
|
glm::vec3 normal{0.f, 1.f, 0.f};
|
||||||
|
|
||||||
|
// distance from the origin to the nearest point in the plane
|
||||||
|
float distance{0.f};
|
||||||
|
|
||||||
|
float getSignedDistanceToPlane(const glm::vec3& point) const
|
||||||
|
{
|
||||||
|
return glm::dot(normal, point) - distance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Plane& getPlane(int i) const
|
||||||
|
{
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
return farFace;
|
||||||
|
case 1:
|
||||||
|
return nearFace;
|
||||||
|
case 2:
|
||||||
|
return leftFace;
|
||||||
|
case 3:
|
||||||
|
return rightFace;
|
||||||
|
case 4:
|
||||||
|
return topFace;
|
||||||
|
case 5:
|
||||||
|
return bottomFace;
|
||||||
|
default:
|
||||||
|
assert(false);
|
||||||
|
return nearFace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Plane farFace;
|
||||||
|
Plane nearFace;
|
||||||
|
|
||||||
|
Plane leftFace;
|
||||||
|
Plane rightFace;
|
||||||
|
|
||||||
|
Plane topFace;
|
||||||
|
Plane bottomFace;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Sphere {
|
||||||
|
glm::vec3 center{};
|
||||||
|
float radius{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AABB {
|
||||||
|
glm::vec3 min;
|
||||||
|
glm::vec3 max;
|
||||||
|
|
||||||
|
glm::vec3 calculateSize() const { return glm::abs(max - min); }
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace edge
|
||||||
|
{
|
||||||
|
std::array<glm::vec3, 8> calculateFrustumCornersWorldSpace(const Camera& camera);
|
||||||
|
|
||||||
|
Frustum createFrustumFromCamera(const Camera& camera);
|
||||||
|
bool isInFrustum(const Frustum& frustum, const Sphere& s);
|
||||||
|
bool isInFrustum(const Frustum& frustum, const AABB& aabb);
|
||||||
|
Sphere calculateBoundingSphereWorld(const glm::mat4& transform, const Sphere& s, bool hasSkeleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif //FRUSTUM_H
|
#endif //FRUSTUM_H
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
#include "Util.h"
|
#include "Util.h"
|
||||||
|
|
||||||
|
class MeshCache;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>;
|
using ImmediateExecuteFunction = std::function<void(VkCommandBuffer)>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,21 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
struct MaterialData {
|
struct alignas(16) MaterialData {
|
||||||
glm::vec4 baseColor;
|
alignas(16) glm::vec4 baseColor;
|
||||||
glm::vec4 metalRoughnessEmissive;
|
alignas(16) glm::vec4 metalRoughnessEmissive;
|
||||||
std::uint32_t diffuseTex;
|
uint32_t textureFilteringMode;
|
||||||
std::uint32_t normalTex;
|
uint32_t diffuseTex;
|
||||||
std::uint32_t metallicRoughnessTex;
|
uint32_t normalTex;
|
||||||
std::uint32_t emissiveTex;
|
uint32_t metallicRoughnessTex;
|
||||||
|
uint32_t emissiveTex;
|
||||||
|
uint32_t _pad0, _pad1, _pad2; // explicit to 64 bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TextureFilteringMode : std::uint32_t {
|
||||||
|
Nearest,
|
||||||
|
Linear,
|
||||||
|
Anisotropic,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Material {
|
struct Material {
|
||||||
@@ -19,6 +27,8 @@ struct Material {
|
|||||||
float roughnessFactor{0.7f};
|
float roughnessFactor{0.7f};
|
||||||
float emissiveFactor{0.f};
|
float emissiveFactor{0.f};
|
||||||
|
|
||||||
|
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
|
||||||
|
|
||||||
ImageID diffuseTexture{NULL_IMAGE_ID};
|
ImageID diffuseTexture{NULL_IMAGE_ID};
|
||||||
// ImageId normalMapTexture{NULL_IMAGE_ID};
|
// ImageId normalMapTexture{NULL_IMAGE_ID};
|
||||||
// ImageId metallicRoughnessTexture{NULL_IMAGE_ID};
|
// ImageId metallicRoughnessTexture{NULL_IMAGE_ID};
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ public:
|
|||||||
void cleanup(GfxDevice& gfxDevice);
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
MaterialID addMaterial(GfxDevice& gfxDevice, Material material);
|
MaterialID addMaterial(GfxDevice& gfxDevice, Material material);
|
||||||
const Material& getMaterial(MaterialID id) const;
|
MaterialID addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID);
|
||||||
|
[[nodiscard]] const Material& getMaterial(MaterialID id) const;
|
||||||
|
|
||||||
MaterialID getFreeMaterialId() const;
|
[[nodiscard]] MaterialID getFreeMaterialId() const;
|
||||||
MaterialID getPlaceholderMaterialId() const;
|
[[nodiscard]] MaterialID getPlaceholderMaterialId() const;
|
||||||
|
|
||||||
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
[[nodiscard]] const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||||
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
[[nodiscard]] VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||||
|
|
||||||
|
|
||||||
Material& getMaterialMutable(MaterialID id);
|
Material& getMaterialMutable(MaterialID id);
|
||||||
@@ -33,7 +34,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::vector<Material> materials;
|
std::vector<Material> materials;
|
||||||
|
|
||||||
static const auto MAX_MATERIALS = 1000;
|
static constexpr auto MAX_MATERIALS = 1000;
|
||||||
GPUBuffer materialDataBuffer;
|
GPUBuffer materialDataBuffer;
|
||||||
|
|
||||||
// material which is used for meshes without materials
|
// material which is used for meshes without materials
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ public:
|
|||||||
|
|
||||||
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
MeshID addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh);
|
||||||
const GPUMesh& getMesh(MeshID id) const;
|
const GPUMesh& getMesh(MeshID id) const;
|
||||||
|
const CPUMesh& getCPUMesh(MeshID id) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
|
void uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh& gpuMesh) const;
|
||||||
|
|
||||||
std::vector<GPUMesh> meshes;
|
std::vector<GPUMesh> meshes;
|
||||||
|
std::vector<CPUMesh> cpuMeshes;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //MESHCACHE_H
|
#endif //MESHCACHE_H
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/ids.h>
|
#include <destrum/Graphics/ids.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Frustum.h>
|
||||||
|
|
||||||
|
|
||||||
struct MeshDrawCommand {
|
struct MeshDrawCommand {
|
||||||
MeshID meshId;
|
MeshID meshId;
|
||||||
glm::mat4 transformMatrix;
|
glm::mat4 transformMatrix;
|
||||||
|
|
||||||
// for frustum culling
|
// for frustum culling
|
||||||
// math::Sphere worldBoundingSphere;
|
Sphere worldBoundingSphere;
|
||||||
|
|
||||||
// If set - mesh will be drawn with overrideMaterialId
|
// If set - mesh will be drawn with overrideMaterialId
|
||||||
// instead of whatever material the mesh has
|
// instead of whatever material the mesh has
|
||||||
@@ -20,8 +22,8 @@ struct MeshDrawCommand {
|
|||||||
bool castShadow{true};
|
bool castShadow{true};
|
||||||
|
|
||||||
// skinned meshes only
|
// skinned meshes only
|
||||||
// const SkinnedMesh* skinnedMesh{nullptr};
|
const SkinnedMesh* skinnedMesh{nullptr};
|
||||||
// std::uint32_t jointMatricesStartIndex;
|
std::uint32_t jointMatricesStartIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //MESHDRAWCOMMAND_H
|
#endif //MESHDRAWCOMMAND_H
|
||||||
|
|||||||
@@ -1,4 +1,58 @@
|
|||||||
#ifndef SKINNINGPIPELINE_H
|
#ifndef SKINNINGPIPELINE_H
|
||||||
#define SKINNINGPIPELINE_H
|
#define SKINNINGPIPELINE_H
|
||||||
|
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include <array>
|
||||||
|
#include <span>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/ComputePipeline.h>
|
||||||
|
#include <destrum/Graphics/Resources/AppendableBuffer.h>
|
||||||
|
|
||||||
|
#include "destrum/Graphics/Resources/NBuffer.h"
|
||||||
|
|
||||||
|
struct MeshDrawCommand;
|
||||||
|
class MeshCache;
|
||||||
|
class GfxDevice;
|
||||||
|
|
||||||
|
class SkinningPipeline final {
|
||||||
|
public:
|
||||||
|
void init(GfxDevice& gfxDevice);
|
||||||
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
|
void doSkinning(
|
||||||
|
VkCommandBuffer cmd,
|
||||||
|
std::size_t frameIndex,
|
||||||
|
const MeshCache& meshCache,
|
||||||
|
const MeshDrawCommand& dc);
|
||||||
|
|
||||||
|
void beginDrawing(std::size_t frameIndex);
|
||||||
|
std::size_t appendJointMatrices(
|
||||||
|
std::span<const glm::mat4> jointMatrices,
|
||||||
|
std::size_t frameIndex);
|
||||||
|
|
||||||
|
private:
|
||||||
|
VkPipelineLayout m_pipelineLayout;
|
||||||
|
std::unique_ptr<ComputePipeline> skinningPipeline;
|
||||||
|
struct PushConstants {
|
||||||
|
VkDeviceAddress jointMatricesBuffer;
|
||||||
|
std::uint32_t jointMatricesStartIndex;
|
||||||
|
std::uint32_t numVertices;
|
||||||
|
VkDeviceAddress inputBuffer;
|
||||||
|
VkDeviceAddress skinningData;
|
||||||
|
VkDeviceAddress outputBuffer;
|
||||||
|
};
|
||||||
|
static constexpr std::size_t MAX_JOINT_MATRICES = 5000;
|
||||||
|
|
||||||
|
struct PerFrameData {
|
||||||
|
AppendableBuffer<glm::mat4> jointMatricesBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<PerFrameData, FRAMES_IN_FLIGHT> framesData;
|
||||||
|
// NBuffer framesData;
|
||||||
|
|
||||||
|
PerFrameData& getCurrentFrameData(std::size_t frameIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif //SKINNINGPIPELINE_H
|
#endif //SKINNINGPIPELINE_H
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
|
||||||
#include "Pipelines/SkyboxPipeline.h"
|
#include "Pipelines/SkyboxPipeline.h"
|
||||||
|
|
||||||
class GameRenderer {
|
class GameRenderer {
|
||||||
@@ -35,6 +36,11 @@ public:
|
|||||||
void cleanup(VkDevice device);
|
void cleanup(VkDevice device);
|
||||||
|
|
||||||
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
|
void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId);
|
||||||
|
void drawSkinnedMesh(MeshID id,
|
||||||
|
const glm::mat4& transform,
|
||||||
|
MaterialID materialId,
|
||||||
|
SkinnedMesh* skinnedMesh,
|
||||||
|
std::size_t jointMatricesStartIndex);
|
||||||
const GPUImage& getDrawImage(const GfxDevice& gfx_device) const;
|
const GPUImage& getDrawImage(const GfxDevice& gfx_device) const;
|
||||||
|
|
||||||
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) {
|
void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) {
|
||||||
@@ -47,6 +53,13 @@ public:
|
|||||||
void setSkyboxTexture(ImageID skyboxImageId);
|
void setSkyboxTexture(ImageID skyboxImageId);
|
||||||
|
|
||||||
void flushMaterialUpdates(GfxDevice& gfxDevice);
|
void flushMaterialUpdates(GfxDevice& gfxDevice);
|
||||||
|
[[nodiscard]] MeshCache& GetMeshCache() const {
|
||||||
|
return meshCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] SkinningPipeline& getSkinningPipeline() const {
|
||||||
|
return *skinningPipeline;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
||||||
@@ -91,6 +104,8 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<MeshPipeline> meshPipeline;
|
std::unique_ptr<MeshPipeline> meshPipeline;
|
||||||
std::unique_ptr<SkyboxPipeline> skyboxPipeline;
|
std::unique_ptr<SkyboxPipeline> skyboxPipeline;
|
||||||
|
|
||||||
|
std::unique_ptr<SkinningPipeline> skinningPipeline;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //RENDERER_H
|
#endif //RENDERER_H
|
||||||
|
|||||||
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
|
#ifndef APPENDABLEBUFFER_H
|
||||||
#define APPENDABLEBUFFER_H
|
#define APPENDABLEBUFFER_H
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct AppendableBuffer {
|
||||||
|
void append(std::span<const T> elements)
|
||||||
|
{
|
||||||
|
assert(size + elements.size() <= capacity);
|
||||||
|
auto arr = (T*)buffer.info.pMappedData;
|
||||||
|
std::memcpy((void*)&arr[size], elements.data(), elements.size() * sizeof(T));
|
||||||
|
size += elements.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void append(const T& element)
|
||||||
|
{
|
||||||
|
assert(size + 1 <= capacity);
|
||||||
|
auto arr = (T*)buffer.info.pMappedData;
|
||||||
|
std::memcpy((void*)&arr[size], &element, sizeof(T));
|
||||||
|
++size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() { size = 0; }
|
||||||
|
|
||||||
|
VkBuffer getVkBuffer() const { return buffer.buffer; }
|
||||||
|
|
||||||
|
GPUBuffer buffer;
|
||||||
|
std::size_t capacity{};
|
||||||
|
std::size_t size{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif //APPENDABLEBUFFER_H
|
#endif //APPENDABLEBUFFER_H
|
||||||
|
|||||||
@@ -2,33 +2,48 @@
|
|||||||
#define MESH_H
|
#define MESH_H
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <glm/vec2.hpp>
|
#include <glm/vec2.hpp>
|
||||||
#include <glm/vec3.hpp>
|
#include <glm/vec3.hpp>
|
||||||
#include <glm/vec4.hpp>
|
#include <glm/vec4.hpp>
|
||||||
|
#include <glm/mat4x4.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Resources/Buffer.h>
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
|
#include <destrum/Graphics/Frustum.h>
|
||||||
|
#include <destrum/Graphics/Skeleton.h>
|
||||||
|
|
||||||
|
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
|
||||||
struct CPUMesh {
|
struct CPUMesh {
|
||||||
std::vector<std::uint32_t> indices;
|
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
float uv_x{};
|
float uv_x{};
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
float uv_y{};
|
float uv_y{};
|
||||||
glm::vec4 tangent;
|
glm::vec4 tangent;
|
||||||
};
|
};
|
||||||
std::vector<Vertex> vertices;
|
|
||||||
|
struct SkinningData {
|
||||||
|
glm::vec<4, std::uint32_t> jointIds;
|
||||||
|
glm::vec4 weights;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::uint32_t> indices;
|
||||||
|
std::vector<Vertex> vertices;
|
||||||
|
std::vector<SkinningData> skinningData; // empty if no skeleton
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
||||||
glm::vec3 minPos;
|
glm::vec3 minPos;
|
||||||
glm::vec3 maxPos;
|
glm::vec3 maxPos;
|
||||||
|
|
||||||
|
std::optional<Skeleton> skeleton; // present only for skinned meshes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
struct GPUMesh {
|
struct GPUMesh {
|
||||||
GPUBuffer vertexBuffer;
|
GPUBuffer vertexBuffer;
|
||||||
@@ -40,78 +55,64 @@ struct GPUMesh {
|
|||||||
// AABB
|
// AABB
|
||||||
glm::vec3 minPos;
|
glm::vec3 minPos;
|
||||||
glm::vec3 maxPos;
|
glm::vec3 maxPos;
|
||||||
// math::Sphere boundingSphere;
|
Sphere boundingSphere;
|
||||||
|
|
||||||
|
bool hasSkeleton{false};
|
||||||
|
GPUBuffer skinningDataBuffer; // valid only when hasSkeleton == true
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<CPUMesh::Vertex> vertices = {
|
// ─── Skinned output buffer (one per entity, not per mesh asset) ───────────────
|
||||||
// =======================
|
|
||||||
|
struct SkinnedMesh {
|
||||||
|
GPUBuffer skinnedVertexBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Cube geometry (dev / test asset) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace CubeMesh {
|
||||||
|
|
||||||
|
inline std::vector<CPUMesh::Vertex> vertices = {
|
||||||
// +Z (Front)
|
// +Z (Front)
|
||||||
// =======================
|
|
||||||
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, 0, 1}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
||||||
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 0, 1}, 1.0f, {1, 0, 0, 1}},
|
||||||
|
|
||||||
// =======================
|
|
||||||
// -Z (Back)
|
// -Z (Back)
|
||||||
// =======================
|
|
||||||
{{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
{{ 0.5f, -0.5f, -0.5f}, 0.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
||||||
{{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
{{-0.5f, -0.5f, -0.5f}, 1.0f, {0, 0, -1}, 0.0f, {-1, 0, 0, 1}},
|
||||||
{{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
{{-0.5f, 0.5f, -0.5f}, 1.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
||||||
{{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
{{ 0.5f, 0.5f, -0.5f}, 0.0f, {0, 0, -1}, 1.0f, {-1, 0, 0, 1}},
|
||||||
|
|
||||||
// =======================
|
|
||||||
// +X (Right)
|
// +X (Right)
|
||||||
// =======================
|
|
||||||
{{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
{{ 0.5f, -0.5f, 0.5f}, 0.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
||||||
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {1, 0, 0}, 0.0f, {0, 0, -1, 1}},
|
||||||
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
||||||
{{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
{{ 0.5f, 0.5f, 0.5f}, 0.0f, {1, 0, 0}, 1.0f, {0, 0, -1, 1}},
|
||||||
|
|
||||||
// =======================
|
|
||||||
// -X (Left)
|
// -X (Left)
|
||||||
// =======================
|
|
||||||
{{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
{{-0.5f, -0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
||||||
{{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
{{-0.5f, -0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 0.0f, {0, 0, 1, 1}},
|
||||||
{{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
{{-0.5f, 0.5f, 0.5f}, 1.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
||||||
{{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
{{-0.5f, 0.5f, -0.5f}, 0.0f, {-1, 0, 0}, 1.0f, {0, 0, 1, 1}},
|
||||||
|
|
||||||
// =======================
|
|
||||||
// +Y (Top)
|
// +Y (Top)
|
||||||
// =======================
|
|
||||||
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
{{-0.5f, 0.5f, 0.5f}, 0.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
{{ 0.5f, 0.5f, 0.5f}, 1.0f, {0, 1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
{{ 0.5f, 0.5f, -0.5f}, 1.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||||
{{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
{{-0.5f, 0.5f, -0.5f}, 0.0f, {0, 1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||||
|
|
||||||
// =======================
|
|
||||||
// -Y (Bottom)
|
// -Y (Bottom)
|
||||||
// =======================
|
|
||||||
{{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
{{-0.5f, -0.5f, -0.5f}, 0.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
{{ 0.5f, -0.5f, -0.5f}, 1.0f, {0, -1, 0}, 0.0f, {1, 0, 0, 1}},
|
||||||
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
{{ 0.5f, -0.5f, 0.5f}, 1.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||||
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
{{-0.5f, -0.5f, 0.5f}, 0.0f, {0, -1, 0}, 1.0f, {1, 0, 0, 1}},
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<uint32_t> indices = {
|
inline std::vector<std::uint32_t> indices = {
|
||||||
// Front (+Z)
|
0, 2, 1, 0, 3, 2, // Front
|
||||||
0, 2, 1, 0, 3, 2,
|
4, 6, 5, 4, 7, 6, // Back
|
||||||
|
8,10, 9, 8,11,10, // Right
|
||||||
// Back (-Z)
|
12,14,13, 12,15,14, // Left
|
||||||
4, 6, 5, 4, 7, 6,
|
16,18,17, 16,19,18, // Top
|
||||||
|
20,22,21, 20,23,22, // Bottom
|
||||||
// Right (+X)
|
|
||||||
8,10, 9, 8,11,10,
|
|
||||||
|
|
||||||
// Left (-X)
|
|
||||||
12,14,13, 12,15,14,
|
|
||||||
|
|
||||||
// Top (+Y)
|
|
||||||
16,18,17, 16,19,18,
|
|
||||||
|
|
||||||
// Bottom (-Y)
|
|
||||||
20,22,21, 20,23,22
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace CubeMesh
|
||||||
|
|
||||||
#endif //MESH_H
|
#endif // MESH_H
|
||||||
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;
|
using BindlessID = std::uint32_t;
|
||||||
constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits<std::uint32_t>::max();
|
constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits<std::uint32_t>::max();
|
||||||
|
|
||||||
|
using JointId = std::uint16_t;
|
||||||
|
static const JointId NULL_JOINT_ID = std::numeric_limits<JointId>::max();
|
||||||
|
static const JointId ROOT_JOINT_ID = 0;
|
||||||
|
|
||||||
#endif //IDS_H
|
#endif //IDS_H
|
||||||
|
|||||||
@@ -1,4 +1,86 @@
|
|||||||
#ifndef MATHUTILS_H
|
#ifndef MATHUTILS_H
|
||||||
#define MATHUTILS_H
|
#define MATHUTILS_H
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Frustum.h>
|
||||||
|
#include <span>
|
||||||
|
#include <array>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "glm/gtx/norm.hpp"
|
||||||
|
|
||||||
|
inline Sphere calculateBoundingSphere(std::span<glm::vec3> positions)
|
||||||
|
{
|
||||||
|
assert(!positions.empty());
|
||||||
|
auto calculateInitialSphere = [](const std::span<glm::vec3>& positions) -> Sphere {
|
||||||
|
constexpr int dirCount = 13;
|
||||||
|
static const std::array<glm::vec3, dirCount> direction = {{
|
||||||
|
{1.f, 0.f, 0.f},
|
||||||
|
{0.f, 1.f, 0.f},
|
||||||
|
{0.f, 0.f, 1.f},
|
||||||
|
{1.f, 1.f, 0.f},
|
||||||
|
{1.f, 0.f, 1.f},
|
||||||
|
{0.f, 1.f, 1.f},
|
||||||
|
{1.f, -1.f, 0.f},
|
||||||
|
{1.f, 0.f, -1.f},
|
||||||
|
{0.f, 1.f, -1.f},
|
||||||
|
{1.f, 1.f, 1.f},
|
||||||
|
{1.f, -1.f, 1.f},
|
||||||
|
{1.f, 1.f, -1.f},
|
||||||
|
{1.f, -1.f, -1.f},
|
||||||
|
}};
|
||||||
|
|
||||||
|
std::array<float, dirCount> dmin{};
|
||||||
|
std::array<float, dirCount> dmax{};
|
||||||
|
std::array<std::size_t, dirCount> imin{};
|
||||||
|
std::array<std::size_t, dirCount> imax{};
|
||||||
|
|
||||||
|
// Find min and max dot products for each direction and record vertex indices.
|
||||||
|
for (int j = 0; j < dirCount; ++j) {
|
||||||
|
const auto& u = direction[j];
|
||||||
|
dmin[j] = glm::dot(u, positions[0]);
|
||||||
|
dmax[j] = dmin[j];
|
||||||
|
for (std::size_t i = 1; i < positions.size(); ++i) {
|
||||||
|
const auto d = glm::dot(u, positions[i]);
|
||||||
|
if (d < dmin[j]) {
|
||||||
|
dmin[j] = d;
|
||||||
|
imin[j] = i;
|
||||||
|
} else if (d > dmax[j]) {
|
||||||
|
dmax[j] = d;
|
||||||
|
imax[j] = i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find direction for which vertices at min and max extents are furthest apart.
|
||||||
|
float d2 = glm::length2(positions[imax[0]] - positions[imin[0]]);
|
||||||
|
int k = 0;
|
||||||
|
for (int j = 1; j < dirCount; j++) {
|
||||||
|
const auto m2 = glm::length2(positions[imax[j]] - positions[imin[j]]);
|
||||||
|
if (m2 > d2) {
|
||||||
|
d2 = m2;
|
||||||
|
k = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto center = (positions[imin[k]] + positions[imax[k]]) * 0.5f;
|
||||||
|
float radius = sqrt(d2) * 0.5f;
|
||||||
|
return {center, radius};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine initial center and radius.
|
||||||
|
auto s = calculateInitialSphere(positions);
|
||||||
|
// Make pass through vertices and adjust sphere as necessary.
|
||||||
|
for (std::size_t i = 0; i < positions.size(); i++) {
|
||||||
|
const auto pv = positions[i] - s.center;
|
||||||
|
float m2 = glm::length2(pv);
|
||||||
|
if (m2 > s.radius * s.radius) {
|
||||||
|
auto q = s.center - (pv * (s.radius / std::sqrt(m2)));
|
||||||
|
s.center = (q + positions[i]) * 0.5f;
|
||||||
|
s.radius = glm::length(q - s.center);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
#endif //MATHUTILS_H
|
#endif //MATHUTILS_H
|
||||||
|
|||||||
@@ -1,498 +1,521 @@
|
|||||||
#ifndef MODELLOADER_H
|
#ifndef MODELLOADER_H
|
||||||
#define MODELLOADER_H
|
#define MODELLOADER_H
|
||||||
|
|
||||||
// CPUMesh loader with tinygltf
|
// CPUMesh loader using Assimp
|
||||||
// - Loads first scene (or default scene), iterates nodes, extracts mesh primitives.
|
// - Loads first scene, iterates nodes recursively, extracts mesh primitives.
|
||||||
// - Handles POSITION/NORMAL/TANGENT/TEXCOORD_0 and indices.
|
// - Handles POSITION / NORMAL / TANGENT / TEXCOORD_0 and indices.
|
||||||
// - Computes minPos/maxPos.
|
// - Handles bones/skinning data and skeletal animations.
|
||||||
// - Can return per-primitive meshes, or merged-per-gltf-mesh meshes.
|
// - Computes minPos / maxPos (in world space).
|
||||||
// - IMPORTANT FIX: Applies node transforms (TRS / matrix) so models don't appear flipped/rotated.
|
// - Can return per-primitive meshes (PerPrimitive) or one merged mesh per
|
||||||
|
// Assimp mesh-node (MergedPerMesh).
|
||||||
|
// - LoadSkinnedModel loads meshes + skeleton + animations in one call.
|
||||||
|
// - For skinned meshes, vertex positions are kept in local/bind-pose space
|
||||||
|
// (identity world transform) so the skinning shader can apply joint matrices
|
||||||
|
// correctly at runtime.
|
||||||
//
|
//
|
||||||
// NOTE: Defining TINYGLTF_IMPLEMENTATION in a header can cause ODR / multiple-definition issues
|
// Link against: assimp (e.g. -lassimp)
|
||||||
// if included in more than one translation unit. Best practice: put the TINYGLTF_IMPLEMENTATION
|
// Supports any format Assimp understands (.gltf, .glb, .fbx, .obj, …).
|
||||||
// define in exactly one .cpp. I keep it here because your original file did, but you may want
|
|
||||||
// to move it.
|
|
||||||
|
|
||||||
#define TINYGLTF_IMPLEMENTATION
|
#include <assimp/Importer.hpp>
|
||||||
#define TINYGLTF_NO_STB_IMAGE_WRITE
|
#include <assimp/scene.h>
|
||||||
#include <tiny_gltf.h>
|
#include <assimp/postprocess.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/matrix_transform.hpp> // translate/scale
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp> // quat
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp> // mat4_cast
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Resources/Mesh.h>
|
#include <destrum/Graphics/Resources/Mesh.h>
|
||||||
|
#include <destrum/Graphics/Skeleton.h>
|
||||||
|
#include <destrum/Graphics/SkeletalAnimation.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <stdexcept>
|
|
||||||
#include <iostream>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace ModelLoader {
|
namespace ModelLoader {
|
||||||
|
|
||||||
// -------------------- helpers --------------------
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static size_t ComponentSizeInBytes(int componentType) {
|
// Convert an Assimp row-major 4×4 matrix to GLM column-major.
|
||||||
switch (componentType) {
|
static glm::mat4 ToGLM(const aiMatrix4x4& m) {
|
||||||
case TINYGLTF_COMPONENT_TYPE_BYTE: return 1;
|
return glm::transpose(glm::make_mat4(&m.a1));
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: return 1;
|
}
|
||||||
case TINYGLTF_COMPONENT_TYPE_SHORT: return 2;
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: return 2;
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_INT: return 4;
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: return 4;
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_FLOAT: return 4;
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_DOUBLE: return 8;
|
|
||||||
default: throw std::runtime_error("Unknown glTF component type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int TypeNumComponents(int type) {
|
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
|
||||||
switch (type) {
|
mn.x = std::min(mn.x, p.x);
|
||||||
case TINYGLTF_TYPE_SCALAR: return 1;
|
mn.y = std::min(mn.y, p.y);
|
||||||
case TINYGLTF_TYPE_VEC2: return 2;
|
mn.z = std::min(mn.z, p.z);
|
||||||
case TINYGLTF_TYPE_VEC3: return 3;
|
mx.x = std::max(mx.x, p.x);
|
||||||
case TINYGLTF_TYPE_VEC4: return 4;
|
mx.y = std::max(mx.y, p.y);
|
||||||
case TINYGLTF_TYPE_MAT2: return 4;
|
mx.z = std::max(mx.z, p.z);
|
||||||
case TINYGLTF_TYPE_MAT3: return 9;
|
}
|
||||||
case TINYGLTF_TYPE_MAT4: return 16;
|
|
||||||
default: throw std::runtime_error("Unknown glTF type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns pointer to the first element, plus stride in bytes.
|
// ─── Post-process flags ───────────────────────────────────────────────────────
|
||||||
static std::pair<const std::uint8_t*, size_t>
|
|
||||||
GetAccessorDataPtrAndStride(const tinygltf::Model& model, const tinygltf::Accessor& accessor) {
|
|
||||||
if (accessor.bufferView < 0) {
|
|
||||||
throw std::runtime_error("Accessor has no bufferView");
|
|
||||||
}
|
|
||||||
const tinygltf::BufferView& bv = model.bufferViews.at(accessor.bufferView);
|
|
||||||
const tinygltf::Buffer& buf = model.buffers.at(bv.buffer);
|
|
||||||
|
|
||||||
const size_t componentSize = ComponentSizeInBytes(accessor.componentType);
|
static constexpr unsigned int kImportFlags =
|
||||||
const int numComps = TypeNumComponents(accessor.type);
|
aiProcess_Triangulate |
|
||||||
const size_t packedStride = componentSize * size_t(numComps);
|
aiProcess_CalcTangentSpace |
|
||||||
|
aiProcess_JoinIdenticalVertices |
|
||||||
|
aiProcess_GenSmoothNormals |
|
||||||
|
aiProcess_FlipUVs |
|
||||||
|
aiProcess_LimitBoneWeights; // guarantees <= 4 weights per vertex
|
||||||
|
|
||||||
size_t stride = bv.byteStride ? size_t(bv.byteStride) : packedStride;
|
static const aiScene* LoadScene(Assimp::Importer& importer, const std::string& path) {
|
||||||
|
const aiScene* scene = importer.ReadFile(path, kImportFlags);
|
||||||
|
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode)
|
||||||
|
throw std::runtime_error(std::string("Assimp failed to load: ") + importer.GetErrorString());
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
const size_t start = size_t(bv.byteOffset) + size_t(accessor.byteOffset);
|
// ─── Primitive extraction ─────────────────────────────────────────────────────
|
||||||
// Conservative bounds check; don't hard-fail on weird stride but catches obvious issues.
|
|
||||||
if (start > buf.data.size()) {
|
|
||||||
throw std::runtime_error("Accessor start is out of buffer bounds");
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::uint8_t* ptr = buf.data.data() + start;
|
// Build one CPUMesh from a single aiMesh, applying the given world transform.
|
||||||
return {ptr, stride};
|
// For skinned meshes, pass glm::mat4{1.f} so vertices stay in bind-pose space.
|
||||||
}
|
// Skinning data is NOT populated here — call LoadSkinningData afterwards.
|
||||||
|
static CPUMesh LoadAiMeshIntoCPUMesh(const aiMesh* mesh,
|
||||||
|
const std::string& name,
|
||||||
|
const glm::mat4& world) {
|
||||||
|
CPUMesh out{};
|
||||||
|
out.name = name;
|
||||||
|
|
||||||
template <typename T>
|
const size_t vertexCount = mesh->mNumVertices;
|
||||||
static T ReadAs(const std::uint8_t* p) {
|
out.vertices.resize(vertexCount);
|
||||||
T v{};
|
|
||||||
std::memcpy(&v, p, sizeof(T));
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
static glm::vec3 ReadVec3Float(const std::uint8_t* base, size_t stride, size_t i) {
|
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
||||||
const std::uint8_t* p = base + i * stride;
|
const glm::mat3 tanMat = glm::mat3(world);
|
||||||
const float x = ReadAs<float>(p + 0);
|
|
||||||
const float y = ReadAs<float>(p + 4);
|
|
||||||
const float z = ReadAs<float>(p + 8);
|
|
||||||
return glm::vec3{x, y, z};
|
|
||||||
}
|
|
||||||
|
|
||||||
static glm::vec2 ReadVec2Float(const std::uint8_t* base, size_t stride, size_t i) {
|
glm::vec3 mn{ std::numeric_limits<float>::infinity()};
|
||||||
const std::uint8_t* p = base + i * stride;
|
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
||||||
const float x = ReadAs<float>(p + 0);
|
|
||||||
const float y = ReadAs<float>(p + 4);
|
|
||||||
return glm::vec2{x, y};
|
|
||||||
}
|
|
||||||
|
|
||||||
static glm::vec4 ReadVec4Float(const std::uint8_t* base, size_t stride, size_t i) {
|
for (size_t i = 0; i < vertexCount; ++i) {
|
||||||
const std::uint8_t* p = base + i * stride;
|
CPUMesh::Vertex v{};
|
||||||
const float x = ReadAs<float>(p + 0);
|
|
||||||
const float y = ReadAs<float>(p + 4);
|
|
||||||
const float z = ReadAs<float>(p + 8);
|
|
||||||
const float w = ReadAs<float>(p + 12);
|
|
||||||
return glm::vec4{x, y, z, w};
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::uint32_t ReadIndexAsU32(const tinygltf::Model& model, const tinygltf::Accessor& accessor, size_t i) {
|
// Position (required)
|
||||||
auto [base, stride] = GetAccessorDataPtrAndStride(model, accessor);
|
const aiVector3D& ap = mesh->mVertices[i];
|
||||||
const std::uint8_t* p = base + i * stride;
|
v.position = glm::vec3(world * glm::vec4(ap.x, ap.y, ap.z, 1.0f));
|
||||||
|
UpdateBounds(mn, mx, v.position);
|
||||||
|
|
||||||
switch (accessor.componentType) {
|
// Normal
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
|
if (mesh->HasNormals()) {
|
||||||
return static_cast<std::uint32_t>(ReadAs<std::uint8_t>(p));
|
const aiVector3D& an = mesh->mNormals[i];
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
|
v.normal = glm::normalize(nrmMat * glm::vec3(an.x, an.y, an.z));
|
||||||
return static_cast<std::uint32_t>(ReadAs<std::uint16_t>(p));
|
|
||||||
case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
|
|
||||||
return static_cast<std::uint32_t>(ReadAs<std::uint32_t>(p));
|
|
||||||
default:
|
|
||||||
throw std::runtime_error("Unsupported index componentType (expected u8/u16/u32)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void UpdateBounds(glm::vec3& mn, glm::vec3& mx, const glm::vec3& p) {
|
|
||||||
mn.x = std::min(mn.x, p.x);
|
|
||||||
mn.y = std::min(mn.y, p.y);
|
|
||||||
mn.z = std::min(mn.z, p.z);
|
|
||||||
mx.x = std::max(mx.x, p.x);
|
|
||||||
mx.y = std::max(mx.y, p.y);
|
|
||||||
mx.z = std::max(mx.z, p.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a node local matrix from either node.matrix or TRS.
|
|
||||||
// glTF stores rotation as quaternion [x,y,z,w].
|
|
||||||
static glm::mat4 NodeLocalMatrix(const tinygltf::Node& node) {
|
|
||||||
if (node.matrix.size() == 16) {
|
|
||||||
glm::mat4 m(1.0f);
|
|
||||||
// glTF is column-major; GLM is column-major -> fill columns.
|
|
||||||
for (int c = 0; c < 4; ++c)
|
|
||||||
for (int r = 0; r < 4; ++r)
|
|
||||||
m[c][r] = static_cast<float>(node.matrix[c * 4 + r]);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 t(0.0f);
|
|
||||||
if (node.translation.size() == 3) {
|
|
||||||
t = glm::vec3(
|
|
||||||
static_cast<float>(node.translation[0]),
|
|
||||||
static_cast<float>(node.translation[1]),
|
|
||||||
static_cast<float>(node.translation[2])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::quat q(1.0f, 0.0f, 0.0f, 0.0f); // w,x,y,z
|
|
||||||
if (node.rotation.size() == 4) {
|
|
||||||
q = glm::quat(
|
|
||||||
static_cast<float>(node.rotation[3]), // w
|
|
||||||
static_cast<float>(node.rotation[0]), // x
|
|
||||||
static_cast<float>(node.rotation[1]), // y
|
|
||||||
static_cast<float>(node.rotation[2]) // z
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 s(1.0f);
|
|
||||||
if (node.scale.size() == 3) {
|
|
||||||
s = glm::vec3(
|
|
||||||
static_cast<float>(node.scale[0]),
|
|
||||||
static_cast<float>(node.scale[1]),
|
|
||||||
static_cast<float>(node.scale[2])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const glm::mat4 T = glm::translate(glm::mat4(1.0f), t);
|
|
||||||
const glm::mat4 R = glm::toMat4(q);
|
|
||||||
const glm::mat4 S = glm::scale(glm::mat4(1.0f), s);
|
|
||||||
return T * R * S;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- primitive extraction --------------------
|
|
||||||
|
|
||||||
static CPUMesh LoadPrimitiveIntoCPUMesh(const tinygltf::Model& model,
|
|
||||||
const tinygltf::Primitive& prim,
|
|
||||||
const std::string& nameForMesh,
|
|
||||||
const glm::mat4& world) {
|
|
||||||
CPUMesh out{};
|
|
||||||
out.name = nameForMesh;
|
|
||||||
|
|
||||||
// POSITION is required
|
|
||||||
auto itPos = prim.attributes.find("POSITION");
|
|
||||||
if (itPos == prim.attributes.end()) {
|
|
||||||
throw std::runtime_error("Primitive has no POSITION attribute");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tinygltf::Accessor& accPos = model.accessors.at(itPos->second);
|
|
||||||
if (accPos.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accPos.type != TINYGLTF_TYPE_VEC3) {
|
|
||||||
throw std::runtime_error("POSITION must be VEC3 float for this loader");
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t vertexCount = size_t(accPos.count);
|
|
||||||
|
|
||||||
// Optional attributes
|
|
||||||
const tinygltf::Accessor* accNormal = nullptr;
|
|
||||||
const tinygltf::Accessor* accTangent = nullptr;
|
|
||||||
const tinygltf::Accessor* accUV0 = nullptr;
|
|
||||||
|
|
||||||
if (auto it = prim.attributes.find("NORMAL"); it != prim.attributes.end()) {
|
|
||||||
accNormal = &model.accessors.at(it->second);
|
|
||||||
if (accNormal->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accNormal->type != TINYGLTF_TYPE_VEC3)
|
|
||||||
accNormal = nullptr;
|
|
||||||
}
|
|
||||||
if (auto it = prim.attributes.find("TANGENT"); it != prim.attributes.end()) {
|
|
||||||
accTangent = &model.accessors.at(it->second);
|
|
||||||
if (accTangent->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accTangent->type != TINYGLTF_TYPE_VEC4)
|
|
||||||
accTangent = nullptr;
|
|
||||||
}
|
|
||||||
if (auto it = prim.attributes.find("TEXCOORD_0"); it != prim.attributes.end()) {
|
|
||||||
accUV0 = &model.accessors.at(it->second);
|
|
||||||
if (accUV0->componentType != TINYGLTF_COMPONENT_TYPE_FLOAT || accUV0->type != TINYGLTF_TYPE_VEC2)
|
|
||||||
accUV0 = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare pointers/strides
|
|
||||||
auto [posBase, posStride] = GetAccessorDataPtrAndStride(model, accPos);
|
|
||||||
|
|
||||||
const std::uint8_t* nrmBase = nullptr;
|
|
||||||
size_t nrmStride = 0;
|
|
||||||
const std::uint8_t* tanBase = nullptr;
|
|
||||||
size_t tanStride = 0;
|
|
||||||
const std::uint8_t* uvBase = nullptr;
|
|
||||||
size_t uvStride = 0;
|
|
||||||
|
|
||||||
if (accNormal) {
|
|
||||||
auto p = GetAccessorDataPtrAndStride(model, *accNormal);
|
|
||||||
nrmBase = p.first;
|
|
||||||
nrmStride = p.second;
|
|
||||||
if (size_t(accNormal->count) != vertexCount) accNormal = nullptr;
|
|
||||||
}
|
|
||||||
if (accTangent) {
|
|
||||||
auto p = GetAccessorDataPtrAndStride(model, *accTangent);
|
|
||||||
tanBase = p.first;
|
|
||||||
tanStride = p.second;
|
|
||||||
if (size_t(accTangent->count) != vertexCount) accTangent = nullptr;
|
|
||||||
}
|
|
||||||
if (accUV0) {
|
|
||||||
auto p = GetAccessorDataPtrAndStride(model, *accUV0);
|
|
||||||
uvBase = p.first;
|
|
||||||
uvStride = p.second;
|
|
||||||
if (size_t(accUV0->count) != vertexCount) accUV0 = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate vertices
|
|
||||||
out.vertices.resize(vertexCount);
|
|
||||||
|
|
||||||
// Bounds init (in world space, because we transform positions)
|
|
||||||
glm::vec3 mn{std::numeric_limits<float>::infinity()};
|
|
||||||
glm::vec3 mx{-std::numeric_limits<float>::infinity()};
|
|
||||||
|
|
||||||
// Normal matrix
|
|
||||||
const glm::mat3 nrmMat = glm::transpose(glm::inverse(glm::mat3(world)));
|
|
||||||
const glm::mat3 tanMat = glm::mat3(world);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < vertexCount; ++i) {
|
|
||||||
CPUMesh::Vertex v{};
|
|
||||||
|
|
||||||
// Position
|
|
||||||
const glm::vec3 pLocal = ReadVec3Float(posBase, posStride, i);
|
|
||||||
v.position = glm::vec3(world * glm::vec4(pLocal, 1.0f));
|
|
||||||
UpdateBounds(mn, mx, v.position);
|
|
||||||
|
|
||||||
// Normal
|
|
||||||
if (accNormal) {
|
|
||||||
const glm::vec3 nLocal = ReadVec3Float(nrmBase, nrmStride, i);
|
|
||||||
v.normal = glm::normalize(nrmMat * nLocal);
|
|
||||||
} else {
|
|
||||||
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UV
|
|
||||||
if (accUV0) {
|
|
||||||
glm::vec2 uv = ReadVec2Float(uvBase, uvStride, i);
|
|
||||||
v.uv_x = uv.x;
|
|
||||||
v.uv_y = uv.y;
|
|
||||||
} else {
|
|
||||||
v.uv_x = 0.0f;
|
|
||||||
v.uv_y = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tangent
|
|
||||||
if (accTangent) {
|
|
||||||
glm::vec4 t = ReadVec4Float(tanBase, tanStride, i);
|
|
||||||
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(t));
|
|
||||||
v.tangent = glm::vec4(t3, t.w); // keep handedness in w
|
|
||||||
} else {
|
|
||||||
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
out.vertices[i] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
|
|
||||||
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
|
|
||||||
|
|
||||||
// Indices
|
|
||||||
if (prim.indices >= 0) {
|
|
||||||
const tinygltf::Accessor& accIdx = model.accessors.at(prim.indices);
|
|
||||||
if (accIdx.type != TINYGLTF_TYPE_SCALAR) {
|
|
||||||
throw std::runtime_error("Indices accessor must be SCALAR");
|
|
||||||
}
|
|
||||||
|
|
||||||
out.indices.resize(size_t(accIdx.count));
|
|
||||||
for (size_t i = 0; i < size_t(accIdx.count); ++i) {
|
|
||||||
out.indices[i] = ReadIndexAsU32(model, accIdx, i);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
out.indices.resize(vertexCount);
|
v.normal = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||||
for (size_t i = 0; i < vertexCount; ++i) out.indices[i] = static_cast<std::uint32_t>(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If your renderer expects opposite winding vs glTF, you can flip triangle order.
|
// UV channel 0
|
||||||
// Keep what you had:
|
if (mesh->HasTextureCoords(0)) {
|
||||||
// for (size_t i = 0; i + 2 < out.indices.size(); i += 3) {
|
v.uv_x = mesh->mTextureCoords[0][i].x;
|
||||||
// std::swap(out.indices[i + 1], out.indices[i + 2]);
|
v.uv_y = mesh->mTextureCoords[0][i].y;
|
||||||
// }
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge "src" into "dst" (offset indices)
|
|
||||||
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
|
|
||||||
const std::uint32_t baseVertex = static_cast<std::uint32_t>(dst.vertices.size());
|
|
||||||
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
|
|
||||||
|
|
||||||
dst.indices.reserve(dst.indices.size() + src.indices.size());
|
|
||||||
for (std::uint32_t idx : src.indices) {
|
|
||||||
dst.indices.push_back(baseVertex + idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dst.vertices.size() == src.vertices.size()) {
|
|
||||||
dst.minPos = src.minPos;
|
|
||||||
dst.maxPos = src.maxPos;
|
|
||||||
} else {
|
} else {
|
||||||
dst.minPos = glm::vec3(
|
v.uv_x = 0.0f;
|
||||||
std::min(dst.minPos.x, src.minPos.x),
|
v.uv_y = 0.0f;
|
||||||
std::min(dst.minPos.y, src.minPos.y),
|
}
|
||||||
std::min(dst.minPos.z, src.minPos.z)
|
|
||||||
);
|
// Tangent + bitangent sign (stored in w as +1/-1 handedness)
|
||||||
dst.maxPos = glm::vec3(
|
if (mesh->HasTangentsAndBitangents()) {
|
||||||
std::max(dst.maxPos.x, src.maxPos.x),
|
const aiVector3D& at = mesh->mTangents[i];
|
||||||
std::max(dst.maxPos.y, src.maxPos.y),
|
const aiVector3D& ab = mesh->mBitangents[i];
|
||||||
std::max(dst.maxPos.z, src.maxPos.z)
|
glm::vec3 t3 = glm::normalize(tanMat * glm::vec3(at.x, at.y, at.z));
|
||||||
);
|
glm::vec3 b3 = glm::normalize(tanMat * glm::vec3(ab.x, ab.y, ab.z));
|
||||||
|
glm::vec3 n3 = v.normal;
|
||||||
|
float sign = (glm::dot(glm::cross(n3, t3), b3) < 0.0f) ? -1.0f : 1.0f;
|
||||||
|
v.tangent = glm::vec4(t3, sign);
|
||||||
|
} else {
|
||||||
|
v.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.vertices[i] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.minPos = (vertexCount > 0) ? mn : glm::vec3(0.0f);
|
||||||
|
out.maxPos = (vertexCount > 0) ? mx : glm::vec3(0.0f);
|
||||||
|
|
||||||
|
// Indices
|
||||||
|
if (mesh->HasFaces()) {
|
||||||
|
out.indices.reserve(size_t(mesh->mNumFaces) * 3);
|
||||||
|
for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
|
||||||
|
const aiFace& face = mesh->mFaces[f];
|
||||||
|
for (unsigned int k = 0; k < face.mNumIndices; ++k)
|
||||||
|
out.indices.push_back(static_cast<std::uint32_t>(face.mIndices[k]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.indices.resize(vertexCount);
|
||||||
|
for (size_t i = 0; i < vertexCount; ++i)
|
||||||
|
out.indices[i] = static_cast<std::uint32_t>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge src into dst, offsetting src indices by the current dst vertex count.
|
||||||
|
static void AppendMesh(CPUMesh& dst, const CPUMesh& src) {
|
||||||
|
const std::uint32_t base = static_cast<std::uint32_t>(dst.vertices.size());
|
||||||
|
dst.vertices.insert(dst.vertices.end(), src.vertices.begin(), src.vertices.end());
|
||||||
|
|
||||||
|
dst.indices.reserve(dst.indices.size() + src.indices.size());
|
||||||
|
for (std::uint32_t idx : src.indices)
|
||||||
|
dst.indices.push_back(base + idx);
|
||||||
|
|
||||||
|
dst.minPos = glm::vec3(
|
||||||
|
std::min(dst.minPos.x, src.minPos.x),
|
||||||
|
std::min(dst.minPos.y, src.minPos.y),
|
||||||
|
std::min(dst.minPos.z, src.minPos.z)
|
||||||
|
);
|
||||||
|
dst.maxPos = glm::vec3(
|
||||||
|
std::max(dst.maxPos.x, src.maxPos.x),
|
||||||
|
std::max(dst.maxPos.y, src.maxPos.y),
|
||||||
|
std::max(dst.maxPos.z, src.maxPos.z)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Skeleton loading ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static Skeleton LoadSkeleton(const aiScene* scene) {
|
||||||
|
Skeleton skeleton;
|
||||||
|
|
||||||
|
// Pass 1: collect all bone names and their inverse bind matrices
|
||||||
|
// from every mesh in the scene.
|
||||||
|
std::unordered_map<std::string, glm::mat4> boneOffsets;
|
||||||
|
for (unsigned int m = 0; m < scene->mNumMeshes; ++m) {
|
||||||
|
const aiMesh* mesh = scene->mMeshes[m];
|
||||||
|
for (unsigned int b = 0; b < mesh->mNumBones; ++b) {
|
||||||
|
const aiBone* bone = mesh->mBones[b];
|
||||||
|
std::string boneName(bone->mName.C_Str());
|
||||||
|
if (!boneOffsets.count(boneName))
|
||||||
|
boneOffsets[boneName] = ToGLM(bone->mOffsetMatrix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- public API --------------------
|
if (boneOffsets.empty()) return skeleton; // static mesh, no skeleton
|
||||||
|
|
||||||
struct NodeStackItem {
|
// Pass 2: walk the node tree in depth-first order (parent before child),
|
||||||
int nodeIdx;
|
// registering only nodes that correspond to actual bones.
|
||||||
glm::mat4 parentWorld;
|
// This guarantees joints are ordered parent-before-child, which is required
|
||||||
};
|
// by Animator::computeJointMatrices.
|
||||||
|
JointId nextId = 0;
|
||||||
|
|
||||||
// Option A: return one CPUMesh per *primitive* encountered in the scene
|
struct StackItem { const aiNode* node; int parentIdx; };
|
||||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
std::vector<StackItem> stack{{ scene->mRootNode, -1 }};
|
||||||
tinygltf::TinyGLTF loader;
|
|
||||||
tinygltf::Model model;
|
|
||||||
std::string err, warn;
|
|
||||||
|
|
||||||
bool ok = false;
|
while (!stack.empty()) {
|
||||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
auto [node, parentIdx] = stack.back();
|
||||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
stack.pop_back();
|
||||||
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
|
||||||
|
|
||||||
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
std::string name(node->mName.C_Str());
|
||||||
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
int myIdx = parentIdx; // default: pass parent through to children
|
||||||
|
|
||||||
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
if (boneOffsets.count(name)) {
|
||||||
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
const JointId id = nextId++;
|
||||||
throw std::runtime_error("glTF has no valid scene");
|
myIdx = static_cast<int>(id);
|
||||||
|
|
||||||
|
Joint joint{};
|
||||||
|
joint.id = id;
|
||||||
|
skeleton.joints.push_back(joint);
|
||||||
|
skeleton.jointNames.push_back(name);
|
||||||
|
skeleton.inverseBindMatrices.push_back(boneOffsets[name]);
|
||||||
|
|
||||||
|
// Grow hierarchy to accommodate this id
|
||||||
|
while (skeleton.hierarchy.size() <= id)
|
||||||
|
skeleton.hierarchy.push_back({});
|
||||||
|
|
||||||
|
skeleton.hierarchy[id].id = id; // record the node's own id
|
||||||
|
|
||||||
|
if (parentIdx >= 0)
|
||||||
|
skeleton.hierarchy[parentIdx].children.push_back(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<CPUMesh> result;
|
// Push children in reverse so left-most child is processed first
|
||||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
for (int c = static_cast<int>(node->mNumChildren) - 1; c >= 0; --c)
|
||||||
|
stack.push_back({ node->mChildren[c], myIdx });
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<NodeStackItem> stack;
|
buildParentIndex(skeleton);
|
||||||
stack.reserve(scene.nodes.size());
|
return skeleton;
|
||||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
}
|
||||||
|
|
||||||
while (!stack.empty()) {
|
// ─── Skinning data ────────────────────────────────────────────────────────────
|
||||||
NodeStackItem it = stack.back();
|
|
||||||
stack.pop_back();
|
|
||||||
|
|
||||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
// Populate cpuMesh.skinningData from the matching aiMesh.
|
||||||
const glm::mat4 local = NodeLocalMatrix(node);
|
// Must be called after LoadAiMeshIntoCPUMesh so vertices are already sized.
|
||||||
const glm::mat4 world = it.parentWorld * local;
|
static void LoadSkinningData(CPUMesh& cpuMesh,
|
||||||
|
const aiMesh* aiMesh,
|
||||||
|
const Skeleton& skeleton) {
|
||||||
|
if (!aiMesh->HasBones() || skeleton.joints.empty()) return;
|
||||||
|
|
||||||
for (int child : node.children) stack.push_back({child, world});
|
cpuMesh.skinningData.assign(cpuMesh.vertices.size(),
|
||||||
|
CPUMesh::SkinningData{ {0, 0, 0, 0}, {0.f, 0.f, 0.f, 0.f} });
|
||||||
|
|
||||||
if (node.mesh < 0) continue;
|
std::vector<int> weightCount(cpuMesh.vertices.size(), 0);
|
||||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
|
||||||
|
|
||||||
for (size_t p = 0; p < mesh.primitives.size(); ++p) {
|
for (unsigned int b = 0; b < aiMesh->mNumBones; ++b) {
|
||||||
const tinygltf::Primitive& prim = mesh.primitives[p];
|
const aiBone* bone = aiMesh->mBones[b];
|
||||||
CPUMesh cpu = LoadPrimitiveIntoCPUMesh(
|
std::string boneName(bone->mName.C_Str());
|
||||||
model,
|
|
||||||
prim,
|
// Find which joint index this bone maps to
|
||||||
mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name,
|
std::uint32_t jointIdx = 0;
|
||||||
world
|
bool found = false;
|
||||||
);
|
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||||
cpu.name += "_prim" + std::to_string(p);
|
if (skeleton.jointNames[j] == boneName) {
|
||||||
result.push_back(std::move(cpu));
|
jointIdx = static_cast<std::uint32_t>(j);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!found) continue;
|
||||||
|
|
||||||
return result;
|
for (unsigned int w = 0; w < bone->mNumWeights; ++w) {
|
||||||
}
|
const unsigned int vertIdx = bone->mWeights[w].mVertexId;
|
||||||
|
const float weight = bone->mWeights[w].mWeight;
|
||||||
|
const int slot = weightCount[vertIdx];
|
||||||
|
|
||||||
// Option B: return one CPUMesh per *glTF mesh instance*, merging all primitives of that mesh into one CPUMesh.
|
if (slot >= 4) continue; // aiProcess_LimitBoneWeights should prevent this
|
||||||
// Note: If the same glTF mesh is instanced by multiple nodes, you'll get one merged CPUMesh PER NODE instance.
|
|
||||||
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
|
|
||||||
tinygltf::TinyGLTF loader;
|
|
||||||
tinygltf::Model model;
|
|
||||||
std::string err, warn;
|
|
||||||
|
|
||||||
bool ok = false;
|
cpuMesh.skinningData[vertIdx].jointIds[slot] = jointIdx;
|
||||||
const bool isGLB = (path.size() >= 4 && path.substr(path.size() - 4) == ".glb");
|
cpuMesh.skinningData[vertIdx].weights[slot] = weight;
|
||||||
if (isGLB) ok = loader.LoadBinaryFromFile(&model, &err, &warn, path);
|
++weightCount[vertIdx];
|
||||||
else ok = loader.LoadASCIIFromFile(&model, &err, &warn, path);
|
|
||||||
|
|
||||||
if (!warn.empty()) std::cerr << "tinygltf warn: " << warn << "\n";
|
|
||||||
if (!ok) throw std::runtime_error("Failed to load glTF: " + err);
|
|
||||||
|
|
||||||
int sceneIndex = model.defaultScene >= 0 ? model.defaultScene : 0;
|
|
||||||
if (sceneIndex < 0 || sceneIndex >= int(model.scenes.size())) {
|
|
||||||
throw std::runtime_error("glTF has no valid scene");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<CPUMesh> result;
|
// ─── Animation loading ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const tinygltf::Scene& scene = model.scenes.at(sceneIndex);
|
static std::vector<SkeletalAnimation> LoadAnimations(const aiScene* scene,
|
||||||
std::vector<NodeStackItem> stack;
|
const Skeleton& skeleton) {
|
||||||
stack.reserve(scene.nodes.size());
|
std::vector<SkeletalAnimation> result;
|
||||||
for (int n : scene.nodes) stack.push_back({n, glm::mat4(1.0f)});
|
if (!scene->HasAnimations()) return result;
|
||||||
|
|
||||||
while (!stack.empty()) {
|
for (unsigned int a = 0; a < scene->mNumAnimations; ++a) {
|
||||||
NodeStackItem it = stack.back();
|
const aiAnimation* aiAnim = scene->mAnimations[a];
|
||||||
stack.pop_back();
|
const double tps = aiAnim->mTicksPerSecond > 0.0 ? aiAnim->mTicksPerSecond : 25.0;
|
||||||
|
|
||||||
const tinygltf::Node& node = model.nodes.at(it.nodeIdx);
|
SkeletalAnimation anim;
|
||||||
const glm::mat4 local = NodeLocalMatrix(node);
|
anim.name = aiAnim->mName.C_Str();
|
||||||
const glm::mat4 world = it.parentWorld * local;
|
anim.duration = static_cast<float>(aiAnim->mDuration / tps);
|
||||||
|
anim.looped = true;
|
||||||
|
|
||||||
for (int child : node.children) stack.push_back({child, world});
|
for (unsigned int c = 0; c < aiAnim->mNumChannels; ++c) {
|
||||||
|
const aiNodeAnim* ch = aiAnim->mChannels[c];
|
||||||
|
std::string boneName(ch->mNodeName.C_Str());
|
||||||
|
|
||||||
if (node.mesh < 0) continue;
|
// Map bone name to joint index
|
||||||
const tinygltf::Mesh& mesh = model.meshes.at(node.mesh);
|
std::uint32_t jointIdx = 0;
|
||||||
|
bool found = false;
|
||||||
CPUMesh merged{};
|
for (std::size_t j = 0; j < skeleton.jointNames.size(); ++j) {
|
||||||
merged.name = mesh.name.empty() ? ("mesh_" + std::to_string(node.mesh)) : mesh.name;
|
if (skeleton.jointNames[j] == boneName) {
|
||||||
merged.minPos = glm::vec3(std::numeric_limits<float>::infinity());
|
jointIdx = static_cast<std::uint32_t>(j);
|
||||||
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
found = true;
|
||||||
|
break;
|
||||||
bool any = false;
|
|
||||||
for (const tinygltf::Primitive& prim : mesh.primitives) {
|
|
||||||
CPUMesh part = LoadPrimitiveIntoCPUMesh(model, prim, merged.name, world);
|
|
||||||
if (!any) {
|
|
||||||
merged = std::move(part);
|
|
||||||
any = true;
|
|
||||||
} else {
|
|
||||||
AppendMesh(merged, part);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!found) continue;
|
||||||
|
|
||||||
if (any) result.push_back(std::move(merged));
|
SkeletalAnimation::Track track;
|
||||||
|
track.jointIndex = jointIdx;
|
||||||
|
|
||||||
|
// FIX: position, rotation and scale channels can have different
|
||||||
|
// keyframe counts and independent time axes. Build the track from
|
||||||
|
// the position channel's timeline and pick the nearest rotation/
|
||||||
|
// scale key for each sample rather than assuming index alignment.
|
||||||
|
// This avoids corrupted keyframes when counts differ.
|
||||||
|
const unsigned int numPos = ch->mNumPositionKeys;
|
||||||
|
const unsigned int numRot = ch->mNumRotationKeys;
|
||||||
|
const unsigned int numSca = ch->mNumScalingKeys;
|
||||||
|
|
||||||
|
// Use position keys to drive the timeline (most common case).
|
||||||
|
// If there are no position keys fall back to rotation keys.
|
||||||
|
const unsigned int numKeys = numPos > 0 ? numPos : numRot;
|
||||||
|
|
||||||
|
for (unsigned int k = 0; k < numKeys; ++k) {
|
||||||
|
SkeletalAnimation::Keyframe kf;
|
||||||
|
|
||||||
|
// Time comes from whichever channel drives this loop
|
||||||
|
if (numPos > 0) {
|
||||||
|
kf.time = static_cast<float>(ch->mPositionKeys[k].mTime / tps);
|
||||||
|
const auto& p = ch->mPositionKeys[k].mValue;
|
||||||
|
kf.translation = { p.x, p.y, p.z };
|
||||||
|
} else {
|
||||||
|
kf.time = static_cast<float>(ch->mRotationKeys[k].mTime / tps);
|
||||||
|
kf.translation = { 0.f, 0.f, 0.f };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nearest rotation key at this index
|
||||||
|
{
|
||||||
|
const unsigned int ri = std::min(k, numRot - 1);
|
||||||
|
const auto& r = ch->mRotationKeys[ri].mValue;
|
||||||
|
kf.rotation = glm::quat(r.w, r.x, r.y, r.z); // glm: (w,x,y,z)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nearest scale key at this index
|
||||||
|
{
|
||||||
|
const unsigned int si = std::min(k, numSca - 1);
|
||||||
|
const auto& s = ch->mScalingKeys[si].mValue;
|
||||||
|
kf.scale = { s.x, s.y, s.z };
|
||||||
|
}
|
||||||
|
|
||||||
|
track.keyframes.push_back(kf);
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.tracks.push_back(std::move(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
result.push_back(std::move(anim));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API: static meshes ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Option A: one CPUMesh per aiMesh reference encountered in the node tree.
|
||||||
|
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_PerPrimitive(const std::string& path) {
|
||||||
|
Assimp::Importer importer;
|
||||||
|
const aiScene* scene = LoadScene(importer, path);
|
||||||
|
|
||||||
|
std::vector<CPUMesh> result;
|
||||||
|
|
||||||
|
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||||
|
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
|
||||||
|
|
||||||
|
while (!stack.empty()) {
|
||||||
|
auto [node, parentWorld] = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
|
||||||
|
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||||
|
|
||||||
|
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||||
|
stack.push_back({ node->mChildren[c], world });
|
||||||
|
|
||||||
|
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||||
|
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
|
||||||
|
std::string name = mesh->mName.length > 0
|
||||||
|
? std::string(mesh->mName.C_Str())
|
||||||
|
: ("mesh_" + std::to_string(node->mMeshes[m]));
|
||||||
|
name += "_prim" + std::to_string(m);
|
||||||
|
|
||||||
|
result.push_back(LoadAiMeshIntoCPUMesh(mesh, name, world));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option B: one CPUMesh per node, merging all aiMesh references of that node.
|
||||||
|
static std::vector<CPUMesh> LoadGLTF_CPUMeshes_MergedPerMesh(const std::string& path) {
|
||||||
|
Assimp::Importer importer;
|
||||||
|
const aiScene* scene = LoadScene(importer, path);
|
||||||
|
|
||||||
|
std::vector<CPUMesh> result;
|
||||||
|
|
||||||
|
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||||
|
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.0f) }};
|
||||||
|
|
||||||
|
while (!stack.empty()) {
|
||||||
|
auto [node, parentWorld] = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
|
||||||
|
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||||
|
|
||||||
|
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||||
|
stack.push_back({ node->mChildren[c], world });
|
||||||
|
|
||||||
|
if (node->mNumMeshes == 0) continue;
|
||||||
|
|
||||||
|
std::string mergedName = node->mName.length > 0
|
||||||
|
? std::string(node->mName.C_Str())
|
||||||
|
: std::string(scene->mMeshes[node->mMeshes[0]]->mName.C_Str());
|
||||||
|
|
||||||
|
CPUMesh merged{};
|
||||||
|
merged.name = mergedName;
|
||||||
|
merged.minPos = glm::vec3( std::numeric_limits<float>::infinity());
|
||||||
|
merged.maxPos = glm::vec3(-std::numeric_limits<float>::infinity());
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
|
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||||
|
const aiMesh* mesh = scene->mMeshes[node->mMeshes[m]];
|
||||||
|
CPUMesh part = LoadAiMeshIntoCPUMesh(mesh, mergedName, world);
|
||||||
|
if (!any) { merged = std::move(part); any = true; }
|
||||||
|
else AppendMesh(merged, part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (any) result.push_back(std::move(merged));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API: skinned model ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct SkinnedModel {
|
||||||
|
std::vector<CPUMesh> meshes;
|
||||||
|
Skeleton skeleton;
|
||||||
|
std::vector<SkeletalAnimation> animations;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loads meshes + skeleton + animations in a single Assimp pass.
|
||||||
|
// Each CPUMesh has skinningData and skeleton populated.
|
||||||
|
//
|
||||||
|
// IMPORTANT: skinned mesh vertices are loaded with an identity world transform
|
||||||
|
// so they remain in bind-pose space. The skinning shader applies joint matrices
|
||||||
|
// at runtime — baking the node transform into positions would break that.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// auto model = ModelLoader::LoadSkinnedModel("character.glb");
|
||||||
|
// MeshId id = meshCache.uploadMesh(model.meshes[0]);
|
||||||
|
// auto* anim = go.AddComponent<Animator>();
|
||||||
|
// for (auto& clip : model.animations)
|
||||||
|
// anim->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
|
||||||
|
// anim->play("Walk");
|
||||||
|
static SkinnedModel LoadSkinnedModel(const std::string& path) {
|
||||||
|
Assimp::Importer importer;
|
||||||
|
const aiScene* scene = LoadScene(importer, path);
|
||||||
|
|
||||||
|
SkinnedModel model;
|
||||||
|
model.skeleton = LoadSkeleton(scene);
|
||||||
|
model.animations = LoadAnimations(scene, model.skeleton);
|
||||||
|
|
||||||
|
struct StackItem { const aiNode* node; glm::mat4 parentWorld; };
|
||||||
|
std::vector<StackItem> stack{{ scene->mRootNode, glm::mat4(1.f) }};
|
||||||
|
|
||||||
|
while (!stack.empty()) {
|
||||||
|
auto [node, parentWorld] = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
|
||||||
|
// We still traverse with world transforms so non-skinned siblings
|
||||||
|
// work correctly, but skinned meshes are loaded with identity below.
|
||||||
|
const glm::mat4 world = parentWorld * ToGLM(node->mTransformation);
|
||||||
|
|
||||||
|
for (unsigned int c = 0; c < node->mNumChildren; ++c)
|
||||||
|
stack.push_back({ node->mChildren[c], world });
|
||||||
|
|
||||||
|
for (unsigned int m = 0; m < node->mNumMeshes; ++m) {
|
||||||
|
const aiMesh* aiMesh = scene->mMeshes[node->mMeshes[m]];
|
||||||
|
std::string name = aiMesh->mName.length > 0
|
||||||
|
? std::string(aiMesh->mName.C_Str())
|
||||||
|
: ("mesh_" + std::to_string(node->mMeshes[m]));
|
||||||
|
|
||||||
|
// FIX: pass identity matrix for skinned meshes so vertices stay in
|
||||||
|
// bind-pose space. The inverse bind matrices and joint transforms
|
||||||
|
// are already expressed in that space — baking the node world
|
||||||
|
// transform into positions would offset everything incorrectly.
|
||||||
|
const bool isSkinned = aiMesh->HasBones() && !model.skeleton.joints.empty();
|
||||||
|
const glm::mat4 meshWorld = isSkinned ? glm::mat4{1.f} : world;
|
||||||
|
|
||||||
|
CPUMesh cpu = LoadAiMeshIntoCPUMesh(aiMesh, name, meshWorld);
|
||||||
|
LoadSkinningData(cpu, aiMesh, model.skeleton);
|
||||||
|
cpu.skeleton = model.skeleton;
|
||||||
|
model.meshes.push_back(std::move(cpu));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ModelLoader
|
} // namespace ModelLoader
|
||||||
|
|
||||||
#endif // MODELLOADER_H
|
#endif // MODELLOADER_H
|
||||||
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/Components/MeshRendererComponent.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
|
||||||
|
#include "destrum/Components/Animator.h"
|
||||||
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
|
#include "destrum/Util/GameState.h"
|
||||||
|
|
||||||
|
|
||||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::Start() {
|
void MeshRendererComponent::Start() {
|
||||||
Component::Start();
|
Component::Start();
|
||||||
|
if (auto* animator = GetGameObject()->GetComponent<Animator>()) {
|
||||||
|
const auto& gfxDevice = GameState::GetInstance().Gfx();
|
||||||
|
const auto& mesh = GameState::GetInstance().Renderer().GetMeshCache().getMesh(meshID);
|
||||||
|
|
||||||
|
m_skinnedMesh = std::make_unique<SkinnedMesh>();
|
||||||
|
m_skinnedMesh->skinnedVertexBuffer = gfxDevice.createBuffer(
|
||||||
|
mesh.numVertices * sizeof(CPUMesh::Vertex),
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||||
|
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
|
||||||
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::Update() {
|
void MeshRendererComponent::Update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::Render(const RenderContext& ctx) {
|
void MeshRendererComponent::Render(const RenderContext& ctx) {
|
||||||
if (meshID != NULL_MESH_ID && materialID != NULL_MATERIAL_ID) {
|
if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return;
|
||||||
|
|
||||||
|
if (auto* animator = GetGameObject()->GetComponent<Animator>(); animator && m_skinnedMesh) {
|
||||||
|
const auto& mesh = ctx.renderer.GetMeshCache().getCPUMesh(meshID);
|
||||||
|
const auto skeleton = GetGameObject()->GetComponent<Animator>()->getSkeleton();
|
||||||
|
std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex();
|
||||||
|
|
||||||
|
const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices(
|
||||||
|
ctx.renderer.getSkinningPipeline(),
|
||||||
|
*skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh)
|
||||||
|
frameIdx
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.renderer.drawSkinnedMesh(
|
||||||
|
meshID,
|
||||||
|
GetTransform().GetWorldMatrix(),
|
||||||
|
materialID,
|
||||||
|
m_skinnedMesh.get(),
|
||||||
|
jointMatricesStartIndex
|
||||||
|
);
|
||||||
|
} else {
|
||||||
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
|
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,13 +98,13 @@ void OrbitAndSpin::Update()
|
|||||||
// GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
|
// GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
|
||||||
|
|
||||||
// material color
|
// material color
|
||||||
auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
|
// auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
|
||||||
mat.baseColor = glm::vec3(
|
// mat.baseColor = glm::vec3(
|
||||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
|
||||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
|
||||||
0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
|
||||||
);
|
// );
|
||||||
GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
|
// GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrbitAndSpin::Start() {
|
void OrbitAndSpin::Start() {
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static const std::uint32_t maxBindlessResources = 16536;
|
constexpr std::uint32_t maxBindlessResources = 16536;
|
||||||
static const std::uint32_t maxSamplers = 32;
|
constexpr std::uint32_t maxSamplers = 32;
|
||||||
|
|
||||||
static const std::uint32_t texturesBinding = 0;
|
constexpr std::uint32_t texturesBinding = 0;
|
||||||
static const std::uint32_t samplersBinding = 1;
|
constexpr std::uint32_t samplersBinding = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
|
void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
|
||||||
@@ -26,7 +26,7 @@ void BindlessSetManager::init(VkDevice device, float maxAnisotropy)
|
|||||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||||
.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT,
|
.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT,
|
||||||
.maxSets = 10,
|
.maxSets = 10,
|
||||||
.poolSizeCount = (std::uint32_t)poolSizesBindless.size(),
|
.poolSizeCount = static_cast<std::uint32_t>(poolSizesBindless.size()),
|
||||||
.pPoolSizes = poolSizesBindless.data(),
|
.pPoolSizes = poolSizesBindless.data(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
auto& mainCommandBuffer = frames[i].commandBuffer;
|
||||||
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
|
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer));
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// { // create white texture
|
{ // create white texture
|
||||||
// std::uint32_t pixel = 0xFFFFFFFF;
|
std::uint32_t pixel = 0xFFFFFFFF;
|
||||||
// whiteImageId = createImage(
|
whiteImageId = createImage(
|
||||||
// {
|
{
|
||||||
// .format = VK_FORMAT_R8G8B8A8_UNORM,
|
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||||
// .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||||
// .extent = VkExtent3D{1, 1, 1},
|
.extent = VkExtent3D{1, 1, 1},
|
||||||
// },
|
},
|
||||||
// "white texture",
|
"white texture",
|
||||||
// &pixel);
|
&pixel);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
{ // create error texture (black/magenta checker)
|
||||||
|
constexpr auto black = 0xFF000000;
|
||||||
|
constexpr auto magenta = 0xFFFF00FF;
|
||||||
|
|
||||||
|
std::array<std::uint32_t, 4> pixels{black, magenta, magenta, black};
|
||||||
|
errorImageId = createImage(
|
||||||
|
{
|
||||||
|
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
||||||
|
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||||
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||||
|
.extent = VkExtent3D{2, 2, 1},
|
||||||
|
},
|
||||||
|
"error texture",
|
||||||
|
pixels.data());
|
||||||
|
imageCache.setErrorImageId(errorImageId);
|
||||||
|
}
|
||||||
|
|
||||||
GameState::GetInstance().SetGfxDevice(this);
|
GameState::GetInstance().SetGfxDevice(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,25 +513,14 @@ GPUImage GfxDevice::createImageRaw(
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
GPUImage GfxDevice::loadImageFromFileRaw(
|
GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const {
|
||||||
const std::filesystem::path& path,
|
|
||||||
VkImageUsageFlags usage,
|
|
||||||
bool mipMap,
|
|
||||||
TextureIntent intent) const {
|
|
||||||
// 1) Decode file to CPU pixels (stb for png/jpg/hdr, tinyexr for exr)
|
|
||||||
// IMPORTANT: util::loadImage should fill:
|
|
||||||
// - width/height/channels(=4)
|
|
||||||
// - hdr + (pixels OR hdrPixels)
|
|
||||||
// - vkFormat (RGBA8_SRGB or RGBA8_UNORM or RGBA32F)
|
|
||||||
// - byteSize (exact bytes to upload)
|
|
||||||
const auto data = util::loadImage(path, intent);
|
const auto data = util::loadImage(path, intent);
|
||||||
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 ||
|
|
||||||
(data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
|
if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) {
|
||||||
spdlog::error("Failed to load image '{}'", path.string());
|
spdlog::error("Failed to load image '{}'", path.string());
|
||||||
return getImage(errorImageId);
|
return getImage(errorImageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Create GPU image using the format the loader chose (matches CPU memory layout)
|
|
||||||
auto image = createImageRaw({
|
auto image = createImageRaw({
|
||||||
.format = data.vkFormat,
|
.format = data.vkFormat,
|
||||||
.usage = usage |
|
.usage = usage |
|
||||||
@@ -527,15 +534,10 @@ GPUImage GfxDevice::loadImageFromFileRaw(
|
|||||||
.mipMap = mipMap,
|
.mipMap = mipMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3) Upload *exactly* byteSize bytes from the correct pointer
|
const void* src = data.hdr ? static_cast<const void*>(data.hdrPixels) : static_cast<const void*>(data.pixels);
|
||||||
const void* src = data.hdr
|
|
||||||
? static_cast<const void*>(data.hdrPixels)
|
|
||||||
: static_cast<const void*>(data.pixels);
|
|
||||||
|
|
||||||
// Use the "sized" upload to avoid BytesPerTexel mismatches
|
|
||||||
uploadImageDataSized(image, src, data.byteSize, 0);
|
uploadImageDataSized(image, src, data.byteSize, 0);
|
||||||
|
|
||||||
// 4) Debug label
|
|
||||||
image.debugName = path.string();
|
image.debugName = path.string();
|
||||||
vkutil::addDebugLabel(device, image.image, path.string().c_str());
|
vkutil::addDebugLabel(device, image.image, path.string().c_str());
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,12 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
|
|
||||||
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice)
|
ImageCache::ImageCache(GfxDevice& gfxDevice) : gfxDevice(gfxDevice) {
|
||||||
{}
|
}
|
||||||
|
|
||||||
ImageID ImageCache::loadImageFromFile(
|
ImageID ImageCache::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) {
|
||||||
const std::filesystem::path& path,
|
for (const auto& [id, info]: loadedImagesInfo) {
|
||||||
VkImageUsageFlags usage,
|
if (info.path == path && info.intent == intent && info.usage == usage && info.mipMap == mipMap) {
|
||||||
bool mipMap,
|
|
||||||
TextureIntent intent)
|
|
||||||
{
|
|
||||||
for (const auto& [id, info] : loadedImagesInfo) {
|
|
||||||
if (info.path == path &&
|
|
||||||
info.intent == intent &&
|
|
||||||
info.usage == usage &&
|
|
||||||
info.mipMap == mipMap)
|
|
||||||
{
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,13 +32,11 @@ ImageID ImageCache::loadImageFromFile(
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageID ImageCache::addImage(GPUImage image)
|
ImageID ImageCache::addImage(GPUImage image) {
|
||||||
{
|
|
||||||
return addImage(getFreeImageId(), std::move(image));
|
return addImage(getFreeImageId(), std::move(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageID ImageCache::addImage(ImageID id, GPUImage image)
|
ImageID ImageCache::addImage(ImageID id, GPUImage image) {
|
||||||
{
|
|
||||||
image.setBindlessId(static_cast<std::uint32_t>(id));
|
image.setBindlessId(static_cast<std::uint32_t>(id));
|
||||||
if (id != images.size()) {
|
if (id != images.size()) {
|
||||||
images[id] = std::move(image); // replacing existing image
|
images[id] = std::move(image); // replacing existing image
|
||||||
@@ -59,20 +48,17 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image)
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GPUImage& ImageCache::getImage(ImageID id) const
|
const GPUImage& ImageCache::getImage(ImageID id) const {
|
||||||
{
|
|
||||||
assert(id != NULL_IMAGE_ID && id < images.size());
|
assert(id != NULL_IMAGE_ID && id < images.size());
|
||||||
return images.at(id);
|
return images.at(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageID ImageCache::getFreeImageId() const
|
ImageID ImageCache::getFreeImageId() const {
|
||||||
{
|
|
||||||
return images.size();
|
return images.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageCache::destroyImages()
|
void ImageCache::destroyImages() {
|
||||||
{
|
for (const auto& image: images) {
|
||||||
for (const auto& image : images) {
|
|
||||||
gfxDevice.destroyImage(image);
|
gfxDevice.destroyImage(image);
|
||||||
}
|
}
|
||||||
images.clear();
|
images.clear();
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ namespace util
|
|||||||
{
|
{
|
||||||
ImageData data;
|
ImageData data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- EXR ----------
|
// ---------- EXR ----------
|
||||||
if (isExrExt(p)) {
|
if (isExrExt(p)) {
|
||||||
float* out = nullptr;
|
float* out = nullptr;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ void MaterialCache::init(GfxDevice& gfxDevice)
|
|||||||
&normal);
|
&normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
Material placeholderMaterial{.name = "PLACEHOLDER_MATERIAL"};
|
Material placeholderMaterial{.diffuseTexture = defaultNormalMapTextureID, .name = "PLACEHOLDER_MATERIAL"};
|
||||||
placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial);
|
placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +41,14 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// store on GPU
|
// store on GPU
|
||||||
MaterialData* data = (MaterialData*)materialDataBuffer.info.pMappedData;
|
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||||
auto whiteTextureID = gfxDevice.getWhiteTextureID();
|
const auto whiteTextureID = gfxDevice.getWhiteTextureID();
|
||||||
auto id = getFreeMaterialId();
|
const auto id = getFreeMaterialId();
|
||||||
assert(id < MAX_MATERIALS);
|
assert(id < MAX_MATERIALS);
|
||||||
data[id] = MaterialData{
|
data[id] = MaterialData{
|
||||||
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
||||||
.metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f},
|
.metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f},
|
||||||
|
.textureFilteringMode = static_cast<std::uint32_t>(material.textureFilteringMode),
|
||||||
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
||||||
.normalTex = whiteTextureID,
|
.normalTex = whiteTextureID,
|
||||||
.metallicRoughnessTex = whiteTextureID,
|
.metallicRoughnessTex = whiteTextureID,
|
||||||
@@ -60,6 +61,15 @@ MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material)
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MaterialID MaterialCache::addSimpleTextureMaterial(GfxDevice& gfxDevice, ImageID textureID) {
|
||||||
|
Material material{};
|
||||||
|
material.name = "idk";
|
||||||
|
material.diffuseTexture = textureID;
|
||||||
|
material.metallicFactor = 0.0f;
|
||||||
|
material.roughnessFactor = 1.0f;
|
||||||
|
return addMaterial(gfxDevice, material);
|
||||||
|
}
|
||||||
|
|
||||||
const Material& MaterialCache::getMaterial(MaterialID id) const
|
const Material& MaterialCache::getMaterial(MaterialID id) const
|
||||||
{
|
{
|
||||||
return materials.at(id);
|
return materials.at(id);
|
||||||
@@ -100,7 +110,7 @@ void MaterialCache::updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id)
|
|||||||
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
||||||
.metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f),
|
.metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f),
|
||||||
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
||||||
.normalTex = whiteTextureID, // if you have this field
|
.normalTex = whiteTextureID,
|
||||||
.metallicRoughnessTex = whiteTextureID,
|
.metallicRoughnessTex = whiteTextureID,
|
||||||
.emissiveTex = whiteTextureID,
|
.emissiveTex = whiteTextureID,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,25 +3,30 @@
|
|||||||
#include <destrum/Graphics/Resources/Mesh.h>
|
#include <destrum/Graphics/Resources/Mesh.h>
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
#include <destrum/Graphics/Util.h>
|
#include <destrum/Graphics/Util.h>
|
||||||
|
|
||||||
|
#include <destrum/Util/MathUtils.h>
|
||||||
// #include <destrum/Math/Util.h>
|
// #include <destrum/Math/Util.h>
|
||||||
|
|
||||||
MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
|
MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh)
|
||||||
{
|
{
|
||||||
auto gpuMesh = GPUMesh{
|
auto gpuMesh = GPUMesh{
|
||||||
.numVertices = (std::uint32_t)cpuMesh.vertices.size(),
|
.numVertices = (std::uint32_t)cpuMesh.vertices.size(),
|
||||||
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
|
.numIndices = (std::uint32_t)cpuMesh.indices.size(),
|
||||||
.minPos = cpuMesh.minPos,
|
.minPos = cpuMesh.minPos,
|
||||||
.maxPos = cpuMesh.maxPos,
|
.maxPos = cpuMesh.maxPos,
|
||||||
|
.hasSkeleton = cpuMesh.skeleton.has_value(),
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<glm::vec3> positions(cpuMesh.vertices.size());
|
std::vector<glm::vec3> positions(cpuMesh.vertices.size());
|
||||||
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i) {
|
for (std::size_t i = 0; i < cpuMesh.vertices.size(); ++i)
|
||||||
positions[i] = cpuMesh.vertices[i].position;
|
positions[i] = cpuMesh.vertices[i].position;
|
||||||
}
|
gpuMesh.boundingSphere = calculateBoundingSphere(positions);
|
||||||
|
|
||||||
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
|
uploadMesh(gfxDevice, cpuMesh, gpuMesh);
|
||||||
|
|
||||||
const auto id = meshes.size();
|
const auto id = meshes.size();
|
||||||
meshes.push_back(std::move(gpuMesh));
|
meshes.push_back(std::move(gpuMesh));
|
||||||
|
cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +75,33 @@ void MeshCache::uploadMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh, GPUMesh
|
|||||||
const auto idxBufferName = cpuMesh.name + " (idx)";
|
const auto idxBufferName = cpuMesh.name + " (idx)";
|
||||||
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
|
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.vertexBuffer.buffer, vtxBufferName.c_str());
|
||||||
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str());
|
vkutil::addDebugLabel(gfxDevice.getDevice(), gpuMesh.indexBuffer.buffer, idxBufferName.c_str());
|
||||||
|
|
||||||
|
if (gpuMesh.hasSkeleton) {
|
||||||
|
// create skinning data buffer
|
||||||
|
const auto skinningDataSize = cpuMesh.vertices.size() * sizeof(CPUMesh::SkinningData);
|
||||||
|
gpuMesh.skinningDataBuffer = gfxDevice.createBuffer(
|
||||||
|
skinningDataSize,
|
||||||
|
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||||
|
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
|
||||||
|
|
||||||
|
const auto staging =
|
||||||
|
gfxDevice.createBuffer(skinningDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||||
|
|
||||||
|
// copy data
|
||||||
|
void* data = staging.info.pMappedData;
|
||||||
|
memcpy(data, cpuMesh.skinningData.data(), skinningDataSize);
|
||||||
|
|
||||||
|
gfxDevice.immediateSubmit([&](VkCommandBuffer cmd) {
|
||||||
|
const auto vertexCopy = VkBufferCopy{
|
||||||
|
.srcOffset = 0,
|
||||||
|
.dstOffset = 0,
|
||||||
|
.size = skinningDataSize,
|
||||||
|
};
|
||||||
|
vkCmdCopyBuffer(cmd, staging.buffer, gpuMesh.skinningDataBuffer.buffer, 1, &vertexCopy);
|
||||||
|
});
|
||||||
|
|
||||||
|
gfxDevice.destroyBuffer(staging);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GPUMesh& MeshCache::getMesh(MeshID id) const
|
const GPUMesh& MeshCache::getMesh(MeshID id) const
|
||||||
@@ -77,6 +109,11 @@ const GPUMesh& MeshCache::getMesh(MeshID id) const
|
|||||||
return meshes.at(id);
|
return meshes.at(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CPUMesh& MeshCache::getCPUMesh(MeshID id) const
|
||||||
|
{
|
||||||
|
return cpuMeshes.at(id);
|
||||||
|
}
|
||||||
|
|
||||||
void MeshCache::cleanup(const GfxDevice& gfxDevice)
|
void MeshCache::cleanup(const GfxDevice& gfxDevice)
|
||||||
{
|
{
|
||||||
for (const auto& mesh : meshes) {
|
for (const auto& mesh : meshes) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
|
#include "destrum/Graphics/Frustum.h"
|
||||||
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
|
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +71,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
|||||||
m_pipeline->bind(cmd);
|
m_pipeline->bind(cmd);
|
||||||
gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout);
|
gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout);
|
||||||
|
|
||||||
|
int ActualDrawCalls = 0;
|
||||||
|
|
||||||
const auto viewport = VkViewport{
|
const auto viewport = VkViewport{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = 0,
|
.y = 0,
|
||||||
@@ -86,16 +91,17 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
|||||||
|
|
||||||
auto prevMeshId = NULL_MESH_ID;
|
auto prevMeshId = NULL_MESH_ID;
|
||||||
|
|
||||||
// const auto frustum = edge::createFrustumFromCamera(camera);
|
const auto frustum = edge::createFrustumFromCamera(camera);
|
||||||
|
|
||||||
for (const auto& dcIdx : drawCommands) {
|
for (const auto& dcIdx : drawCommands) {
|
||||||
// const auto& dc = drawCommands[dcIdx];
|
|
||||||
const auto& dc = dcIdx;
|
const auto& dc = dcIdx;
|
||||||
|
|
||||||
// if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
|
// if (!edge::isInFrustum(frustum, dc.worldBoundingSphere)) {
|
||||||
// continue;
|
// continue;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
ActualDrawCalls++;
|
||||||
|
|
||||||
const auto& mesh = meshCache.getMesh(dc.meshId);
|
const auto& mesh = meshCache.getMesh(dc.meshId);
|
||||||
if (dc.meshId != prevMeshId) {
|
if (dc.meshId != prevMeshId) {
|
||||||
prevMeshId = dc.meshId;
|
prevMeshId = dc.meshId;
|
||||||
@@ -106,7 +112,7 @@ void MeshPipeline::draw(VkCommandBuffer cmd,
|
|||||||
const auto pushConstants = PushConstants{
|
const auto pushConstants = PushConstants{
|
||||||
.transform = dc.transformMatrix,
|
.transform = dc.transformMatrix,
|
||||||
.sceneDataBuffer = sceneDataBuffer.address,
|
.sceneDataBuffer = sceneDataBuffer.address,
|
||||||
.vertexBuffer = mesh.vertexBuffer.address,
|
.vertexBuffer = dc.skinnedMesh != nullptr ? dc.skinnedMesh->skinnedVertexBuffer.address : mesh.vertexBuffer.address,
|
||||||
.materialId = dc.materialId,
|
.materialId = dc.materialId,
|
||||||
};
|
};
|
||||||
vkCmdPushConstants(
|
vkCmdPushConstants(
|
||||||
|
|||||||
@@ -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 = std::make_unique<SkyboxPipeline>();
|
||||||
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
||||||
|
|
||||||
|
skinningPipeline = std::make_unique<SkinningPipeline>();
|
||||||
|
skinningPipeline->init(gfxDevice);
|
||||||
|
|
||||||
|
|
||||||
GameState::GetInstance().SetRenderer(this);
|
GameState::GetInstance().SetRenderer(this);
|
||||||
}
|
}
|
||||||
@@ -30,6 +33,7 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
|
|||||||
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
|
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
|
||||||
flushMaterialUpdates(gfxDevice);
|
flushMaterialUpdates(gfxDevice);
|
||||||
meshDrawCommands.clear();
|
meshDrawCommands.clear();
|
||||||
|
skinningPipeline->beginDrawing(gfxDevice.getCurrentFrameIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::endDrawing() {
|
void GameRenderer::endDrawing() {
|
||||||
@@ -37,6 +41,13 @@ void GameRenderer::endDrawing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) {
|
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) {
|
||||||
|
|
||||||
|
for (const auto& dc : meshDrawCommands) {
|
||||||
|
if (!dc.skinnedMesh) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skinningPipeline->doSkinning(cmd, gfxDevice.getCurrentFrameIndex(), meshCache, dc);
|
||||||
|
}
|
||||||
const auto gpuSceneData = GPUSceneData{
|
const auto gpuSceneData = GPUSceneData{
|
||||||
.view = sceneData.camera.GetViewMatrix(),
|
.view = sceneData.camera.GetViewMatrix(),
|
||||||
.proj = sceneData.camera.GetProjectionMatrix(),
|
.proj = sceneData.camera.GetProjectionMatrix(),
|
||||||
@@ -107,7 +118,7 @@ void GameRenderer::cleanup(VkDevice device) {
|
|||||||
|
|
||||||
void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) {
|
void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) {
|
||||||
const auto& mesh = meshCache.getMesh(id);
|
const auto& mesh = meshCache.getMesh(id);
|
||||||
// const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
||||||
assert(materialId != NULL_MATERIAL_ID);
|
assert(materialId != NULL_MATERIAL_ID);
|
||||||
|
|
||||||
meshDrawCommands.push_back(MeshDrawCommand{
|
meshDrawCommands.push_back(MeshDrawCommand{
|
||||||
@@ -117,6 +128,25 @@ void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID ma
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameRenderer::drawSkinnedMesh(MeshID id,
|
||||||
|
const glm::mat4& transform,
|
||||||
|
MaterialID materialId,
|
||||||
|
SkinnedMesh* skinnedMesh,
|
||||||
|
std::size_t jointMatricesStartIndex) {
|
||||||
|
const auto& mesh = meshCache.getMesh(id);
|
||||||
|
const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false);
|
||||||
|
assert(materialId != NULL_MATERIAL_ID);
|
||||||
|
assert(skinnedMesh != nullptr);
|
||||||
|
|
||||||
|
meshDrawCommands.push_back(MeshDrawCommand{
|
||||||
|
.meshId = id,
|
||||||
|
.transformMatrix = transform,
|
||||||
|
.materialId = materialId,
|
||||||
|
.skinnedMesh = skinnedMesh,
|
||||||
|
.jointMatricesStartIndex = static_cast<std::uint32_t>(jointMatricesStartIndex),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
|
const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
|
||||||
return gfx_device.getImage(drawImageId);
|
return gfx_device.getImage(drawImageId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,16 @@ void vkutil::addDebugLabel(VkDevice device, VkShaderModule shaderModule, const c
|
|||||||
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
|
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void vkutil::addDebugLabel(VkDevice device, VkPipeline pipeline, const char* label) {
|
||||||
|
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
|
||||||
|
.objectType = VK_OBJECT_TYPE_PIPELINE,
|
||||||
|
.objectHandle = (std::uint64_t)pipeline,
|
||||||
|
.pObjectName = label,
|
||||||
|
};
|
||||||
|
vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
|
||||||
|
}
|
||||||
|
|
||||||
void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
|
void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) {
|
||||||
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
|
const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{
|
||||||
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
|
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
|
||||||
|
|||||||
8
destrum/third_party/CMakeLists.txt
vendored
8
destrum/third_party/CMakeLists.txt
vendored
@@ -1,4 +1,5 @@
|
|||||||
# glm
|
# glm
|
||||||
|
set(BUILD_SHARED_LIBS OFF)
|
||||||
add_subdirectory(glm)
|
add_subdirectory(glm)
|
||||||
|
|
||||||
# stb
|
# stb
|
||||||
@@ -56,3 +57,10 @@ add_subdirectory(tinygltf)
|
|||||||
|
|
||||||
add_subdirectory(tinyexr)
|
add_subdirectory(tinyexr)
|
||||||
target_include_directories(tinyexr PUBLIC "${CMAKE_CURRENT_LIST_DIR}/tinyexr")
|
target_include_directories(tinyexr PUBLIC "${CMAKE_CURRENT_LIST_DIR}/tinyexr")
|
||||||
|
|
||||||
|
|
||||||
|
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
add_subdirectory(assimp)
|
||||||
|
|||||||
1
destrum/third_party/assimp
vendored
Submodule
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)
|
target_link_libraries(lightkeeper PRIVATE destrum::destrum)
|
||||||
|
|
||||||
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
#set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
||||||
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
#set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
||||||
set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game")
|
#set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets/game")
|
||||||
|
#
|
||||||
|
#add_custom_command(TARGET lightkeeper POST_BUILD
|
||||||
|
# COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}"
|
||||||
|
# COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}"
|
||||||
|
# COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}"
|
||||||
|
# VERBATIM
|
||||||
|
#)
|
||||||
|
|
||||||
|
set(ASSETS_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_src")
|
||||||
|
set(ASSETS_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/assets_runtime")
|
||||||
|
set(OUTPUT_GAME_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets/game")
|
||||||
|
|
||||||
add_custom_command(TARGET lightkeeper POST_BUILD
|
add_custom_command(TARGET lightkeeper POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_GAME_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/assets"
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${OUTPUT_GAME_ASSETS_DIR}"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${OUTPUT_GAME_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${CMAKE_CURRENT_BINARY_DIR}/assets/game"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "${ASSETS_RUNTIME_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/assets/game"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,5 +59,4 @@ add_custom_target(_internal_cook_game_assets ALL
|
|||||||
DEPENDS TheChef
|
DEPENDS TheChef
|
||||||
)
|
)
|
||||||
|
|
||||||
|
destrum_cook_engine_assets(lightkeeper "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
|
||||||
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/Components/OrbitAndSpin.h"
|
||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/Util/ModelLoader.h"
|
#include "destrum/Util/ModelLoader.h"
|
||||||
|
#include "destrum/Components/Animator.h"
|
||||||
|
|
||||||
LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) {
|
LightKeeper::LightKeeper(): App(), renderer(meshCache, materialCache) {
|
||||||
}
|
}
|
||||||
@@ -52,19 +53,19 @@ void LightKeeper::customInit() {
|
|||||||
const float orbitRadius = 5.0f;
|
const float orbitRadius = 5.0f;
|
||||||
|
|
||||||
for (int i = 0; i < count; ++i) {
|
for (int i = 0; i < count; ++i) {
|
||||||
auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
// auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
||||||
|
//
|
||||||
auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
// auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
||||||
childMeshComp->SetMeshID(testMeshID);
|
// childMeshComp->SetMeshID(testMeshID);
|
||||||
childMeshComp->SetMaterialID(testMaterialID);
|
// childMeshComp->SetMaterialID(testMaterialID);
|
||||||
|
//
|
||||||
childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
// childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
||||||
|
//
|
||||||
// Add orbit + self spin
|
// // Add orbit + self spin
|
||||||
auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
// auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
||||||
orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
// orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
||||||
|
//
|
||||||
scene.Add(childCube);
|
// scene.Add(childCube);
|
||||||
}
|
}
|
||||||
testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec
|
testCube->AddComponent<Spinner>(glm::vec3(0, 1, 0), glm::radians(10.0f)); // spin around Y, rad/sec
|
||||||
//rotate 180 around X axis
|
//rotate 180 around X axis
|
||||||
@@ -94,6 +95,68 @@ void LightKeeper::customInit() {
|
|||||||
skyboxCubemap->CreateCubeMap();
|
skyboxCubemap->CreateCubeMap();
|
||||||
|
|
||||||
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
|
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
|
||||||
|
|
||||||
|
|
||||||
|
const auto planeObj = std::make_shared<GameObject>("GroundPlane");
|
||||||
|
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
|
||||||
|
const auto planeModel = ModelLoader::LoadGLTF_CPUMeshes_MergedPerMesh(AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string());
|
||||||
|
const auto planeMeshID = meshCache.addMesh(gfxDevice, planeModel[0]);
|
||||||
|
|
||||||
|
const auto planeTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("game://grass.png"));
|
||||||
|
const auto planeMaterialID = materialCache.addMaterial(gfxDevice, {
|
||||||
|
.baseColor = glm::vec3(1.f),
|
||||||
|
.textureFilteringMode = TextureFilteringMode::Nearest,
|
||||||
|
.diffuseTexture = planeTextureID,
|
||||||
|
.name = "GroundPlaneMaterial",
|
||||||
|
});
|
||||||
|
planeMeshComp->SetMeshID(planeMeshID);
|
||||||
|
planeMeshComp->SetMaterialID(planeMaterialID);
|
||||||
|
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f));
|
||||||
|
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
|
||||||
|
scene.Add(planeObj);
|
||||||
|
|
||||||
|
|
||||||
|
// At the bottom of customInit(), replace the incomplete CharObj block:
|
||||||
|
|
||||||
|
const auto CharObj = std::make_shared<GameObject>("Character");
|
||||||
|
|
||||||
|
auto charModel = ModelLoader::LoadSkinnedModel(
|
||||||
|
AssetFS::GetInstance().GetFullPath("engine://char.fbx").generic_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto charMeshID = meshCache.addMesh(gfxDevice, charModel.meshes[0]);
|
||||||
|
|
||||||
|
const auto charTextureID = gfxDevice.loadImageFromFile(
|
||||||
|
AssetFS::GetInstance().GetFullPath("engine://char.jpg"));
|
||||||
|
const auto charMaterialID = materialCache.addMaterial(gfxDevice, {
|
||||||
|
.baseColor = glm::vec3(1.f),
|
||||||
|
.diffuseTexture = charTextureID,
|
||||||
|
.name = "CharacterMaterial",
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto charMeshComp = CharObj->AddComponent<MeshRendererComponent>();
|
||||||
|
charMeshComp->SetMeshID(charMeshID);
|
||||||
|
charMeshComp->SetMaterialID(charMaterialID);
|
||||||
|
|
||||||
|
|
||||||
|
const auto animator = CharObj->AddComponent<Animator>();
|
||||||
|
animator->setSkeleton(std::move(charModel.skeleton));
|
||||||
|
for (auto& clip : charModel.animations) {
|
||||||
|
animator->addClip(std::make_shared<SkeletalAnimation>(std::move(clip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& clip : charModel.animations)
|
||||||
|
spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration);
|
||||||
|
|
||||||
|
if (!charModel.animations.empty())
|
||||||
|
// animator->play(charModel.animations[0].name);
|
||||||
|
// animator->play("Armature|main");
|
||||||
|
animator->play("Armature|mixamo.com");
|
||||||
|
// or: animator->play("Run", 0.2f); // 0.2s cross-fade
|
||||||
|
|
||||||
|
CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f));
|
||||||
|
CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f);
|
||||||
|
scene.Add(CharObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LightKeeper::customUpdate(float dt) {
|
void LightKeeper::customUpdate(float dt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user