feat: add vec3 type to nhollman json / fix: scene serialisation stuff
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
@@ -20,12 +21,18 @@
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
using ValidationError = std::optional<std::string>;
|
||||
|
||||
constexpr int CurrentSceneVersion = 2;
|
||||
|
||||
[[nodiscard]] bool IsFiniteNumber(const json& value) {
|
||||
if (!value.is_number()) {
|
||||
return false;
|
||||
@@ -48,164 +55,221 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsFiniteJson(const json& value) {
|
||||
if (value.is_number()) {
|
||||
return IsFiniteNumber(value);
|
||||
// Returns the JSON path of the first NaN/Inf, nlohmann would write it as null
|
||||
[[nodiscard]] std::optional<std::string> FindNonFinite(const json& value, const std::string& path) {
|
||||
if (value.is_number_float()) {
|
||||
if (!std::isfinite(value.get<double>())) {
|
||||
return path;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
if (value.is_array()) {
|
||||
for (const auto& element : value) {
|
||||
if (!IsFiniteJson(element)) {
|
||||
return false;
|
||||
for (std::size_t i = 0; i < value.size(); ++i) {
|
||||
if (auto bad = FindNonFinite(value.at(i), path + "[" + std::to_string(i) + "]")) {
|
||||
return bad;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (value.is_object()) {
|
||||
for (auto it = value.begin(); it != value.end(); ++it) {
|
||||
if (!IsFiniteJson(it.value())) {
|
||||
return false;
|
||||
if (auto bad = FindNonFinite(it.value(), path + "." + it.key())) {
|
||||
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")) {
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto& transform = objectJson.at("transform");
|
||||
if (!transform.is_object()) {
|
||||
return false;
|
||||
return "transform must be an object";
|
||||
}
|
||||
|
||||
// Version 1 wrote an empty placeholder transform object.
|
||||
if (version <= 1 && transform.empty()) {
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!transform.contains("position") || !transform.contains("rotation") ||
|
||||
!transform.contains("scale")) {
|
||||
return false;
|
||||
for (const char* key : {"position", "rotation", "scale"}) {
|
||||
if (!transform.contains(key)) {
|
||||
return std::string{"transform is missing \""} + key + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsFiniteArray(transform.at("position"), 3) ||
|
||||
!IsFiniteArray(transform.at("scale"), 3)) {
|
||||
return false;
|
||||
if (!IsFiniteArray(transform.at("position"), 3)) {
|
||||
return "transform.position must be 3 finite numbers";
|
||||
}
|
||||
if (!IsFiniteArray(transform.at("scale"), 3)) {
|
||||
return "transform.scale must be 3 finite numbers";
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!root.is_object() || !root.contains("objects") || !root.at("objects").is_array()) {
|
||||
return false;
|
||||
[[nodiscard]] ValidationError ValidateComponents(const json& objectJson) {
|
||||
if (!objectJson.contains("components")) {
|
||||
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()) {
|
||||
return false;
|
||||
return "\"name\" must be a string";
|
||||
}
|
||||
|
||||
int version = 1;
|
||||
if (root.contains("version")) {
|
||||
if (!root.at("version").is_number_integer()) {
|
||||
return false;
|
||||
return "\"version\" must be an integer";
|
||||
}
|
||||
version = root.at("version").get<int>();
|
||||
if (version < 1 || version > 2) {
|
||||
return false;
|
||||
if (version < 1 || version > CurrentSceneVersion) {
|
||||
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_map<ObjectId, ObjectId> parents;
|
||||
std::unordered_map<ObjectId, std::string> descriptions;
|
||||
|
||||
for (const auto& objectJson : root.at("objects")) {
|
||||
if (!objectJson.is_object() || !objectJson.contains("id")) {
|
||||
return false;
|
||||
const auto& objects = root.at("objects");
|
||||
for (std::size_t index = 0; index < objects.size(); ++index) {
|
||||
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>();
|
||||
if (id == InvalidObjectId ||
|
||||
id >= std::numeric_limits<ObjectId>::max() - 1 ||
|
||||
!ids.insert(id).second) {
|
||||
return false;
|
||||
if (id == InvalidObjectId || id >= std::numeric_limits<ObjectId>::max() - 1) {
|
||||
return where + " uses reserved id " + std::to_string(id);
|
||||
}
|
||||
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()) {
|
||||
return false;
|
||||
return where + ": \"name\" must be a string";
|
||||
}
|
||||
if (objectJson.contains("active") && !objectJson.at("active").is_boolean()) {
|
||||
return false;
|
||||
return where + ": \"active\" must be a boolean";
|
||||
}
|
||||
if (!ValidateTransform(objectJson, version)) {
|
||||
return false;
|
||||
if (auto error = ValidateTransform(objectJson, version)) {
|
||||
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) {
|
||||
return false;
|
||||
return where + " is its own parent";
|
||||
}
|
||||
parents.emplace(id, parent);
|
||||
|
||||
if (objectJson.contains("components")) {
|
||||
const auto& components = objectJson.at("components");
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (auto error = ValidateComponents(objectJson)) {
|
||||
return where + " " + *error;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [id, parent] : parents) {
|
||||
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;
|
||||
ObjectId current = id;
|
||||
while (current != InvalidObjectId) {
|
||||
if (!visited.insert(current).second) {
|
||||
return false;
|
||||
return descriptions.at(id) + " is part of a parent cycle";
|
||||
}
|
||||
|
||||
const auto parentIt = parents.find(current);
|
||||
if (parentIt == parents.end()) {
|
||||
return false;
|
||||
return descriptions.at(id) + " has missing ancestor id " + std::to_string(current);
|
||||
}
|
||||
current = parentIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool AssetFileExists(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
if (path.empty() || PrimitiveMeshes::IsPrimitiveKey(path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -225,8 +289,8 @@ namespace {
|
||||
|
||||
void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) {
|
||||
const auto report = [&](const std::string& objectName, const std::string& detail) {
|
||||
std::cerr << "Scene asset warning (" << scenePath << "): " << objectName
|
||||
<< " references \"" << detail << "\" which could not be found\n";
|
||||
spdlog::warn("Scene asset warning ({}): {} references \"{}\" which could not be found",
|
||||
scenePath.string(), objectName, detail);
|
||||
};
|
||||
|
||||
for (const auto& objectJson : root.at("objects")) {
|
||||
@@ -247,7 +311,10 @@ namespace {
|
||||
const std::string cacheKey = data.at(key).get<std::string>();
|
||||
if (cacheKey.empty()) continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
if (scene.IsIterating()) {
|
||||
std::cerr << "Cannot save a scene during an update or render phase: "
|
||||
<< scene.GetName() << '\n';
|
||||
spdlog::error("Cannot save a scene during an update or render phase: {}", scene.GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -288,13 +387,12 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
try {
|
||||
scene.CommitPendingAdditions();
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "Failed to commit scene additions before save: "
|
||||
<< exception.what() << '\n';
|
||||
spdlog::error("Failed to commit scene additions before save: {}", exception.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
json root;
|
||||
root["version"] = 2;
|
||||
root["version"] = CurrentSceneVersion;
|
||||
root["name"] = scene.GetName();
|
||||
root["objects"] = json::array();
|
||||
if (scene.m_CurrentCameraId != InvalidObjectId) {
|
||||
@@ -302,9 +400,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
}
|
||||
const SceneEnvironment& environment = scene.GetEnvironment();
|
||||
root["environment"] = {
|
||||
{"ambientColor", {environment.ambientColor.x, environment.ambientColor.y, environment.ambientColor.z}},
|
||||
{"ambientColor", environment.ambientColor},
|
||||
{"ambientIntensity", environment.ambientIntensity},
|
||||
{"fogColor", {environment.fogColor.x, environment.fogColor.y, environment.fogColor.z}},
|
||||
{"fogColor", environment.fogColor},
|
||||
{"fogDensity", environment.fogDensity},
|
||||
{"skyboxRotationSpeed", environment.skyboxRotationSpeed},
|
||||
{"skyboxRotation", environment.skyboxRotation},
|
||||
@@ -318,23 +416,19 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
const GameObject& object = *objectPtr;
|
||||
if (object.GetComponent<Rigidbody>() != nullptr &&
|
||||
object.GetComponent<Collider>() == nullptr) {
|
||||
std::cerr << "Cannot save object with a Rigidbody but no Collider: "
|
||||
<< object.GetName() << '\n';
|
||||
spdlog::error("Cannot save object with a Rigidbody but no Collider: {}", object.GetName());
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
objectJson["id"] = object.GetId();
|
||||
objectJson["name"] = object.GetName();
|
||||
objectJson["active"] = object.IsActive();
|
||||
objectJson["transform"] = {
|
||||
{"position", {position.x, position.y, position.z}},
|
||||
{"rotation", {rotation.x, rotation.y, rotation.z, rotation.w}},
|
||||
{"scale", {scale.x, scale.y, scale.z}}
|
||||
{"position", transform.GetLocalPosition()},
|
||||
{"rotation", transform.GetLocalRotation()},
|
||||
{"scale", transform.GetLocalScale()}
|
||||
};
|
||||
|
||||
const Transform* parent = transform.GetParent();
|
||||
@@ -351,51 +445,78 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
}
|
||||
|
||||
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({
|
||||
{"type", component.GetTypeName()},
|
||||
{"enabled", component.isEnabled()},
|
||||
{"data", component.Serialize()}
|
||||
{"data", std::move(data)}
|
||||
});
|
||||
}
|
||||
|
||||
root["objects"].push_back(std::move(objectJson));
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open scene file for writing: " << path << '\n';
|
||||
if (auto bad = FindNonFinite(root, "scene")) {
|
||||
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
||||
return false;
|
||||
}
|
||||
|
||||
file << root.dump(4);
|
||||
if (!file.good()) {
|
||||
std::cerr << "Failed to write scene file: " << path << '\n';
|
||||
// Refuse to write anything Load would reject
|
||||
if (auto error = ValidateSceneJson(root)) {
|
||||
spdlog::error("Cannot save scene \"{}\", it would not load back: {}", scene.GetName(), *error);
|
||||
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) {
|
||||
LoadResult result;
|
||||
RegisterEngineComponents();
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
result.errorMessage = "Failed to open scene file for reading: " + path.string();
|
||||
std::cerr << result.errorMessage << '\n';
|
||||
spdlog::error("{}", result.errorMessage);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
file >> result.root;
|
||||
if (!ValidateSceneJson(result.root)) {
|
||||
result.errorMessage = "Invalid scene data: " + path.string();
|
||||
std::cerr << result.errorMessage << '\n';
|
||||
return result;
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
result.errorMessage = "Failed to parse scene file: " + std::string(exception.what());
|
||||
std::cerr << result.errorMessage << '\n';
|
||||
result.errorMessage = "Failed to parse scene file " + path.string() + ": " + exception.what();
|
||||
spdlog::error("{}", result.errorMessage);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (auto error = ValidateSceneJson(result.root)) {
|
||||
result.errorMessage = "Invalid scene data in " + path.string() + ": " + *error;
|
||||
spdlog::error("{}", result.errorMessage);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -407,19 +528,17 @@ SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem
|
||||
bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
std::function<void(float)> progressCallback) {
|
||||
if (scene.IsIterating()) {
|
||||
std::cerr << "Cannot load a scene during an update or render phase: "
|
||||
<< scene.GetName() << '\n';
|
||||
spdlog::error("Cannot load a scene during an update or render phase: {}", scene.GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
std::cerr << "Cannot construct scene from a failed load result: "
|
||||
<< result.errorMessage << '\n';
|
||||
spdlog::error("Cannot construct scene from a failed load result: {}", result.errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -436,6 +555,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
std::unordered_map<ObjectId, GameObject*> idMap;
|
||||
std::vector<const json*> objectJsonList;
|
||||
std::vector<GameObject*> registeredObjects;
|
||||
std::string context = "scene";
|
||||
|
||||
try {
|
||||
for (const auto& objectJson : root.at("objects")) {
|
||||
@@ -465,42 +585,24 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
for (const json* objectJson : objectJsonList) {
|
||||
const ObjectId id = objectJson->at("id").get<ObjectId>();
|
||||
GameObject* object = idMap.at(id);
|
||||
context = "\"" + object->GetName() + "\" transform";
|
||||
|
||||
if (objectJson->contains("transform") &&
|
||||
!objectJson->at("transform").empty()) {
|
||||
const auto& transformJson = objectJson->at("transform");
|
||||
const auto& position = transformJson.at("position");
|
||||
const auto& rotation = transformJson.at("rotation");
|
||||
const auto& scale = transformJson.at("scale");
|
||||
|
||||
object->GetTransform().SetLocalPosition({
|
||||
position.at(0).get<float>(),
|
||||
position.at(1).get<float>(),
|
||||
position.at(2).get<float>()
|
||||
});
|
||||
object->GetTransform().SetLocalPosition(transformJson.at("position").get<glm::vec3>());
|
||||
|
||||
if (rotation.size() == 4) {
|
||||
object->GetTransform().SetLocalRotation({
|
||||
rotation.at(3).get<float>(),
|
||||
rotation.at(0).get<float>(),
|
||||
rotation.at(1).get<float>(),
|
||||
rotation.at(2).get<float>()
|
||||
});
|
||||
object->GetTransform().SetLocalRotation(rotation.get<glm::quat>());
|
||||
} else {
|
||||
// Version 1 scene files used three Euler angles in
|
||||
// degrees. Continue reading that format safely.
|
||||
object->GetTransform().SetLocalRotation({
|
||||
rotation.at(0).get<float>(),
|
||||
rotation.at(1).get<float>(),
|
||||
rotation.at(2).get<float>()
|
||||
});
|
||||
object->GetTransform().SetLocalRotation(rotation.get<glm::vec3>());
|
||||
}
|
||||
|
||||
object->GetTransform().SetLocalScale({
|
||||
scale.at(0).get<float>(),
|
||||
scale.at(1).get<float>(),
|
||||
scale.at(2).get<float>()
|
||||
});
|
||||
object->GetTransform().SetLocalScale(transformJson.at("scale").get<glm::vec3>());
|
||||
}
|
||||
|
||||
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")) {
|
||||
const std::string type = componentJson.at("type").get<std::string>();
|
||||
context = type + " on \"" + object->GetName() + "\"";
|
||||
Component* component = ComponentFactory::Create(type, *object);
|
||||
if (component == nullptr) {
|
||||
throw std::runtime_error("Unknown component type: " + type);
|
||||
throw std::runtime_error("unknown component type");
|
||||
}
|
||||
|
||||
component->SetEnabled(componentJson.value("enabled", true));
|
||||
@@ -540,6 +643,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
|
||||
for (const auto& componentPtr : objectPtr->GetComponents()) {
|
||||
if (componentPtr) {
|
||||
context = componentPtr->GetTypeName() + " on \"" + objectPtr->GetName() + "\" references";
|
||||
componentPtr->ResolveReferences(idMap);
|
||||
}
|
||||
}
|
||||
@@ -552,10 +656,10 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
// its physics state can remain intact.
|
||||
for (const auto& objectPtr : stagingScene.GetObjects()) {
|
||||
if (objectPtr) {
|
||||
context = "\"" + objectPtr->GetName() + "\" physics";
|
||||
if (objectPtr->GetComponent<Rigidbody>() != nullptr &&
|
||||
objectPtr->GetComponent<Collider>() == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"Rigidbody requires a collider on object " + objectPtr->GetName());
|
||||
throw std::runtime_error("Rigidbody requires a collider");
|
||||
}
|
||||
scene.GetPhysics().RegisterGameObject(*objectPtr);
|
||||
registeredObjects.push_back(objectPtr.get());
|
||||
@@ -564,7 +668,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
|
||||
if (progressCallback) progressCallback(0.8f);
|
||||
} 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) {
|
||||
if (object != nullptr) {
|
||||
scene.GetPhysics().UnregisterGameObject(*object);
|
||||
@@ -574,6 +678,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bindingsWereLoaded = scene.m_bindingsLoaded;
|
||||
scene.UnloadBindings();
|
||||
scene.RemoveAll();
|
||||
scene.m_objects = std::move(stagingScene.m_objects);
|
||||
@@ -581,23 +686,29 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
if (root.contains("name")) {
|
||||
scene.m_name = root.at("name").get<std::string>();
|
||||
}
|
||||
scene.m_SelectedObjectId = 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{};
|
||||
if (root.contains("environment") && root.at("environment").is_object()) {
|
||||
const auto& data = root.at("environment");
|
||||
SceneEnvironment& environment = scene.m_Environment;
|
||||
const auto readColor = [&data](const char* key, glm::vec3& color) {
|
||||
if (data.contains(key) && data.at(key).is_array() && data.at(key).size() == 3) {
|
||||
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);
|
||||
readColor("fogColor", environment.fogColor);
|
||||
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
|
||||
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
|
||||
environment.skyboxRotation = data.value("skyboxRotation", environment.skyboxRotation);
|
||||
try {
|
||||
environment.ambientColor = data.value("ambientColor", environment.ambientColor);
|
||||
environment.ambientIntensity = data.value("ambientIntensity", environment.ambientIntensity);
|
||||
environment.fogColor = data.value("fogColor", environment.fogColor);
|
||||
environment.fogDensity = data.value("fogDensity", environment.fogDensity);
|
||||
environment.skyboxRotationSpeed = data.value("skyboxRotationSpeed", environment.skyboxRotationSpeed);
|
||||
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);
|
||||
@@ -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);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user