feat: add vec3 type to nhollman json / fix: scene serialisation stuff
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <destrum/ObjectModel/Component.h>
|
#include <destrum/ObjectModel/Component.h>
|
||||||
#include <destrum/Physics/PhysicsTypes.h>
|
#include <destrum/Physics/PhysicsTypes.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
class Collider : public Component {
|
class Collider : public Component {
|
||||||
public:
|
public:
|
||||||
@@ -31,23 +32,14 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
[[nodiscard]] nlohmann::json SerializeCollider() const {
|
[[nodiscard]] nlohmann::json SerializeCollider() const {
|
||||||
return {
|
return {
|
||||||
{"centerOffset", {m_CenterOffset.x, m_CenterOffset.y, m_CenterOffset.z}},
|
{"centerOffset", m_CenterOffset},
|
||||||
{"isTrigger", m_IsTrigger}
|
{"isTrigger", m_IsTrigger}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeserializeCollider(const nlohmann::json& data) {
|
void DeserializeCollider(const nlohmann::json& data) {
|
||||||
if (data.contains("centerOffset")) {
|
m_CenterOffset = data.value("centerOffset", m_CenterOffset);
|
||||||
const auto& offset = data.at("centerOffset");
|
m_IsTrigger = data.value("isTrigger", m_IsTrigger);
|
||||||
m_CenterOffset = {
|
|
||||||
offset.at(0).get<float>(),
|
|
||||||
offset.at(1).get<float>(),
|
|
||||||
offset.at(2).get<float>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (data.contains("isTrigger")) {
|
|
||||||
m_IsTrigger = data.at("isTrigger").get<bool>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool m_IsTrigger{false};
|
bool m_IsTrigger{false};
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
#ifndef PRIMITIVEMESHES_H
|
#ifndef PRIMITIVEMESHES_H
|
||||||
#define PRIMITIVEMESHES_H
|
#define PRIMITIVEMESHES_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#include <destrum/Graphics/Resources/CPUMesh.h>
|
#include <destrum/Graphics/Resources/CPUMesh.h>
|
||||||
|
|
||||||
namespace PrimitiveMeshes {
|
namespace PrimitiveMeshes {
|
||||||
[[nodiscard]] CPUMesh MakeBox(const glm::vec3& size);
|
[[nodiscard]] CPUMesh MakeBox(const glm::vec3& size);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string BoxKey(const glm::vec3& size);
|
||||||
|
[[nodiscard]] bool IsPrimitiveKey(std::string_view key);
|
||||||
|
[[nodiscard]] std::string_view StripDuplicateSuffix(std::string_view key);
|
||||||
|
[[nodiscard]] std::optional<CPUMesh> MakeFromKey(std::string_view key);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // PRIMITIVEMESHES_H
|
#endif // PRIMITIVEMESHES_H
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public:
|
|||||||
virtual std::string GetTypeName() const = 0;
|
virtual std::string GetTypeName() const = 0;
|
||||||
|
|
||||||
virtual nlohmann::json Serialize() const {
|
virtual nlohmann::json Serialize() const {
|
||||||
return {};
|
return nlohmann::json::object();
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void Deserialize(const nlohmann::json&) {
|
virtual void Deserialize(const nlohmann::json&) {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#ifndef DESTRUM_GLMJSON_H
|
||||||
|
#define DESTRUM_GLMJSON_H
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace destrum::json_detail {
|
||||||
|
inline void RequireNumberArray(const nlohmann::json& j, std::size_t size) {
|
||||||
|
if (!j.is_array() || j.size() != size) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"expected an array of " + std::to_string(size) + " numbers, got " + j.dump());
|
||||||
|
}
|
||||||
|
for (const auto& element : j) {
|
||||||
|
if (!element.is_number()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"expected an array of " + std::to_string(size) + " numbers, got " + j.dump());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace nlohmann {
|
||||||
|
template <glm::length_t L, typename T, glm::qualifier Q>
|
||||||
|
struct adl_serializer<glm::vec<L, T, Q>> {
|
||||||
|
static void to_json(json& j, const glm::vec<L, T, Q>& value) {
|
||||||
|
j = json::array();
|
||||||
|
for (glm::length_t i = 0; i < L; ++i) {
|
||||||
|
j.push_back(value[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void from_json(const json& j, glm::vec<L, T, Q>& value) {
|
||||||
|
destrum::json_detail::RequireNumberArray(j, static_cast<std::size_t>(L));
|
||||||
|
for (glm::length_t i = 0; i < L; ++i) {
|
||||||
|
value[i] = j.at(static_cast<std::size_t>(i)).get<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stored as [x, y, z, w], the same order as scene transforms
|
||||||
|
template <typename T, glm::qualifier Q>
|
||||||
|
struct adl_serializer<glm::qua<T, Q>> {
|
||||||
|
static void to_json(json& j, const glm::qua<T, Q>& value) {
|
||||||
|
j = json::array({value.x, value.y, value.z, value.w});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void from_json(const json& j, glm::qua<T, Q>& value) {
|
||||||
|
destrum::json_detail::RequireNumberArray(j, 4);
|
||||||
|
value = glm::qua<T, Q>{
|
||||||
|
j.at(3).get<T>(),
|
||||||
|
j.at(0).get<T>(),
|
||||||
|
j.at(1).get<T>(),
|
||||||
|
j.at(2).get<T>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_GLMJSON_H
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/RenderContext.h>
|
#include <destrum/Graphics/RenderContext.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
#include <glm/gtc/constants.hpp>
|
#include <glm/gtc/constants.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
@@ -54,7 +55,7 @@ void LightComponent::Render(const RenderContext& ctx)
|
|||||||
nlohmann::json LightComponent::Serialize() const {
|
nlohmann::json LightComponent::Serialize() const {
|
||||||
return {
|
return {
|
||||||
{"type", static_cast<std::uint32_t>(m_Type)},
|
{"type", static_cast<std::uint32_t>(m_Type)},
|
||||||
{"color", {m_Color.x, m_Color.y, m_Color.z}},
|
{"color", m_Color},
|
||||||
{"intensity", m_Intensity},
|
{"intensity", m_Intensity},
|
||||||
{"range", m_Range},
|
{"range", m_Range},
|
||||||
{"innerConeAngle", m_InnerConeAngle},
|
{"innerConeAngle", m_InnerConeAngle},
|
||||||
@@ -70,10 +71,7 @@ void LightComponent::Deserialize(const nlohmann::json& data) {
|
|||||||
m_Type = static_cast<LightType>(type);
|
m_Type = static_cast<LightType>(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.contains("color")) {
|
m_Color = data.value("color", m_Color);
|
||||||
const auto& value = data.at("color");
|
|
||||||
m_Color = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
|
||||||
}
|
|
||||||
if (data.contains("intensity")) {
|
if (data.contains("intensity")) {
|
||||||
SetIntensity(data.at("intensity").get<float>());
|
SetIntensity(data.at("intensity").get<float>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <destrum/Components/MeshRendererComponent.h>
|
#include <destrum/Components/MeshRendererComponent.h>
|
||||||
#include <destrum/Assets/AssetReference.h>
|
#include <destrum/Assets/AssetReference.h>
|
||||||
#include <destrum/Graphics/GfxDevice.h>
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
#include <destrum/Graphics/Renderer.h>
|
#include <destrum/Graphics/Renderer.h>
|
||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
|
||||||
@@ -10,8 +11,11 @@
|
|||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -54,27 +58,39 @@ void MeshRendererComponent::Update(float) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nlohmann::json MeshRendererComponent::Serialize() const {
|
nlohmann::json MeshRendererComponent::Serialize() const {
|
||||||
|
// Only keys are saved, mesh and material ids are cache slots that change between runs
|
||||||
|
std::string savedMeshKey = meshKey;
|
||||||
|
std::string savedMaterialKey = materialKey;
|
||||||
|
if (GameState::GetInstance().HasRenderer()) {
|
||||||
|
if (const auto* resources = GameState::GetInstance().Renderer().getResources()) {
|
||||||
|
if (savedMeshKey.empty() && meshID != NULL_MESH_ID) {
|
||||||
|
savedMeshKey = resources->meshes().getMeshKey(meshID);
|
||||||
|
}
|
||||||
|
if (savedMaterialKey.empty() && materialID != NULL_MATERIAL_ID) {
|
||||||
|
savedMaterialKey = resources->materials().getMaterialKey(materialID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savedMeshKey.empty() && meshID != NULL_MESH_ID) {
|
||||||
|
throw std::runtime_error("MeshRendererComponent has a mesh without a key, it cannot be saved");
|
||||||
|
}
|
||||||
|
if (savedMaterialKey.empty() && materialID != NULL_MATERIAL_ID) {
|
||||||
|
throw std::runtime_error("MeshRendererComponent has a material without a key, it cannot be saved");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{"meshId", meshID},
|
{"meshKey", savedMeshKey},
|
||||||
{"materialId", materialID},
|
{"materialKey", savedMaterialKey}
|
||||||
{"meshKey", meshKey},
|
|
||||||
{"materialKey", materialKey}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
void MeshRendererComponent::Deserialize(const nlohmann::json& data) {
|
||||||
if (data.contains("meshId")) {
|
meshKey = data.value("meshKey", std::string{});
|
||||||
meshID = data.at("meshId").get<MeshID>();
|
materialKey = data.value("materialKey", std::string{});
|
||||||
}
|
|
||||||
if (data.contains("materialId")) {
|
// Older scenes also stored raw ids, only trust them when there is no key
|
||||||
materialID = data.at("materialId").get<MaterialID>();
|
meshID = meshKey.empty() ? data.value("meshId", NULL_MESH_ID) : NULL_MESH_ID;
|
||||||
}
|
materialID = materialKey.empty() ? data.value("materialId", NULL_MATERIAL_ID) : NULL_MATERIAL_ID;
|
||||||
if (data.contains("meshKey")) {
|
|
||||||
meshKey = data.at("meshKey").get<std::string>();
|
|
||||||
}
|
|
||||||
if (data.contains("materialKey")) {
|
|
||||||
materialKey = data.at("materialKey").get<std::string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
||||||
@@ -89,7 +105,9 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
|
|
||||||
const auto loadAssetForKey = [&](const std::string& key) {
|
const auto loadAssetForKey = [&](const std::string& key) {
|
||||||
const auto asset = AssetReference::fromCacheKey(key);
|
const auto asset = AssetReference::fromCacheKey(key);
|
||||||
if (!asset) {
|
// Runtime-only keys such as "BulbMaterial#2" also contain a '#', they are not files
|
||||||
|
if (!asset || (asset->path.find("://") == std::string::npos &&
|
||||||
|
!std::filesystem::exists(asset->path))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!GameState::GetInstance().HasGfxDevice()) {
|
if (!GameState::GetInstance().HasGfxDevice()) {
|
||||||
@@ -102,6 +120,20 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
|
|
||||||
if (!meshKey.empty()) {
|
if (!meshKey.empty()) {
|
||||||
auto resolved = resources->meshes().findMeshByKey(meshKey);
|
auto resolved = resources->meshes().findMeshByKey(meshKey);
|
||||||
|
if (!resolved && PrimitiveMeshes::IsPrimitiveKey(meshKey)) {
|
||||||
|
resolved = resources->meshes().findMeshByKey(PrimitiveMeshes::StripDuplicateSuffix(meshKey));
|
||||||
|
if (!resolved) {
|
||||||
|
auto primitive = PrimitiveMeshes::MakeFromKey(meshKey);
|
||||||
|
if (!primitive) {
|
||||||
|
throw std::runtime_error("Invalid primitive mesh key: " + meshKey);
|
||||||
|
}
|
||||||
|
if (!GameState::GetInstance().HasGfxDevice()) {
|
||||||
|
throw std::runtime_error("Cannot build primitive mesh without a graphics device: " + meshKey);
|
||||||
|
}
|
||||||
|
resolved = resources->meshes().addMesh(GameState::GetInstance().Gfx(), *primitive);
|
||||||
|
}
|
||||||
|
meshKey = resources->meshes().getMeshKey(*resolved);
|
||||||
|
}
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
loadAssetForKey(meshKey);
|
loadAssetForKey(meshKey);
|
||||||
resolved = resources->meshes().findMeshByKey(meshKey);
|
resolved = resources->meshes().findMeshByKey(meshKey);
|
||||||
@@ -129,11 +161,15 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
resolved = resources->materials().findMaterialByName(materialKey);
|
resolved = resources->materials().findMaterialByName(materialKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolved) {
|
if (resolved) {
|
||||||
throw std::runtime_error("Material resource not found: " + materialKey);
|
|
||||||
}
|
|
||||||
materialID = *resolved;
|
materialID = *resolved;
|
||||||
materialKey = resources->materials().getMaterialKey(*resolved);
|
materialKey = resources->materials().getMaterialKey(*resolved);
|
||||||
|
} else {
|
||||||
|
// Keep the key so saving again does not lose which material was meant
|
||||||
|
spdlog::warn("Material \"{}\" on \"{}\" not found, using the placeholder material",
|
||||||
|
materialKey, GetGameObject()->GetName());
|
||||||
|
materialID = resources->materials().getPlaceholderMaterialId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meshID != NULL_MESH_ID && meshKey.empty()) {
|
if (meshID != NULL_MESH_ID && meshKey.empty()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/ObjectModel/Transform.h"
|
#include "destrum/ObjectModel/Transform.h"
|
||||||
#include "destrum/Components/MeshRendererComponent.h"
|
#include "destrum/Components/MeshRendererComponent.h"
|
||||||
|
#include "destrum/Serialization/GlmJson.h"
|
||||||
#include "destrum/Util/GameState.h"
|
#include "destrum/Util/GameState.h"
|
||||||
#include "destrum/Util/DeltaTime.h"
|
#include "destrum/Util/DeltaTime.h"
|
||||||
#include "destrum/Util/ImGuiUtils.h"
|
#include "destrum/Util/ImGuiUtils.h"
|
||||||
@@ -128,8 +129,8 @@ void OrbitAndSpin::Start() {
|
|||||||
nlohmann::json OrbitAndSpin::Serialize() const {
|
nlohmann::json OrbitAndSpin::Serialize() const {
|
||||||
return {
|
return {
|
||||||
{"radius", m_Radius},
|
{"radius", m_Radius},
|
||||||
{"center", {m_Center.x, m_Center.y, m_Center.z}},
|
{"center", m_Center},
|
||||||
{"orbitAxis", {m_OrbitAxis.x, m_OrbitAxis.y, m_OrbitAxis.z}},
|
{"orbitAxis", m_OrbitAxis},
|
||||||
{"orbitSpeed", m_OrbitSpeed},
|
{"orbitSpeed", m_OrbitSpeed},
|
||||||
{"orbitAngle", m_OrbitAngle},
|
{"orbitAngle", m_OrbitAngle},
|
||||||
{"orbitPhase", m_OrbitPhase},
|
{"orbitPhase", m_OrbitPhase},
|
||||||
@@ -137,38 +138,27 @@ nlohmann::json OrbitAndSpin::Serialize() const {
|
|||||||
{"growSpeed", m_GrowSpeed},
|
{"growSpeed", m_GrowSpeed},
|
||||||
{"growMin", m_GrowMin},
|
{"growMin", m_GrowMin},
|
||||||
{"growMax", m_GrowMax},
|
{"growMax", m_GrowMax},
|
||||||
{"spinAxis", {m_SpinAxis.x, m_SpinAxis.y, m_SpinAxis.z}},
|
{"spinAxis", m_SpinAxis},
|
||||||
{"spinSpeed", m_SpinSpeed},
|
{"spinSpeed", m_SpinSpeed},
|
||||||
{"baseScale", {m_BaseScale.x, m_BaseScale.y, m_BaseScale.z}},
|
{"baseScale", m_BaseScale}
|
||||||
{"materialId", m_MaterialID}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
||||||
const auto readVec3 = [&data](const char* key, glm::vec3& value) {
|
m_Radius = data.value("radius", m_Radius);
|
||||||
if (!data.contains(key)) {
|
m_Center = data.value("center", m_Center);
|
||||||
return;
|
m_OrbitAxis = data.value("orbitAxis", m_OrbitAxis);
|
||||||
}
|
m_OrbitSpeed = data.value("orbitSpeed", m_OrbitSpeed);
|
||||||
|
m_OrbitAngle = data.value("orbitAngle", m_OrbitAngle);
|
||||||
const auto& array = data.at(key);
|
m_OrbitPhase = data.value("orbitPhase", m_OrbitPhase);
|
||||||
value = {array.at(0).get<float>(), array.at(1).get<float>(), array.at(2).get<float>()};
|
m_GrowPhase = data.value("growPhase", m_GrowPhase);
|
||||||
};
|
m_GrowSpeed = data.value("growSpeed", m_GrowSpeed);
|
||||||
|
m_GrowMin = data.value("growMin", m_GrowMin);
|
||||||
if (data.contains("radius")) m_Radius = data.at("radius").get<float>();
|
m_GrowMax = data.value("growMax", m_GrowMax);
|
||||||
readVec3("center", m_Center);
|
m_SpinAxis = data.value("spinAxis", m_SpinAxis);
|
||||||
readVec3("orbitAxis", m_OrbitAxis);
|
m_BaseScale = data.value("baseScale", m_BaseScale);
|
||||||
if (data.contains("orbitSpeed")) m_OrbitSpeed = data.at("orbitSpeed").get<float>();
|
|
||||||
if (data.contains("orbitAngle")) m_OrbitAngle = data.at("orbitAngle").get<float>();
|
|
||||||
if (data.contains("orbitPhase")) m_OrbitPhase = data.at("orbitPhase").get<float>();
|
|
||||||
if (data.contains("growPhase")) m_GrowPhase = data.at("growPhase").get<float>();
|
|
||||||
if (data.contains("growSpeed")) m_GrowSpeed = data.at("growSpeed").get<float>();
|
|
||||||
if (data.contains("growMin")) m_GrowMin = data.at("growMin").get<float>();
|
|
||||||
if (data.contains("growMax")) m_GrowMax = data.at("growMax").get<float>();
|
|
||||||
readVec3("spinAxis", m_SpinAxis);
|
|
||||||
readVec3("baseScale", m_BaseScale);
|
|
||||||
m_BaseScaleLoaded = data.contains("baseScale");
|
m_BaseScaleLoaded = data.contains("baseScale");
|
||||||
if (data.contains("spinSpeed")) m_SpinSpeed = data.at("spinSpeed").get<float>();
|
m_SpinSpeed = data.value("spinSpeed", m_SpinSpeed);
|
||||||
if (data.contains("materialId")) m_MaterialID = data.at("materialId").get<MaterialID>();
|
|
||||||
|
|
||||||
if (m_GrowMin > m_GrowMax) {
|
if (m_GrowMin > m_GrowMax) {
|
||||||
std::swap(m_GrowMin, m_GrowMax);
|
std::swap(m_GrowMin, m_GrowMax);
|
||||||
|
|||||||
@@ -11,19 +11,14 @@ void BoxCollider::DrawDebugLines(const glm::vec4& color) const {
|
|||||||
|
|
||||||
nlohmann::json BoxCollider::Serialize() const {
|
nlohmann::json BoxCollider::Serialize() const {
|
||||||
auto data = SerializeCollider();
|
auto data = SerializeCollider();
|
||||||
data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z};
|
data["halfExtents"] = m_HalfExtents;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoxCollider::Deserialize(const nlohmann::json& data) {
|
void BoxCollider::Deserialize(const nlohmann::json& data) {
|
||||||
DeserializeCollider(data);
|
DeserializeCollider(data);
|
||||||
if (data.contains("halfExtents")) {
|
if (data.contains("halfExtents")) {
|
||||||
const auto& extents = data.at("halfExtents");
|
SetHalfExtents(data.at("halfExtents").get<glm::vec3>());
|
||||||
SetHalfExtents({
|
|
||||||
extents.at(0).get<float>(),
|
|
||||||
extents.at(1).get<float>(),
|
|
||||||
extents.at(2).get<float>()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis
|
#include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis
|
||||||
#include <glm/gtx/quaternion.hpp> // operator*(quat, vec3)
|
#include <glm/gtx/quaternion.hpp> // operator*(quat, vec3)
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
#include <destrum/Util/DeltaTime.h>
|
#include <destrum/Util/DeltaTime.h>
|
||||||
#include <destrum/Util/ImGuiUtils.h>
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
@@ -102,34 +103,21 @@ nlohmann::json Rotator::Serialize() const {
|
|||||||
{"distance", m_Distance},
|
{"distance", m_Distance},
|
||||||
{"speed", m_Speed},
|
{"speed", m_Speed},
|
||||||
{"currentAngle", m_CurrentAngle},
|
{"currentAngle", m_CurrentAngle},
|
||||||
{"pivot", {m_Pivot.x, m_Pivot.y, m_Pivot.z}},
|
{"pivot", m_Pivot},
|
||||||
{"axis", {m_Axis.x, m_Axis.y, m_Axis.z}},
|
{"axis", m_Axis},
|
||||||
{"initialOffset", {m_InitialOffset.x, m_InitialOffset.y, m_InitialOffset.z}}
|
{"initialOffset", m_InitialOffset}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rotator::Deserialize(const nlohmann::json& data) {
|
void Rotator::Deserialize(const nlohmann::json& data) {
|
||||||
if (data.contains("distance")) {
|
m_Distance = data.value("distance", m_Distance);
|
||||||
m_Distance = data.at("distance").get<float>();
|
m_Speed = data.value("speed", m_Speed);
|
||||||
}
|
m_CurrentAngle = data.value("currentAngle", m_CurrentAngle);
|
||||||
if (data.contains("speed")) {
|
m_Pivot = data.value("pivot", m_Pivot);
|
||||||
m_Speed = data.at("speed").get<float>();
|
|
||||||
}
|
|
||||||
if (data.contains("currentAngle")) {
|
|
||||||
m_CurrentAngle = data.at("currentAngle").get<float>();
|
|
||||||
}
|
|
||||||
if (data.contains("pivot")) {
|
|
||||||
const auto& value = data.at("pivot");
|
|
||||||
m_Pivot = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
|
||||||
}
|
|
||||||
if (data.contains("axis")) {
|
if (data.contains("axis")) {
|
||||||
const auto& value = data.at("axis");
|
SetAxis(data.at("axis").get<glm::vec3>());
|
||||||
SetAxis({value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()});
|
|
||||||
}
|
|
||||||
if (data.contains("initialOffset")) {
|
|
||||||
const auto& value = data.at("initialOffset");
|
|
||||||
m_InitialOffset = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
|
||||||
}
|
}
|
||||||
|
m_InitialOffset = data.value("initialOffset", m_InitialOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rotator::ImGuiInspector() {
|
void Rotator::ImGuiInspector() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
|
||||||
#include "destrum/ObjectModel/Transform.h"
|
#include "destrum/ObjectModel/Transform.h"
|
||||||
|
#include "destrum/Serialization/GlmJson.h"
|
||||||
#include "destrum/Util/DeltaTime.h"
|
#include "destrum/Util/DeltaTime.h"
|
||||||
#include "destrum/Util/ImGuiUtils.h"
|
#include "destrum/Util/ImGuiUtils.h"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ void Spinner::Update(float dt)
|
|||||||
|
|
||||||
nlohmann::json Spinner::Serialize() const {
|
nlohmann::json Spinner::Serialize() const {
|
||||||
return {
|
return {
|
||||||
{"axis", {m_Axis.x, m_Axis.y, m_Axis.z}},
|
{"axis", m_Axis},
|
||||||
{"speed", m_Speed},
|
{"speed", m_Speed},
|
||||||
{"angle", m_Angle}
|
{"angle", m_Angle}
|
||||||
};
|
};
|
||||||
@@ -26,15 +27,10 @@ nlohmann::json Spinner::Serialize() const {
|
|||||||
|
|
||||||
void Spinner::Deserialize(const nlohmann::json& data) {
|
void Spinner::Deserialize(const nlohmann::json& data) {
|
||||||
if (data.contains("axis")) {
|
if (data.contains("axis")) {
|
||||||
const auto& value = data.at("axis");
|
SetAxis(data.at("axis").get<glm::vec3>());
|
||||||
SetAxis({value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()});
|
|
||||||
}
|
|
||||||
if (data.contains("speed")) {
|
|
||||||
m_Speed = data.at("speed").get<float>();
|
|
||||||
}
|
|
||||||
if (data.contains("angle")) {
|
|
||||||
m_Angle = data.at("angle").get<float>();
|
|
||||||
}
|
}
|
||||||
|
m_Speed = data.value("speed", m_Speed);
|
||||||
|
m_Angle = data.value("angle", m_Angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spinner::ImGuiInspector() {
|
void Spinner::ImGuiInspector() {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <charconv>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
||||||
{
|
{
|
||||||
@@ -22,7 +24,8 @@ CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
CPUMesh mesh{};
|
CPUMesh mesh{};
|
||||||
mesh.name = "Box";
|
// The name becomes the mesh cache key, saved scenes rebuild the box from it
|
||||||
|
mesh.name = BoxKey(size);
|
||||||
mesh.minPos = -size * 0.5f;
|
mesh.minPos = -size * 0.5f;
|
||||||
mesh.maxPos = size * 0.5f;
|
mesh.maxPos = size * 0.5f;
|
||||||
|
|
||||||
@@ -50,3 +53,65 @@ CPUMesh PrimitiveMeshes::MakeBox(const glm::vec3& size)
|
|||||||
|
|
||||||
return mesh;
|
return mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::string_view PrimitiveScheme = "prim://";
|
||||||
|
constexpr std::string_view BoxPrefix = "prim://box?size=";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PrimitiveMeshes::BoxKey(const glm::vec3& size)
|
||||||
|
{
|
||||||
|
return std::format("{}{},{},{}", BoxPrefix, size.x, size.y, size.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PrimitiveMeshes::IsPrimitiveKey(std::string_view key)
|
||||||
|
{
|
||||||
|
return key.starts_with(PrimitiveScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view PrimitiveMeshes::StripDuplicateSuffix(std::string_view key)
|
||||||
|
{
|
||||||
|
// MeshCache appends "#N" when the same key is added more than once
|
||||||
|
const auto separator = key.rfind('#');
|
||||||
|
if (separator == std::string_view::npos || separator + 1 >= key.size()) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
for (const char c : key.substr(separator + 1)) {
|
||||||
|
if (c < '0' || c > '9') {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key.substr(0, separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<CPUMesh> PrimitiveMeshes::MakeFromKey(std::string_view key)
|
||||||
|
{
|
||||||
|
key = StripDuplicateSuffix(key);
|
||||||
|
if (!key.starts_with(BoxPrefix)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view rest = key.substr(BoxPrefix.size());
|
||||||
|
glm::vec3 size{0.0f};
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
const char* end = rest.data() + rest.size();
|
||||||
|
float value = 0.0f;
|
||||||
|
const auto [next, error] = std::from_chars(rest.data(), end, value);
|
||||||
|
if (error != std::errc{} || !(value > 0.0f)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
size[i] = value;
|
||||||
|
rest.remove_prefix(static_cast<std::size_t>(next - rest.data()));
|
||||||
|
if (i < 2) {
|
||||||
|
if (!rest.starts_with(',')) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
rest.remove_prefix(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rest.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MakeBox(size);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/EngineComponentList.h>
|
#include <destrum/Serialization/EngineComponentList.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
void RegisterEngineComponents()
|
void RegisterEngineComponents()
|
||||||
{
|
{
|
||||||
static bool registered = false;
|
static std::once_flag registered;
|
||||||
if (registered) {
|
std::call_once(registered, [] {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registered = true;
|
|
||||||
|
|
||||||
#define DESTRUM_REGISTER_COMPONENT(ComponentType) \
|
#define DESTRUM_REGISTER_COMPONENT(ComponentType) \
|
||||||
ComponentFactory::Register<ComponentType>(#ComponentType);
|
ComponentFactory::Register<ComponentType>(#ComponentType);
|
||||||
DESTRUM_ENGINE_COMPONENTS(DESTRUM_REGISTER_COMPONENT)
|
DESTRUM_ENGINE_COMPONENTS(DESTRUM_REGISTER_COMPONENT)
|
||||||
#undef DESTRUM_REGISTER_COMPONENT
|
#undef DESTRUM_REGISTER_COMPONENT
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/ObjectModel/Component.h>
|
#include <destrum/ObjectModel/Component.h>
|
||||||
@@ -20,12 +21,18 @@
|
|||||||
#include <destrum/Components/Physics/Collider.h>
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
using ValidationError = std::optional<std::string>;
|
||||||
|
|
||||||
|
constexpr int CurrentSceneVersion = 2;
|
||||||
|
|
||||||
[[nodiscard]] bool IsFiniteNumber(const json& value) {
|
[[nodiscard]] bool IsFiniteNumber(const json& value) {
|
||||||
if (!value.is_number()) {
|
if (!value.is_number()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -48,164 +55,221 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsFiniteJson(const json& value) {
|
// Returns the JSON path of the first NaN/Inf, nlohmann would write it as null
|
||||||
if (value.is_number()) {
|
[[nodiscard]] std::optional<std::string> FindNonFinite(const json& value, const std::string& path) {
|
||||||
return IsFiniteNumber(value);
|
if (value.is_number_float()) {
|
||||||
|
if (!std::isfinite(value.get<double>())) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (value.is_array()) {
|
if (value.is_array()) {
|
||||||
for (const auto& element : value) {
|
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||||
if (!IsFiniteJson(element)) {
|
if (auto bad = FindNonFinite(value.at(i), path + "[" + std::to_string(i) + "]")) {
|
||||||
return false;
|
return bad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (value.is_object()) {
|
if (value.is_object()) {
|
||||||
for (auto it = value.begin(); it != value.end(); ++it) {
|
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||||
if (!IsFiniteJson(it.value())) {
|
if (auto bad = FindNonFinite(it.value(), path + "." + it.key())) {
|
||||||
return false;
|
return bad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool ValidateTransform(const json& objectJson, int version) {
|
[[nodiscard]] std::string DescribeObject(std::size_t index, const json& objectJson) {
|
||||||
|
std::string description = "objects[" + std::to_string(index) + "]";
|
||||||
|
if (objectJson.is_object() && objectJson.contains("name") && objectJson.at("name").is_string()) {
|
||||||
|
description += " \"" + objectJson.at("name").get<std::string>() + "\"";
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] ValidationError ValidateTransform(const json& objectJson, int version) {
|
||||||
if (!objectJson.contains("transform")) {
|
if (!objectJson.contains("transform")) {
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& transform = objectJson.at("transform");
|
const auto& transform = objectJson.at("transform");
|
||||||
if (!transform.is_object()) {
|
if (!transform.is_object()) {
|
||||||
return false;
|
return "transform must be an object";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version 1 wrote an empty placeholder transform object.
|
// Version 1 wrote an empty placeholder transform object.
|
||||||
if (version <= 1 && transform.empty()) {
|
if (version <= 1 && transform.empty()) {
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transform.contains("position") || !transform.contains("rotation") ||
|
for (const char* key : {"position", "rotation", "scale"}) {
|
||||||
!transform.contains("scale")) {
|
if (!transform.contains(key)) {
|
||||||
return false;
|
return std::string{"transform is missing \""} + key + "\"";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsFiniteArray(transform.at("position"), 3) ||
|
if (!IsFiniteArray(transform.at("position"), 3)) {
|
||||||
!IsFiniteArray(transform.at("scale"), 3)) {
|
return "transform.position must be 3 finite numbers";
|
||||||
return false;
|
}
|
||||||
|
if (!IsFiniteArray(transform.at("scale"), 3)) {
|
||||||
|
return "transform.scale must be 3 finite numbers";
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& rotation = transform.at("rotation");
|
const auto& rotation = transform.at("rotation");
|
||||||
return IsFiniteArray(rotation, 3) || IsFiniteArray(rotation, 4);
|
if (!IsFiniteArray(rotation, 3) && !IsFiniteArray(rotation, 4)) {
|
||||||
|
return "transform.rotation must be 3 (euler) or 4 (quaternion) finite numbers";
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool ValidateSceneJson(const json& root) {
|
[[nodiscard]] ValidationError ValidateComponents(const json& objectJson) {
|
||||||
if (!root.is_object() || !root.contains("objects") || !root.at("objects").is_array()) {
|
if (!objectJson.contains("components")) {
|
||||||
return false;
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& components = objectJson.at("components");
|
||||||
|
if (!components.is_array()) {
|
||||||
|
return "components must be an array";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < components.size(); ++i) {
|
||||||
|
const auto& componentJson = components.at(i);
|
||||||
|
const std::string where = "components[" + std::to_string(i) + "]";
|
||||||
|
|
||||||
|
if (!componentJson.is_object() || !componentJson.contains("type") ||
|
||||||
|
!componentJson.at("type").is_string()) {
|
||||||
|
return where + " must be an object with a string \"type\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string type = componentJson.at("type").get<std::string>();
|
||||||
|
const std::string named = where + " " + type;
|
||||||
|
|
||||||
|
if (componentJson.contains("enabled") && !componentJson.at("enabled").is_boolean()) {
|
||||||
|
return named + ": \"enabled\" must be a boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentJson.contains("data")) {
|
||||||
|
const auto& data = componentJson.at("data");
|
||||||
|
if (!data.is_object()) {
|
||||||
|
return named + ": \"data\" must be an object, got " + std::string{data.type_name()};
|
||||||
|
}
|
||||||
|
if (auto bad = FindNonFinite(data, "data")) {
|
||||||
|
return named + ": " + *bad + " is not a finite number";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ComponentFactory::IsRegistered(type)) {
|
||||||
|
return named + ": unknown component type";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] ValidationError ValidateSceneJson(const json& root) {
|
||||||
|
if (!root.is_object()) {
|
||||||
|
return "root must be an object";
|
||||||
|
}
|
||||||
|
if (!root.contains("objects") || !root.at("objects").is_array()) {
|
||||||
|
return "root must contain an \"objects\" array";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.contains("name") && !root.at("name").is_string()) {
|
if (root.contains("name") && !root.at("name").is_string()) {
|
||||||
return false;
|
return "\"name\" must be a string";
|
||||||
}
|
}
|
||||||
|
|
||||||
int version = 1;
|
int version = 1;
|
||||||
if (root.contains("version")) {
|
if (root.contains("version")) {
|
||||||
if (!root.at("version").is_number_integer()) {
|
if (!root.at("version").is_number_integer()) {
|
||||||
return false;
|
return "\"version\" must be an integer";
|
||||||
}
|
}
|
||||||
version = root.at("version").get<int>();
|
version = root.at("version").get<int>();
|
||||||
if (version < 1 || version > 2) {
|
if (version < 1 || version > CurrentSceneVersion) {
|
||||||
return false;
|
return "unsupported scene version " + std::to_string(version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.contains("currentCamera") && !root.at("currentCamera").is_number_unsigned()) {
|
||||||
|
return "\"currentCamera\" must be an object id";
|
||||||
|
}
|
||||||
|
|
||||||
std::unordered_set<ObjectId> ids;
|
std::unordered_set<ObjectId> ids;
|
||||||
std::unordered_map<ObjectId, ObjectId> parents;
|
std::unordered_map<ObjectId, ObjectId> parents;
|
||||||
|
std::unordered_map<ObjectId, std::string> descriptions;
|
||||||
|
|
||||||
for (const auto& objectJson : root.at("objects")) {
|
const auto& objects = root.at("objects");
|
||||||
if (!objectJson.is_object() || !objectJson.contains("id")) {
|
for (std::size_t index = 0; index < objects.size(); ++index) {
|
||||||
return false;
|
const auto& objectJson = objects.at(index);
|
||||||
|
const std::string where = DescribeObject(index, objectJson);
|
||||||
|
|
||||||
|
if (!objectJson.is_object()) {
|
||||||
|
return where + " must be an object";
|
||||||
|
}
|
||||||
|
if (!objectJson.contains("id") || !objectJson.at("id").is_number_unsigned()) {
|
||||||
|
return where + " must have a non-negative integer \"id\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
const ObjectId id = objectJson.at("id").get<ObjectId>();
|
||||||
if (id == InvalidObjectId ||
|
if (id == InvalidObjectId || id >= std::numeric_limits<ObjectId>::max() - 1) {
|
||||||
id >= std::numeric_limits<ObjectId>::max() - 1 ||
|
return where + " uses reserved id " + std::to_string(id);
|
||||||
!ids.insert(id).second) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
if (!ids.insert(id).second) {
|
||||||
|
return where + " reuses id " + std::to_string(id);
|
||||||
|
}
|
||||||
|
descriptions.emplace(id, where);
|
||||||
|
|
||||||
if (objectJson.contains("name") && !objectJson.at("name").is_string()) {
|
if (objectJson.contains("name") && !objectJson.at("name").is_string()) {
|
||||||
return false;
|
return where + ": \"name\" must be a string";
|
||||||
}
|
}
|
||||||
if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) {
|
if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) {
|
||||||
return false;
|
return where + ": \"active\" must be a boolean";
|
||||||
}
|
}
|
||||||
if (!ValidateTransform(objectJson, version)) {
|
if (auto error = ValidateTransform(objectJson, version)) {
|
||||||
return false;
|
return where + ": " + *error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObjectId parent = objectJson.value("parent", InvalidObjectId);
|
ObjectId parent = InvalidObjectId;
|
||||||
|
if (objectJson.contains("parent")) {
|
||||||
|
if (!objectJson.at("parent").is_number_unsigned()) {
|
||||||
|
return where + ": \"parent\" must be an object id";
|
||||||
|
}
|
||||||
|
parent = objectJson.at("parent").get<ObjectId>();
|
||||||
|
}
|
||||||
if (parent == id) {
|
if (parent == id) {
|
||||||
return false;
|
return where + " is its own parent";
|
||||||
}
|
}
|
||||||
parents.emplace(id, parent);
|
parents.emplace(id, parent);
|
||||||
|
|
||||||
if (objectJson.contains("components")) {
|
if (auto error = ValidateComponents(objectJson)) {
|
||||||
const auto& components = objectJson.at("components");
|
return where + " " + *error;
|
||||||
if (!components.is_array()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& componentJson : components) {
|
|
||||||
if (!componentJson.is_object() || !componentJson.contains("type") ||
|
|
||||||
!componentJson.at("type").is_string()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentJson.contains("enabled") &&
|
|
||||||
!componentJson.at("enabled").is_boolean()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentJson.contains("data") &&
|
|
||||||
(!componentJson.at("data").is_object() ||
|
|
||||||
!IsFiniteJson(componentJson.at("data")))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ComponentFactory::IsRegistered(componentJson.at("type").get<std::string>())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [id, parent] : parents) {
|
for (const auto& [id, parent] : parents) {
|
||||||
if (parent != InvalidObjectId && !ids.contains(parent)) {
|
if (parent != InvalidObjectId && !ids.contains(parent)) {
|
||||||
return false;
|
return descriptions.at(id) + " has missing parent id " + std::to_string(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_set<ObjectId> visited;
|
std::unordered_set<ObjectId> visited;
|
||||||
ObjectId current = id;
|
ObjectId current = id;
|
||||||
while (current != InvalidObjectId) {
|
while (current != InvalidObjectId) {
|
||||||
if (!visited.insert(current).second) {
|
if (!visited.insert(current).second) {
|
||||||
return false;
|
return descriptions.at(id) + " is part of a parent cycle";
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto parentIt = parents.find(current);
|
const auto parentIt = parents.find(current);
|
||||||
if (parentIt == parents.end()) {
|
if (parentIt == parents.end()) {
|
||||||
return false;
|
return descriptions.at(id) + " has missing ancestor id " + std::to_string(current);
|
||||||
}
|
}
|
||||||
current = parentIt->second;
|
current = parentIt->second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool AssetFileExists(const std::string& path) {
|
[[nodiscard]] bool AssetFileExists(const std::string& path) {
|
||||||
if (path.empty()) {
|
if (path.empty() || PrimitiveMeshes::IsPrimitiveKey(path)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +289,8 @@ namespace {
|
|||||||
|
|
||||||
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
|
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
|
||||||
const auto report = [&](const std::string& objectName, const std::string& detail) {
|
const auto report = [&](const std::string& objectName, const std::string& detail) {
|
||||||
std::cerr << "Scene asset warning (" << scenePath << "): " << objectName
|
spdlog::warn("Scene asset warning ({}): {} references \"{}\" which could not be found",
|
||||||
<< " references \"" << detail << "\" which could not be found\n";
|
scenePath.string(), objectName, detail);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const auto& objectJson : root.at("objects")) {
|
for (const auto& objectJson : root.at("objects")) {
|
||||||
@@ -247,7 +311,10 @@ namespace {
|
|||||||
const std::string cacheKey = data.at(key).get<std::string>();
|
const std::string cacheKey = data.at(key).get<std::string>();
|
||||||
if (cacheKey.empty()) continue;
|
if (cacheKey.empty()) continue;
|
||||||
const auto asset = AssetReference::fromCacheKey(cacheKey);
|
const auto asset = AssetReference::fromCacheKey(cacheKey);
|
||||||
if (asset && !AssetFileExists(asset->path)) {
|
// Runtime-only keys like "BulbMaterial#2" are not file paths
|
||||||
|
const bool looksLikePath = asset &&
|
||||||
|
asset->path.find_first_of(":/\\") != std::string::npos;
|
||||||
|
if (looksLikePath && !AssetFileExists(asset->path)) {
|
||||||
report(objectName, cacheKey);
|
report(objectName, cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,12 +342,44 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool WriteFileAtomically(const std::filesystem::path& path, const std::string& contents) {
|
||||||
|
std::filesystem::path tempPath = path;
|
||||||
|
tempPath += ".tmp";
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream file(tempPath, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
spdlog::error("Failed to open scene file for writing: {}", tempPath.string());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file << contents;
|
||||||
|
file.flush();
|
||||||
|
if (!file.good()) {
|
||||||
|
spdlog::error("Failed to write scene file: {}", tempPath.string());
|
||||||
|
file.close();
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(tempPath, ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::rename(tempPath, path, error);
|
||||||
|
if (error) {
|
||||||
|
spdlog::error("Failed to replace scene file {}: {}", path.string(), error.message());
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(tempPath, ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||||
if (scene.IsIterating()) {
|
if (scene.IsIterating()) {
|
||||||
std::cerr << "Cannot save a scene during an update or render phase: "
|
spdlog::error("Cannot save a scene during an update or render phase: {}", scene.GetName());
|
||||||
<< scene.GetName() << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,13 +387,12 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
try {
|
try {
|
||||||
scene.CommitPendingAdditions();
|
scene.CommitPendingAdditions();
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
std::cerr << "Failed to commit scene additions before save: "
|
spdlog::error("Failed to commit scene additions before save: {}", exception.what());
|
||||||
<< exception.what() << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
json root;
|
json root;
|
||||||
root["version"] = 2;
|
root["version"] = CurrentSceneVersion;
|
||||||
root["name"] = scene.GetName();
|
root["name"] = scene.GetName();
|
||||||
root["objects"] = json::array();
|
root["objects"] = json::array();
|
||||||
if (scene.m_CurrentCameraId != InvalidObjectId) {
|
if (scene.m_CurrentCameraId != InvalidObjectId) {
|
||||||
@@ -302,9 +400,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
}
|
}
|
||||||
const SceneEnvironment& environment = scene.GetEnvironment();
|
const SceneEnvironment& environment = scene.GetEnvironment();
|
||||||
root["environment"] = {
|
root["environment"] = {
|
||||||
{"ambientColor", {environment.ambientColor.x, environment.ambientColor.y, environment.ambientColor.z}},
|
{"ambientColor", environment.ambientColor},
|
||||||
{"ambientIntensity", environment.ambientIntensity},
|
{"ambientIntensity", environment.ambientIntensity},
|
||||||
{"fogColor", {environment.fogColor.x, environment.fogColor.y, environment.fogColor.z}},
|
{"fogColor", environment.fogColor},
|
||||||
{"fogDensity", environment.fogDensity},
|
{"fogDensity", environment.fogDensity},
|
||||||
{"skyboxRotationSpeed", environment.skyboxRotationSpeed},
|
{"skyboxRotationSpeed", environment.skyboxRotationSpeed},
|
||||||
{"skyboxRotation", environment.skyboxRotation},
|
{"skyboxRotation", environment.skyboxRotation},
|
||||||
@@ -318,23 +416,19 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
const GameObject& object = *objectPtr;
|
const GameObject& object = *objectPtr;
|
||||||
if (object.GetComponent<Rigidbody>() != nullptr &&
|
if (object.GetComponent<Rigidbody>() != nullptr &&
|
||||||
object.GetComponent<Collider>() == nullptr) {
|
object.GetComponent<Collider>() == nullptr) {
|
||||||
std::cerr << "Cannot save object with a Rigidbody but no Collider: "
|
spdlog::error("Cannot save object with a Rigidbody but no Collider: {}", object.GetName());
|
||||||
<< object.GetName() << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Transform& transform = object.GetTransform();
|
const Transform& transform = object.GetTransform();
|
||||||
const glm::vec3& position = transform.GetLocalPosition();
|
|
||||||
const glm::quat& rotation = transform.GetLocalRotation();
|
|
||||||
const glm::vec3& scale = transform.GetLocalScale();
|
|
||||||
|
|
||||||
json objectJson;
|
json objectJson;
|
||||||
objectJson["id"] = object.GetId();
|
objectJson["id"] = object.GetId();
|
||||||
objectJson["name"] = object.GetName();
|
objectJson["name"] = object.GetName();
|
||||||
objectJson["active"] = object.IsActive();
|
objectJson["active"] = object.IsActive();
|
||||||
objectJson["transform"] = {
|
objectJson["transform"] = {
|
||||||
{"position", {position.x, position.y, position.z}},
|
{"position", transform.GetLocalPosition()},
|
||||||
{"rotation", {rotation.x, rotation.y, rotation.z, rotation.w}},
|
{"rotation", transform.GetLocalRotation()},
|
||||||
{"scale", {scale.x, scale.y, scale.z}}
|
{"scale", transform.GetLocalScale()}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Transform* parent = transform.GetParent();
|
const Transform* parent = transform.GetParent();
|
||||||
@@ -351,51 +445,78 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Component& component = *componentPtr;
|
const Component& component = *componentPtr;
|
||||||
|
json data;
|
||||||
|
try {
|
||||||
|
data = component.Serialize();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to serialize {} on \"{}\": {}",
|
||||||
|
component.GetTypeName(), object.GetName(), exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.is_null()) {
|
||||||
|
data = json::object();
|
||||||
|
}
|
||||||
|
if (auto bad = FindNonFinite(data, "data")) {
|
||||||
|
spdlog::error("Cannot save {} on \"{}\": {} is NaN or Inf",
|
||||||
|
component.GetTypeName(), object.GetName(), *bad);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
objectJson["components"].push_back({
|
objectJson["components"].push_back({
|
||||||
{"type", component.GetTypeName()},
|
{"type", component.GetTypeName()},
|
||||||
{"enabled", component.isEnabled()},
|
{"enabled", component.isEnabled()},
|
||||||
{"data", component.Serialize()}
|
{"data", std::move(data)}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
root["objects"].push_back(std::move(objectJson));
|
root["objects"].push_back(std::move(objectJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ofstream file(path);
|
if (auto bad = FindNonFinite(root, "scene")) {
|
||||||
if (!file.is_open()) {
|
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
||||||
std::cerr << "Failed to open scene file for writing: " << path << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file << root.dump(4);
|
// Refuse to write anything Load would reject
|
||||||
if (!file.good()) {
|
if (auto error = ValidateSceneJson(root)) {
|
||||||
std::cerr << "Failed to write scene file: " << path << '\n';
|
spdlog::error("Cannot save scene \"{}\", it would not load back: {}", scene.GetName(), *error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
std::string contents;
|
||||||
|
try {
|
||||||
|
contents = root.dump(4);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Failed to encode scene \"{}\": {}", scene.GetName(), exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WriteFileAtomically(path, contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
|
SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) {
|
||||||
LoadResult result;
|
LoadResult result;
|
||||||
|
RegisterEngineComponents();
|
||||||
|
|
||||||
std::ifstream file(path);
|
std::ifstream file(path);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
result.errorMessage = "Failed to open scene file for reading: " + path.string();
|
result.errorMessage = "Failed to open scene file for reading: " + path.string();
|
||||||
std::cerr << result.errorMessage << '\n';
|
spdlog::error("{}", result.errorMessage);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
file >> result.root;
|
file >> result.root;
|
||||||
if (!ValidateSceneJson(result.root)) {
|
} catch (const std::exception& exception) {
|
||||||
result.errorMessage = "Invalid scene data: " + path.string();
|
result.errorMessage = "Failed to parse scene file " + path.string() + ": " + exception.what();
|
||||||
std::cerr << result.errorMessage << '\n';
|
spdlog::error("{}", result.errorMessage);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (const std::exception& exception) {
|
|
||||||
result.errorMessage = "Failed to parse scene file: " + std::string(exception.what());
|
if (auto error = ValidateSceneJson(result.root)) {
|
||||||
std::cerr << result.errorMessage << '\n';
|
result.errorMessage = "Invalid scene data in " + path.string() + ": " + *error;
|
||||||
|
spdlog::error("{}", result.errorMessage);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,19 +528,17 @@ SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem
|
|||||||
bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||||
std::function<void(float)> progressCallback) {
|
std::function<void(float)> progressCallback) {
|
||||||
if (scene.IsIterating()) {
|
if (scene.IsIterating()) {
|
||||||
std::cerr << "Cannot load a scene during an update or render phase: "
|
spdlog::error("Cannot load a scene during an update or render phase: {}", scene.GetName());
|
||||||
<< scene.GetName() << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
std::cerr << "Cannot construct scene from a failed load result: "
|
spdlog::error("Cannot construct scene from a failed load result: {}", result.errorMessage);
|
||||||
<< result.errorMessage << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.IsBeingUnloaded()) {
|
if (scene.IsBeingUnloaded()) {
|
||||||
std::cerr << "Cannot load a scene that is being unloaded: " << scene.GetName() << '\n';
|
spdlog::error("Cannot load a scene that is being unloaded: {}", scene.GetName());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +555,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
std::unordered_map<ObjectId, GameObject*> idMap;
|
std::unordered_map<ObjectId, GameObject*> idMap;
|
||||||
std::vector<const json*> objectJsonList;
|
std::vector<const json*> objectJsonList;
|
||||||
std::vector<GameObject*> registeredObjects;
|
std::vector<GameObject*> registeredObjects;
|
||||||
|
std::string context = "scene";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const auto& objectJson : root.at("objects")) {
|
for (const auto& objectJson : root.at("objects")) {
|
||||||
@@ -465,42 +585,24 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
for (const json* objectJson : objectJsonList) {
|
for (const json* objectJson : objectJsonList) {
|
||||||
const ObjectId id = objectJson->at("id").get<ObjectId>();
|
const ObjectId id = objectJson->at("id").get<ObjectId>();
|
||||||
GameObject* object = idMap.at(id);
|
GameObject* object = idMap.at(id);
|
||||||
|
context = "\"" + object->GetName() + "\" transform";
|
||||||
|
|
||||||
if (objectJson->contains("transform") &&
|
if (objectJson->contains("transform") &&
|
||||||
!objectJson->at("transform").empty()) {
|
!objectJson->at("transform").empty()) {
|
||||||
const auto& transformJson = objectJson->at("transform");
|
const auto& transformJson = objectJson->at("transform");
|
||||||
const auto& position = transformJson.at("position");
|
|
||||||
const auto& rotation = transformJson.at("rotation");
|
const auto& rotation = transformJson.at("rotation");
|
||||||
const auto& scale = transformJson.at("scale");
|
|
||||||
|
|
||||||
object->GetTransform().SetLocalPosition({
|
object->GetTransform().SetLocalPosition(transformJson.at("position").get<glm::vec3>());
|
||||||
position.at(0).get<float>(),
|
|
||||||
position.at(1).get<float>(),
|
|
||||||
position.at(2).get<float>()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rotation.size() == 4) {
|
if (rotation.size() == 4) {
|
||||||
object->GetTransform().SetLocalRotation({
|
object->GetTransform().SetLocalRotation(rotation.get<glm::quat>());
|
||||||
rotation.at(3).get<float>(),
|
|
||||||
rotation.at(0).get<float>(),
|
|
||||||
rotation.at(1).get<float>(),
|
|
||||||
rotation.at(2).get<float>()
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Version 1 scene files used three Euler angles in
|
// Version 1 scene files used three Euler angles in
|
||||||
// degrees. Continue reading that format safely.
|
// degrees. Continue reading that format safely.
|
||||||
object->GetTransform().SetLocalRotation({
|
object->GetTransform().SetLocalRotation(rotation.get<glm::vec3>());
|
||||||
rotation.at(0).get<float>(),
|
|
||||||
rotation.at(1).get<float>(),
|
|
||||||
rotation.at(2).get<float>()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object->GetTransform().SetLocalScale({
|
object->GetTransform().SetLocalScale(transformJson.at("scale").get<glm::vec3>());
|
||||||
scale.at(0).get<float>(),
|
|
||||||
scale.at(1).get<float>(),
|
|
||||||
scale.at(2).get<float>()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObjectId parentId = objectJson->value("parent", InvalidObjectId);
|
const ObjectId parentId = objectJson->value("parent", InvalidObjectId);
|
||||||
@@ -519,9 +621,10 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
|
|
||||||
for (const auto& componentJson : objectJson->at("components")) {
|
for (const auto& componentJson : objectJson->at("components")) {
|
||||||
const std::string type = componentJson.at("type").get<std::string>();
|
const std::string type = componentJson.at("type").get<std::string>();
|
||||||
|
context = type + " on \"" + object->GetName() + "\"";
|
||||||
Component* component = ComponentFactory::Create(type, *object);
|
Component* component = ComponentFactory::Create(type, *object);
|
||||||
if (component == nullptr) {
|
if (component == nullptr) {
|
||||||
throw std::runtime_error("Unknown component type: " + type);
|
throw std::runtime_error("unknown component type");
|
||||||
}
|
}
|
||||||
|
|
||||||
component->SetEnabled(componentJson.value("enabled", true));
|
component->SetEnabled(componentJson.value("enabled", true));
|
||||||
@@ -540,6 +643,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
|
|
||||||
for (const auto& componentPtr : objectPtr->GetComponents()) {
|
for (const auto& componentPtr : objectPtr->GetComponents()) {
|
||||||
if (componentPtr) {
|
if (componentPtr) {
|
||||||
|
context = componentPtr->GetTypeName() + " on \"" + objectPtr->GetName() + "\" references";
|
||||||
componentPtr->ResolveReferences(idMap);
|
componentPtr->ResolveReferences(idMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,10 +656,10 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
// its physics state can remain intact.
|
// its physics state can remain intact.
|
||||||
for (const auto& objectPtr : stagingScene.GetObjects()) {
|
for (const auto& objectPtr : stagingScene.GetObjects()) {
|
||||||
if (objectPtr) {
|
if (objectPtr) {
|
||||||
|
context = "\"" + objectPtr->GetName() + "\" physics";
|
||||||
if (objectPtr->GetComponent<Rigidbody>() != nullptr &&
|
if (objectPtr->GetComponent<Rigidbody>() != nullptr &&
|
||||||
objectPtr->GetComponent<Collider>() == nullptr) {
|
objectPtr->GetComponent<Collider>() == nullptr) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error("Rigidbody requires a collider");
|
||||||
"Rigidbody requires a collider on object " + objectPtr->GetName());
|
|
||||||
}
|
}
|
||||||
scene.GetPhysics().RegisterGameObject(*objectPtr);
|
scene.GetPhysics().RegisterGameObject(*objectPtr);
|
||||||
registeredObjects.push_back(objectPtr.get());
|
registeredObjects.push_back(objectPtr.get());
|
||||||
@@ -564,7 +668,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
|
|
||||||
if (progressCallback) progressCallback(0.8f);
|
if (progressCallback) progressCallback(0.8f);
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
std::cerr << "Failed to load scene: " << exception.what() << '\n';
|
spdlog::error("Failed to load scene ({}): {}", context, exception.what());
|
||||||
for (GameObject* object : registeredObjects) {
|
for (GameObject* object : registeredObjects) {
|
||||||
if (object != nullptr) {
|
if (object != nullptr) {
|
||||||
scene.GetPhysics().UnregisterGameObject(*object);
|
scene.GetPhysics().UnregisterGameObject(*object);
|
||||||
@@ -574,6 +678,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool bindingsWereLoaded = scene.m_bindingsLoaded;
|
||||||
scene.UnloadBindings();
|
scene.UnloadBindings();
|
||||||
scene.RemoveAll();
|
scene.RemoveAll();
|
||||||
scene.m_objects = std::move(stagingScene.m_objects);
|
scene.m_objects = std::move(stagingScene.m_objects);
|
||||||
@@ -581,23 +686,29 @@ 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_SelectedObjectId = InvalidObjectId;
|
||||||
scene.m_CurrentCameraId = root.value("currentCamera", InvalidObjectId);
|
scene.m_CurrentCameraId = root.value("currentCamera", InvalidObjectId);
|
||||||
|
if (scene.m_CurrentCameraId != InvalidObjectId && !idMap.contains(scene.m_CurrentCameraId)) {
|
||||||
|
spdlog::warn("Scene \"{}\" current camera id {} does not exist, ignoring it",
|
||||||
|
scene.m_name, scene.m_CurrentCameraId);
|
||||||
|
scene.m_CurrentCameraId = InvalidObjectId;
|
||||||
|
}
|
||||||
scene.m_Environment = SceneEnvironment{};
|
scene.m_Environment = SceneEnvironment{};
|
||||||
if (root.contains("environment") && root.at("environment").is_object()) {
|
if (root.contains("environment") && root.at("environment").is_object()) {
|
||||||
const auto& data = root.at("environment");
|
const auto& data = root.at("environment");
|
||||||
SceneEnvironment& environment = scene.m_Environment;
|
SceneEnvironment& environment = scene.m_Environment;
|
||||||
const auto readColor = [&data](const char* key, glm::vec3& color) {
|
try {
|
||||||
if (data.contains(key) && data.at(key).is_array() && data.at(key).size() == 3) {
|
environment.ambientColor = data.value("ambientColor", environment.ambientColor);
|
||||||
const auto& value = data.at(key);
|
|
||||||
color = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
readColor("ambientColor", environment.ambientColor);
|
|
||||||
environment.ambientIntensity = data.value("ambientIntensity", environment.ambientIntensity);
|
environment.ambientIntensity = data.value("ambientIntensity", environment.ambientIntensity);
|
||||||
readColor("fogColor", environment.fogColor);
|
environment.fogColor = data.value("fogColor", environment.fogColor);
|
||||||
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
|
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
|
||||||
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
|
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
|
||||||
environment.skyboxRotation = data.value("skyboxRotation", environment.skyboxRotation);
|
environment.skyboxRotation = data.value("skyboxRotation", environment.skyboxRotation);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::warn("Scene \"{}\" has invalid environment settings, using defaults: {}",
|
||||||
|
scene.m_name, exception.what());
|
||||||
|
environment = SceneEnvironment{};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) progressCallback(0.9f);
|
if (progressCallback) progressCallback(0.9f);
|
||||||
@@ -613,6 +724,14 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bindingsWereLoaded) {
|
||||||
|
try {
|
||||||
|
scene.LoadBindings();
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::error("Scene \"{}\" loaded but its bindings failed: {}", scene.m_name, exception.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (progressCallback) progressCallback(1.0f);
|
if (progressCallback) progressCallback(1.0f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ void Door::ApplyPosition()
|
|||||||
nlohmann::json Door::Serialize() const
|
nlohmann::json Door::Serialize() const
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
{"openOffset", {m_OpenOffset.x, m_OpenOffset.y, m_OpenOffset.z}},
|
{"openOffset", m_OpenOffset},
|
||||||
{"openDuration", m_OpenDuration},
|
{"openDuration", m_OpenDuration},
|
||||||
{"toggleable", m_Toggleable},
|
{"toggleable", m_Toggleable},
|
||||||
{"open", m_IsOpen},
|
{"open", m_IsOpen},
|
||||||
@@ -70,10 +71,7 @@ nlohmann::json Door::Serialize() const
|
|||||||
|
|
||||||
void Door::Deserialize(const nlohmann::json& data)
|
void Door::Deserialize(const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (data.contains("openOffset")) {
|
m_OpenOffset = data.value("openOffset", m_OpenOffset);
|
||||||
const auto& offset = data.at("openOffset");
|
|
||||||
m_OpenOffset = {offset.at(0).get<float>(), offset.at(1).get<float>(), offset.at(2).get<float>()};
|
|
||||||
}
|
|
||||||
m_OpenDuration = std::max(data.value("openDuration", m_OpenDuration), 0.0f);
|
m_OpenDuration = std::max(data.value("openDuration", m_OpenDuration), 0.0f);
|
||||||
m_Toggleable = data.value("toggleable", m_Toggleable);
|
m_Toggleable = data.value("toggleable", m_Toggleable);
|
||||||
m_IsOpen = data.value("open", m_IsOpen);
|
m_IsOpen = data.value("open", m_IsOpen);
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
#include <components/GameComponentRegistry.h>
|
#include <components/GameComponentRegistry.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
|
|
||||||
#include <components/GameComponentList.h>
|
#include <components/GameComponentList.h>
|
||||||
|
|
||||||
void RegisterGameComponents()
|
void RegisterGameComponents()
|
||||||
{
|
{
|
||||||
static bool registered = false;
|
static std::once_flag registered;
|
||||||
if (registered) {
|
std::call_once(registered, [] {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registered = true;
|
|
||||||
|
|
||||||
#define LIGHTKEEPER_REGISTER_COMPONENT(ComponentType) \
|
#define LIGHTKEEPER_REGISTER_COMPONENT(ComponentType) \
|
||||||
ComponentFactory::Register<ComponentType>(#ComponentType);
|
ComponentFactory::Register<ComponentType>(#ComponentType);
|
||||||
LIGHTKEEPER_GAME_COMPONENTS(LIGHTKEEPER_REGISTER_COMPONENT)
|
LIGHTKEEPER_GAME_COMPONENTS(LIGHTKEEPER_REGISTER_COMPONENT)
|
||||||
#undef LIGHTKEEPER_REGISTER_COMPONENT
|
#undef LIGHTKEEPER_REGISTER_COMPONENT
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ glm::vec3 MovingPlatform::GetOffsetAt(float progress) const
|
|||||||
nlohmann::json MovingPlatform::Serialize() const
|
nlohmann::json MovingPlatform::Serialize() const
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
{"travelOffset", {m_TravelOffset.x, m_TravelOffset.y, m_TravelOffset.z}},
|
{"travelOffset", m_TravelOffset},
|
||||||
{"travelDuration", m_TravelDuration},
|
{"travelDuration", m_TravelDuration},
|
||||||
{"pauseDuration", m_PauseDuration},
|
{"pauseDuration", m_PauseDuration},
|
||||||
{"elapsedTime", m_ElapsedTime},
|
{"elapsedTime", m_ElapsedTime},
|
||||||
@@ -61,10 +62,7 @@ nlohmann::json MovingPlatform::Serialize() const
|
|||||||
|
|
||||||
void MovingPlatform::Deserialize(const nlohmann::json& data)
|
void MovingPlatform::Deserialize(const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
if (data.contains("travelOffset")) {
|
m_TravelOffset = data.value("travelOffset", m_TravelOffset);
|
||||||
const auto& offset = data.at("travelOffset");
|
|
||||||
m_TravelOffset = {offset.at(0).get<float>(), offset.at(1).get<float>(), offset.at(2).get<float>()};
|
|
||||||
}
|
|
||||||
m_TravelDuration = std::max(data.value("travelDuration", m_TravelDuration), 0.01f);
|
m_TravelDuration = std::max(data.value("travelDuration", m_TravelDuration), 0.01f);
|
||||||
m_PauseDuration = std::max(data.value("pauseDuration", m_PauseDuration), 0.0f);
|
m_PauseDuration = std::max(data.value("pauseDuration", m_PauseDuration), 0.0f);
|
||||||
m_ElapsedTime = std::max(data.value("elapsedTime", m_ElapsedTime), 0.0f);
|
m_ElapsedTime = std::max(data.value("elapsedTime", m_ElapsedTime), 0.0f);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ void PrintComponent::ImGuiInspector()
|
|||||||
|
|
||||||
nlohmann::json PrintComponent::Serialize() const
|
nlohmann::json PrintComponent::Serialize() const
|
||||||
{
|
{
|
||||||
return {"message", m_message};
|
return {{"message", m_message}};
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintComponent::Deserialize(const nlohmann::json& basic_jsons)
|
void PrintComponent::Deserialize(const nlohmann::json& basic_jsons)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
#include <destrum/Serialization/ComponentFactory.h>
|
#include <destrum/Serialization/ComponentFactory.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
#include <destrum/Physics/SimplePhysicsWorld.h>
|
#include <destrum/Physics/SimplePhysicsWorld.h>
|
||||||
#include <destrum/Physics/JoltPhysicsWorld.h>
|
#include <destrum/Physics/JoltPhysicsWorld.h>
|
||||||
@@ -1341,6 +1342,137 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
std::string readFile(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSceneSaveRejectsNonFinite()
|
||||||
|
{
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("non-finite");
|
||||||
|
GameObject* object = scene.CreateGameObject("object");
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_non_finite_scene.json";
|
||||||
|
check(SceneSerializer::Save(scene, path), "a finite scene must save");
|
||||||
|
const std::string goodContents = readFile(path);
|
||||||
|
|
||||||
|
object->GetTransform().SetLocalPosition({std::nanf(""), 0.0f, 0.0f});
|
||||||
|
check(!SceneSerializer::Save(scene, path), "a scene with NaN values must not save");
|
||||||
|
check(readFile(path) == goodContents, "a failed save must leave the previous file untouched");
|
||||||
|
check(!std::filesystem::exists(std::filesystem::path{path} += ".tmp"),
|
||||||
|
"a failed save must not leave its temp file behind");
|
||||||
|
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSceneValidationErrors()
|
||||||
|
{
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_invalid_scene.json";
|
||||||
|
std::ofstream{path}
|
||||||
|
<< R"({"version":2,"name":"bad","objects":[{"id":7,"name":"Lamp","transform":{"position":[0,0,0],"rotation":[0,0,0,1],"scale":[1,1,1]},"parent":0,"components":[{"type":"NotAComponent","enabled":true,"data":{}}]}]})";
|
||||||
|
|
||||||
|
const auto result = SceneSerializer::LoadSceneFile(path);
|
||||||
|
check(!result.success, "a scene with an unknown component must not load");
|
||||||
|
check(result.errorMessage.find("\"Lamp\"") != std::string::npos &&
|
||||||
|
result.errorMessage.find("NotAComponent") != std::string::npos &&
|
||||||
|
result.errorMessage.find("unknown component type") != std::string::npos,
|
||||||
|
"scene validation errors must name the object, component and problem");
|
||||||
|
|
||||||
|
std::ofstream{path}
|
||||||
|
<< R"({"version":2,"objects":[{"id":7,"name":"Lamp","components":[{"type":"Spinner","data":["message","hi"]}]}]})";
|
||||||
|
const auto arrayData = SceneSerializer::LoadSceneFile(path);
|
||||||
|
check(!arrayData.success && arrayData.errorMessage.find("must be an object") != std::string::npos,
|
||||||
|
"component data that is not an object must be rejected with a reason");
|
||||||
|
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSceneLoadRestoresBindings()
|
||||||
|
{
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("bindings");
|
||||||
|
int registered = 0;
|
||||||
|
int unregistered = 0;
|
||||||
|
scene.SetRegisterBindings([®istered] { ++registered; });
|
||||||
|
scene.SetUnregisterBindings([&unregistered] { ++unregistered; });
|
||||||
|
scene.Load();
|
||||||
|
const int registeredBefore = registered;
|
||||||
|
const int unregisteredBefore = unregistered;
|
||||||
|
scene.CreateGameObject("object");
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_bindings_scene.json";
|
||||||
|
check(SceneSerializer::Save(scene, path), "the bindings scene must save");
|
||||||
|
check(SceneSerializer::Load(scene, path), "the bindings scene must load");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
check(registered - registeredBefore == 1 && unregistered - unregisteredBefore == 1, "loading a scene in place must register its bindings again");
|
||||||
|
|
||||||
|
scene.SetRegisterBindings(nullptr);
|
||||||
|
scene.SetUnregisterBindings(nullptr);
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSceneLoadDropsMissingCamera()
|
||||||
|
{
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("missing camera");
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_missing_camera_scene.json";
|
||||||
|
std::ofstream{path}
|
||||||
|
<< R"({"version":2,"currentCamera":4242,"objects":[{"id":11,"name":"object"}]})";
|
||||||
|
|
||||||
|
check(SceneSerializer::Load(scene, path), "a scene with a dangling camera id must still load");
|
||||||
|
check(scene.GetCurrentCamera() == nullptr, "a dangling current camera id must be ignored");
|
||||||
|
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testGlmJson()
|
||||||
|
{
|
||||||
|
const nlohmann::json vec = glm::vec3{1.0f, 2.5f, -3.0f};
|
||||||
|
check(vec == nlohmann::json::array({1.0f, 2.5f, -3.0f}), "vec3 must serialize as a 3 number array");
|
||||||
|
check(vec.get<glm::vec3>() == glm::vec3{1.0f, 2.5f, -3.0f}, "vec3 must round-trip through json");
|
||||||
|
|
||||||
|
const glm::quat rotation = glm::angleAxis(0.5f, glm::vec3{0.0f, 1.0f, 0.0f});
|
||||||
|
const nlohmann::json quat = rotation;
|
||||||
|
check(quat.size() == 4 && close(quat.at(3).get<float>(), rotation.w),
|
||||||
|
"quaternions must serialize as [x, y, z, w]");
|
||||||
|
check(quat.get<glm::quat>() == rotation, "quaternions must round-trip through json");
|
||||||
|
|
||||||
|
bool rejected = false;
|
||||||
|
try {
|
||||||
|
(void)nlohmann::json::array({1.0f, 2.0f}).get<glm::vec3>();
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
rejected = true;
|
||||||
|
}
|
||||||
|
check(rejected, "a vec3 must reject arrays of the wrong size");
|
||||||
|
|
||||||
|
const nlohmann::json data = {{"axis", nlohmann::json::array({0, 1, 0})}};
|
||||||
|
check(data.value("axis", glm::vec3{0.0f}) == glm::vec3{0.0f, 1.0f, 0.0f} &&
|
||||||
|
data.value("missing", glm::vec3{7.0f}) == glm::vec3{7.0f},
|
||||||
|
"json value() must work with glm defaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testPrimitiveMeshKeys()
|
||||||
|
{
|
||||||
|
const glm::vec3 size{0.35f, 0.12f, 3.0f};
|
||||||
|
const std::string key = PrimitiveMeshes::BoxKey(size);
|
||||||
|
check(PrimitiveMeshes::IsPrimitiveKey(key), "box keys must be primitive keys");
|
||||||
|
check(PrimitiveMeshes::MakeBox(size).name == key, "boxes must be named by their key");
|
||||||
|
|
||||||
|
const auto rebuilt = PrimitiveMeshes::MakeFromKey(key + "#3");
|
||||||
|
check(rebuilt.has_value() && rebuilt->maxPos == size * 0.5f,
|
||||||
|
"a box must rebuild exactly from its key, ignoring the duplicate suffix");
|
||||||
|
check(PrimitiveMeshes::StripDuplicateSuffix("BulbMaterial#12") == "BulbMaterial" &&
|
||||||
|
PrimitiveMeshes::StripDuplicateSuffix("game://a.glb#mesh") == "game://a.glb#mesh",
|
||||||
|
"only numeric duplicate suffixes must be stripped");
|
||||||
|
check(!PrimitiveMeshes::MakeFromKey("prim://box?size=1,2").has_value() &&
|
||||||
|
!PrimitiveMeshes::MakeFromKey("prim://box?size=1,-2,3").has_value() &&
|
||||||
|
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
|
||||||
|
"invalid primitive keys must not build a mesh");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -1380,6 +1512,12 @@ int main()
|
|||||||
testKinematicBodiesMoveWithVelocity();
|
testKinematicBodiesMoveWithVelocity();
|
||||||
testRenderer2DShapes();
|
testRenderer2DShapes();
|
||||||
testFontAtlasAndLayout();
|
testFontAtlasAndLayout();
|
||||||
|
testSceneSaveRejectsNonFinite();
|
||||||
|
testSceneValidationErrors();
|
||||||
|
testSceneLoadRestoresBindings();
|
||||||
|
testSceneLoadDropsMissingCamera();
|
||||||
|
testGlmJson();
|
||||||
|
testPrimitiveMeshKeys();
|
||||||
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