Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce55f75ac3 | ||
|
|
fa98bf08e8 |
+1
-1
@@ -9,5 +9,5 @@ lightkeeper/assets_runtime
|
|||||||
|
|
||||||
build/**
|
build/**
|
||||||
|
|
||||||
# Claude Code
|
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
docs/**
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ if (TARGET _internal_validate_engine_shaders)
|
|||||||
add_dependencies(ValidateShaders _internal_validate_engine_shaders)
|
add_dependencies(ValidateShaders _internal_validate_engine_shaders)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
destrum_enable_warnings(destrum_assets)
|
||||||
destrum_enable_warnings(destrum)
|
destrum_enable_warnings(destrum)
|
||||||
destrum_enable_warnings(lightkeeper)
|
destrum_enable_warnings(lightkeeper)
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule TheChef updated: 38230f5092...0273340e3b
+35
-14
@@ -1,10 +1,42 @@
|
|||||||
add_subdirectory(third_party)
|
add_subdirectory(third_party)
|
||||||
|
|
||||||
|
# CPU side asset types and the cooked model format, shared with TheChef (no Vulkan / SDL dependencies)
|
||||||
|
add_library(destrum_assets
|
||||||
|
"src/Assets/ModelFile.cpp"
|
||||||
|
"src/Assets/TextureFile.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(destrum::assets ALIAS destrum_assets)
|
||||||
|
|
||||||
|
set_target_properties(destrum_assets PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_EXTENSIONS OFF
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(destrum_assets PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||||
|
|
||||||
|
target_link_libraries(destrum_assets
|
||||||
|
PUBLIC
|
||||||
|
glm::glm
|
||||||
|
assimp
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(destrum_assets
|
||||||
|
PUBLIC
|
||||||
|
GLM_FORCE_CTOR_INIT
|
||||||
|
GLM_FORCE_XYZW_ONLY
|
||||||
|
GLM_FORCE_EXPLICIT_CTOR
|
||||||
|
GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
GLM_ENABLE_EXPERIMENTAL
|
||||||
|
)
|
||||||
|
|
||||||
set(SRC_FILES
|
set(SRC_FILES
|
||||||
"src/App.cpp"
|
"src/App.cpp"
|
||||||
"src/Event.cpp"
|
"src/Event.cpp"
|
||||||
|
|
||||||
"src/Components/Animator.cpp"
|
"src/Components/Animator.cpp"
|
||||||
|
"src/Components/CameraComponent.cpp"
|
||||||
"src/Components/LightComponent.cpp"
|
"src/Components/LightComponent.cpp"
|
||||||
"src/Components/MeshRendererComponent.cpp"
|
"src/Components/MeshRendererComponent.cpp"
|
||||||
"src/Components/Rotator.cpp"
|
"src/Components/Rotator.cpp"
|
||||||
@@ -100,6 +132,8 @@ target_include_directories(destrum PRIVATE
|
|||||||
|
|
||||||
target_link_libraries(destrum
|
target_link_libraries(destrum
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
destrum_assets
|
||||||
|
|
||||||
volk::volk_headers
|
volk::volk_headers
|
||||||
vk-bootstrap::vk-bootstrap
|
vk-bootstrap::vk-bootstrap
|
||||||
GPUOpen::VulkanMemoryAllocator
|
GPUOpen::VulkanMemoryAllocator
|
||||||
@@ -137,15 +171,6 @@ else ()
|
|||||||
target_link_libraries(destrum PUBLIC SDL2::SDL2-static)
|
target_link_libraries(destrum PUBLIC SDL2::SDL2-static)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
target_compile_definitions(destrum
|
|
||||||
PUBLIC
|
|
||||||
GLM_FORCE_CTOR_INIT
|
|
||||||
GLM_FORCE_XYZW_ONLY
|
|
||||||
GLM_FORCE_EXPLICIT_CTOR
|
|
||||||
GLM_FORCE_DEPTH_ZERO_TO_ONE
|
|
||||||
GLM_ENABLE_EXPERIMENTAL
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -169,10 +194,6 @@ add_custom_target(_internal_clean_engine_assets
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(_internal_cook_engine_assets
|
add_custom_target(_internal_cook_engine_assets
|
||||||
COMMAND TheChef
|
|
||||||
--input "${ASSETS_SRC_DIR}"
|
|
||||||
--output "${ASSETS_RUNTIME_DIR}"
|
|
||||||
--clean
|
|
||||||
COMMAND TheChef
|
COMMAND TheChef
|
||||||
--input "${ASSETS_SRC_DIR}"
|
--input "${ASSETS_SRC_DIR}"
|
||||||
--output "${ASSETS_RUNTIME_DIR}"
|
--output "${ASSETS_RUNTIME_DIR}"
|
||||||
@@ -229,7 +250,7 @@ function(destrum_cook_engine_assets GAME_TARGET GAME_OUTPUT_DIR)
|
|||||||
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
|
install(DIRECTORY "${_engine_runtime}/" DESTINATION "bin/assets/engine")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
install(TARGETS destrum
|
install(TARGETS destrum destrum_assets
|
||||||
ARCHIVE DESTINATION lib
|
ARCHIVE DESTINATION lib
|
||||||
LIBRARY DESTINATION lib
|
LIBRARY DESTINATION lib
|
||||||
RUNTIME DESTINATION bin)
|
RUNTIME DESTINATION bin)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
AssetManager() = default;
|
AssetManager() = default;
|
||||||
|
|
||||||
|
[[nodiscard]] ModelDoc::ModelAsset LoadCookedOrImport(
|
||||||
|
const std::string& reference,
|
||||||
|
const ModelDoc::LoadOptions& options) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DESTRUM_ASSETMANAGER_H
|
#endif // DESTRUM_ASSETMANAGER_H
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef DESTRUM_MODELFILE_H
|
||||||
|
#define DESTRUM_MODELFILE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <destrum/Util/ModelDoc.h>
|
||||||
|
|
||||||
|
namespace ModelFile {
|
||||||
|
inline constexpr std::uint32_t Version = 1;
|
||||||
|
inline constexpr std::string_view Extension = ".dmodel";
|
||||||
|
|
||||||
|
inline constexpr ModelDoc::LoadOptions CookOptions{};
|
||||||
|
|
||||||
|
[[nodiscard]] bool SupportsLoadOptions(const ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options);
|
||||||
|
void ApplyLoadOptions(ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<std::uint8_t> Write(const ModelDoc::ModelAsset& model);
|
||||||
|
[[nodiscard]] ModelDoc::ModelAsset Read(std::span<const std::uint8_t> bytes);
|
||||||
|
|
||||||
|
void WriteToFile(const ModelDoc::ModelAsset& model, const std::filesystem::path& path);
|
||||||
|
[[nodiscard]] ModelDoc::ModelAsset ReadFromFile(const std::filesystem::path& path);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::uint32_t> ReadVersionFromFile(const std::filesystem::path& path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_MODELFILE_H
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef DESTRUM_TEXTUREFILE_H
|
||||||
|
#define DESTRUM_TEXTUREFILE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace TextureFile {
|
||||||
|
inline constexpr std::uint32_t Version = 1;
|
||||||
|
inline constexpr std::string_view Extension = ".dtex";
|
||||||
|
|
||||||
|
enum class PixelFormat : std::uint32_t {
|
||||||
|
RGBA16Float,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Texture {
|
||||||
|
std::uint32_t width{0};
|
||||||
|
std::uint32_t height{0};
|
||||||
|
PixelFormat format{PixelFormat::RGBA16Float};
|
||||||
|
std::vector<std::uint8_t> pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t BytesPerPixel(PixelFormat format);
|
||||||
|
|
||||||
|
[[nodiscard]] Texture FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba);
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<std::uint8_t> Write(const Texture& texture);
|
||||||
|
[[nodiscard]] Texture Read(std::span<const std::uint8_t> bytes);
|
||||||
|
|
||||||
|
void WriteToFile(const Texture& texture, const std::filesystem::path& path);
|
||||||
|
[[nodiscard]] Texture ReadFromFile(const std::filesystem::path& path);
|
||||||
|
[[nodiscard]] std::optional<std::uint32_t> ReadVersionFromFile(const std::filesystem::path& path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_TEXTUREFILE_H
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#ifndef CAMERACOMPONENT_H
|
||||||
|
#define CAMERACOMPONENT_H
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
#include <destrum/Graphics/Camera.h>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
class CameraComponent final : public Component {
|
||||||
|
public:
|
||||||
|
explicit CameraComponent(GameObject& parent);
|
||||||
|
|
||||||
|
CameraComponent(const CameraComponent& other) = delete;
|
||||||
|
CameraComponent(CameraComponent&& other) noexcept = delete;
|
||||||
|
CameraComponent& operator=(const CameraComponent& other) = delete;
|
||||||
|
CameraComponent& operator=(CameraComponent&& other) noexcept = delete;
|
||||||
|
|
||||||
|
std::string GetTypeName() const override { return "CameraComponent"; }
|
||||||
|
|
||||||
|
nlohmann::json Serialize() const override;
|
||||||
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
|
// Places the camera at this object's world transform, looking down its local -Z.
|
||||||
|
[[nodiscard]] Camera& GetCamera(float aspectRatio);
|
||||||
|
|
||||||
|
void SetFieldOfView(float fovDegrees);
|
||||||
|
[[nodiscard]] float GetFieldOfView() const { return m_FieldOfView; }
|
||||||
|
|
||||||
|
void SetClipPlanes(float zNear, float zFar);
|
||||||
|
[[nodiscard]] float GetZNear() const { return m_ZNear; }
|
||||||
|
[[nodiscard]] float GetZFar() const { return m_ZFar; }
|
||||||
|
|
||||||
|
void MakeCurrent();
|
||||||
|
[[nodiscard]] bool IsCurrent() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Camera m_Camera{glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}};
|
||||||
|
float m_FieldOfView{90.0f};
|
||||||
|
float m_ZNear{0.01f};
|
||||||
|
float m_ZFar{1000.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CAMERACOMPONENT_H
|
||||||
@@ -101,6 +101,7 @@ public:
|
|||||||
float GetZFar() const { return m_zFar; }
|
float GetZFar() const { return m_zFar; }
|
||||||
void SetZFar(float zFar) { m_zFar = zFar; }
|
void SetZFar(float zFar) { m_zFar = zFar; }
|
||||||
float GetFOVY() const { return fovAngle; }
|
float GetFOVY() const { return fovAngle; }
|
||||||
|
void SetFOVY(float fovDegrees) { fovAngle = fovDegrees; }
|
||||||
float GetAspectRatio() const { return m_aspectRatio; }
|
float GetAspectRatio() const { return m_aspectRatio; }
|
||||||
|
|
||||||
glm::vec3 m_position{2.f, 0, 0};
|
glm::vec3 m_position{2.f, 0, 0};
|
||||||
@@ -127,6 +128,7 @@ public:
|
|||||||
|
|
||||||
void SetRotation(float yawRadians, float pitchRadians);
|
void SetRotation(float yawRadians, float pitchRadians);
|
||||||
void SetRotation(const glm::vec2& yawPitchRadians);
|
void SetRotation(const glm::vec2& yawPitchRadians);
|
||||||
|
void SetOrientation(const glm::vec3& forward, const glm::vec3& up);
|
||||||
|
|
||||||
float GetYaw() const { return m_yaw; }
|
float GetYaw() const { return m_yaw; }
|
||||||
float GetPitch() const { return m_pitch; }
|
float GetPitch() const { return m_pitch; }
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef CPUMESH_H
|
||||||
|
#define CPUMESH_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/vec3.hpp>
|
||||||
|
#include <glm/vec4.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Skeleton.h>
|
||||||
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
|
||||||
|
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
|
||||||
|
struct CPUMesh {
|
||||||
|
struct Vertex {
|
||||||
|
glm::vec3 position;
|
||||||
|
float uv_x{};
|
||||||
|
glm::vec3 normal;
|
||||||
|
float uv_y{};
|
||||||
|
glm::vec4 tangent;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
AssetReference assetReference;
|
||||||
|
|
||||||
|
glm::vec3 minPos;
|
||||||
|
glm::vec3 maxPos;
|
||||||
|
|
||||||
|
std::optional<Skeleton> skeleton; // present only for skinned meshes
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CPUMESH_H
|
||||||
@@ -13,37 +13,8 @@
|
|||||||
#include <glm/mat4x4.hpp>
|
#include <glm/mat4x4.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Resources/Buffer.h>
|
#include <destrum/Graphics/Resources/Buffer.h>
|
||||||
|
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||||
#include <destrum/Graphics/Frustum.h>
|
#include <destrum/Graphics/Frustum.h>
|
||||||
#include <destrum/Graphics/Skeleton.h>
|
|
||||||
#include <destrum/Assets/AssetReference.h>
|
|
||||||
|
|
||||||
// ─── CPU Mesh ─────────────────────────────────────────────────────────────────
|
|
||||||
struct CPUMesh {
|
|
||||||
struct Vertex {
|
|
||||||
glm::vec3 position;
|
|
||||||
float uv_x{};
|
|
||||||
glm::vec3 normal;
|
|
||||||
float uv_y{};
|
|
||||||
glm::vec4 tangent;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
AssetReference assetReference;
|
|
||||||
|
|
||||||
glm::vec3 minPos;
|
|
||||||
glm::vec3 maxPos;
|
|
||||||
|
|
||||||
std::optional<Skeleton> skeleton; // present only for skinned meshes
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
|
// ─── GPU Mesh ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
#include "destrum/Graphics/TextureIntent.h"
|
#include "destrum/Graphics/TextureIntent.h"
|
||||||
@@ -19,11 +20,12 @@ struct ImageData {
|
|||||||
if (this == &o) return *this;
|
if (this == &o) return *this;
|
||||||
|
|
||||||
// free current contents first
|
// free current contents first
|
||||||
this->~ImageData();
|
freePixels();
|
||||||
|
|
||||||
// steal
|
// steal
|
||||||
pixels = o.pixels;
|
pixels = o.pixels;
|
||||||
hdrPixels = o.hdrPixels;
|
hdrPixels = o.hdrPixels;
|
||||||
|
cookedPixels = std::move(o.cookedPixels);
|
||||||
hdr = o.hdr;
|
hdr = o.hdr;
|
||||||
width = o.width;
|
width = o.width;
|
||||||
height = o.height;
|
height = o.height;
|
||||||
@@ -50,8 +52,11 @@ struct ImageData {
|
|||||||
ImageData(const ImageData&) = delete;
|
ImageData(const ImageData&) = delete;
|
||||||
ImageData& operator=(const ImageData&) = delete;
|
ImageData& operator=(const ImageData&) = delete;
|
||||||
|
|
||||||
|
void freePixels();
|
||||||
|
|
||||||
unsigned char* pixels{nullptr};
|
unsigned char* pixels{nullptr};
|
||||||
float* hdrPixels{nullptr};
|
float* hdrPixels{nullptr};
|
||||||
|
std::vector<unsigned char> cookedPixels; // owns `pixels` for cooked textures
|
||||||
bool hdr{false};
|
bool hdr{false};
|
||||||
|
|
||||||
int width{0};
|
int width{0};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "destrum/Physics/JoltPhysicsWorld.h"
|
#include "destrum/Physics/JoltPhysicsWorld.h"
|
||||||
#include "destrum/Physics/PhysicsSceneBridge.h"
|
#include "destrum/Physics/PhysicsSceneBridge.h"
|
||||||
|
|
||||||
|
class CameraComponent;
|
||||||
class GameObject;
|
class GameObject;
|
||||||
class SceneSerializer;
|
class SceneSerializer;
|
||||||
class PhysicsWorld;
|
class PhysicsWorld;
|
||||||
@@ -105,6 +106,10 @@ public:
|
|||||||
Event<> OnSceneLoaded;
|
Event<> OnSceneLoaded;
|
||||||
|
|
||||||
PhysicsSceneBridge& GetPhysics() { return m_Physics; }
|
PhysicsSceneBridge& GetPhysics() { return m_Physics; }
|
||||||
|
|
||||||
|
[[nodiscard]] CameraComponent* GetCurrentCamera() const;
|
||||||
|
[[nodiscard]] std::vector<CameraComponent*> GetCameras() const;
|
||||||
|
void SetCurrentCamera(const CameraComponent* camera);
|
||||||
private:
|
private:
|
||||||
explicit Scene(const std::string& name);
|
explicit Scene(const std::string& name);
|
||||||
explicit Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld);
|
explicit Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld);
|
||||||
@@ -127,6 +132,8 @@ private:
|
|||||||
bool m_ShowDemoWindow{false};
|
bool m_ShowDemoWindow{false};
|
||||||
ObjectId m_SelectedObjectId{InvalidObjectId};
|
ObjectId m_SelectedObjectId{InvalidObjectId};
|
||||||
|
|
||||||
|
ObjectId m_CurrentCameraId{InvalidObjectId};
|
||||||
|
|
||||||
std::function<void()> m_registerBindings;
|
std::function<void()> m_registerBindings;
|
||||||
std::function<void()> m_unregisterBindings;
|
std::function<void()> m_unregisterBindings;
|
||||||
bool m_bindingsUnloaded{false};
|
bool m_bindingsUnloaded{false};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define DESTRUM_ENGINECOMPONENTLIST_H
|
#define DESTRUM_ENGINECOMPONENTLIST_H
|
||||||
|
|
||||||
#include <destrum/Components/Animator.h>
|
#include <destrum/Components/Animator.h>
|
||||||
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/Components/LightComponent.h>
|
#include <destrum/Components/LightComponent.h>
|
||||||
#include <destrum/Components/MeshRendererComponent.h>
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
#include <destrum/Components/OrbitAndSpin.h>
|
#include <destrum/Components/OrbitAndSpin.h>
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
#define DESTRUM_ENGINE_COMPONENTS(X) \
|
#define DESTRUM_ENGINE_COMPONENTS(X) \
|
||||||
X(MeshRendererComponent) \
|
X(MeshRendererComponent) \
|
||||||
X(LightComponent) \
|
X(LightComponent) \
|
||||||
|
X(CameraComponent) \
|
||||||
X(Rotator) \
|
X(Rotator) \
|
||||||
X(Spinner) \
|
X(Spinner) \
|
||||||
X(OrbitAndSpin) \
|
X(OrbitAndSpin) \
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef DESTRUM_IMGUIUTILS_H
|
#ifndef DESTRUM_IMGUIUTILS_H
|
||||||
#define DESTRUM_IMGUIUTILS_H
|
#define DESTRUM_IMGUIUTILS_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -22,6 +23,7 @@ namespace ImGuiUtils {
|
|||||||
const std::vector<std::shared_ptr<GameObject>>& objects,
|
const std::vector<std::shared_ptr<GameObject>>& objects,
|
||||||
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||||
ObjectId& selectedObjectId,
|
ObjectId& selectedObjectId,
|
||||||
|
const std::function<GameObject*()>& createGameObject,
|
||||||
bool* open = nullptr);
|
bool* open = nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Resources/Mesh.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||||
#include <destrum/Assets/AssetReference.h>
|
#include <destrum/Assets/AssetReference.h>
|
||||||
#include <destrum/Graphics/SkeletalAnimation.h>
|
#include <destrum/Graphics/SkeletalAnimation.h>
|
||||||
#include <destrum/Graphics/Skeleton.h>
|
#include <destrum/Graphics/Skeleton.h>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include <destrum/Assets/AssetManager.h>
|
#include <destrum/Assets/AssetManager.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include <destrum/Assets/ModelFile.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -11,6 +13,34 @@ namespace {
|
|||||||
}
|
}
|
||||||
return std::string{reference};
|
return std::string{reference};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::filesystem::path> FindCookedModel(const std::string& reference) {
|
||||||
|
if (reference.find("://") == std::string::npos) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const std::filesystem::path cookedPath = AssetFS::GetInstance().GetCookedPathForFile(reference);
|
||||||
|
if (cookedPath.extension() == ModelFile::Extension) {
|
||||||
|
return cookedPath;
|
||||||
|
}
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::warn("Importing {} instead of using its cooked model: {}", reference, exception.what());
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooked texture paths are relative to the model file, imported ones are absolute
|
||||||
|
void ResolveTexturePaths(ModelDoc::ModelAsset& model, const std::filesystem::path& modelDir) {
|
||||||
|
for (ModelDoc::MaterialInfo& material : model.materials) {
|
||||||
|
for (ModelDoc::TextureInfo& texture : material.textures) {
|
||||||
|
if (texture.embedded || texture.path.empty() || std::filesystem::path{texture.path}.is_absolute()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
texture.path = (modelDir / texture.path).lexically_normal().generic_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path AssetManager::Resolve(std::string_view reference) const {
|
std::filesystem::path AssetManager::Resolve(std::string_view reference) const {
|
||||||
@@ -32,9 +62,7 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
|||||||
const ModelDoc::LoadOptions& options) const
|
const ModelDoc::LoadOptions& options) const
|
||||||
{
|
{
|
||||||
const std::string sourceReference = ToReference(reference);
|
const std::string sourceReference = ToReference(reference);
|
||||||
ModelDoc::ModelAsset model = ModelDoc::LoadModel(
|
ModelDoc::ModelAsset model = LoadCookedOrImport(sourceReference, options);
|
||||||
Resolve(sourceReference).generic_string(),
|
|
||||||
options);
|
|
||||||
|
|
||||||
model.sourcePath = sourceReference;
|
model.sourcePath = sourceReference;
|
||||||
model.skeleton.assetReference.path = sourceReference;
|
model.skeleton.assetReference.path = sourceReference;
|
||||||
@@ -44,6 +72,9 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
|||||||
primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex
|
primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex
|
||||||
? "mesh:" + std::to_string(primitive.sourceMeshIndex)
|
? "mesh:" + std::to_string(primitive.sourceMeshIndex)
|
||||||
: "primitive:" + std::to_string(index);
|
: "primitive:" + std::to_string(index);
|
||||||
|
if (primitive.mesh.skeleton) {
|
||||||
|
primitive.mesh.skeleton->assetReference.path = sourceReference;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (std::size_t index = 0; index < model.materials.size(); ++index) {
|
for (std::size_t index = 0; index < model.materials.size(); ++index) {
|
||||||
auto& material = model.materials[index];
|
auto& material = model.materials[index];
|
||||||
@@ -59,6 +90,24 @@ ModelDoc::ModelAsset AssetManager::LoadModel(
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset AssetManager::LoadCookedOrImport(
|
||||||
|
const std::string& reference,
|
||||||
|
const ModelDoc::LoadOptions& options) const
|
||||||
|
{
|
||||||
|
if (const auto cookedPath = FindCookedModel(reference)) {
|
||||||
|
ModelDoc::ModelAsset model = ModelFile::ReadFromFile(*cookedPath);
|
||||||
|
if (ModelFile::SupportsLoadOptions(model, options)) {
|
||||||
|
ModelFile::ApplyLoadOptions(model, options);
|
||||||
|
ResolveTexturePaths(model, cookedPath->parent_path());
|
||||||
|
spdlog::debug("Loaded cooked model {}", reference);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
spdlog::debug("Importing {}, its cooked model doesn't match the requested load options", reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModelDoc::LoadModel(Resolve(reference).generic_string(), options);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
std::vector<SkeletalAnimation> AssetManager::LoadAnimationClips(
|
||||||
std::string_view reference,
|
std::string_view reference,
|
||||||
const Skeleton& targetSkeleton) const
|
const Skeleton& targetSkeleton) const
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#ifndef DESTRUM_BINARYSTREAM_H
|
||||||
|
#define DESTRUM_BINARYSTREAM_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <bit>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace AssetBinary {
|
||||||
|
static_assert(std::endian::native == std::endian::little, "Cooked assets are stored little endian");
|
||||||
|
|
||||||
|
using Magic = std::array<char, 4>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept RawCopyable = std::is_trivially_copyable_v<T>;
|
||||||
|
|
||||||
|
class BinaryWriter {
|
||||||
|
public:
|
||||||
|
template <RawCopyable T>
|
||||||
|
void Write(const T& value) {
|
||||||
|
const auto* bytes = reinterpret_cast<const std::uint8_t*>(&value);
|
||||||
|
m_Bytes.insert(m_Bytes.end(), bytes, bytes + sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <RawCopyable T>
|
||||||
|
void WriteArray(const std::vector<T>& values) {
|
||||||
|
WriteCount(values.size());
|
||||||
|
const auto* bytes = reinterpret_cast<const std::uint8_t*>(values.data());
|
||||||
|
m_Bytes.insert(m_Bytes.end(), bytes, bytes + values.size() * sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteCount(std::size_t count) {
|
||||||
|
Write(static_cast<std::uint64_t>(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteBool(bool value) {
|
||||||
|
Write(static_cast<std::uint8_t>(value ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteString(std::string_view value) {
|
||||||
|
WriteCount(value.size());
|
||||||
|
m_Bytes.insert(m_Bytes.end(), value.begin(), value.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteStrings(const std::vector<std::string>& values) {
|
||||||
|
WriteCount(values.size());
|
||||||
|
for (const std::string& value : values) {
|
||||||
|
WriteString(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteHeader(const Magic& magic, std::uint32_t version) {
|
||||||
|
Write(magic);
|
||||||
|
Write(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<std::uint8_t> TakeBytes() {
|
||||||
|
return std::move(m_Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::uint8_t> m_Bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BinaryReader {
|
||||||
|
public:
|
||||||
|
explicit BinaryReader(std::span<const std::uint8_t> bytes)
|
||||||
|
: m_Bytes(bytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
template <RawCopyable T>
|
||||||
|
[[nodiscard]] T Read() {
|
||||||
|
T value{};
|
||||||
|
std::memcpy(&value, Take(sizeof(T)).data(), sizeof(T));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <RawCopyable T>
|
||||||
|
[[nodiscard]] std::vector<T> ReadArray() {
|
||||||
|
const std::size_t count = ReadCount(sizeof(T));
|
||||||
|
std::vector<T> values(count);
|
||||||
|
if (count > 0) {
|
||||||
|
std::memcpy(values.data(), Take(count * sizeof(T)).data(), count * sizeof(T));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::size_t ReadCount(std::size_t minElementSize) {
|
||||||
|
const auto count = Read<std::uint64_t>();
|
||||||
|
if (count > Remaining() / minElementSize) {
|
||||||
|
throw std::runtime_error("Cooked asset file is corrupt: element count is out of range");
|
||||||
|
}
|
||||||
|
return static_cast<std::size_t>(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool ReadBool() {
|
||||||
|
return Read<std::uint8_t>() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::string ReadString() {
|
||||||
|
const std::size_t length = ReadCount(1);
|
||||||
|
const auto bytes = Take(length);
|
||||||
|
return std::string(bytes.begin(), bytes.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<std::string> ReadStrings() {
|
||||||
|
std::vector<std::string> values(ReadCount(sizeof(std::uint64_t)));
|
||||||
|
for (std::string& value : values) {
|
||||||
|
value = ReadString();
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadHeader(const Magic& magic, std::uint32_t version, std::string_view kind) {
|
||||||
|
if (Read<Magic>() != magic) {
|
||||||
|
throw std::runtime_error("Not a cooked " + std::string{kind} + " file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fileVersion = Read<std::uint32_t>();
|
||||||
|
if (fileVersion != version) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Cooked " + std::string{kind} + " file has version " + std::to_string(fileVersion) +
|
||||||
|
", expected " + std::to_string(version) + " (recook the asset)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectEnd(std::string_view kind) const {
|
||||||
|
if (m_Offset != m_Bytes.size()) {
|
||||||
|
throw std::runtime_error("Cooked " + std::string{kind} + " file has unexpected data at the end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] std::size_t Remaining() const {
|
||||||
|
return m_Bytes.size() - m_Offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const std::uint8_t> Take(std::size_t size) {
|
||||||
|
if (size > Remaining()) {
|
||||||
|
throw std::runtime_error("Cooked asset file is truncated");
|
||||||
|
}
|
||||||
|
const auto bytes = m_Bytes.subspan(m_Offset, size);
|
||||||
|
m_Offset += size;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::uint8_t> m_Bytes;
|
||||||
|
std::size_t m_Offset{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void WriteAllBytes(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
|
||||||
|
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open file for writing: " + path.string());
|
||||||
|
}
|
||||||
|
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to write file: " + path.string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::vector<std::uint8_t> ReadAllBytes(const std::filesystem::path& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(file.tellg()));
|
||||||
|
file.seekg(0);
|
||||||
|
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read file: " + path.string());
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::optional<std::uint32_t> ReadVersion(const std::filesystem::path& path, const Magic& magic) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<std::uint8_t, sizeof(Magic) + sizeof(std::uint32_t)> header{};
|
||||||
|
file.read(reinterpret_cast<char*>(header.data()), static_cast<std::streamsize>(header.size()));
|
||||||
|
if (!file) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryReader reader{header};
|
||||||
|
if (reader.Read<Magic>() != magic) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return reader.Read<std::uint32_t>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_BINARYSTREAM_H
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
#include <destrum/Assets/ModelFile.h>
|
||||||
|
|
||||||
|
#include "BinaryStream.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using AssetBinary::BinaryReader;
|
||||||
|
using AssetBinary::BinaryWriter;
|
||||||
|
|
||||||
|
static_assert(sizeof(CPUMesh::Vertex) == 48);
|
||||||
|
static_assert(sizeof(CPUMesh::SkinningData) == 32);
|
||||||
|
static_assert(sizeof(SkeletalAnimation::Keyframe) == 44);
|
||||||
|
static_assert(sizeof(glm::mat4) == 64);
|
||||||
|
|
||||||
|
constexpr AssetBinary::Magic Magic{'D', 'M', 'D', 'L'};
|
||||||
|
constexpr std::string_view Kind = "model";
|
||||||
|
|
||||||
|
void WriteAssetReference(BinaryWriter& writer, const AssetReference& reference) {
|
||||||
|
writer.WriteString(reference.path);
|
||||||
|
writer.WriteString(reference.subresource);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetReference ReadAssetReference(BinaryReader& reader) {
|
||||||
|
AssetReference reference;
|
||||||
|
reference.path = reader.ReadString();
|
||||||
|
reference.subresource = reader.ReadString();
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteSkeleton(BinaryWriter& writer, const Skeleton& skeleton) {
|
||||||
|
WriteAssetReference(writer, skeleton.assetReference);
|
||||||
|
|
||||||
|
writer.WriteCount(skeleton.hierarchy.size());
|
||||||
|
for (const Skeleton::JointNode& node : skeleton.hierarchy) {
|
||||||
|
writer.Write(node.id);
|
||||||
|
writer.WriteArray(node.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteArray(skeleton.inverseBindMatrices);
|
||||||
|
|
||||||
|
writer.WriteCount(skeleton.joints.size());
|
||||||
|
for (const Joint& joint : skeleton.joints) {
|
||||||
|
writer.Write(joint.id);
|
||||||
|
writer.Write(joint.localTranslation);
|
||||||
|
writer.Write(joint.localRotation);
|
||||||
|
writer.Write(joint.localScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteStrings(skeleton.jointNames);
|
||||||
|
writer.WriteArray(skeleton.parentIndex);
|
||||||
|
writer.Write(skeleton.rootPreTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
Skeleton ReadSkeleton(BinaryReader& reader) {
|
||||||
|
Skeleton skeleton;
|
||||||
|
skeleton.assetReference = ReadAssetReference(reader);
|
||||||
|
|
||||||
|
skeleton.hierarchy.resize(reader.ReadCount(sizeof(JointId)));
|
||||||
|
for (Skeleton::JointNode& node : skeleton.hierarchy) {
|
||||||
|
node.id = reader.Read<JointId>();
|
||||||
|
node.children = reader.ReadArray<JointId>();
|
||||||
|
}
|
||||||
|
|
||||||
|
skeleton.inverseBindMatrices = reader.ReadArray<glm::mat4>();
|
||||||
|
|
||||||
|
skeleton.joints.resize(reader.ReadCount(sizeof(JointId)));
|
||||||
|
for (Joint& joint : skeleton.joints) {
|
||||||
|
joint.id = reader.Read<JointId>();
|
||||||
|
joint.localTranslation = reader.Read<glm::vec3>();
|
||||||
|
joint.localRotation = reader.Read<glm::quat>();
|
||||||
|
joint.localScale = reader.Read<glm::vec3>();
|
||||||
|
}
|
||||||
|
|
||||||
|
skeleton.jointNames = reader.ReadStrings();
|
||||||
|
skeleton.parentIndex = reader.ReadArray<int>();
|
||||||
|
skeleton.rootPreTransform = reader.Read<glm::mat4>();
|
||||||
|
return skeleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteMesh(BinaryWriter& writer, const CPUMesh& mesh) {
|
||||||
|
writer.WriteArray(mesh.indices);
|
||||||
|
writer.WriteArray(mesh.vertices);
|
||||||
|
writer.WriteArray(mesh.skinningData);
|
||||||
|
writer.WriteString(mesh.name);
|
||||||
|
WriteAssetReference(writer, mesh.assetReference);
|
||||||
|
writer.Write(mesh.minPos);
|
||||||
|
writer.Write(mesh.maxPos);
|
||||||
|
|
||||||
|
writer.WriteBool(mesh.skeleton.has_value());
|
||||||
|
if (mesh.skeleton) {
|
||||||
|
WriteSkeleton(writer, *mesh.skeleton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CPUMesh ReadMesh(BinaryReader& reader) {
|
||||||
|
CPUMesh mesh;
|
||||||
|
mesh.indices = reader.ReadArray<std::uint32_t>();
|
||||||
|
mesh.vertices = reader.ReadArray<CPUMesh::Vertex>();
|
||||||
|
mesh.skinningData = reader.ReadArray<CPUMesh::SkinningData>();
|
||||||
|
mesh.name = reader.ReadString();
|
||||||
|
mesh.assetReference = ReadAssetReference(reader);
|
||||||
|
mesh.minPos = reader.Read<glm::vec3>();
|
||||||
|
mesh.maxPos = reader.Read<glm::vec3>();
|
||||||
|
|
||||||
|
if (reader.ReadBool()) {
|
||||||
|
mesh.skeleton = ReadSkeleton(reader);
|
||||||
|
}
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePrimitive(BinaryWriter& writer, const ModelDoc::MeshPrimitive& primitive) {
|
||||||
|
WriteMesh(writer, primitive.mesh);
|
||||||
|
writer.Write(primitive.materialIndex);
|
||||||
|
writer.WriteString(primitive.nodeName);
|
||||||
|
writer.Write(primitive.sourceMeshIndex);
|
||||||
|
writer.WriteBool(primitive.skinned);
|
||||||
|
writer.Write(primitive.nodeWorldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::MeshPrimitive ReadPrimitive(BinaryReader& reader) {
|
||||||
|
ModelDoc::MeshPrimitive primitive;
|
||||||
|
primitive.mesh = ReadMesh(reader);
|
||||||
|
primitive.materialIndex = reader.Read<std::uint32_t>();
|
||||||
|
primitive.nodeName = reader.ReadString();
|
||||||
|
primitive.sourceMeshIndex = reader.Read<std::uint32_t>();
|
||||||
|
primitive.skinned = reader.ReadBool();
|
||||||
|
primitive.nodeWorldTransform = reader.Read<glm::mat4>();
|
||||||
|
return primitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteTexture(BinaryWriter& writer, const ModelDoc::TextureInfo& texture) {
|
||||||
|
writer.Write(static_cast<std::uint32_t>(texture.semantic));
|
||||||
|
writer.WriteString(texture.rawPath);
|
||||||
|
writer.WriteString(texture.path);
|
||||||
|
writer.Write(static_cast<std::uint32_t>(texture.assimpType));
|
||||||
|
writer.Write(texture.slot);
|
||||||
|
writer.WriteBool(texture.embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::TextureInfo ReadTexture(BinaryReader& reader) {
|
||||||
|
ModelDoc::TextureInfo texture;
|
||||||
|
texture.semantic = static_cast<ModelDoc::TextureSemantic>(reader.Read<std::uint32_t>());
|
||||||
|
texture.rawPath = reader.ReadString();
|
||||||
|
texture.path = reader.ReadString();
|
||||||
|
texture.assimpType = static_cast<aiTextureType>(reader.Read<std::uint32_t>());
|
||||||
|
texture.slot = reader.Read<std::uint32_t>();
|
||||||
|
texture.embedded = reader.ReadBool();
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteMaterial(BinaryWriter& writer, const ModelDoc::MaterialInfo& material) {
|
||||||
|
writer.WriteString(material.name);
|
||||||
|
WriteAssetReference(writer, material.assetReference);
|
||||||
|
writer.Write(material.baseColor);
|
||||||
|
writer.Write(material.emissiveColor);
|
||||||
|
writer.Write(material.metallicFactor);
|
||||||
|
writer.Write(material.roughnessFactor);
|
||||||
|
writer.Write(material.opacity);
|
||||||
|
|
||||||
|
writer.WriteCount(material.textures.size());
|
||||||
|
for (const ModelDoc::TextureInfo& texture : material.textures) {
|
||||||
|
WriteTexture(writer, texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::MaterialInfo ReadMaterial(BinaryReader& reader) {
|
||||||
|
ModelDoc::MaterialInfo material;
|
||||||
|
material.name = reader.ReadString();
|
||||||
|
material.assetReference = ReadAssetReference(reader);
|
||||||
|
material.baseColor = reader.Read<glm::vec4>();
|
||||||
|
material.emissiveColor = reader.Read<glm::vec3>();
|
||||||
|
material.metallicFactor = reader.Read<float>();
|
||||||
|
material.roughnessFactor = reader.Read<float>();
|
||||||
|
material.opacity = reader.Read<float>();
|
||||||
|
|
||||||
|
material.textures.resize(reader.ReadCount(1));
|
||||||
|
for (ModelDoc::TextureInfo& texture : material.textures) {
|
||||||
|
texture = ReadTexture(reader);
|
||||||
|
}
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteAnimation(BinaryWriter& writer, const SkeletalAnimation& animation) {
|
||||||
|
WriteAssetReference(writer, animation.assetReference);
|
||||||
|
|
||||||
|
writer.WriteCount(animation.tracks.size());
|
||||||
|
for (const SkeletalAnimation::Track& track : animation.tracks) {
|
||||||
|
writer.Write(track.jointIndex);
|
||||||
|
writer.WriteArray(track.keyframes);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(animation.duration);
|
||||||
|
writer.WriteBool(animation.looped);
|
||||||
|
writer.WriteString(animation.name);
|
||||||
|
writer.Write(animation.startFrame);
|
||||||
|
|
||||||
|
writer.WriteCount(animation.events.size());
|
||||||
|
for (const auto& [frame, events] : animation.events) {
|
||||||
|
writer.Write(frame);
|
||||||
|
writer.WriteStrings(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SkeletalAnimation ReadAnimation(BinaryReader& reader) {
|
||||||
|
SkeletalAnimation animation;
|
||||||
|
animation.assetReference = ReadAssetReference(reader);
|
||||||
|
|
||||||
|
animation.tracks.resize(reader.ReadCount(sizeof(std::uint32_t)));
|
||||||
|
for (SkeletalAnimation::Track& track : animation.tracks) {
|
||||||
|
track.jointIndex = reader.Read<std::uint32_t>();
|
||||||
|
track.keyframes = reader.ReadArray<SkeletalAnimation::Keyframe>();
|
||||||
|
}
|
||||||
|
|
||||||
|
animation.duration = reader.Read<float>();
|
||||||
|
animation.looped = reader.ReadBool();
|
||||||
|
animation.name = reader.ReadString();
|
||||||
|
animation.startFrame = reader.Read<int>();
|
||||||
|
|
||||||
|
const std::size_t eventFrameCount = reader.ReadCount(sizeof(int));
|
||||||
|
for (std::size_t i = 0; i < eventFrameCount; ++i) {
|
||||||
|
const int frame = reader.Read<int>();
|
||||||
|
animation.events[frame] = reader.ReadStrings();
|
||||||
|
}
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> ModelFile::Write(const ModelDoc::ModelAsset& model) {
|
||||||
|
BinaryWriter writer;
|
||||||
|
writer.WriteHeader(Magic, Version);
|
||||||
|
|
||||||
|
writer.WriteString(model.sourcePath);
|
||||||
|
|
||||||
|
writer.WriteCount(model.primitives.size());
|
||||||
|
for (const ModelDoc::MeshPrimitive& primitive : model.primitives) {
|
||||||
|
WritePrimitive(writer, primitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteCount(model.materials.size());
|
||||||
|
for (const ModelDoc::MaterialInfo& material : model.materials) {
|
||||||
|
WriteMaterial(writer, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSkeleton(writer, model.skeleton);
|
||||||
|
|
||||||
|
writer.WriteCount(model.animations.size());
|
||||||
|
for (const SkeletalAnimation& animation : model.animations) {
|
||||||
|
WriteAnimation(writer, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.TakeBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset ModelFile::Read(std::span<const std::uint8_t> bytes) {
|
||||||
|
BinaryReader reader{bytes};
|
||||||
|
reader.ReadHeader(Magic, Version, Kind);
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset model;
|
||||||
|
model.sourcePath = reader.ReadString();
|
||||||
|
|
||||||
|
model.primitives.resize(reader.ReadCount(1));
|
||||||
|
for (ModelDoc::MeshPrimitive& primitive : model.primitives) {
|
||||||
|
primitive = ReadPrimitive(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.materials.resize(reader.ReadCount(1));
|
||||||
|
for (ModelDoc::MaterialInfo& material : model.materials) {
|
||||||
|
material = ReadMaterial(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.skeleton = ReadSkeleton(reader);
|
||||||
|
|
||||||
|
model.animations.resize(reader.ReadCount(1));
|
||||||
|
for (SkeletalAnimation& animation : model.animations) {
|
||||||
|
animation = ReadAnimation(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.ExpectEnd(Kind);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModelFile::WriteToFile(const ModelDoc::ModelAsset& model, const std::filesystem::path& path) {
|
||||||
|
AssetBinary::WriteAllBytes(path, Write(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset ModelFile::ReadFromFile(const std::filesystem::path& path) {
|
||||||
|
return Read(AssetBinary::ReadAllBytes(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::uint32_t> ModelFile::ReadVersionFromFile(const std::filesystem::path& path) {
|
||||||
|
return AssetBinary::ReadVersion(path, Magic);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModelFile::SupportsLoadOptions(const ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
|
||||||
|
const bool sameImport =
|
||||||
|
options.meshImportMode == CookOptions.meshImportMode &&
|
||||||
|
options.flipUVs == CookOptions.flipUVs &&
|
||||||
|
options.generateTangents == CookOptions.generateTangents &&
|
||||||
|
options.generateSmoothNormals == CookOptions.generateSmoothNormals &&
|
||||||
|
options.joinIdenticalVertices == CookOptions.joinIdenticalVertices &&
|
||||||
|
options.limitBoneWeights == CookOptions.limitBoneWeights &&
|
||||||
|
options.bakeStaticNodeTransforms == CookOptions.bakeStaticNodeTransforms;
|
||||||
|
|
||||||
|
// Meshes are built against the skeleton, so skinned models without it import differently
|
||||||
|
const bool sameMeshes = options.loadSkeleton || !model.IsSkinned();
|
||||||
|
|
||||||
|
return sameImport && sameMeshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModelFile::ApplyLoadOptions(ModelDoc::ModelAsset& model, const ModelDoc::LoadOptions& options) {
|
||||||
|
if (!options.loadMaterials) {
|
||||||
|
model.materials.clear();
|
||||||
|
for (ModelDoc::MeshPrimitive& primitive : model.primitives) {
|
||||||
|
primitive.materialIndex = ModelDoc::InvalidIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!options.loadSkeleton) {
|
||||||
|
model.skeleton = Skeleton{};
|
||||||
|
}
|
||||||
|
if (!options.loadAnimations) {
|
||||||
|
model.animations.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include <destrum/Assets/TextureFile.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/packing.hpp>
|
||||||
|
#include <glm/vec4.hpp>
|
||||||
|
|
||||||
|
#include "BinaryStream.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using AssetBinary::BinaryReader;
|
||||||
|
using AssetBinary::BinaryWriter;
|
||||||
|
|
||||||
|
constexpr AssetBinary::Magic Magic{'D', 'T', 'E', 'X'};
|
||||||
|
constexpr std::string_view Kind = "texture";
|
||||||
|
|
||||||
|
std::size_t ExpectedByteSize(const TextureFile::Texture& texture) {
|
||||||
|
return std::size_t{texture.width} * texture.height * TextureFile::BytesPerPixel(texture.format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t TextureFile::BytesPerPixel(PixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case PixelFormat::RGBA16Float:
|
||||||
|
return 4 * sizeof(std::uint16_t);
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Unknown cooked texture pixel format");
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFile::Texture TextureFile::FromRGBA32Float(std::uint32_t width, std::uint32_t height, std::span<const float> rgba) {
|
||||||
|
const std::size_t pixelCount = std::size_t{width} * height;
|
||||||
|
if (rgba.size() != pixelCount * 4) {
|
||||||
|
throw std::invalid_argument("RGBA pixel data doesn't match the texture size");
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture texture{
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.format = PixelFormat::RGBA16Float,
|
||||||
|
};
|
||||||
|
texture.pixels.resize(pixelCount * BytesPerPixel(texture.format));
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < pixelCount; ++i) {
|
||||||
|
const glm::vec4 color{rgba[i * 4 + 0], rgba[i * 4 + 1], rgba[i * 4 + 2], rgba[i * 4 + 3]};
|
||||||
|
const std::uint64_t packed = glm::packHalf4x16(color);
|
||||||
|
std::memcpy(&texture.pixels[i * sizeof(packed)], &packed, sizeof(packed));
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> TextureFile::Write(const Texture& texture) {
|
||||||
|
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||||
|
throw std::invalid_argument("Texture pixel data doesn't match its size and format");
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryWriter writer;
|
||||||
|
writer.WriteHeader(Magic, Version);
|
||||||
|
writer.Write(texture.width);
|
||||||
|
writer.Write(texture.height);
|
||||||
|
writer.Write(static_cast<std::uint32_t>(texture.format));
|
||||||
|
writer.WriteArray(texture.pixels);
|
||||||
|
return writer.TakeBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFile::Texture TextureFile::Read(std::span<const std::uint8_t> bytes) {
|
||||||
|
BinaryReader reader{bytes};
|
||||||
|
reader.ReadHeader(Magic, Version, Kind);
|
||||||
|
|
||||||
|
Texture texture;
|
||||||
|
texture.width = reader.Read<std::uint32_t>();
|
||||||
|
texture.height = reader.Read<std::uint32_t>();
|
||||||
|
texture.format = static_cast<PixelFormat>(reader.Read<std::uint32_t>());
|
||||||
|
texture.pixels = reader.ReadArray<std::uint8_t>();
|
||||||
|
reader.ExpectEnd(Kind);
|
||||||
|
|
||||||
|
if (texture.pixels.size() != ExpectedByteSize(texture)) {
|
||||||
|
throw std::runtime_error("Cooked texture file is corrupt: pixel data doesn't match its size");
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureFile::WriteToFile(const Texture& texture, const std::filesystem::path& path) {
|
||||||
|
AssetBinary::WriteAllBytes(path, Write(texture));
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFile::Texture TextureFile::ReadFromFile(const std::filesystem::path& path) {
|
||||||
|
return Read(AssetBinary::ReadAllBytes(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::uint32_t> TextureFile::ReadVersionFromFile(const std::filesystem::path& path) {
|
||||||
|
return AssetBinary::ReadVersion(path, Magic);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include <destrum/Components/CameraComponent.h>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
CameraComponent::CameraComponent(GameObject& parent)
|
||||||
|
: Component(parent, "CameraComponent")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera& CameraComponent::GetCamera(float aspectRatio)
|
||||||
|
{
|
||||||
|
auto& transform = GetTransform();
|
||||||
|
const glm::quat rotation = transform.GetWorldRotation();
|
||||||
|
|
||||||
|
m_Camera.m_position = transform.GetWorldPosition();
|
||||||
|
m_Camera.SetOrientation(rotation * glm::vec3{0.0f, 0.0f, -1.0f}, rotation * glm::vec3{0.0f, 1.0f, 0.0f});
|
||||||
|
m_Camera.SetFOVY(m_FieldOfView);
|
||||||
|
m_Camera.SetZNear(m_ZNear);
|
||||||
|
m_Camera.SetZFar(m_ZFar);
|
||||||
|
m_Camera.setAspectRatio(aspectRatio);
|
||||||
|
return m_Camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraComponent::SetFieldOfView(float fovDegrees)
|
||||||
|
{
|
||||||
|
m_FieldOfView = std::clamp(fovDegrees, 1.0f, 179.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraComponent::SetClipPlanes(float zNear, float zFar)
|
||||||
|
{
|
||||||
|
m_ZNear = std::max(zNear, 0.0001f);
|
||||||
|
m_ZFar = std::max(zFar, m_ZNear + 0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraComponent::MakeCurrent()
|
||||||
|
{
|
||||||
|
if (Scene* scene = GetGameObject()->GetScene()) {
|
||||||
|
scene->SetCurrentCamera(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CameraComponent::IsCurrent() const
|
||||||
|
{
|
||||||
|
const Scene* scene = GetGameObject()->GetScene();
|
||||||
|
return scene != nullptr && scene->GetCurrentCamera() == this;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json CameraComponent::Serialize() const {
|
||||||
|
return {
|
||||||
|
{"fieldOfView", m_FieldOfView},
|
||||||
|
{"zNear", m_ZNear},
|
||||||
|
{"zFar", m_ZFar}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraComponent::Deserialize(const nlohmann::json& data) {
|
||||||
|
if (data.contains("fieldOfView")) {
|
||||||
|
SetFieldOfView(data.at("fieldOfView").get<float>());
|
||||||
|
}
|
||||||
|
SetClipPlanes(data.value("zNear", m_ZNear), data.value("zFar", m_ZFar));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraComponent::ImGuiInspector() {
|
||||||
|
bool current = IsCurrent();
|
||||||
|
if (ImGui::Checkbox("Current Camera", ¤t) && current) {
|
||||||
|
MakeCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
float fieldOfView = m_FieldOfView;
|
||||||
|
if (ImGui::DragFloat("Field Of View", &fieldOfView, 0.5f, 1.0f, 179.0f)) {
|
||||||
|
SetFieldOfView(fieldOfView);
|
||||||
|
}
|
||||||
|
|
||||||
|
float zNear = m_ZNear;
|
||||||
|
float zFar = m_ZFar;
|
||||||
|
const bool nearChanged = ImGui::DragFloat("Near", &zNear, 0.01f, 0.0001f, 100.0f);
|
||||||
|
const bool farChanged = ImGui::DragFloat("Far", &zFar, 1.0f, 0.1f, 100000.0f);
|
||||||
|
if (nearChanged || farChanged) {
|
||||||
|
SetClipPlanes(zNear, zFar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,15 @@ void Camera::SetRotation(const glm::vec2& yawPitchRadians) {
|
|||||||
SetRotation(yawPitchRadians.x, yawPitchRadians.y);
|
SetRotation(yawPitchRadians.x, yawPitchRadians.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Camera::SetOrientation(const glm::vec3& forward, const glm::vec3& up) {
|
||||||
|
m_useTarget = false;
|
||||||
|
m_forward = glm::normalize(forward);
|
||||||
|
m_right = glm::normalize(glm::cross(m_forward, up));
|
||||||
|
m_up = glm::normalize(glm::cross(m_right, m_forward));
|
||||||
|
m_yaw = std::atan2(m_forward.z, m_forward.x);
|
||||||
|
m_pitch = std::asin(glm::clamp(m_forward.y, -1.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
void Camera::SetTarget(const glm::vec3&) {
|
void Camera::SetTarget(const glm::vec3&) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
|
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
|
#include <destrum/Assets/TextureFile.h>
|
||||||
|
|
||||||
ImageData::~ImageData()
|
ImageData::~ImageData()
|
||||||
|
{
|
||||||
|
freePixels();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageData::freePixels()
|
||||||
{
|
{
|
||||||
if (shouldSTBFree) {
|
if (shouldSTBFree) {
|
||||||
if (pixels) stbi_image_free(pixels);
|
if (pixels) stbi_image_free(pixels);
|
||||||
@@ -22,6 +29,12 @@ ImageData::~ImageData()
|
|||||||
if (hdrPixels) free(hdrPixels);
|
if (hdrPixels) free(hdrPixels);
|
||||||
spdlog::debug("Freed TinyEXR image memory");
|
spdlog::debug("Freed TinyEXR image memory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cookedPixels.clear();
|
||||||
|
pixels = nullptr;
|
||||||
|
hdrPixels = nullptr;
|
||||||
|
shouldSTBFree = false;
|
||||||
|
shouldEXRFree = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace util
|
namespace util
|
||||||
@@ -33,13 +46,46 @@ namespace util
|
|||||||
return ext == ".exr";
|
return ext == ".exr";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool isCookedTextureExt(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
auto ext = p.extension().string();
|
||||||
|
for (auto& c : ext) c = char(::tolower(c));
|
||||||
|
return ext == TextureFile::Extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VkFormat toVkFormat(TextureFile::PixelFormat format)
|
||||||
|
{
|
||||||
|
switch (format) {
|
||||||
|
case TextureFile::PixelFormat::RGBA16Float: return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||||
|
}
|
||||||
|
return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
ImageData loadImage(const std::filesystem::path& p, TextureIntent intent)
|
ImageData loadImage(const std::filesystem::path& p, TextureIntent intent)
|
||||||
{
|
{
|
||||||
ImageData data;
|
ImageData data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- EXR ----------
|
if (isCookedTextureExt(p)) {
|
||||||
|
try {
|
||||||
|
TextureFile::Texture texture = TextureFile::ReadFromFile(p);
|
||||||
|
|
||||||
|
data.cookedPixels = std::move(texture.pixels);
|
||||||
|
data.pixels = data.cookedPixels.data();
|
||||||
|
data.width = static_cast<int>(texture.width);
|
||||||
|
data.height = static_cast<int>(texture.height);
|
||||||
|
data.channels = 4;
|
||||||
|
|
||||||
|
data.vkFormat = toVkFormat(texture.format);
|
||||||
|
data.byteSize = data.cookedPixels.size();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to read cooked texture '{}': {}", p.string(), exception.what());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
if (isExrExt(p)) {
|
if (isExrExt(p)) {
|
||||||
float* out = nullptr;
|
float* out = nullptr;
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Util/ImGuiUtils.h>
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
@@ -164,12 +165,41 @@ void Scene::Render(const RenderContext& ctx) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CameraComponent* Scene::GetCurrentCamera() const {
|
||||||
|
const auto cameras = GetCameras();
|
||||||
|
for (CameraComponent* camera : cameras) {
|
||||||
|
if (camera->GetGameObject()->GetId() == m_CurrentCameraId) {
|
||||||
|
return camera;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cameras.empty() ? nullptr : cameras.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CameraComponent*> Scene::GetCameras() const {
|
||||||
|
std::vector<CameraComponent*> cameras;
|
||||||
|
for (const auto& object : m_objects) {
|
||||||
|
if (object == nullptr || object->IsBeingDestroyed() || !object->IsActiveInHierarchy()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CameraComponent* camera = object->GetComponent<CameraComponent>();
|
||||||
|
if (camera != nullptr && camera->isEnabled()) {
|
||||||
|
cameras.push_back(camera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cameras;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::SetCurrentCamera(const CameraComponent* camera) {
|
||||||
|
m_CurrentCameraId = camera != nullptr ? camera->GetGameObject()->GetId() : InvalidObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
void Scene::RenderImgui(bool* open) {
|
void Scene::RenderImgui(bool* open) {
|
||||||
ImGuiUtils::RenderSceneInspector(
|
ImGuiUtils::RenderSceneInspector(
|
||||||
m_name,
|
m_name,
|
||||||
m_objects,
|
m_objects,
|
||||||
m_pendingAdditions,
|
m_pendingAdditions,
|
||||||
m_SelectedObjectId,
|
m_SelectedObjectId,
|
||||||
|
[this] { return CreateGameObject("GameObject"); },
|
||||||
open);
|
open);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
root["version"] = 2;
|
root["version"] = 2;
|
||||||
root["name"] = scene.GetName();
|
root["name"] = scene.GetName();
|
||||||
root["objects"] = json::array();
|
root["objects"] = json::array();
|
||||||
|
if (scene.m_CurrentCameraId != InvalidObjectId) {
|
||||||
|
root["currentCamera"] = scene.m_CurrentCameraId;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& objectPtr : scene.GetObjects()) {
|
for (const auto& objectPtr : scene.GetObjects()) {
|
||||||
if (!objectPtr || objectPtr->IsBeingDestroyed()) {
|
if (!objectPtr || objectPtr->IsBeingDestroyed()) {
|
||||||
@@ -569,6 +572,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
if (root.contains("name")) {
|
if (root.contains("name")) {
|
||||||
scene.m_name = root.at("name").get<std::string>();
|
scene.m_name = root.at("name").get<std::string>();
|
||||||
}
|
}
|
||||||
|
scene.m_CurrentCameraId = root.value("currentCamera", InvalidObjectId);
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.9f);
|
if (progressCallback) progressCallback(0.9f);
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ namespace ImGuiUtils {
|
|||||||
const std::vector<std::shared_ptr<GameObject>>& objectsSource,
|
const std::vector<std::shared_ptr<GameObject>>& objectsSource,
|
||||||
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||||
ObjectId& selectedObjectId,
|
ObjectId& selectedObjectId,
|
||||||
|
const std::function<GameObject*()>& createGameObject,
|
||||||
bool* open) {
|
bool* open) {
|
||||||
RegisterEngineComponents();
|
RegisterEngineComponents();
|
||||||
|
|
||||||
@@ -166,6 +167,15 @@ namespace ImGuiUtils {
|
|||||||
ImGui::Text("Scene: %s", displaySceneName.c_str());
|
ImGui::Text("Scene: %s", displaySceneName.c_str());
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
|
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
|
||||||
|
if (createGameObject) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("+ Add GameObject")) {
|
||||||
|
if (GameObject* created = createGameObject()) {
|
||||||
|
objects.push_back(created);
|
||||||
|
selectedObjectId = created->GetId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|||||||
@@ -72,10 +72,6 @@ add_custom_target(_internal_clean_game_assets
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(_internal_cook_game_assets
|
add_custom_target(_internal_cook_game_assets
|
||||||
COMMAND TheChef
|
|
||||||
--input "${ASSETS_SRC_DIR}"
|
|
||||||
--output "${ASSETS_RUNTIME_DIR}"
|
|
||||||
--clean
|
|
||||||
COMMAND TheChef
|
COMMAND TheChef
|
||||||
--input "${ASSETS_SRC_DIR}"
|
--input "${ASSETS_SRC_DIR}"
|
||||||
--output "${ASSETS_RUNTIME_DIR}"
|
--output "${ASSETS_RUNTIME_DIR}"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "destrum/Graphics/Resources/Cubemap.h"
|
#include "destrum/Graphics/Resources/Cubemap.h"
|
||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
|
|
||||||
|
class CameraComponent;
|
||||||
class FirstPersonController;
|
class FirstPersonController;
|
||||||
class LightKeeper final : public App {
|
class LightKeeper final : public App {
|
||||||
public:
|
public:
|
||||||
@@ -30,6 +31,7 @@ private:
|
|||||||
void loadScene();
|
void loadScene();
|
||||||
void createEmptyScene();
|
void createEmptyScene();
|
||||||
void submitBoxColliderDebugLines();
|
void submitBoxColliderDebugLines();
|
||||||
|
[[nodiscard]] CameraComponent* getSceneCamera() const;
|
||||||
|
|
||||||
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ private:
|
|||||||
bool renderQuads{true};
|
bool renderQuads{true};
|
||||||
bool renderDebugLines{true};
|
bool renderDebugLines{true};
|
||||||
bool showTestWindow{true};
|
bool showTestWindow{true};
|
||||||
|
bool useFreeCamera{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //LIGHTKEEPER_H
|
#endif //LIGHTKEEPER_H
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "destrum/Util/ModelDoc.h"
|
#include "destrum/Util/ModelDoc.h"
|
||||||
#include "destrum/Components/Animator.h"
|
#include "destrum/Components/Animator.h"
|
||||||
#include "destrum/Components/LightComponent.h"
|
#include "destrum/Components/LightComponent.h"
|
||||||
|
#include "destrum/Components/CameraComponent.h"
|
||||||
#include <components/FirstPersonController.h>
|
#include <components/FirstPersonController.h>
|
||||||
#include <components/TriggerLoggerComponent.h>
|
#include <components/TriggerLoggerComponent.h>
|
||||||
|
|
||||||
@@ -109,10 +110,7 @@ void LightKeeper::customInit()
|
|||||||
const auto planeObj = scene.CreateGameObject("GroundPlane");
|
const auto planeObj = scene.CreateGameObject("GroundPlane");
|
||||||
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
|
const auto planeMeshComp = planeObj->AddComponent<MeshRendererComponent>();
|
||||||
|
|
||||||
auto planeModel = ModelDoc::LoadModel(
|
auto planeModel = AssetManager::GetInstance().LoadModel("game://plane.glb", staticModelOptions);
|
||||||
AssetFS::GetInstance().GetFullPath("game://plane.glb").generic_string(),
|
|
||||||
staticModelOptions
|
|
||||||
);
|
|
||||||
ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
|
ModelDocUtils::LogModelDocSummary(planeModel, "plane.glb");
|
||||||
|
|
||||||
const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
|
const auto& planePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(planeModel, "game://plane.glb");
|
||||||
@@ -153,8 +151,8 @@ void LightKeeper::customInit()
|
|||||||
characterModelOptions.loadSkeleton = true;
|
characterModelOptions.loadSkeleton = true;
|
||||||
characterModelOptions.loadAnimations = true;
|
characterModelOptions.loadAnimations = true;
|
||||||
|
|
||||||
auto charModel = ModelDoc::LoadModel(
|
auto charModel = AssetManager::GetInstance().LoadModel(
|
||||||
AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(),
|
"engine://cotw-capybara-male/source/capybara.fbx",
|
||||||
characterModelOptions
|
characterModelOptions
|
||||||
);
|
);
|
||||||
ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
|
ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx");
|
||||||
@@ -521,7 +519,7 @@ void LightKeeper::customUpdate(float dt)
|
|||||||
camera.CalculateViewMatrix();
|
camera.CalculateViewMatrix();
|
||||||
camera.CalculateProjectionMatrix();
|
camera.CalculateProjectionMatrix();
|
||||||
}
|
}
|
||||||
else
|
else if (getSceneCamera() == nullptr)
|
||||||
{
|
{
|
||||||
camera.Update(dt);
|
camera.Update(dt);
|
||||||
}
|
}
|
||||||
@@ -563,6 +561,15 @@ void LightKeeper::customUpdate(float dt)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CameraComponent* LightKeeper::getSceneCamera() const
|
||||||
|
{
|
||||||
|
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
|
||||||
|
if (useFreeCamera || activeScenes.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return activeScenes.front()->GetCurrentCamera();
|
||||||
|
}
|
||||||
|
|
||||||
void LightKeeper::customWindowsMenu()
|
void LightKeeper::customWindowsMenu()
|
||||||
{
|
{
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -616,6 +623,27 @@ void LightKeeper::drawDebugMenuBar()
|
|||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenu("Camera")) {
|
||||||
|
ImGui::MenuItem("Free Camera", nullptr, &useFreeCamera);
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
auto& scene = SceneManager::GetInstance().GetCurrentScene();
|
||||||
|
const auto cameras = scene.GetCameras();
|
||||||
|
if (cameras.empty()) {
|
||||||
|
ImGui::TextDisabled("No cameras in the scene");
|
||||||
|
}
|
||||||
|
for (CameraComponent* sceneCamera : cameras) {
|
||||||
|
const GameObject* owner = sceneCamera->GetGameObject();
|
||||||
|
ImGui::PushID(owner);
|
||||||
|
if (ImGui::MenuItem(owner->GetName().c_str(), nullptr, !useFreeCamera && sceneCamera->IsCurrent())) {
|
||||||
|
scene.SetCurrentCamera(sceneCamera);
|
||||||
|
useFreeCamera = false;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::EndMainMenuBar();
|
ImGui::EndMainMenuBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,11 +779,14 @@ void LightKeeper::customDraw()
|
|||||||
glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f,
|
glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f,
|
||||||
glm::vec4{1.0f}, texID);
|
glm::vec4{1.0f}, texID);
|
||||||
|
|
||||||
|
CameraComponent* sceneCamera = getSceneCamera();
|
||||||
|
Camera& viewCamera = sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera;
|
||||||
|
|
||||||
const RenderContext ctx{
|
const RenderContext ctx{
|
||||||
.renderer = renderer,
|
.renderer = renderer,
|
||||||
.camera = camera,
|
.camera = viewCamera,
|
||||||
.sceneData = {
|
.sceneData = {
|
||||||
.camera = camera,
|
.camera = viewCamera,
|
||||||
.ambientColor = glm::vec3(0.1f),
|
.ambientColor = glm::vec3(0.1f),
|
||||||
.ambientIntensity = 0.5f,
|
.ambientIntensity = 0.5f,
|
||||||
.fogColor = glm::vec3(0.5f),
|
.fogColor = glm::vec3(0.5f),
|
||||||
@@ -769,8 +800,8 @@ void LightKeeper::customDraw()
|
|||||||
const auto& drawImage = renderer.getDrawImage();
|
const auto& drawImage = renderer.getDrawImage();
|
||||||
|
|
||||||
renderer.draw(
|
renderer.draw(
|
||||||
cmd, gfxDevice, camera, GameRenderer::SceneData{
|
cmd, gfxDevice, viewCamera, GameRenderer::SceneData{
|
||||||
camera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f
|
viewCamera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f
|
||||||
});
|
});
|
||||||
|
|
||||||
gfxDevice.endFrame(
|
gfxDevice.endFrame(
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ set_target_properties(destrum_tests PROPERTIES
|
|||||||
CXX_EXTENSIONS OFF
|
CXX_EXTENSIONS OFF
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(destrum_tests PRIVATE SDL_MAIN_HANDLED)
|
target_compile_definitions(destrum_tests PRIVATE
|
||||||
|
SDL_MAIN_HANDLED
|
||||||
|
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
|
||||||
|
)
|
||||||
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
|
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
|
||||||
|
|
||||||
destrum_enable_warnings(destrum_tests)
|
destrum_enable_warnings(destrum_tests)
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <glm/gtc/packing.hpp>
|
||||||
|
|
||||||
#include <destrum/Event.h>
|
#include <destrum/Event.h>
|
||||||
|
#include <destrum/Assets/AssetManager.h>
|
||||||
#include <destrum/Assets/AssetReference.h>
|
#include <destrum/Assets/AssetReference.h>
|
||||||
|
#include <destrum/Assets/ModelFile.h>
|
||||||
|
#include <destrum/Assets/TextureFile.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
#include <destrum/ObjectModel/Component.h>
|
#include <destrum/ObjectModel/Component.h>
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
@@ -22,9 +28,11 @@
|
|||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
#include <destrum/Components/Physics/Collider.h>
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
#include <destrum/Components/Animator.h>
|
#include <destrum/Components/Animator.h>
|
||||||
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/Components/LightComponent.h>
|
#include <destrum/Components/LightComponent.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/Graphics/Material.h>
|
#include <destrum/Graphics/Material.h>
|
||||||
|
#include <destrum/Graphics/imageLoader.h>
|
||||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -485,6 +493,362 @@ namespace {
|
|||||||
"LightComponent range must stay positive");
|
"LightComponent range must stay positive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testSceneCurrentCamera()
|
||||||
|
{
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("cameras");
|
||||||
|
check(scene.GetCurrentCamera() == nullptr, "a scene without cameras must have no current camera");
|
||||||
|
|
||||||
|
GameObject* first = scene.CreateGameObject("first camera");
|
||||||
|
auto* firstCamera = first->AddComponent<CameraComponent>();
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
check(scene.GetCurrentCamera() == firstCamera, "the only camera in a scene must be the current camera");
|
||||||
|
|
||||||
|
GameObject* second = scene.CreateGameObject("second camera");
|
||||||
|
auto* secondCamera = second->AddComponent<CameraComponent>();
|
||||||
|
secondCamera->SetFieldOfView(60.0f);
|
||||||
|
second->GetTransform().SetWorldPosition({1.0f, 2.0f, 3.0f});
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
secondCamera->MakeCurrent();
|
||||||
|
check(scene.GetCurrentCamera() == secondCamera && !firstCamera->IsCurrent(),
|
||||||
|
"the selected camera must be the current camera");
|
||||||
|
|
||||||
|
const Camera& camera = secondCamera->GetCamera(16.0f / 9.0f);
|
||||||
|
check(camera.GetPosition() == glm::vec3{1.0f, 2.0f, 3.0f} &&
|
||||||
|
close(camera.GetForward().z, -1.0f) && camera.GetFOVY() == 60.0f,
|
||||||
|
"a camera component must place its camera at its transform, looking down -Z");
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_camera_scene_test.json";
|
||||||
|
check(SceneSerializer::Save(scene, path), "scene with cameras must save");
|
||||||
|
check(SceneSerializer::Load(scene, path), "scene with cameras must load");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
const CameraComponent* loadedCamera = scene.GetCurrentCamera();
|
||||||
|
check(loadedCamera != nullptr && loadedCamera->GetGameObject()->GetName() == "second camera" &&
|
||||||
|
loadedCamera->GetFieldOfView() == 60.0f,
|
||||||
|
"scene round-trip must preserve the current camera");
|
||||||
|
|
||||||
|
scene.GetCurrentCamera()->GetGameObject()->SetActive(false);
|
||||||
|
check(scene.GetCurrentCamera() != nullptr &&
|
||||||
|
scene.GetCurrentCamera()->GetGameObject()->GetName() == "first camera",
|
||||||
|
"an inactive current camera must fall back to another camera");
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset makeTestModelAsset()
|
||||||
|
{
|
||||||
|
Skeleton skeleton;
|
||||||
|
skeleton.assetReference = {.path = "engine://character.fbx", .subresource = "skeleton"};
|
||||||
|
skeleton.hierarchy = {{.id = 0, .children = {1}}, {.id = 1, .children = {}}};
|
||||||
|
skeleton.inverseBindMatrices = {glm::mat4{1.0f}, glm::mat4{2.0f}};
|
||||||
|
skeleton.joints = {
|
||||||
|
{.id = 0, .localTranslation = glm::vec3{0.0f, 1.0f, 0.0f}},
|
||||||
|
{.id = 1, .localScale = glm::vec3{2.0f}},
|
||||||
|
};
|
||||||
|
skeleton.jointNames = {"root", "spine"};
|
||||||
|
buildParentIndex(skeleton);
|
||||||
|
|
||||||
|
CPUMesh mesh;
|
||||||
|
mesh.indices = {0, 1, 2};
|
||||||
|
mesh.vertices = {
|
||||||
|
{.position = glm::vec3{0.0f}, .uv_x = 0.25f, .normal = glm::vec3{0.0f, 1.0f, 0.0f}},
|
||||||
|
{.position = glm::vec3{1.0f, 0.0f, 0.0f}, .tangent = glm::vec4{1.0f, 0.0f, 0.0f, -1.0f}},
|
||||||
|
{.position = glm::vec3{0.0f, 0.0f, 1.0f}, .uv_y = 0.75f},
|
||||||
|
};
|
||||||
|
mesh.skinningData = {
|
||||||
|
{.jointIds = glm::uvec4{0, 1, 0, 0}, .weights = glm::vec4{0.5f, 0.5f, 0.0f, 0.0f}},
|
||||||
|
{.jointIds = glm::uvec4{1, 0, 0, 0}, .weights = glm::vec4{1.0f, 0.0f, 0.0f, 0.0f}},
|
||||||
|
{.jointIds = glm::uvec4{0, 0, 0, 0}, .weights = glm::vec4{1.0f, 0.0f, 0.0f, 0.0f}},
|
||||||
|
};
|
||||||
|
mesh.name = "body";
|
||||||
|
mesh.assetReference = {.path = "engine://character.fbx", .subresource = "mesh:0"};
|
||||||
|
mesh.minPos = glm::vec3{0.0f};
|
||||||
|
mesh.maxPos = glm::vec3{1.0f};
|
||||||
|
mesh.skeleton = skeleton;
|
||||||
|
|
||||||
|
ModelDoc::MeshPrimitive primitive;
|
||||||
|
primitive.mesh = mesh;
|
||||||
|
primitive.materialIndex = 0;
|
||||||
|
primitive.nodeName = "Body";
|
||||||
|
primitive.sourceMeshIndex = 3;
|
||||||
|
primitive.skinned = true;
|
||||||
|
primitive.nodeWorldTransform = glm::mat4{3.0f};
|
||||||
|
|
||||||
|
ModelDoc::MaterialInfo material;
|
||||||
|
material.name = "skin";
|
||||||
|
material.assetReference = {.path = "engine://character.fbx", .subresource = "material:0"};
|
||||||
|
material.baseColor = glm::vec4{0.8f, 0.6f, 0.4f, 1.0f};
|
||||||
|
material.emissiveColor = glm::vec3{0.1f};
|
||||||
|
material.metallicFactor = 0.2f;
|
||||||
|
material.roughnessFactor = 0.9f;
|
||||||
|
material.opacity = 0.5f;
|
||||||
|
material.textures = {{
|
||||||
|
.semantic = ModelDoc::TextureSemantic::BaseColor,
|
||||||
|
.rawPath = "skin.png",
|
||||||
|
.path = "textures/skin.png",
|
||||||
|
.assimpType = aiTextureType_DIFFUSE,
|
||||||
|
.slot = 1,
|
||||||
|
.embedded = false,
|
||||||
|
}};
|
||||||
|
|
||||||
|
SkeletalAnimation animation;
|
||||||
|
animation.assetReference = {.path = "engine://character.fbx", .subresource = "animation:0"};
|
||||||
|
animation.tracks = {{
|
||||||
|
.jointIndex = 1,
|
||||||
|
.keyframes = {
|
||||||
|
{.time = 0.0f, .translation = glm::vec3{0.0f}, .rotation = glm::identity<glm::quat>(), .scale = glm::vec3{1.0f}},
|
||||||
|
{.time = 0.5f, .translation = glm::vec3{0.0f, 2.0f, 0.0f}, .rotation = glm::quat{0.0f, 1.0f, 0.0f, 0.0f}, .scale = glm::vec3{1.0f}},
|
||||||
|
},
|
||||||
|
}};
|
||||||
|
animation.duration = 0.5f;
|
||||||
|
animation.looped = false;
|
||||||
|
animation.name = "wave";
|
||||||
|
animation.startFrame = 4;
|
||||||
|
animation.events = {{2, {"footstep", "dust"}}};
|
||||||
|
|
||||||
|
ModelDoc::ModelAsset model;
|
||||||
|
model.sourcePath = "engine://character.fbx";
|
||||||
|
model.primitives = {primitive};
|
||||||
|
model.materials = {material};
|
||||||
|
model.skeleton = skeleton;
|
||||||
|
model.animations = {animation};
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool modelFileReadThrows(const std::vector<std::uint8_t>& bytes)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(void)ModelFile::Read(bytes);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testModelFileRoundTrip()
|
||||||
|
{
|
||||||
|
const auto bytes = ModelFile::Write(makeTestModelAsset());
|
||||||
|
const auto loaded = ModelFile::Read(bytes);
|
||||||
|
|
||||||
|
check(ModelFile::Write(loaded) == bytes,
|
||||||
|
"a cooked model must write back to exactly the same bytes");
|
||||||
|
|
||||||
|
check(loaded.sourcePath == "engine://character.fbx",
|
||||||
|
"cooked models must keep their source path");
|
||||||
|
|
||||||
|
check(loaded.primitives.size() == 1, "cooked models must keep their primitives");
|
||||||
|
const auto& primitive = loaded.primitives.front();
|
||||||
|
check(primitive.nodeName == "Body" && primitive.sourceMeshIndex == 3 && primitive.skinned,
|
||||||
|
"cooked primitives must keep their import metadata");
|
||||||
|
check(primitive.mesh.indices == std::vector<std::uint32_t>{0, 1, 2},
|
||||||
|
"cooked meshes must keep their indices");
|
||||||
|
check(primitive.mesh.vertices.size() == 3 &&
|
||||||
|
primitive.mesh.vertices[0].uv_x == 0.25f &&
|
||||||
|
primitive.mesh.vertices[1].tangent.w == -1.0f,
|
||||||
|
"cooked meshes must keep their vertices");
|
||||||
|
check(primitive.mesh.skinningData[0].jointIds.y == 1 &&
|
||||||
|
primitive.mesh.skinningData[0].weights.x == 0.5f,
|
||||||
|
"cooked meshes must keep their skinning data");
|
||||||
|
check(primitive.mesh.skeleton.has_value() &&
|
||||||
|
primitive.mesh.skeleton->jointNames.size() == 2,
|
||||||
|
"cooked skinned meshes must keep their skeleton");
|
||||||
|
|
||||||
|
check(loaded.materials.size() == 1 && loaded.materials.front().opacity == 0.5f,
|
||||||
|
"cooked models must keep their materials");
|
||||||
|
const auto& texture = loaded.materials.front().textures.front();
|
||||||
|
check(texture.semantic == ModelDoc::TextureSemantic::BaseColor &&
|
||||||
|
texture.path == "textures/skin.png" &&
|
||||||
|
texture.assimpType == aiTextureType_DIFFUSE &&
|
||||||
|
texture.slot == 1 &&
|
||||||
|
!texture.embedded,
|
||||||
|
"cooked materials must keep their textures");
|
||||||
|
|
||||||
|
check(loaded.skeleton.jointNames == std::vector<std::string>{"root", "spine"} &&
|
||||||
|
loaded.skeleton.parentIndex == std::vector<int>{-1, 0} &&
|
||||||
|
loaded.skeleton.joints[1].localScale == glm::vec3{2.0f},
|
||||||
|
"cooked models must keep their skeleton");
|
||||||
|
|
||||||
|
check(loaded.animations.size() == 1, "cooked models must keep their animations");
|
||||||
|
const auto& animation = loaded.animations.front();
|
||||||
|
check(animation.name == "wave" && !animation.looped && animation.startFrame == 4,
|
||||||
|
"cooked animations must keep their settings");
|
||||||
|
check(animation.tracks.front().keyframes[1].translation.y == 2.0f &&
|
||||||
|
animation.tracks.front().keyframes[1].rotation.x == 1.0f,
|
||||||
|
"cooked animations must keep their keyframes");
|
||||||
|
check(animation.events.at(2) == std::vector<std::string>{"footstep", "dust"},
|
||||||
|
"cooked animations must keep their events");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testModelFileRejectsBadData()
|
||||||
|
{
|
||||||
|
const auto bytes = ModelFile::Write(makeTestModelAsset());
|
||||||
|
|
||||||
|
auto truncated = bytes;
|
||||||
|
truncated.resize(bytes.size() / 2);
|
||||||
|
check(modelFileReadThrows(truncated), "truncated cooked models must be rejected");
|
||||||
|
|
||||||
|
auto wrongMagic = bytes;
|
||||||
|
wrongMagic[0] = 'X';
|
||||||
|
check(modelFileReadThrows(wrongMagic), "files that aren't cooked models must be rejected");
|
||||||
|
|
||||||
|
auto wrongVersion = bytes;
|
||||||
|
wrongVersion[4] = static_cast<std::uint8_t>(wrongVersion[4] + 1);
|
||||||
|
check(modelFileReadThrows(wrongVersion), "cooked models from another version must be rejected");
|
||||||
|
|
||||||
|
auto trailingData = bytes;
|
||||||
|
trailingData.push_back(0);
|
||||||
|
check(modelFileReadThrows(trailingData), "cooked models with trailing data must be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testModelFileFromImportedModel()
|
||||||
|
{
|
||||||
|
const auto source = std::filesystem::path{DESTRUM_ENGINE_ASSETS_SRC_DIR} / "sphere.fbx";
|
||||||
|
const auto imported = ModelDoc::LoadModel(source.generic_string());
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_model_test.dmodel";
|
||||||
|
ModelFile::WriteToFile(imported, path);
|
||||||
|
const auto loaded = ModelFile::ReadFromFile(path);
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
check(!imported.primitives.empty(), "the test model must import with at least one primitive");
|
||||||
|
check(loaded.primitives.size() == imported.primitives.size() &&
|
||||||
|
loaded.primitives.front().mesh.vertices.size() == imported.primitives.front().mesh.vertices.size() &&
|
||||||
|
loaded.primitives.front().mesh.indices == imported.primitives.front().mesh.indices,
|
||||||
|
"an imported model must survive being cooked");
|
||||||
|
check(ModelFile::Write(loaded) == ModelFile::Write(imported),
|
||||||
|
"a cooked imported model must match the import exactly");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadModelThrows(std::string_view reference, const ModelDoc::LoadOptions& options)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(void)AssetManager::GetInstance().LoadModel(reference, options);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testAssetManagerLoadsCookedModels()
|
||||||
|
{
|
||||||
|
const auto root = std::filesystem::temp_directory_path() / "destrum_cooked_model_test";
|
||||||
|
const auto engineDir = root / "assets" / "engine";
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
std::filesystem::create_directories(engineDir);
|
||||||
|
std::filesystem::create_directories(root / "assets" / "game");
|
||||||
|
|
||||||
|
// The source can't be imported, so a successful load has to come from the cooked file
|
||||||
|
std::ofstream{engineDir / "model.fbx"} << "not a model";
|
||||||
|
ModelFile::WriteToFile(makeTestModelAsset(), engineDir / "model.fbx.dmodel");
|
||||||
|
std::ofstream{engineDir / "manifest.json"}
|
||||||
|
<< R"({"version":1,"assets":[{"src":"model.fbx","out":"model.fbx.dmodel","type":"model","mtime_epoch_ns":0,"size_bytes":0}]})";
|
||||||
|
|
||||||
|
auto& assetFs = AssetFS::GetInstance();
|
||||||
|
assetFs.Init(root);
|
||||||
|
|
||||||
|
const auto model = AssetManager::GetInstance().LoadModel("engine://model.fbx");
|
||||||
|
check(model.primitives.size() == 1 && model.animations.size() == 1,
|
||||||
|
"cooked models must be loaded instead of importing the source");
|
||||||
|
check(model.sourcePath == "engine://model.fbx" &&
|
||||||
|
model.primitives.front().mesh.assetReference.path == "engine://model.fbx" &&
|
||||||
|
model.primitives.front().mesh.skeleton->assetReference.path == "engine://model.fbx",
|
||||||
|
"cooked models must be referenced by their virtual path");
|
||||||
|
check(model.materials.front().textures.front().path ==
|
||||||
|
std::filesystem::weakly_canonical(engineDir / "textures/skin.png").generic_string(),
|
||||||
|
"cooked texture paths must be resolved next to the cooked model");
|
||||||
|
|
||||||
|
ModelDoc::LoadOptions withoutExtras{};
|
||||||
|
withoutExtras.loadMaterials = false;
|
||||||
|
withoutExtras.loadAnimations = false;
|
||||||
|
const auto stripped = AssetManager::GetInstance().LoadModel("engine://model.fbx", withoutExtras);
|
||||||
|
check(stripped.materials.empty() &&
|
||||||
|
stripped.animations.empty() &&
|
||||||
|
stripped.primitives.front().materialIndex == ModelDoc::InvalidIndex,
|
||||||
|
"cooked models must drop what the load options didn't ask for");
|
||||||
|
|
||||||
|
ModelDoc::LoadOptions withoutSkeleton{};
|
||||||
|
withoutSkeleton.loadSkeleton = false;
|
||||||
|
check(loadModelThrows("engine://model.fbx", withoutSkeleton),
|
||||||
|
"skinned cooked models must be imported when loaded without a skeleton");
|
||||||
|
|
||||||
|
ModelDoc::LoadOptions otherImport{};
|
||||||
|
otherImport.flipUVs = false;
|
||||||
|
check(loadModelThrows("engine://model.fbx", otherImport),
|
||||||
|
"cooked models must be imported when the import options differ from the cook options");
|
||||||
|
|
||||||
|
assetFs.Reset();
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureFile::Texture makeTestTexture()
|
||||||
|
{
|
||||||
|
const std::vector<float> rgba{
|
||||||
|
0.0f, 0.25f, 0.5f, 1.0f,
|
||||||
|
2.0f, 16.0f, 100.0f, 1.0f,
|
||||||
|
};
|
||||||
|
return TextureFile::FromRGBA32Float(2, 1, rgba);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool textureFileReadThrows(const std::vector<std::uint8_t>& bytes)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(void)TextureFile::Read(bytes);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testTextureFileRoundTrip()
|
||||||
|
{
|
||||||
|
const auto texture = makeTestTexture();
|
||||||
|
check(texture.pixels.size() == 2 * TextureFile::BytesPerPixel(TextureFile::PixelFormat::RGBA16Float),
|
||||||
|
"cooked textures must store every pixel");
|
||||||
|
|
||||||
|
std::uint64_t secondPixel = 0;
|
||||||
|
std::memcpy(&secondPixel, texture.pixels.data() + 8, sizeof(secondPixel));
|
||||||
|
const glm::vec4 unpacked = glm::unpackHalf4x16(secondPixel);
|
||||||
|
check(unpacked == glm::vec4{2.0f, 16.0f, 100.0f, 1.0f},
|
||||||
|
"half float conversion must keep HDR values above 1");
|
||||||
|
|
||||||
|
const auto bytes = TextureFile::Write(texture);
|
||||||
|
const auto loaded = TextureFile::Read(bytes);
|
||||||
|
check(loaded.width == 2 && loaded.height == 1 &&
|
||||||
|
loaded.format == TextureFile::PixelFormat::RGBA16Float &&
|
||||||
|
loaded.pixels == texture.pixels,
|
||||||
|
"cooked textures must round-trip");
|
||||||
|
|
||||||
|
auto truncated = bytes;
|
||||||
|
truncated.resize(bytes.size() - 1);
|
||||||
|
check(textureFileReadThrows(truncated), "truncated cooked textures must be rejected");
|
||||||
|
|
||||||
|
auto wrongMagic = bytes;
|
||||||
|
wrongMagic[0] = 'X';
|
||||||
|
check(textureFileReadThrows(wrongMagic), "files that aren't cooked textures must be rejected");
|
||||||
|
|
||||||
|
auto wrongSize = bytes;
|
||||||
|
wrongSize[8] = 3;
|
||||||
|
check(textureFileReadThrows(wrongSize), "cooked textures whose size doesn't match their pixels must be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testImageLoaderReadsCookedTextures()
|
||||||
|
{
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_texture_test.dtex";
|
||||||
|
const auto texture = makeTestTexture();
|
||||||
|
TextureFile::WriteToFile(texture, path);
|
||||||
|
|
||||||
|
const auto image = util::loadImage(path, TextureIntent::ColorSrgb);
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
check(image.vkFormat == VK_FORMAT_R16G16B16A16_SFLOAT &&
|
||||||
|
image.width == 2 && image.height == 1 &&
|
||||||
|
image.byteSize == texture.pixels.size(),
|
||||||
|
"cooked textures must load as half float images");
|
||||||
|
check(image.pixels != nullptr &&
|
||||||
|
std::memcmp(image.pixels, texture.pixels.data(), texture.pixels.size()) == 0,
|
||||||
|
"cooked texture pixels must be uploaded as stored");
|
||||||
|
}
|
||||||
|
|
||||||
void testSimpleColorMaterial()
|
void testSimpleColorMaterial()
|
||||||
{
|
{
|
||||||
const auto material = Material::SimpleColor(
|
const auto material = Material::SimpleColor(
|
||||||
@@ -600,9 +964,16 @@ int main()
|
|||||||
testSceneJoltSphereCollisions();
|
testSceneJoltSphereCollisions();
|
||||||
testAnimatorAssetReferences();
|
testAnimatorAssetReferences();
|
||||||
testLightComponentRoundTrip();
|
testLightComponentRoundTrip();
|
||||||
|
testModelFileRoundTrip();
|
||||||
|
testModelFileRejectsBadData();
|
||||||
|
testModelFileFromImportedModel();
|
||||||
|
testAssetManagerLoadsCookedModels();
|
||||||
|
testTextureFileRoundTrip();
|
||||||
|
testImageLoaderReadsCookedTextures();
|
||||||
testSimpleColorMaterial();
|
testSimpleColorMaterial();
|
||||||
testAssetReferenceCacheKeys();
|
testAssetReferenceCacheKeys();
|
||||||
testTriggerZone();
|
testTriggerZone();
|
||||||
|
testSceneCurrentCamera();
|
||||||
std::cout << "destrum tests passed\n";
|
std::cout << "destrum tests passed\n";
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user