diff --git a/src/esp/assets/ResourceManager.cpp b/src/esp/assets/ResourceManager.cpp index 7cc5faefc7..4755b229a9 100644 --- a/src/esp/assets/ResourceManager.cpp +++ b/src/esp/assets/ResourceManager.cpp @@ -2975,6 +2975,20 @@ void ResourceManager::joinSemanticHierarchy( } } +void ResourceManager::setLightSetup(gfx::LightSetup setup, + const Mn::ResourceKey& key) { + // add lights to recorder keyframe + if (gfxReplayRecorder_) { + gfxReplayRecorder_->clearLightsFromKeyframe(); + for (const auto& light : setup) { + gfxReplayRecorder_->addLightToKeyframe(light); + } + } + + shaderManager_.set(key, std::move(setup), Mn::ResourceDataState::Mutable, + Mn::ResourcePolicy::Manual); +} + std::unique_ptr ResourceManager::createJoinedCollisionMesh( const std::string& filename) const { std::unique_ptr mesh = std::make_unique(); diff --git a/src/esp/assets/ResourceManager.h b/src/esp/assets/ResourceManager.h index f759995326..f506e963f6 100644 --- a/src/esp/assets/ResourceManager.h +++ b/src/esp/assets/ResourceManager.h @@ -427,10 +427,7 @@ class ResourceManager { */ void setLightSetup(gfx::LightSetup setup, const Mn::ResourceKey& key = Mn::ResourceKey{ - DEFAULT_LIGHTING_KEY}) { - shaderManager_.set(key, std::move(setup), Mn::ResourceDataState::Mutable, - Mn::ResourcePolicy::Manual); - } + DEFAULT_LIGHTING_KEY}); /** * @brief Construct a unified @ref MeshData from a loaded asset's collision diff --git a/src/esp/gfx/replay/Keyframe.h b/src/esp/gfx/replay/Keyframe.h index 1c33e2f780..770ecf1c35 100644 --- a/src/esp/gfx/replay/Keyframe.h +++ b/src/esp/gfx/replay/Keyframe.h @@ -55,6 +55,7 @@ struct Keyframe { std::vector> stateUpdates; std::unordered_map userTransforms; + std::vector lights; ESP_SMART_POINTERS(Keyframe) }; diff --git a/src/esp/gfx/replay/Player.cpp b/src/esp/gfx/replay/Player.cpp index 82b5600f47..f4fadac7bb 100644 --- a/src/esp/gfx/replay/Player.cpp +++ b/src/esp/gfx/replay/Player.cpp @@ -27,8 +27,10 @@ Keyframe Player::keyframeFromString(const std::string& keyframe) { return res; } -Player::Player(const LoadAndCreateRenderAssetInstanceCallback& callback) - : loadAndCreateRenderAssetInstanceCallback(callback) {} +Player::Player(const LoadAndCreateRenderAssetInstanceCallback& callback, + const SetLightsCallback& lightsCallback) + : loadAndCreateRenderAssetInstanceCallback(callback), + setLightsCallback(lightsCallback) {} void Player::readKeyframesFromFile(const std::string& filepath) { close(); @@ -166,6 +168,8 @@ void Player::applyKeyframe(const Keyframe& keyframe) { node->setRotation(state.absTransform.rotation); setSemanticIdForSubtree(node, state.semanticId); } + + setLightsCallback(keyframe.lights); } void Player::appendKeyframe(Keyframe&& keyframe) { diff --git a/src/esp/gfx/replay/Player.h b/src/esp/gfx/replay/Player.h index f012370082..42a5c920b9 100644 --- a/src/esp/gfx/replay/Player.h +++ b/src/esp/gfx/replay/Player.h @@ -44,12 +44,14 @@ class Player { std::function; + using SetLightsCallback = std::function; /** * @brief Construct a Player. * @param callback A function to load and create a render asset instance. */ - explicit Player(const LoadAndCreateRenderAssetInstanceCallback& callback); + explicit Player(const LoadAndCreateRenderAssetInstanceCallback& callback, + const SetLightsCallback& lightsCallback); ~Player(); @@ -126,6 +128,8 @@ class Player { LoadAndCreateRenderAssetInstanceCallback loadAndCreateRenderAssetInstanceCallback; + SetLightsCallback setLightsCallback; + int frameIndex_ = -1; std::vector keyframes_; std::map assetInfos_; diff --git a/src/esp/gfx/replay/Recorder.cpp b/src/esp/gfx/replay/Recorder.cpp index 36201bad97..ccfb4aef0e 100644 --- a/src/esp/gfx/replay/Recorder.cpp +++ b/src/esp/gfx/replay/Recorder.cpp @@ -96,6 +96,14 @@ void Recorder::addUserTransformToKeyframe(const std::string& name, getKeyframe().userTransforms[name] = Transform{translation, rotation}; } +void Recorder::addLightToKeyframe(const LightInfo& lightInfo) { + getKeyframe().lights.emplace_back(lightInfo); +} + +void Recorder::clearLightsFromKeyframe() { + getKeyframe().lights.clear(); +} + void Recorder::addLoadsCreationsDeletions(KeyframeIterator begin, KeyframeIterator end, Keyframe* dest) { @@ -180,6 +188,8 @@ void Recorder::updateInstanceStates() { void Recorder::advanceKeyframe() { savedKeyframes_.emplace_back(std::move(currKeyframe_)); currKeyframe_ = Keyframe{}; + // TODO: record light deltas rather than absolute states + currKeyframe_.lights = savedKeyframes_.back().lights; } void Recorder::writeSavedKeyframesToFile(const std::string& filepath, diff --git a/src/esp/gfx/replay/Recorder.h b/src/esp/gfx/replay/Recorder.h index 0108c1794e..52323b38fc 100644 --- a/src/esp/gfx/replay/Recorder.h +++ b/src/esp/gfx/replay/Recorder.h @@ -90,6 +90,18 @@ class Recorder { const Magnum::Vector3& translation, const Magnum::Quaternion& rotation); + /** + * @brief Add a light to the current keyframe. + * + * @param lightInfo Parameters of the light to be added to the keyframe. + */ + void addLightToKeyframe(const LightInfo& lightInfo); + + /** + * @brief Delete all lights from the current keyframe. + */ + void clearLightsFromKeyframe(); + /** * @brief write saved keyframes to file. * @param filepath diff --git a/src/esp/gfx/replay/ReplayManager.cpp b/src/esp/gfx/replay/ReplayManager.cpp index 07f83b9919..8f3f8a54fa 100644 --- a/src/esp/gfx/replay/ReplayManager.cpp +++ b/src/esp/gfx/replay/ReplayManager.cpp @@ -10,7 +10,8 @@ namespace replay { std::shared_ptr ReplayManager::readKeyframesFromFile( const std::string& filepath) { - auto player = std::make_shared(playerCallback_); + auto player = + std::make_shared(playerCallback_, playerLightsCallback_); player->readKeyframesFromFile(filepath); if (player->getNumKeyframes() == 0) { ESP_ERROR(Mn::Debug::Flag::NoSpace) @@ -21,7 +22,8 @@ std::shared_ptr ReplayManager::readKeyframesFromFile( } std::shared_ptr ReplayManager::createEmptyPlayer() { - auto player = std::make_shared(playerCallback_); + auto player = + std::make_shared(playerCallback_, playerLightsCallback_); return player; } diff --git a/src/esp/gfx/replay/ReplayManager.h b/src/esp/gfx/replay/ReplayManager.h index b238a8984f..3059cbae0b 100644 --- a/src/esp/gfx/replay/ReplayManager.h +++ b/src/esp/gfx/replay/ReplayManager.h @@ -41,6 +41,14 @@ class ReplayManager { playerCallback_ = callback; } + /** + * @brief Set a Player light creation callback; this is needed to construct + * Player instances. + */ + void setPlayerLightsCallback(const Player::SetLightsCallback& callback) { + playerLightsCallback_ = callback; + } + /** * @brief Read keyframes from a file and construct a Player. Returns nullptr * if no keyframes could be read. @@ -56,6 +64,7 @@ class ReplayManager { private: std::shared_ptr recorder_; Player::LoadAndCreateRenderAssetInstanceCallback playerCallback_; + Player::SetLightsCallback playerLightsCallback_; ESP_SMART_POINTERS(ReplayManager) }; diff --git a/src/esp/io/JsonEspTypes.cpp b/src/esp/io/JsonEspTypes.cpp index eb79ae6945..14716000a4 100644 --- a/src/esp/io/JsonEspTypes.cpp +++ b/src/esp/io/JsonEspTypes.cpp @@ -49,6 +49,16 @@ JsonGenericValue toJsonValue(const gfx::replay::Keyframe& keyframe, io::addMember(obj, "userTransforms", userTransformsArray, allocator); } + if (!keyframe.lights.empty()) { + JsonGenericValue lightsArray(rapidjson::kArrayType); + for (const auto& light : keyframe.lights) { + JsonGenericValue lightObj(rapidjson::kObjectType); + io::addMember(lightObj, "light", light, allocator); + lightsArray.PushBack(lightObj, allocator); + } + io::addMember(obj, "lights", lightsArray, allocator); + } + return obj; } @@ -98,6 +108,16 @@ bool fromJsonValue(const JsonGenericValue& obj, } } + itr = obj.FindMember("lights"); + if (itr != obj.MemberEnd()) { + const JsonGenericValue& lightsArray = itr->value; + for (const auto& lightObj : lightsArray.GetArray()) { + gfx::LightInfo light; + io::readMember(lightObj, "light", light); + keyframe.lights.emplace_back(std::move(light)); + } + } + return true; } diff --git a/src/esp/io/JsonEspTypes.h b/src/esp/io/JsonEspTypes.h index 731ff6229b..a6b71ea7cf 100644 --- a/src/esp/io/JsonEspTypes.h +++ b/src/esp/io/JsonEspTypes.h @@ -169,6 +169,32 @@ inline bool fromJsonValue(const JsonGenericValue& obj, return true; } +inline JsonGenericValue toJsonValue(const esp::gfx::LightInfo& x, + JsonAllocator& allocator) { + JsonGenericValue obj(rapidjson::kObjectType); + addMember(obj, "vector", x.vector, allocator); + addMember(obj, "color", x.color, allocator); + addMember(obj, "model", x.model, allocator); + return obj; +} + +inline bool fromJsonValue(const JsonGenericValue& obj, esp::gfx::LightInfo& x) { + readMember(obj, "vector", x.vector); + readMember(obj, "color", x.color); + readMember(obj, "model", x.model); + return true; +} + +inline JsonGenericValue toJsonValue(const esp::gfx::LightPositionModel& x, + JsonAllocator& allocator) { + return toJsonValue(static_cast(x), allocator); +} + +inline bool fromJsonValue(const JsonGenericValue& obj, + esp::gfx::LightPositionModel& x) { + return fromJsonValue(obj, (int&)x); // TODO: unsafe cast of external data. +} + JsonGenericValue toJsonValue(const esp::gfx::replay::Keyframe& x, JsonAllocator& allocator); diff --git a/src/esp/io/JsonMagnumTypes.h b/src/esp/io/JsonMagnumTypes.h index f743e78478..df34e78d98 100644 --- a/src/esp/io/JsonMagnumTypes.h +++ b/src/esp/io/JsonMagnumTypes.h @@ -65,6 +65,68 @@ inline bool fromJsonValue(const JsonGenericValue& obj, Magnum::Vector3& val) { return false; } +inline JsonGenericValue toJsonValue(const Magnum::Vector4& vec, + JsonAllocator& allocator) { + return toJsonArrayHelper(vec.data(), 4, allocator); +} + +/** + * @brief Specialization to handle Magnum::Vector4 values. Populate passed @p + * val with value. Returns whether successfully populated, or not. Logs an error + * if inappropriate type. + * + * @param obj json value to parse + * @param val destination value to be populated + * @return whether successful or not + */ +inline bool fromJsonValue(const JsonGenericValue& obj, Magnum::Vector4& val) { + if (obj.IsArray() && obj.Size() == 4) { + for (rapidjson::SizeType i = 0; i < 4; ++i) { + if (obj[i].IsNumber()) { + val[i] = obj[i].GetDouble(); + } else { + ESP_ERROR() << "Invalid numeric value specified in JSON Vec4, index :" + << i; + return false; + } + } + return true; + } + return false; +} + +inline JsonGenericValue toJsonValue(const Magnum::Color3& color, + JsonAllocator& allocator) { + return toJsonArrayHelper(color.data(), 3, allocator); +} + +/** + * @brief Specialization to handle Magnum::Color3 values. Populate passed @p + * val with value. Returns whether successfully populated, or not. Logs an error + * if inappropriate type. + * + * @param obj json value to parse + * @param val destination value to be populated + * @return whether successful or not + */ +inline bool fromJsonValue(const JsonGenericValue& obj, Magnum::Color3& val) { + if (obj.IsArray() && obj.Size() == 3) { + Magnum::Vector3 vec3; + for (rapidjson::SizeType i = 0; i < 3; ++i) { + if (obj[i].IsNumber()) { + vec3[i] = obj[i].GetDouble(); + } else { + ESP_ERROR() << "Invalid numeric value specified in JSON Color3, index :" + << i; + return false; + } + } + val = Magnum::Color3(vec3); + return true; + } + return false; +} + inline JsonGenericValue toJsonValue(const Magnum::Color4& color, JsonAllocator& allocator) { return toJsonArrayHelper(color.data(), 4, allocator); diff --git a/src/esp/sim/Simulator.cpp b/src/esp/sim/Simulator.cpp index 1f976a885c..1bbff48187 100644 --- a/src/esp/sim/Simulator.cpp +++ b/src/esp/sim/Simulator.cpp @@ -633,6 +633,11 @@ void Simulator::reconfigureReplayManager(bool enableGfxReplaySave) { -> scene::SceneNode* { return loadAndCreateRenderAssetInstance(assetInfo, creation); }); + // provide Player light creation callback + gfxReplayMgr_->setPlayerLightsCallback( + [this](const gfx::LightSetup& lights) -> void { + this->setLightSetup(lights); + }); } void Simulator::updateShadowMapDrawableGroup() { diff --git a/src/tests/GfxReplayTest.cpp b/src/tests/GfxReplayTest.cpp index 1ef8c57d58..6ea2579182 100644 --- a/src/tests/GfxReplayTest.cpp +++ b/src/tests/GfxReplayTest.cpp @@ -213,7 +213,8 @@ void GfxReplayTest::testPlayer() { return resourceManager.loadAndCreateRenderAssetInstance( assetInfo, creation, &sceneManager_, tempIDs); }; - esp::gfx::replay::Player player(callback); + auto dummyLightsCallback = [&](const esp::gfx::LightSetup& lights) -> void {}; + esp::gfx::replay::Player player(callback, dummyLightsCallback); std::vector keyframes; @@ -346,7 +347,8 @@ void GfxReplayTest::testPlayerReadMissingFile() { const esp::assets::RenderAssetInstanceCreationInfo& creation) { return nullptr; }; - esp::gfx::replay::Player player(dummyCallback); + auto dummyLightsCallback = [&](const esp::gfx::LightSetup& lights) -> void {}; + esp::gfx::replay::Player player(dummyCallback, dummyLightsCallback); player.readKeyframesFromFile("file_that_does_not_exist.json"); CORRADE_COMPARE(player.getNumKeyframes(), 0); @@ -366,7 +368,8 @@ void GfxReplayTest::testPlayerReadInvalidFile() { const esp::assets::RenderAssetInstanceCreationInfo& creation) { return nullptr; }; - esp::gfx::replay::Player player(dummyCallback); + auto dummyLightsCallback = [&](const esp::gfx::LightSetup& lights) -> void {}; + esp::gfx::replay::Player player(dummyCallback, dummyLightsCallback); player.readKeyframesFromFile(testFilepath); CORRADE_COMPARE(player.getNumKeyframes(), 0); @@ -474,17 +477,13 @@ void GfxReplayTest::testSimulatorIntegration() { // test lights data by recording and playback through the simulator interface void GfxReplayTest::testLightIntegration() { - const auto compareLights = [&](const LightInfo& a, const LightInfo& b) { - CORRADE_COMPARE(a.vector, b.vector); - CORRADE_COMPARE(static_cast(a.model), static_cast(b.model)); - CORRADE_COMPARE(a.color, b.color); - }; - const auto compareLightSetups = [&](const LightSetup& a, const LightSetup& b) { - CORRADE_COMPARE(a.size(), b.size()); for (int i = 0; i < a.size(); ++i) { - compareLights(a[i], b[i]); + CORRADE_COMPARE(a[i].vector, b[i].vector); + CORRADE_COMPARE(static_cast(a[i].model), + static_cast(b[i].model)); + CORRADE_COMPARE(a[i].color, b[i].color); } }; @@ -522,6 +521,7 @@ void GfxReplayTest::testLightIntegration() { recorder->saveKeyframe(); sim->setLightSetup(lightSetup1); recorder->saveKeyframe(); + recorder->saveKeyframe(); sim->setLightSetup(lightSetup2); recorder->saveKeyframe(); @@ -540,18 +540,25 @@ void GfxReplayTest::testLightIntegration() { auto player = sim->getGfxReplayManager()->readKeyframesFromFile(testFilepath); CORRADE_VERIFY(player); - CORRADE_COMPARE(player->getNumKeyframes(), 3); + CORRADE_COMPARE(player->getNumKeyframes(), 4); + + CORRADE_COMPARE(sim->getLightSetup().size(), 5); // 5 default lights player->setKeyframeIndex(0); + CORRADE_COMPARE(sim->getLightSetup().size(), lightSetup0.size()); compareLightSetups(sim->getLightSetup(), lightSetup0); + player->setKeyframeIndex(1); + CORRADE_COMPARE(sim->getLightSetup().size(), lightSetup1.size()); compareLightSetups(sim->getLightSetup(), lightSetup1); + player->setKeyframeIndex(2); - compareLightSetups(sim->getLightSetup(), lightSetup2); + CORRADE_COMPARE(sim->getLightSetup().size(), lightSetup1.size()); + compareLightSetups(sim->getLightSetup(), lightSetup1); - // light setups are unloaded upon deleting the player - player = nullptr; - CORRADE_COMPARE(sim->getLightSetup().size(), 0); + player->setKeyframeIndex(3); + CORRADE_COMPARE(sim->getLightSetup().size(), lightSetup2.size()); + compareLightSetups(sim->getLightSetup(), lightSetup2); } // remove file created for this test