diff --git a/AUTHORS.md b/AUTHORS.md index 1b49584d2d..cfe88ca6b0 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -49,6 +49,7 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) diff --git a/CHANGELOG.md b/CHANGELOG.md index a506079e8b..d440073854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,7 @@ Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. Feature #5813: Instanced groundcover support Feature #5814: Bsatool should be able to create BSA archives, not only to extract it + Feature #5828: Support more than 8 lights Feature #5910: Fall back to delta time when physics can't keep up Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 683d441193..d1ae6df94a 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,5 +1,7 @@ #include "advancedpage.hpp" +#include + #include #include #include @@ -138,6 +140,13 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); viewingDistanceComboBox->setValue(convertToCells(mEngineSettings.getInt("viewing distance", "Camera"))); + + int lightingMethod = 1; + if (mEngineSettings.getString("lighting method", "Shaders") == "legacy") + lightingMethod = 0; + else if (mEngineSettings.getString("lighting method", "Shaders") == "shaders") + lightingMethod = 2; + lightingMethodComboBox->setCurrentIndex(lightingMethod); } // Audio @@ -288,6 +297,9 @@ void Launcher::AdvancedPage::saveSettings() { mEngineSettings.setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); } + + static std::array lightingMethodMap = {"legacy", "shaders compatibility", "shaders"}; + mEngineSettings.setString("lighting method", "Shaders", lightingMethodMap[lightingMethodComboBox->currentIndex()]); } // Audio diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 70c496e3f2..319334c9bf 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -83,6 +83,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat defines["clamp"] = "1"; // Clamp lighting defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; + defines["lightingModel"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 6342433c47..9a7c234e6c 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -11,12 +11,16 @@ #include #include +#include #include #include #include #include #include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -106,11 +110,27 @@ namespace if (!widget->getUserString(settingMax).empty()) max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } + + const char* getLightingMethodCaptionText(SceneUtil::LightingMethod lightingMethod) + { + switch (lightingMethod) + { + case SceneUtil::LightingMethod::FFP: + return "Emulates fixed function pipeline lighting, advanced light settings are disabled when this mode is active"; + case SceneUtil::LightingMethod::PerObjectUniform: + return "Removes limit of 8 lights per object, fixes lighting attenuation, and enables groundcover lighting and light fade." + "\n\nDesigned for compatibility across hardware, and is not meant for large max light counts."; + case SceneUtil::LightingMethod::SingleUBO: + return "Removes limit of 8 lights per object, fixes lighting attenuation, and enables groundcover lighting and light fade." + "\n\nDesigned for more modern hardware and large max light counts."; + } + return ""; + } } namespace MWGui { - void SettingsWindow::configureWidgets(MyGUI::Widget* widget) + void SettingsWindow::configureWidgets(MyGUI::Widget* widget, bool init) { MyGUI::EnumeratorWidgetPtr widgets = widget->getEnumerator(); while (widgets.next()) @@ -124,7 +144,8 @@ namespace MWGui getSettingCategory(current)) ? "#{sOn}" : "#{sOff}"; current->castType()->setCaptionWithReplacing(initialValue); - current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); + if (init) + current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); } if (type == sliderType) { @@ -144,6 +165,12 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); @@ -158,12 +185,13 @@ namespace MWGui valueStr = MyGUI::utility::toString(value); scroll->setScrollPosition(value); } - scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); + if (init) + scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } - configureWidgets(current); + configureWidgets(current, init); } } @@ -190,7 +218,7 @@ namespace MWGui getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); - configureWidgets(mMainWidget); + configureWidgets(mMainWidget, true); setTitle("#{sOptions}"); @@ -207,6 +235,9 @@ namespace MWGui getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mLightingMethodText, "LightingMethodText"); + getWidget(mLightsResetButton, "LightsResetButton"); + getWidget(mLightSettingOverlay, "LightSettingOverlay"); #ifndef WIN32 // hide gamma controls since it currently does not work under Linux @@ -232,6 +263,8 @@ namespace MWGui mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); + mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); @@ -363,6 +396,23 @@ namespace MWGui apply(); } + void SettingsWindow::onLightsResetButtonClicked(MyGUI::Widget* _sender) + { + std::vector buttons = {"#{sYes}", "#{sNo}"}; + std::string message = "Resets to default values, would you like to continue?"; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons, true); + int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); + if (selectedButton == 1 || selectedButton == -1) + return; + + constexpr std::array settings = {"light bounds multiplier", "maximum light distance", "light fade start", "minimum interior brightness", "max lights"}; + for (const auto& setting : settings) + Settings::Manager::setString(setting, "Shaders", Settings::Manager::mDefaultSettings[{"Shaders", setting}]); + + apply(); + configureWidgets(mMainWidget, false); + } + void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); @@ -465,6 +515,12 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); } @@ -555,6 +611,25 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::updateLightSettings() + { + auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); + mLightingMethodText->setCaption(SceneUtil::LightManager::getLightingMethodString(lightingMethod)); + + if (lightingMethod == SceneUtil::LightingMethod::FFP || !Settings::Manager::getBool("force shaders", "Shaders")) + { + MyGUI::Widget* parent = mLightSettingOverlay->getParent(); + mLightSettingOverlay->setEnabled(false); + mLightSettingOverlay->setAlpha(0.8); + parent->setUserString("ToolTipType", "Layout"); + parent->setUserString("ToolTipLayout", "TextToolTip"); + parent->setUserString("Caption_Text", "Unavailable with current settings."); + parent->setEnabled(true); + } + + mLightingMethodText->setUserString("Caption_Text", getLightingMethodCaptionText(lightingMethod)); + } + void SettingsWindow::layoutControlsBox() { const int h = 18; @@ -617,6 +692,7 @@ namespace MWGui { highlightCurrentResolution(); updateControlsBox(); + updateLightSettings(); resetScrollbars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index c268514dc6..0baf52c405 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -14,6 +14,8 @@ namespace MWGui void updateControlsBox(); + void updateLightSettings(); + void onResChange(int, int) override { center(); } protected: @@ -30,6 +32,10 @@ namespace MWGui MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::Widget* mLightSettingOverlay; + MyGUI::TextBox* mLightingMethodText; + MyGUI::Button* mLightsResetButton; + // controls MyGUI::ScrollView* mControlsBox; MyGUI::Button* mResetControlsButton; @@ -50,6 +56,8 @@ namespace MWGui void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + void onLightsResetButtonClicked(MyGUI::Widget* _sender); + void onRebindAction(MyGUI::Widget* _sender); void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); void onResetDefaultBindings(MyGUI::Widget* _sender); @@ -61,7 +69,7 @@ namespace MWGui void apply(); - void configureWidgets(MyGUI::Widget* widget); + void configureWidgets(MyGUI::Widget* widget, bool init); void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); void layoutControlsBox(); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index d8c7599358..b578ee25ba 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -499,6 +499,11 @@ namespace MWRender mAlpha = alpha; } + void setLightSource(const osg::ref_ptr& lightSource) + { + mLightSource = lightSource; + } + protected: void setDefaults(osg::StateSet* stateset) override { @@ -521,10 +526,13 @@ namespace MWRender { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); + if (mLightSource) + mLightSource->setActorFade(mAlpha); } private: float mAlpha; + osg::ref_ptr mLightSource; }; struct Animation::AnimSource @@ -1613,7 +1621,7 @@ namespace MWRender { bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); - SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1763,6 +1771,7 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); + mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 04c5825c94..213a4f7049 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -278,6 +278,7 @@ protected: osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; + osg::ref_ptr mExtraLightSource; float mAlpha; diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 2c9b28e78e..63dd5ff70b 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -174,7 +174,9 @@ namespace MWRender mCamera->setNodeMask(Mask_RenderToTexture); - osg::ref_ptr lightManager = new SceneUtil::LightManager; + bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; + + osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); @@ -237,6 +239,7 @@ namespace MWRender light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); + lightManager->setSunlight(light); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 0baa85c52a..fd22462539 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" @@ -271,6 +272,8 @@ namespace MWRender group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); group->getBound(); group->setNodeMask(Mask_Groundcover); + if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) + group->setCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", false, true); return group; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 25d859e547..9a16232467 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -19,7 +19,10 @@ #include #include #include +#include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -220,6 +223,10 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + // override sun for local map + auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); + SceneUtil::configureStateSetSunOverride(lightingMethod, light, stateset); + camera->addChild(lightSource); camera->setStateSet(stateset); camera->setViewport(0, 0, mMapResolution, mMapResolution); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index b6fe05d0f8..2e342ad3af 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -52,6 +52,7 @@ #include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -195,14 +196,16 @@ namespace MWRender , mWorkQueue(workQueue) , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) + , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) { resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); + bool explicitlyForceShaders = Settings::Manager::getBool("force shaders", "Shaders"); // Shadows and radial fog have problems with fixed-function mode - bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows"); + bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || explicitlyForceShaders || Settings::Manager::getBool("enable shadows", "Shadows"); resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); @@ -214,7 +217,14 @@ namespace MWRender resourceSystem->getSceneManager()->setApplyLightingToEnvMaps(Settings::Manager::getBool("apply lighting to environment maps", "Shaders")); resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1); - osg::ref_ptr sceneRoot = new SceneUtil::LightManager; + auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); + // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. + osg::ref_ptr sceneRoot = new SceneUtil::LightManager(!explicitlyForceShaders || lightingMethod == SceneUtil::LightingMethod::FFP); + resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); + + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); + sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); @@ -236,6 +246,7 @@ namespace MWRender mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) @@ -247,6 +258,9 @@ namespace MWRender globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; globalDefines["useGPUShader4"] = "0"; + for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++) + globalDefines[itr->first] = itr->second; + float groundcoverDistance = (Constants::CellSizeInUnits * std::max(1, Settings::Manager::getInt("distance", "Groundcover")) - 1024) * 0.93; globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f); globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance); @@ -352,6 +366,7 @@ namespace MWRender mSunLight->setAmbient(osg::Vec4f(0,0,0,1)); mSunLight->setSpecular(osg::Vec4f(0,0,0,0)); mSunLight->setConstantAttenuation(1.f); + sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); sceneRoot->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); @@ -520,7 +535,32 @@ namespace MWRender void RenderingManager::configureAmbient(const ESM::Cell *cell) { - setAmbientColour(SceneUtil::colourFromRGB(cell->mAmbi.mAmbient)); + bool needsAdjusting = false; + if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) + needsAdjusting = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); + + auto ambient = SceneUtil::colourFromRGB(cell->mAmbi.mAmbient); + + if (needsAdjusting) + { + constexpr float pR = 0.2126; + constexpr float pG = 0.7152; + constexpr float pB = 0.0722; + + // we already work in linear RGB so no conversions are needed for the luminosity function + float relativeLuminance = pR*ambient.r() + pG*ambient.g() + pB*ambient.b(); + if (relativeLuminance < mMinimumAmbientLuminance) + { + // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data as least we can + float targetBrightnessIncreaseFactor = mMinimumAmbientLuminance / relativeLuminance; + if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) + ambient = osg::Vec4(targetBrightnessIncreaseFactor, targetBrightnessIncreaseFactor, targetBrightnessIncreaseFactor, ambient.a()); + else + ambient *= targetBrightnessIncreaseFactor; + } + } + + setAmbientColour(ambient); osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); mSunLight->setDiffuse(diffuse); @@ -1103,9 +1143,47 @@ namespace MWRender else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) + { updateTextureFiltering(); + } else if (it->first == "Water") + { mWater->processChangedSettings(changed); + } + else if (it->first == "Shaders" && it->second == "minimum interior brightness") + { + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); + if (MWMechanics::getPlayer().isInCell()) + configureAmbient(MWMechanics::getPlayer().getCell()->getCell()); + } + else if (it->first == "Shaders" && (it->second == "light bounds multiplier" || + it->second == "maximum light distance" || + it->second == "light fade start" || + it->second == "max lights")) + { + auto* lightManager = static_cast(getLightRoot()); + lightManager->processChangedSettings(changed); + + if (it->second == "max lights" && !lightManager->usingFFP()) + { + mViewer->stopThreading(); + + lightManager->updateMaxLights(); + + auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + for (const auto& [name, key] : lightManager->getLightDefines()) + defines[name] = key; + mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); + + mSceneRoot->removeUpdateCallback(mStateUpdater); + mStateUpdater = new StateUpdater; + mSceneRoot->addUpdateCallback(mStateUpdater); + mStateUpdater->setFogEnd(mViewDistance); + updateAmbient(); + + mViewer->startThreading(); + } + } } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a7afa2fa0d..a0a74bd5c4 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -296,6 +296,7 @@ namespace MWRender osg::ref_ptr mStateUpdater; osg::Vec4f mAmbientColor; + float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index caa0af434b..a938db9bc0 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -670,6 +671,9 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R osg::ref_ptr program (new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); + auto method = mResourceSystem->getSceneManager()->getLightingMethod(); + if (method == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); node->setStateSet(shaderStateset); diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp new file mode 100644 index 0000000000..30a9c41ee9 --- /dev/null +++ b/components/misc/hash.hpp @@ -0,0 +1,15 @@ +#ifndef MISC_HASH_H +#define MISC_HASH_H + +namespace Misc +{ + /// Implemented similar to the boost::hash_combine + template + inline void hashCombine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + } +} + +#endif \ No newline at end of file diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 287365a830..18fe3dae49 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -226,6 +226,7 @@ namespace Resource , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) + , mLightingMethod(SceneUtil::LightingMethod::FFP) , mConvertAlphaTestToAlphaToCoverage(false) , mInstanceCache(new MultiObjectCache) , mSharedStateManager(new SharedStateManager) @@ -304,6 +305,16 @@ namespace Resource mApplyLightingToEnvMaps = apply; } + void SceneManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + } + + SceneUtil::LightingMethod SceneManager::getLightingMethod() const + { + return mLightingMethod; + } + void SceneManager::setConvertAlphaTestToAlphaToCoverage(bool convert) { mConvertAlphaTestToAlphaToCoverage = convert; diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index bf69a8c4be..fdc7d334f8 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -12,6 +12,8 @@ #include "resourcemanager.hpp" +#include + namespace Resource { class ImageManager; @@ -105,6 +107,9 @@ namespace Resource void setApplyLightingToEnvMaps(bool apply); + void setLightingMethod(SceneUtil::LightingMethod method); + SceneUtil::LightingMethod getLightingMethod() const; + void setConvertAlphaTestToAlphaToCoverage(bool convert); void setShaderPath(const std::string& path); @@ -191,6 +196,7 @@ namespace Resource bool mAutoUseSpecularMaps; std::string mSpecularMapPattern; bool mApplyLightingToEnvMaps; + SceneUtil::LightingMethod mLightingMethod; bool mConvertAlphaTestToAlphaToCoverage; osg::ref_ptr mInstanceCache; diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index c759fabc79..cc320aecf8 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -62,7 +62,8 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness); + auto* lightSource = static_cast(node); + lightSource->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * lightSource->getActorFade()); traverse(node, nv); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 2ebce241d5..c03bbca597 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,34 +1,327 @@ #include "lightmanager.hpp" +#include + +#include +#include +#include + #include #include +#include +#include + +#include + +namespace +{ + bool sortLights(const SceneUtil::LightManager::LightSourceViewBound* left, const SceneUtil::LightManager::LightSourceViewBound* right) + { + static auto constexpr illuminationBias = 81.f; + return left->mViewBound.center().length2() - left->mViewBound.radius2()*illuminationBias < right->mViewBound.center().length2() - right->mViewBound.radius2()*illuminationBias; + } + + float getLightRadius(const osg::Light* light) + { + float value = 0.0; + light->getUserValue("radius", value); + return value; + } + + void setLightRadius(osg::Light* light, float value) + { + light->setUserValue("radius", value); + } + + void configurePosition(osg::Matrixf& mat, const osg::Vec4& pos) + { + mat(0, 0) = pos.x(); + mat(0, 1) = pos.y(); + mat(0, 2) = pos.z(); + } + + void configureAmbient(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(1, 0) = color.r(); + mat(1, 1) = color.g(); + mat(1, 2) = color.b(); + } + + void configureDiffuse(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(2, 0) = color.r(); + mat(2, 1) = color.g(); + mat(2, 2) = color.b(); + } + + void configureSpecular(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(3, 0) = color.r(); + mat(3, 1) = color.g(); + mat(3, 2) = color.b(); + mat(3, 3) = color.a(); + } + + void configureAttenuation(osg::Matrixf& mat, float c, float l, float q, float r) + { + mat(0, 3) = c; + mat(1, 3) = l; + mat(2, 3) = q; + mat(3, 3) = r; + } + + bool isReflectionCamera(osg::Camera* camera) + { + return (camera->getName() == "ReflectionCamera"); + } +} + namespace SceneUtil { + static int sLightId = 0; + + // Handles a GLSL shared layout by using configured offsets and strides to fill a continuous buffer, making the data upload to GPU simpler. + class LightBuffer : public osg::Referenced + { + public: + + enum LayoutOffset + { + Diffuse, + DiffuseSign, + Ambient, + Specular, + Position, + AttenuationRadius + }; + + LightBuffer(int count) + : mData(new osg::FloatArray(3*4*count)) + , mEndian(osg::getCpuByteOrder()) + , mCount(count) + , mStride(12) + { + mOffsets[Diffuse] = 0; + mOffsets[Ambient] = 1; + mOffsets[Specular] = 2; + mOffsets[DiffuseSign] = 3; + mOffsets[Position] = 4; + mOffsets[AttenuationRadius] = 8; + } + + LightBuffer(const LightBuffer& copy) + : osg::Referenced() + , mData(copy.mData) + , mEndian(copy.mEndian) + , mCount(copy.mCount) + , mStride(copy.mStride) + , mOffsets(copy.mOffsets) + {} + + void setDiffuse(int index, const osg::Vec4& value) + { + // Deal with negative lights (negative diffuse) by passing a sign bit in the unused alpha component + auto positiveColor = value; + unsigned int signBit = 1; + if (value[0] < 0) + { + positiveColor *= -1.0; + signBit = -1; + } + unsigned int packedColor = asRGBA(positiveColor); + std::memcpy(&(*mData)[getOffset(index, Diffuse)], &packedColor, sizeof(unsigned int)); + std::memcpy(&(*mData)[getOffset(index, DiffuseSign)], &signBit, sizeof(unsigned int)); + } + + void setAmbient(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Ambient)], &packed, sizeof(unsigned int)); + } + + void setSpecular(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Specular)], &packed, sizeof(unsigned int)); + } + + void setPosition(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, Position)], value.ptr(), sizeof(osg::Vec4f)); + } + + void setAttenuationRadius(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, AttenuationRadius)], value.ptr(), sizeof(osg::Vec4f)); + } + + auto& getData() + { + return mData; + } + + void dirty() + { + mData->dirty(); + } + + static constexpr int queryBlockSize(int sz) + { + return 3 * osg::Vec4::num_components * sizeof(GL_FLOAT) * sz; + } + + unsigned int asRGBA(const osg::Vec4& value) const + { + return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA(); + } + + int getOffset(int index, LayoutOffset slot) + { + return mStride * index + mOffsets[slot]; + } + + void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) + { + constexpr auto sizeofFloat = sizeof(GL_FLOAT); + constexpr auto sizeofVec4 = sizeofFloat * osg::Vec4::num_components; + + mOffsets[Diffuse] = offsetColors / sizeofFloat; + mOffsets[Ambient] = mOffsets[Diffuse] + 1; + mOffsets[Specular] = mOffsets[Diffuse] + 2; + mOffsets[DiffuseSign] = mOffsets[Diffuse] + 3; + mOffsets[Position] = offsetPosition / sizeofFloat; + mOffsets[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; + mStride = (offsetAttenuationRadius + sizeofVec4 + stride) / 4; + + // Copy over previous buffers light data. Buffers populate before we know the layout. + LightBuffer oldBuffer = LightBuffer(*this); + mData->resize(size / sizeofFloat); + for (int i = 0; i < oldBuffer.mCount; ++i) + { + std::memcpy(&(*mData)[getOffset(i, Diffuse)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Diffuse)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, Position)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Position)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, AttenuationRadius)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, AttenuationRadius)], sizeof(osg::Vec4f)); + } + } + + private: + osg::ref_ptr mData; + osg::Endian mEndian; + int mCount; + int mStride; + std::array mOffsets; + }; class LightStateCache { public: - osg::Light* lastAppliedLight[8]; + std::vector lastAppliedLight; }; - LightStateCache* getLightStateCache(unsigned int contextid) + LightStateCache* getLightStateCache(size_t contextid, size_t size = 8) { static std::vector cacheVector; if (cacheVector.size() < contextid+1) cacheVector.resize(contextid+1); + cacheVector[contextid].lastAppliedLight.resize(size); return &cacheVector[contextid]; } - // Resets the modelview matrix to just the view matrix before applying lights. - class LightStateAttribute : public osg::StateAttribute + void configureStateSetSunOverride(LightingMethod method, const osg::Light* light, osg::StateSet* stateset, int mode) + { + switch (method) + { + case LightingMethod::FFP: + { + break; + } + case LightingMethod::PerObjectUniform: + { + osg::Matrixf lightMat; + configurePosition(lightMat, light->getPosition()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureSpecular(lightMat, light->getSpecular()); + stateset->addUniform(new osg::Uniform("LightBuffer", lightMat), mode); + break; + } + case LightingMethod::SingleUBO: + { + osg::ref_ptr buffer = new LightBuffer(1); + + buffer->setDiffuse(0, light->getDiffuse()); + buffer->setAmbient(0, light->getAmbient()); + buffer->setSpecular(0, light->getSpecular()); + buffer->setPosition(0, light->getPosition()); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + buffer->getData()->setBufferObject(ubo); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->getData().get(), 0, buffer->getData()->getTotalDataSize()); + + stateset->setAttributeAndModes(ubb, mode); + + break; + } + } + } + + class DisableLight : public osg::StateAttribute { public: - LightStateAttribute() : mIndex(0) {} - LightStateAttribute(unsigned int index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + DisableLight() : mIndex(0) {} + DisableLight(int index) : mIndex(index) {} - LightStateAttribute(const LightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} + + osg::Object* cloneType() const override { return new DisableLight(mIndex); } + osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); } + bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast(obj)!=nullptr; } + const char* libraryName() const override { return "SceneUtil"; } + const char* className() const override { return "DisableLight"; } + Type getType() const override { return LIGHT; } + + unsigned int getMember() const override + { + return mIndex; + } + + bool getModeUsage(ModeUsage & usage) const override + { + usage.usesMode(GL_LIGHT0 + mIndex); + return true; + } + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("DisableLight::compare: unimplemented"); + } + + void apply(osg::State& state) const override + { + int lightNum = GL_LIGHT0 + mIndex; + glLightfv(lightNum, GL_AMBIENT, mNullptr.ptr()); + glLightfv(lightNum, GL_DIFFUSE, mNullptr.ptr()); + glLightfv(lightNum, GL_SPECULAR, mNullptr.ptr()); + + LightStateCache* cache = getLightStateCache(state.getContextID()); + cache->lastAppliedLight[mIndex] = nullptr; + } + + private: + size_t mIndex; + osg::Vec4f mNullptr; + }; + + class FFPLightStateAttribute : public osg::StateAttribute + { + public: + FFPLightStateAttribute() : mIndex(0) {} + FFPLightStateAttribute(size_t index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + + FFPLightStateAttribute(const FFPLightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex), mLights(copy.mLights) {} unsigned int getMember() const override @@ -38,17 +331,17 @@ namespace SceneUtil bool getModeUsage(ModeUsage & usage) const override { - for (unsigned int i=0; ilastAppliedLight[i+mIndex]; if (current != mLights[i].get()) @@ -75,29 +368,28 @@ namespace SceneUtil void applyLight(GLenum lightNum, const osg::Light* light) const { - glLightfv( lightNum, GL_AMBIENT, light->getAmbient().ptr() ); - glLightfv( lightNum, GL_DIFFUSE, light->getDiffuse().ptr() ); - glLightfv( lightNum, GL_SPECULAR, light->getSpecular().ptr() ); - glLightfv( lightNum, GL_POSITION, light->getPosition().ptr() ); + glLightfv(lightNum, GL_AMBIENT, light->getAmbient().ptr()); + glLightfv(lightNum, GL_DIFFUSE, light->getDiffuse().ptr()); + glLightfv(lightNum, GL_SPECULAR, light->getSpecular().ptr()); + glLightfv(lightNum, GL_POSITION, light->getPosition().ptr()); // TODO: enable this once spot lights are supported // need to transform SPOT_DIRECTION by the world matrix? - //glLightfv( lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr() ); - //glLightf ( lightNum, GL_SPOT_EXPONENT, light->getSpotExponent() ); - //glLightf ( lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff() ); - glLightf ( lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation() ); - glLightf ( lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation() ); - glLightf ( lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation() ); + //glLightfv(lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr()); + //glLightf(lightNum, GL_SPOT_EXPONENT, light->getSpotExponent()); + //glLightf(lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff()); + glLightf(lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation()); + glLightf(lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation()); + glLightf(lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation()); } private: - unsigned int mIndex; - - std::vector > mLights; + size_t mIndex; + std::vector> mLights; }; LightManager* findLightManager(const osg::NodePath& path) { - for (unsigned int i=0;i(path[i])) return lightManager; @@ -105,6 +397,164 @@ namespace SceneUtil return nullptr; } + class LightStateAttributePerObjectUniform : public osg::StateAttribute + { + public: + LightStateAttributePerObjectUniform() {} + LightStateAttributePerObjectUniform(const std::vector>& lights, LightManager* lightManager) : mLights(lights), mLightManager(lightManager) {} + + LightStateAttributePerObjectUniform(const LightStateAttributePerObjectUniform& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLights(copy.mLights), mLightManager(copy.mLightManager) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightStateAttributePerObjectUniform::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightStateAttributePerObjectUniform, osg::StateAttribute::LIGHT) + + void resize(int numLights) + { + mLights.resize(std::min(static_cast(numLights), mLights.size())); + } + + void apply(osg::State &state) const override + { + auto* lightUniform = mLightManager->getStateSet()->getUniform("LightBuffer"); + for (size_t i = 0; i < mLights.size(); ++i) + { + auto light = mLights[i]; + osg::Matrixf lightMat; + + configurePosition(lightMat, light->getPosition() * state.getInitialViewMatrix()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), getLightRadius(light)); + + lightUniform->setElement(i+1, lightMat); + } + lightUniform->dirty(); + } + + private: + std::vector> mLights; + LightManager* mLightManager; + }; + + struct StateSetGenerator + { + LightManager* mLightManager; + + virtual ~StateSetGenerator() {} + + virtual osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) = 0; + + virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {} + }; + + struct StateSetGeneratorFFP : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + std::vector> lights; + lights.reserve(lightList.size()); + for (size_t i = 0; i < lightList.size(); ++i) + lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum)); + + // the first light state attribute handles the actual state setting for all lights + // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary + // don't use setAttributeAndModes, that does not support light indices! + stateset->setAttribute(new FFPLightStateAttribute(mLightManager->getStartLight(), std::move(lights)), osg::StateAttribute::ON); + + for (size_t i = 0; i < lightList.size(); ++i) + stateset->setMode(GL_LIGHT0 + mLightManager->getStartLight() + i, osg::StateAttribute::ON); + + // need to push some dummy attributes to ensure proper state tracking + // lights need to reset to their default when the StateSet is popped + for (size_t i = 1; i < lightList.size(); ++i) + stateset->setAttribute(mLightManager->getDummies()[i + mLightManager->getStartLight()].get(), osg::StateAttribute::ON); + + return stateset; + } + }; + + struct StateSetGeneratorSingleUBO : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr indices = new osg::IntArray(mLightManager->getMaxLights()); + osg::ref_ptr indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", indices->size()); + int pointCount = 0; + + for (size_t i = 0; i < lightList.size(); ++i) + { + int bufIndex = mLightManager->getLightIndexMap(frameNum)[lightList[i]->mLightSource->getId()]; + indices->at(pointCount++) = bufIndex; + } + indicesUni->setArray(indices); + stateset->addUniform(indicesUni); + stateset->addUniform(new osg::Uniform("PointLightCount", pointCount)); + + return stateset; + } + + // Cached statesets must be revalidated in case the light indices change. There is no actual link between + // a light's ID and the buffer index it will eventually be assigned (or reassigned) to. + void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) override + { + int newCount = 0; + int oldCount; + + auto uOldArray = stateset->getUniform("PointLightIndex"); + auto uOldCount = stateset->getUniform("PointLightCount"); + + uOldCount->get(oldCount); + + // max lights count can change during runtime + oldCount = std::min(mLightManager->getMaxLights(), oldCount); + + auto& lightData = mLightManager->getLightIndexMap(frameNum); + + for (int i = 0; i < oldCount; ++i) + { + auto* lightSource = lightList[i]->mLightSource; + auto it = lightData.find(lightSource->getId()); + if (it != lightData.end()) + uOldArray->setElement(newCount++, it->second); + } + + uOldArray->dirty(); + uOldCount->set(newCount); + } + }; + + struct StateSetGeneratorPerObjectUniform : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + std::vector> lights(lightList.size()); + + for (size_t i = 0; i < lightList.size(); ++i) + { + auto* light = lightList[i]->mLightSource->getLight(frameNum); + lights[i] = light; + setLightRadius(light, lightList[i]->mLightSource->getRadius()); + } + + stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); + + return stateset; + } + }; + // Set on a LightSource. Adds the light source to its light manager for the current frame. // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager. class CollectLightCallback : public osg::NodeCallback @@ -154,194 +604,504 @@ namespace SceneUtil void operator()(osg::Node* node, osg::NodeVisitor* nv) override { LightManager* lightManager = static_cast(node); - lightManager->update(); + lightManager->update(nv->getTraversalNumber()); traverse(node, nv); } }; - LightManager::LightManager() + class LightManagerCullCallback : public osg::NodeCallback + { + public: + LightManagerCullCallback(LightManager* lightManager) : mLightManager(lightManager) {} + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + bool pop = false; + + if (mLastFrameNumber != cv->getTraversalNumber()) + { + mLastFrameNumber = cv->getTraversalNumber(); + + if (mLightManager->getLightingMethod() == LightingMethod::SingleUBO) + { + auto stateset = mLightManager->getStateSet(); + auto bo = mLightManager->getLightBuffer(mLastFrameNumber); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData().get(), 0, bo->getData()->getTotalDataSize()); + stateset->setAttributeAndModes(ubb.get(), osg::StateAttribute::ON); + } + + auto sun = mLightManager->getSunlight(); + + if (sun) + { + if (mLightManager->getLightingMethod() == LightingMethod::PerObjectUniform) + { + osg::Matrixf lightMat; + configurePosition(lightMat, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); + configureAmbient(lightMat, sun->getAmbient()); + configureDiffuse(lightMat, sun->getDiffuse()); + configureSpecular(lightMat, sun->getSpecular()); + mLightManager->setSunlightBuffer(lightMat, mLastFrameNumber); + } + else + { + auto buf = mLightManager->getLightBuffer(mLastFrameNumber); + + buf->setPosition(0, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); + buf->setAmbient(0, sun->getAmbient()); + buf->setDiffuse(0, sun->getDiffuse()); + buf->setSpecular(0, sun->getSpecular()); + } + } + } + else if (isReflectionCamera(cv->getCurrentCamera())) + { + auto sun = mLightManager->getSunlight(); + if (sun) + { + osg::Vec4 originalPos = sun->getPosition(); + sun->setPosition(originalPos * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); + + osg::ref_ptr stateset = new osg::StateSet; + configureStateSetSunOverride(mLightManager->getLightingMethod(), sun, stateset); + + sun->setPosition(originalPos); + cv->pushStateSet(stateset); + pop = true; + } + } + + traverse(node, nv); + if (pop) + cv->popStateSet(); + } + + private: + LightManager* mLightManager; + size_t mLastFrameNumber; + }; + + class LightManagerStateAttribute : public osg::StateAttribute + { + public: + LightManagerStateAttribute() + : mLightManager(nullptr) {} + + LightManagerStateAttribute(LightManager* lightManager) + : mLightManager(lightManager) + , mDummyProgram(new osg::Program) + { + static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene()); + + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); + // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably + // available, regardless of extensions, until GLSL 140. + mLightManager->getOrCreateStateSet()->setAttributeAndModes(mDummyProgram, osg::StateAttribute::ON); + } + + LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT) + + void initSharedLayout(osg::GLExtensions* ext, int handle) const + { + constexpr std::array index = { static_cast(Shader::UBOBinding::LightBuffer) }; + int totalBlockSize = -1; + int stride = -1; + + ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); + ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); + + std::array names = { + "LightBuffer[0].packedColors", + "LightBuffer[0].position", + "LightBuffer[0].attenuation", + }; + std::vector indices(names.size()); + std::vector offsets(names.size()); + + ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); + ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); + + for (int i = 0; i < 2; ++i) + { + auto& buf = mLightManager->getLightBuffer(i); + buf->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } + } + + void apply(osg::State& state) const override + { + static bool init = false; + if (!init) + { + auto handle = mDummyProgram->getPCP(state)->getHandle(); + auto* ext = state.get(); + + int activeUniformBlocks = 0; + ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); + + // wait until the UBO binding is created + if (activeUniformBlocks > 0) + { + initSharedLayout(ext, handle); + init = true; + } + } + else + { + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); + } + } + + private: + + std::string generateDummyShader(int maxLightsInScene) + { + const std::string define = "@maxLightsInScene"; + + std::string shader = R"GLSL( + #version 120 + #extension GL_ARB_uniform_buffer_object : require + struct LightData { + ivec4 packedColors; + vec4 position; + vec4 attenuation; + }; + uniform LightBufferBinding { + LightData LightBuffer[@maxLightsInScene]; + }; + void main() + { + gl_Position = vec4(0.0); + } + )GLSL"; + + shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); + return shader; + } + + LightManager* mLightManager; + osg::ref_ptr mDummyProgram; + }; + + class LightManagerStateAttributePerObjectUniform : public osg::StateAttribute + { + public: + LightManagerStateAttributePerObjectUniform() + : mLightManager(nullptr) {} + + LightManagerStateAttributePerObjectUniform(LightManager* lightManager) + : mLightManager(lightManager) + { + } + + LightManagerStateAttributePerObjectUniform(const LightManagerStateAttributePerObjectUniform& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightManagerStateAttributePerObjectUniform::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightManagerStateAttributePerObjectUniform, osg::StateAttribute::LIGHT) + + void apply(osg::State& state) const override + { + mLightManager->getStateSet()->getUniform("LightBuffer")->setElement(0, mLightManager->getSunlightBuffer(state.getFrameStamp()->getFrameNumber())); + } + + private: + LightManager* mLightManager; + }; + + const std::unordered_map LightManager::mLightingMethodSettingMap = { + {"legacy", LightingMethod::FFP} + ,{"shaders compatibility", LightingMethod::PerObjectUniform} + ,{"shaders", LightingMethod::SingleUBO} + }; + + LightingMethod LightManager::getLightingMethodFromString(const std::string& value) + { + auto it = LightManager::mLightingMethodSettingMap.find(value); + if (it != LightManager::mLightingMethodSettingMap.end()) + return it->second; + + constexpr const char* fallback = "shaders compatibility"; + Log(Debug::Warning) << "Unknown lighting method '" << value << "', returning fallback '" << fallback << "'"; + return LightingMethod::PerObjectUniform; + } + + std::string LightManager::getLightingMethodString(LightingMethod method) + { + for (const auto& p : LightManager::mLightingMethodSettingMap) + if (p.second == method) + return p.first; + return ""; + } + + LightManager::LightManager(bool ffp) : mStartLight(0) , mLightingMask(~0u) + , mSun(nullptr) + , mPointLightRadiusMultiplier(1.f) + , mPointLightFadeEnd(0.f) + , mPointLightFadeStart(0.f) { setUpdateCallback(new LightManagerUpdateCallback); - for (unsigned int i=0; i<8; ++i) - mDummies.push_back(new LightStateAttribute(i, std::vector >())); + + if (ffp) + { + initFFP(LightManager::mFFPMaxLights); + return; + } + + std::string lightingMethodString = Settings::Manager::getString("lighting method", "Shaders"); + auto lightingMethod = LightManager::getLightingMethodFromString(lightingMethodString); + + updateSettings(); + + osg::GLExtensions* exts = osg::GLExtensions::Get(0, false); + bool supportsUBO = exts && exts->isUniformBufferObjectSupported; + bool supportsGPU4 = exts && exts->isGpuShader4Supported; + + static bool hasLoggedWarnings = false; + + if (lightingMethod == LightingMethod::SingleUBO && !hasLoggedWarnings) + { + if (!supportsUBO) + Log(Debug::Warning) << "GL_ARB_uniform_buffer_object not supported: switching to shader compatibility lighting mode"; + if (!supportsGPU4) + Log(Debug::Warning) << "GL_EXT_gpu_shader4 not supported: switching to shader compatibility lighting mode"; + hasLoggedWarnings = true; + } + + int targetLights = Settings::Manager::getInt("max lights", "Shaders"); + + if (!supportsUBO || !supportsGPU4 || lightingMethod == LightingMethod::PerObjectUniform) + initPerObjectUniform(targetLights); + else + initSingleUBO(targetLights); + + getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); + + addCullCallback(new LightManagerCullCallback(this)); } LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) : osg::Group(copy, copyop) , mStartLight(copy.mStartLight) , mLightingMask(copy.mLightingMask) + , mSun(copy.mSun) + , mLightingMethod(copy.mLightingMethod) { - } - void LightManager::setLightingMask(unsigned int mask) + LightingMethod LightManager::getLightingMethod() const + { + return mLightingMethod; + } + + bool LightManager::usingFFP() const + { + return mLightingMethod == LightingMethod::FFP; + } + + int LightManager::getMaxLights() const + { + return mMaxLights; + } + + void LightManager::setMaxLights(int value) + { + mMaxLights = value; + } + + int LightManager::getMaxLightsInScene() const + { + static constexpr int max = 16384 / LightBuffer::queryBlockSize(1); + return max; + } + + Shader::ShaderManager::DefineMap LightManager::getLightDefines() const + { + Shader::ShaderManager::DefineMap defines; + + defines["maxLights"] = std::to_string(getMaxLights()); + defines["maxLightsInScene"] = std::to_string(getMaxLightsInScene()); + defines["lightingMethodFFP"] = getLightingMethod() == LightingMethod::FFP ? "1" : "0"; + defines["lightingMethodPerObjectUniform"] = getLightingMethod() == LightingMethod::PerObjectUniform ? "1" : "0"; + defines["lightingMethodUBO"] = getLightingMethod() == LightingMethod::SingleUBO ? "1" : "0"; + defines["useUBO"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + // exposes bitwise operators + defines["useGPUShader4"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + defines["getLight"] = getLightingMethod() == LightingMethod::FFP ? "gl_LightSource" : "LightBuffer"; + defines["startLight"] = getLightingMethod() == LightingMethod::SingleUBO ? "0" : "1"; + defines["endLight"] = getLightingMethod() == LightingMethod::FFP ? defines["maxLights"] : "PointLightCount"; + + return defines; + } + + void LightManager::processChangedSettings(const Settings::CategorySettingVector& changed) + { + updateSettings(); + } + + void LightManager::updateMaxLights() + { + if (usingFFP()) + return; + + setMaxLights(std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit)); + + if (getLightingMethod() == LightingMethod::PerObjectUniform) + { + auto* prevUniform = getStateSet()->getUniform("LightBuffer"); + osg::ref_ptr newUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); + + for (int i = 0; i < getMaxLights(); ++i) + { + osg::Matrixf prevLightData; + prevUniform->getElement(i, prevLightData); + newUniform->setElement(i, prevLightData); + } + + getStateSet()->removeUniform(prevUniform); + getStateSet()->addUniform(newUniform); + + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + static_cast(pair.second->getAttribute(osg::StateAttribute::LIGHT))->resize(getMaxLights()); + mStateSetCache[i].clear(); + } + } + else + { + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + { + auto& stateset = pair.second; + osg::Uniform* uOldArray = stateset->getUniform("PointLightIndex"); + osg::Uniform* uOldCount = stateset->getUniform("PointLightCount"); + + int prevCount; + uOldCount->get(prevCount); + int newCount = std::min(getMaxLights(), prevCount); + uOldCount->set(newCount); + + osg::ref_ptr newArray = uOldArray->getIntArray(); + newArray->resize(newCount); + + stateset->removeUniform(uOldArray); + stateset->addUniform(new osg::Uniform("PointLightIndex", newArray)); + } + mStateSetCache[i].clear(); + } + } + } + + void LightManager::updateSettings() + { + if (getLightingMethod() == LightingMethod::FFP) + return; + + mPointLightRadiusMultiplier = std::clamp(Settings::Manager::getFloat("light bounds multiplier", "Shaders"), 0.f, 5.f); + + mPointLightFadeEnd = std::max(0.f, Settings::Manager::getFloat("maximum light distance", "Shaders")); + if (mPointLightFadeEnd > 0) + { + mPointLightFadeStart = std::clamp(Settings::Manager::getFloat("light fade start", "Shaders"), 0.f, 1.f); + mPointLightFadeStart = mPointLightFadeEnd * mPointLightFadeStart; + } + } + + void LightManager::initFFP(int targetLights) + { + setLightingMethod(LightingMethod::FFP); + setMaxLights(targetLights); + + for (int i = 0; i < getMaxLights(); ++i) + mDummies.push_back(new FFPLightStateAttribute(i, std::vector>())); + } + + void LightManager::initPerObjectUniform(int targetLights) + { + auto* stateset = getOrCreateStateSet(); + + setLightingMethod(LightingMethod::PerObjectUniform); + setMaxLights(std::clamp(targetLights, mMaxLightsLowerLimit, LightManager::mMaxLightsUpperLimit)); + + stateset->setAttributeAndModes(new LightManagerStateAttributePerObjectUniform(this), osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights())); + } + + void LightManager::initSingleUBO(int targetLights) + { + setLightingMethod(LightingMethod::SingleUBO); + setMaxLights(std::clamp(targetLights, mMaxLightsLowerLimit, LightManager::mMaxLightsUpperLimit)); + + for (int i = 0; i < 2; ++i) + { + mLightBuffers[i] = new LightBuffer(getMaxLightsInScene()); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + ubo->setUsage(GL_STREAM_DRAW); + + mLightBuffers[i]->getData()->setBufferObject(ubo); + } + + getOrCreateStateSet()->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON); + } + + void LightManager::setLightingMethod(LightingMethod method) + { + mLightingMethod = method; + switch (method) + { + case LightingMethod::FFP: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::SingleUBO: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::PerObjectUniform: + mStateSetGenerator = std::make_unique(); + break; + } + mStateSetGenerator->mLightManager = this; + } + + void LightManager::setLightingMask(size_t mask) { mLightingMask = mask; } - unsigned int LightManager::getLightingMask() const + size_t LightManager::getLightingMask() const { return mLightingMask; } - void LightManager::update() - { - mLights.clear(); - mLightsInViewSpace.clear(); - - // do an occasional cleanup for orphaned lights - for (int i=0; i<2; ++i) - { - if (mStateSetCache[i].size() > 5000) - mStateSetCache[i].clear(); - } - } - - void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum) - { - LightSourceTransform l; - l.mLightSource = lightSource; - l.mWorldMatrix = worldMat; - lightSource->getLight(frameNum)->setPosition(osg::Vec4f(worldMat.getTrans().x(), - worldMat.getTrans().y(), - worldMat.getTrans().z(), 1.f)); - mLights.push_back(l); - } - - /* similar to the boost::hash_combine */ - template - inline void hash_combine(std::size_t& seed, const T& v) - { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); - } - - osg::ref_ptr LightManager::getLightListStateSet(const LightList &lightList, unsigned int frameNum) - { - // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - size_t hash = 0; - for (unsigned int i=0; imLightSource->getId()); - - LightStateSetMap& stateSetCache = mStateSetCache[frameNum%2]; - - LightStateSetMap::iterator found = stateSetCache.find(hash); - if (found != stateSetCache.end()) - return found->second; - else - { - osg::ref_ptr stateset = new osg::StateSet; - std::vector > lights; - lights.reserve(lightList.size()); - for (unsigned int i=0; imLightSource->getLight(frameNum)); - - // the first light state attribute handles the actual state setting for all lights - // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary - // don't use setAttributeAndModes, that does not support light indices! - stateset->setAttribute(new LightStateAttribute(mStartLight, std::move(lights)), osg::StateAttribute::ON); - - for (unsigned int i=0; isetMode(GL_LIGHT0 + mStartLight + i, osg::StateAttribute::ON); - - // need to push some dummy attributes to ensure proper state tracking - // lights need to reset to their default when the StateSet is popped - for (unsigned int i=1; isetAttribute(mDummies[i+mStartLight].get(), osg::StateAttribute::ON); - - stateSetCache.emplace(hash, stateset); - return stateset; - } - } - - const std::vector& LightManager::getLights() const - { - return mLights; - } - - const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix) - { - osg::observer_ptr camPtr (camera); - std::map, LightSourceViewBoundCollection>::iterator it = mLightsInViewSpace.find(camPtr); - - if (it == mLightsInViewSpace.end()) - { - it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; - - for (std::vector::iterator lightIt = mLights.begin(); lightIt != mLights.end(); ++lightIt) - { - osg::Matrixf worldViewMat = lightIt->mWorldMatrix * (*viewMatrix); - osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), lightIt->mLightSource->getRadius()); - transformBoundingSphere(worldViewMat, viewBound); - - LightSourceViewBound l; - l.mLightSource = lightIt->mLightSource; - l.mViewBound = viewBound; - it->second.push_back(l); - } - } - return it->second; - } - - class DisableLight : public osg::StateAttribute - { - public: - DisableLight() : mIndex(0) {} - DisableLight(int index) : mIndex(index) {} - - DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} - - osg::Object* cloneType() const override { return new DisableLight(mIndex); } - osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); } - bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast(obj)!=nullptr; } - const char* libraryName() const override { return "SceneUtil"; } - const char* className() const override { return "DisableLight"; } - Type getType() const override { return LIGHT; } - - unsigned int getMember() const override - { - return mIndex; - } - - bool getModeUsage(ModeUsage & usage) const override - { - usage.usesMode(GL_LIGHT0 + mIndex); - return true; - } - - int compare(const StateAttribute &sa) const override - { - throw std::runtime_error("DisableLight::compare: unimplemented"); - } - - void apply(osg::State& state) const override - { - int lightNum = GL_LIGHT0 + mIndex; - glLightfv( lightNum, GL_AMBIENT, mnullptr.ptr() ); - glLightfv( lightNum, GL_DIFFUSE, mnullptr.ptr() ); - glLightfv( lightNum, GL_SPECULAR, mnullptr.ptr() ); - - LightStateCache* cache = getLightStateCache(state.getContextID()); - cache->lastAppliedLight[mIndex] = nullptr; - } - - private: - unsigned int mIndex; - osg::Vec4f mnullptr; - }; - void LightManager::setStartLight(int start) { mStartLight = start; + if (!usingFFP()) return; + // Set default light state to zero // This is necessary because shaders don't respect glDisable(GL_LIGHTX) so in addition to disabling // we'll have to set a light state that has no visible effect - for (int i=start; i<8; ++i) + for (int i = start; i < getMaxLights(); ++i) { osg::ref_ptr defaultLight (new DisableLight(i)); getOrCreateStateSet()->setAttributeAndModes(defaultLight, osg::StateAttribute::OFF); @@ -353,10 +1113,142 @@ namespace SceneUtil return mStartLight; } - static int sLightId = 0; + void LightManager::update(size_t frameNum) + { + getLightIndexMap(frameNum).clear(); + mLights.clear(); + mLightsInViewSpace.clear(); + + // Do an occasional cleanup for orphaned lights. + for (int i = 0; i < 2; ++i) + { + if (mStateSetCache[i].size() > 5000) + mStateSetCache[i].clear(); + } + } + + void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum) + { + LightSourceTransform l; + l.mLightSource = lightSource; + l.mWorldMatrix = worldMat; + osg::Vec3f pos = osg::Vec3f(worldMat.getTrans().x(), worldMat.getTrans().y(), worldMat.getTrans().z()); + lightSource->getLight(frameNum)->setPosition(osg::Vec4f(pos, 1.f)); + + mLights.push_back(l); + } + + void LightManager::setSunlight(osg::ref_ptr sun) + { + if (usingFFP()) return; + + mSun = sun; + } + + osg::ref_ptr LightManager::getSunlight() + { + return mSun; + } + + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) + { + // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) + size_t hash = 0; + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + Misc::hashCombine(hash, id); + + if (getLightingMethod() != LightingMethod::SingleUBO) + continue; + + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; + + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + } + + auto& stateSetCache = mStateSetCache[frameNum%2]; + + auto found = stateSetCache.find(hash); + if (found != stateSetCache.end()) + { + mStateSetGenerator->update(found->second, lightList, frameNum); + return found->second; + } + + auto stateset = mStateSetGenerator->generate(lightList, frameNum); + stateSetCache.emplace(hash, stateset); + return stateset; + } + + const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum) + { + bool isReflection = isReflectionCamera(camera); + osg::observer_ptr camPtr (camera); + auto it = mLightsInViewSpace.find(camPtr); + + if (it == mLightsInViewSpace.end()) + { + it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; + + for (const auto& transform : mLights) + { + osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); + + float radius = transform.mLightSource->getRadius(); + + osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius * mPointLightRadiusMultiplier); + transformBoundingSphere(worldViewMat, viewBound); + + if (!isReflection && mPointLightFadeEnd != 0.f) + { + const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; + float fade = 1 - std::clamp((viewBound.center().length() - mPointLightFadeStart) / fadeDelta, 0.f, 1.f); + if (fade == 0.f) + continue; + + auto* light = transform.mLightSource->getLight(frameNum); + light->setDiffuse(light->getDiffuse() * fade); + } + + LightSourceViewBound l; + l.mLightSource = transform.mLightSource; + l.mViewBound = viewBound; + it->second.push_back(l); + } + } + + if (getLightingMethod() == LightingMethod::SingleUBO) + { + if (it->second.size() > static_cast(getMaxLightsInScene() - 1)) + { + auto sorter = [] (const LightSourceViewBound& left, const LightSourceViewBound& right) { + return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2(); + }; + std::sort(it->second.begin() + 1, it->second.end(), sorter); + it->second.erase((it->second.begin() + 1) + (getMaxLightsInScene() - 2), it->second.end()); + } + } + + return it->second; + } + + void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix) + { + auto* light = lightSource->getLight(frameNum); + auto& buf = getLightBuffer(frameNum); + buf->setDiffuse(index, light->getDiffuse()); + buf->setAmbient(index, light->getAmbient()); + buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); + buf->setPosition(index, light->getPosition() * (*viewMatrix)); + } LightSource::LightSource() : mRadius(0.f) + , mActorFade(1.f) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; @@ -365,19 +1257,14 @@ namespace SceneUtil LightSource::LightSource(const LightSource ©, const osg::CopyOp ©op) : osg::Node(copy, copyop) , mRadius(copy.mRadius) + , mActorFade(copy.mActorFade) { mId = sLightId++; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } - - bool sortLights (const LightManager::LightSourceViewBound* left, const LightManager::LightSourceViewBound* right) - { - return left->mViewBound.center().length2() - left->mViewBound.radius2()*81 < right->mViewBound.center().length2() - right->mViewBound.radius2()*81; - } - void LightListCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osgUtil::CullVisitor* cv = static_cast(nv); @@ -413,7 +1300,7 @@ namespace SceneUtil // Don't use Camera::getViewMatrix, that one might be relative to another camera! const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); - const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix); + const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix, mLastFrameNumber); // get the node bounds in view space // NB do not node->getBound() * modelView, that would apply the node's transformation twice @@ -421,7 +1308,7 @@ namespace SceneUtil osg::Transform* transform = node->asTransform(); if (transform) { - for (unsigned int i=0; igetNumChildren(); ++i) + for (size_t i = 0; i < transform->getNumChildren(); ++i) nodeBound.expandBy(transform->getChild(i)->getBound()); } else @@ -430,7 +1317,7 @@ namespace SceneUtil transformBoundingSphere(mat, nodeBound); mLightList.clear(); - for (unsigned int i=0; i (8 - mLightManager->getStartLight()); + size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); osg::StateSet* stateset = nullptr; @@ -451,12 +1338,12 @@ namespace SceneUtil { // remove lights culled by this camera LightManager::LightList lightList = mLightList; - for (LightManager::LightList::iterator it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights; ) + for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) { osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius*2; + bs._radius = bs._radius * 2.0; osg::CullingSet& cullingSet = stack.front(); if (cullingSet.isCulled(bs)) { @@ -474,10 +1361,10 @@ namespace SceneUtil while (lightList.size() > maxLights) lightList.pop_back(); } - stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber()); + stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); } else - stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber()); + stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); cv->pushStateSet(stateset); diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index c370f1b7f0..9a1fd54700 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -2,6 +2,9 @@ #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H #include +#include +#include +#include #include @@ -9,6 +12,10 @@ #include #include +#include + +#include + namespace osgUtil { class CullVisitor; @@ -16,6 +23,17 @@ namespace osgUtil namespace SceneUtil { + class LightBuffer; + class StateSetGenerator; + + enum class LightingMethod + { + FFP, + PerObjectUniform, + SingleUBO, + }; + + void configureStateSetSunOverride(LightingMethod method, const osg::Light* light, osg::StateSet* stateset, int mode = osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); /// LightSource managed by a LightManager. /// @par Typically used for point lights. Spot lights are not supported yet. Directional lights affect the whole scene @@ -35,6 +53,8 @@ namespace SceneUtil int mId; + float mActorFade; + public: META_Node(SceneUtil, LightSource) @@ -54,10 +74,20 @@ namespace SceneUtil mRadius = radius; } + void setActorFade(float alpha) + { + mActorFade = alpha; + } + + float getActorFade() const + { + return mActorFade; + } + /// Get the osg::Light safe for modification in the given frame. /// @par May be used externally to animate the light's color/attenuation properties, /// and is used internally to synchronize the light's position with the position of the LightSource. - osg::Light* getLight(unsigned int frame) + osg::Light* getLight(size_t frame) { return mLight[frame % 2]; } @@ -83,31 +113,9 @@ namespace SceneUtil class LightManager : public osg::Group { public: - - META_Node(SceneUtil, LightManager) - - LightManager(); - - LightManager(const LightManager& copy, const osg::CopyOp& copyop); - - /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired. - /// By default, it's ~0u i.e. always on. - /// If you have some views that do not require lighting, then set the Camera's cull mask to not include - /// the lightingMask for a much faster cull and rendering. - void setLightingMask (unsigned int mask); - - unsigned int getLightingMask() const; - - /// Set the first light index that should be used by this manager, typically the number of directional lights in the scene. - void setStartLight(int start); - - int getStartLight() const; - - /// Internal use only, called automatically by the LightManager's UpdateCallback - void update(); - - /// Internal use only, called automatically by the LightSource's UpdateCallback - void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum); + static LightingMethod getLightingMethodFromString(const std::string& value); + /// Returns string as used in settings file, or the empty string if the method is undefined + static std::string getLightingMethodString(LightingMethod method); struct LightSourceTransform { @@ -115,36 +123,120 @@ namespace SceneUtil osg::Matrixf mWorldMatrix; }; - const std::vector& getLights() const; - struct LightSourceViewBound { LightSource* mLightSource; osg::BoundingSphere mViewBound; }; - const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix); + using LightList = std::vector; - typedef std::vector LightList; + META_Node(SceneUtil, LightManager) - osg::ref_ptr getLightListStateSet(const LightList& lightList, unsigned int frameNum); + LightManager(bool ffp = true); + + LightManager(const LightManager& copy, const osg::CopyOp& copyop); + + /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired. + /// By default, it's ~0u i.e. always on. + /// If you have some views that do not require lighting, then set the Camera's cull mask to not include + /// the lightingMask for a much faster cull and rendering. + void setLightingMask(size_t mask); + size_t getLightingMask() const; + + /// Set the first light index that should be used by this manager, typically the number of directional lights in the scene. + void setStartLight(int start); + int getStartLight() const; + + /// Internal use only, called automatically by the LightManager's UpdateCallback + void update(size_t frameNum); + + /// Internal use only, called automatically by the LightSource's UpdateCallback + void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum); + + const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix, size_t frameNum); + + osg::ref_ptr getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix); + + void setSunlight(osg::ref_ptr sun); + osg::ref_ptr getSunlight(); + + bool usingFFP() const; + + LightingMethod getLightingMethod() const; + + int getMaxLights() const; + + int getMaxLightsInScene() const; + + auto& getDummies() { return mDummies; } + + auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; } + + auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + + osg::Matrixf getSunlightBuffer(size_t frameNum) const { return mSunlightBuffers[frameNum%2]; } + void setSunlightBuffer(const osg::Matrixf& buffer, size_t frameNum) { mSunlightBuffers[frameNum%2] = buffer; } + + std::map getLightDefines() const; + + void processChangedSettings(const Settings::CategorySettingVector& changed); + + /// Not thread safe, it is the responsibility of the caller to stop/start threading on the viewer + void updateMaxLights(); private: - // Lights collected from the scene graph. Only valid during the cull traversal. + void initFFP(int targetLights); + void initPerObjectUniform(int targetLights); + void initSingleUBO(int targetLights); + + void updateSettings(); + + void setLightingMethod(LightingMethod method); + void setMaxLights(int value); + + void updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum, const osg::RefMatrix* viewMatrix); + std::vector mLights; - typedef std::vector LightSourceViewBoundCollection; + using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; // < Light list hash , StateSet > - typedef std::map > LightStateSetMap; + using LightStateSetMap = std::map>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; int mStartLight; - unsigned int mLightingMask; + size_t mLightingMask; + + osg::ref_ptr mSun; + + osg::ref_ptr mLightBuffers[2]; + + osg::Matrixf mSunlightBuffers[2]; + + // < Light ID , Buffer Index > + using LightIndexMap = std::unordered_map; + LightIndexMap mLightIndexMaps[2]; + + std::unique_ptr mStateSetGenerator; + + LightingMethod mLightingMethod; + + float mPointLightRadiusMultiplier; + float mPointLightFadeEnd; + float mPointLightFadeStart; + + int mMaxLights; + + static constexpr auto mMaxLightsLowerLimit = 2; + static constexpr auto mMaxLightsUpperLimit = 64; + static constexpr auto mFFPMaxLights = 8; + + static const std::unordered_map mLightingMethodSettingMap; }; /// To receive lighting, objects must be decorated by a LightListCallback. Light list callbacks must be added via @@ -180,7 +272,7 @@ namespace SceneUtil private: LightManager* mLightManager; - unsigned int mLastFrameNumber; + size_t mLastFrameNumber; LightManager::LightList mLightList; std::set mIgnoredLightSources; }; diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index e9be05908e..6a1a1376ec 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -58,7 +58,7 @@ namespace SceneUtil light->setQuadraticAttenuation(quadraticAttenuation); } - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior) + osg::ref_ptr addLight(osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior) { SceneUtil::FindByNameVisitor visitor("AttachLight"); node->accept(visitor); @@ -85,8 +85,9 @@ namespace SceneUtil attachTo = trans; } - osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior); + osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1)); attachTo->addChild(lightSource); + return lightSource; } osg::ref_ptr createLightSource(const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient) diff --git a/components/sceneutil/lightutil.hpp b/components/sceneutil/lightutil.hpp index 7096c38b20..1ddfa3d45f 100644 --- a/components/sceneutil/lightutil.hpp +++ b/components/sceneutil/lightutil.hpp @@ -32,7 +32,7 @@ namespace SceneUtil /// @param partsysMask Node mask to ignore when computing the sub graph's bounding box. /// @param lightMask Mask to assign to the newly created LightSource. /// @param isExterior Is the light outside? May be used for deciding which attenuation settings to use. - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior); + osg::ref_ptr addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior); /// @brief Convert an ESM::Light to a SceneUtil::LightSource, and return it. /// @param esmLight The light definition coming from the game files containing radius, color, flicker, etc. diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 4f887e659b..3a5b464405 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -9,17 +9,28 @@ #include #include +#include #include #include namespace Shader { + ShaderManager::ShaderManager() + : mLightingMethod(SceneUtil::LightingMethod::FFP) + { + } + void ShaderManager::setShaderPath(const std::string &path) { mPath = path; } + void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + } + bool addLineDirectivesAfterConditionalBlocks(std::string& source) { for (size_t position = 0; position < source.length(); ) @@ -344,6 +355,8 @@ namespace Shader program->addShader(fragmentShader); program->addBindAttribLocation("aOffset", 6); program->addBindAttribLocation("aRotation", 7); + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 13db30b019..2450f0d6dc 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -11,16 +11,38 @@ #include +#include + +namespace Resource +{ + class SceneManager; +} + +namespace SceneUtil +{ + enum class LightingMethod; +} + namespace Shader { + enum class UBOBinding + { + LightBuffer + }; + /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's. /// @par Shader templates can get the value of a define with the syntax @define. class ShaderManager { public: + + ShaderManager(); + void setShaderPath(const std::string& path); + void setLightingMethod(SceneUtil::LightingMethod method); + typedef std::map DefineMap; /// Create or retrieve a shader instance. @@ -59,6 +81,8 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; + SceneUtil::LightingMethod mLightingMethod; + std::mutex mMutex; }; diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index acc8482991..4814b206b9 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -40,7 +40,7 @@ Only affects objects that render with shaders (see 'force shaders' option). Always affects terrain. Leaving this option at its default makes the lighting compatible with Morrowind's fixed-function method, -but the lighting may appear dull and there might be colour shifts. +but the lighting may appear dull and there might be colour shifts. Setting this option to 'false' results in more dynamic lighting. auto use object normal maps @@ -148,6 +148,115 @@ By default, the fog becomes thicker proportionally to your distance from the cli This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +lighting method +--------------- + +:Type: string +:Range: legacy|shaders compatibility|shaders +:Default: default + +Sets the internal handling of light sources. + +'legacy' is restricted to 8 lights per object and emulates fixed function +pipeline compatible lighting. + +'shaders compatibility' removes the light limit controllable through :ref:`max +lights` and follows a modifed attenuation formula which can drastically reduce +light popping and seams. This mode also enables lighting on groundcover and a +configurable light fade. It is recommended to use this with older hardware and a +light limit closer to 8. Because of its wide range of compatibility it is set as +the default. + +'shaders' carries all of the benefits that 'shaders compatibility' does, but +uses a modern approach that allows for a higher :ref:`max lights` count with +little to no performance penalties on modern hardware. It is recommended to use +this mode when supported and where the GPU is not a bottleneck. On some weaker +devices, using this mode along with :ref:`force per pixel lighting` can carry +performance penalties. + +Note that when enabled, groundcover lighting is forced to be vertex lighting, +unless normal maps are provided. This is due to some groundcover mods using the +Z-Up normals technique to avoid some common issues with shading. As a +consequence, per pixel lighting would give undesirable results. + +This setting has no effect if :ref:`force shaders` is 'false'. + +light bounds multiplier +----------------------- + +:Type: float +:Range: 0.0-5.0 +:Default: 1.65 + +Controls the bounding sphere radius of point lights, which is used to determine +if an object should receive lighting from a particular light source. Note, this +has no direct effect on the overall illumination of lights. Larger multipliers +will allow for smoother transitions of light sources, but may require an +increase in :ref:`max lights` and thus carries a performance penalty. This +especially helps with abrupt light popping with handheld light sources such as +torches and lanterns. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +maximum light distance +---------------------- + +:Type: float +:Range: The whole range of 32-bit floating point +:Default: 8192 + +The maximum distance from the camera that lights will be illuminated, applies to +both interiors and exteriors. A lower distance will improve performance. Set +this to a non-positive value to disable fading. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +light fade start +---------------- + +:Type: float +:Range: 0.0-1.0 +:Default: 0.85 + +The fraction of the maximum distance at which lights will begin to fade away. +Tweaking it will make the transition proportionally more or less smooth. + +This setting has no effect if the :ref:`maximum light distance` is non-positive +or :ref:`lighting method` is 'legacy'. + +max lights +---------- + +:Type: integer +:Range: 2-64 +:Default: 8 + +Sets the maximum number of lights that each object can receive lighting from. +Increasing this too much can cause significant performance loss, especially if +:ref:`lighting method` is not set to 'shaders' or :ref:`force per pixel +lighting` is on. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +minimum interior brightness +------------------------ + +:Type: float +:Range: 0.0-1.0 +:Default: 0.08 + +Sets the minimum interior ambient brightness for interior cells when +:ref:`lighting method` is not 'legacy'. A consequence of the new lighting system +is that interiors will sometimes be darker since light sources now have sensible +fall-offs. A couple solutions are to either add more lights or increase their +radii to compensate, but these require content changes. For best results it is +recommended to set this to 0.0 to retain the colors that level designers +intended. If brighter interiors are wanted, however, this setting should be +increased. Note, it is advised to keep this number small (< 0.1) to avoid the +aforementioned changes in visuals. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + antialias alpha test --------------------------------------- diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index babb5c28f9..11ad9fd396 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -457,6 +457,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +