#include "weather.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwsound/sound.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/sky.hpp" #include "player.hpp" #include "esmstore.hpp" #include "cellstore.hpp" #include using namespace MWWorld; namespace { static const int invalidWeatherID = -1; // linear interpolate between x and y based on factor. float lerp (float x, float y, float factor) { return x * (1-factor) + y * factor; } // linear interpolate between x and y based on factor. osg::Vec4f lerp (const osg::Vec4f& x, const osg::Vec4f& y, float factor) { return x * (1-factor) + y * factor; } } template T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings) const { // TODO: use pre/post sunset/sunrise time values in [Weather] section // night if (gameHour <= timeSettings.mNightEnd || gameHour >= timeSettings.mNightStart + 1) return mNightValue; // sunrise else if (gameHour >= timeSettings.mNightEnd && gameHour <= timeSettings.mDayStart + 1) { if (gameHour <= timeSettings.mSunriseTime) { // fade in float advance = timeSettings.mSunriseTime - gameHour; float factor = advance / 0.5f; return lerp(mSunriseValue, mNightValue, factor); } else { // fade out float advance = gameHour - timeSettings.mSunriseTime; float factor = advance / 3.f; return lerp(mSunriseValue, mDayValue, factor); } } // day else if (gameHour >= timeSettings.mDayStart + 1 && gameHour <= timeSettings.mDayEnd - 1) return mDayValue; // sunset else if (gameHour >= timeSettings.mDayEnd - 1 && gameHour <= timeSettings.mNightStart + 1) { if (gameHour <= timeSettings.mDayEnd + 1) { // fade in float advance = (timeSettings.mDayEnd + 1) - gameHour; float factor = (advance / 2); return lerp(mSunsetValue, mDayValue, factor); } else { // fade out float advance = gameHour - (timeSettings.mDayEnd + 1); float factor = advance / 2.f; return lerp(mSunsetValue, mNightValue, factor); } } // shut up compiler return T(); } template class TimeOfDayInterpolator; template class TimeOfDayInterpolator; Weather::Weather(const std::string& name, const Fallback::Map& fallback, float stormWindSpeed, float rainSpeed, const std::string& particleEffect) : mCloudTexture(fallback.getFallbackString("Weather_" + name + "_Cloud_Texture")) , mSkyColor(fallback.getFallbackColour("Weather_" + name +"_Sky_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Sky_Night_Color")) , mFogColor(fallback.getFallbackColour("Weather_" + name + "_Fog_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Fog_Night_Color")) , mAmbientColor(fallback.getFallbackColour("Weather_" + name + "_Ambient_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Ambient_Night_Color")) , mSunColor(fallback.getFallbackColour("Weather_" + name + "_Sun_Sunrise_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Day_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Sunset_Color"), fallback.getFallbackColour("Weather_" + name + "_Sun_Night_Color")) , mLandFogDepth(fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Day_Depth"), fallback.getFallbackFloat("Weather_" + name + "_Land_Fog_Night_Depth")) , mSunDiscSunsetColor(fallback.getFallbackColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) , mWindSpeed(fallback.getFallbackFloat("Weather_" + name + "_Wind_Speed")) , mCloudSpeed(fallback.getFallbackFloat("Weather_" + name + "_Cloud_Speed")) , mGlareView(fallback.getFallbackFloat("Weather_" + name + "_Glare_View")) , mIsStorm(mWindSpeed > stormWindSpeed) , mRainSpeed(rainSpeed) , mRainFrequency(fallback.getFallbackFloat("Weather_" + name + "_Rain_Entrance_Speed")) , mParticleEffect(particleEffect) , mRainEffect(fallback.getFallbackBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") , mTransitionDelta(fallback.getFallbackFloat("Weather_" + name + "_Transition_Delta")) , mCloudsMaximumPercent(fallback.getFallbackFloat("Weather_" + name + "_Clouds_Maximum_Percent")) , mThunderFrequency(fallback.getFallbackFloat("Weather_" + name + "_Thunder_Frequency")) , mThunderThreshold(fallback.getFallbackFloat("Weather_" + name + "_Thunder_Threshold")) , mThunderSoundID() , mFlashDecrement(fallback.getFallbackFloat("Weather_" + name + "_Flash_Decrement")) , mFlashBrightness(0.0f) { mThunderSoundID[0] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_0"); mThunderSoundID[1] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_1"); mThunderSoundID[2] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_2"); mThunderSoundID[3] = fallback.getFallbackString("Weather_" + name + "_Thunder_Sound_ID_3"); // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect { mAmbientLoopSoundID = fallback.getFallbackString("Weather_" + name + "_Rain_Loop_Sound_ID"); if (mAmbientLoopSoundID.empty()) // default to "rain" if not set mAmbientLoopSoundID = "rain"; } else mAmbientLoopSoundID = fallback.getFallbackString("Weather_" + name + "_Ambient_Loop_Sound_ID"); if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) mAmbientLoopSoundID.clear(); /* Unhandled: Rain Diameter=600 ? Rain Height Min=200 ? Rain Height Max=700 ? Rain Threshold=0.6 ? Max Raindrops=650 ? */ } float Weather::transitionDelta() const { // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the // measurement is in real time, not in-game time. return mTransitionDelta; } float Weather::cloudBlendFactor(const float transitionRatio) const { // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. return transitionRatio / mCloudsMaximumPercent; } float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) { // When paused, the flash brightness remains the same and no new strikes can occur. if(!isPaused) { // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) { flashDecrement(elapsedSeconds); if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) { lightningAndThunder(); } } else { mFlashBrightness = 0.0f; } } return mFlashBrightness; } inline void Weather::flashDecrement(const float elapsedSeconds) { // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). float decrement = mFlashDecrement * elapsedSeconds; mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; } inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const { // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to // scaled based on how far past it is past the Thunder Threshold. float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; } inline void Weather::lightningAndThunder(void) { // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. // TODO: Determine the distribution of each distance to see if it's evenly weighted. unsigned int distance = Misc::Rng::rollDice(4); // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. mFlashBrightness += 1 - (distance * 0.25f); MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); } RegionWeather::RegionWeather(const ESM::Region& region) : mWeather(invalidWeatherID) , mChances() { mChances.reserve(10); mChances.push_back(region.mData.mClear); mChances.push_back(region.mData.mCloudy); mChances.push_back(region.mData.mFoggy); mChances.push_back(region.mData.mOvercast); mChances.push_back(region.mData.mRain); mChances.push_back(region.mData.mThunder); mChances.push_back(region.mData.mAsh); mChances.push_back(region.mData.mBlight); mChances.push_back(region.mData.mA); mChances.push_back(region.mData.mB); } RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) : mWeather(state.mWeather) , mChances(state.mChances) { } RegionWeather::operator ESM::RegionWeatherState() const { ESM::RegionWeatherState state = { mWeather, mChances }; return state; } void RegionWeather::setChances(const std::vector& chances) { if(mChances.size() < chances.size()) { mChances.reserve(chances.size()); } std::vector::const_iterator it = chances.begin(); for(size_t i = 0; it != chances.end(); ++it, ++i) { mChances[i] = *it; } // Regional weather no longer supports the current type, select a new weather pattern. if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) { chooseNewWeather(); } } void RegionWeather::setWeather(int weatherID) { mWeather = weatherID; } int RegionWeather::getWeather() { // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. // Note that the region weather will be expired periodically when the weather update timer expires. if(mWeather == invalidWeatherID) { chooseNewWeather(); } return mWeather; } void RegionWeather::chooseNewWeather() { // All probabilities must add to 100 (responsibility of the user). // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 // and 70% will be greater than 30 (in theory). int chance = Misc::Rng::rollDice(100) + 1; // 1..100 int sum = 0; int i = 0; for(; static_cast(i) < mChances.size(); ++i) { sum += mChances[i]; if(chance <= sum) { mWeather = i; return; } } // if we hit this path then the chances don't add to 100, choose a default weather instead mWeather = 0; } MoonModel::MoonModel(const std::string& name, const Fallback::Map& fallback) : mFadeInStart(fallback.getFallbackFloat("Moons_" + name + "_Fade_In_Start")) , mFadeInFinish(fallback.getFallbackFloat("Moons_" + name + "_Fade_In_Finish")) , mFadeOutStart(fallback.getFallbackFloat("Moons_" + name + "_Fade_Out_Start")) , mFadeOutFinish(fallback.getFallbackFloat("Moons_" + name + "_Fade_Out_Finish")) , mAxisOffset(fallback.getFallbackFloat("Moons_" + name + "_Axis_Offset")) , mSpeed(fallback.getFallbackFloat("Moons_" + name + "_Speed")) , mDailyIncrement(fallback.getFallbackFloat("Moons_" + name + "_Daily_Increment")) , mFadeStartAngle(fallback.getFallbackFloat("Moons_" + name + "_Fade_Start_Angle")) , mFadeEndAngle(fallback.getFallbackFloat("Moons_" + name + "_Fade_End_Angle")) , mMoonShadowEarlyFadeAngle(fallback.getFallbackFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. mSpeed = std::min(mSpeed, 180.0f / 23.0f); } MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { float rotationFromHorizon = angle(gameTime); MWRender::MoonState state = { rotationFromHorizon, mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. static_cast(phase(gameTime)), shadowBlend(rotationFromHorizon), earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) }; return state; } inline float MoonModel::angle(const TimeStamp& gameTime) const { // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. // When calculating the angle of the moon, several cases have to be taken into account: // 1. Moon rises and then sets in one day. // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). // 3. Moon sets and then rises in one day. float moonRiseHourToday = moonRiseHour(gameTime.getDay()); float moonRiseAngleToday = 0; if(gameTime.getHour() < moonRiseHourToday) { float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); if(moonRiseHourYesterday < 24) { float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); if(moonRiseAngleYesterday < 180) { // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; } } } else { moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); } if(moonRiseAngleToday >= 180) { // The moon set today, reset the angle to the horizon. moonRiseAngleToday = 0; } return moonRiseAngleToday; } inline float MoonModel::moonRiseHour(unsigned int daysPassed) const { // This arises from the start date of 16 Last Seed, 427 // TODO: Find an alternate formula that doesn't rely on this day being fixed. static const unsigned int startDay = 16; // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. // Note that we don't modulo after adding the latest daily increment because other calculations need to // know if doing so would cause the moon rise to be postponed until the next day (which happens when // the moon rise hour is >= 24 in Morrowind). return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); } inline float MoonModel::rotation(float hours) const { // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure // of whole rotations that could be completed in a day. return 15.0f * mSpeed * hours; } inline unsigned int MoonModel::phase(const TimeStamp& gameTime) const { // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. // Note: this is an internal helper, and as such we don't want to return MWRender::MoonState::Phase since we can't // forward declare it (C++11 strongly typed enums solve this). // If the moon didn't rise yet today, use yesterday's moon phase. if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) return (gameTime.getDay() / 3) % 8; else return ((gameTime.getDay() + 1) / 3) % 8; } inline float MoonModel::shadowBlend(float angle) const { // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk // that is roughly the color of the sky, to a textured surface. // Depending on the current angle, the following values describe the ratio between the textured moon // and the solid disk: // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) float fadeAngle = mFadeStartAngle - mFadeEndAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float fadeStartAngle2 = 180.0f - mFadeStartAngle; if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) return (angle - mFadeEndAngle) / fadeAngle; else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) return 1.0f; else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) return (fadeEndAngle2 - angle) / fadeAngle; else return 0.0f; } inline float MoonModel::hourlyAlpha(float gameHour) const { // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon // appears and disappears. // Depending on the current hour, the following values describe how transparent the moon is. // 1. From Fade Out Start to Fade Out Finish: 1..0 // 2. From Fade Out Finish to Fade In Start: 0 (transparent) // 3. From Fade In Start to Fade In Finish: 0..1 // 4. From Fade In Finish to Fade Out Start: 1 (solid) if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) return 0.0f; else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); else return 1.0f; } inline float MoonModel::earlyMoonShadowAlpha(float angle) const { // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. // Depending on the current angle, the following values describe how transparent the moon is. // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; float fadeEndAngle2 = 180.0f - mFadeEndAngle; float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) return 1.0f; else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; else return 0.0f; } WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, const Fallback::Map& fallback, MWWorld::ESMStore& store) : mStore(store) , mRendering(rendering) , mSunriseTime(fallback.getFallbackFloat("Weather_Sunrise_Time")) , mSunsetTime(fallback.getFallbackFloat("Weather_Sunset_Time")) , mSunriseDuration(fallback.getFallbackFloat("Weather_Sunrise_Duration")) , mSunsetDuration(fallback.getFallbackFloat("Weather_Sunset_Duration")) , mSunPreSunsetTime(fallback.getFallbackFloat("Weather_Sun_Pre-Sunset_Time")) , mNightFade(0, 0, 0, 1) , mHoursBetweenWeatherChanges(fallback.getFallbackFloat("Weather_Hours_Between_Weather_Changes")) , mRainSpeed(fallback.getFallbackFloat("Weather_Precip_Gravity")) , mUnderwaterFog(fallback.getFallbackFloat("Water_UnderwaterSunriseFog"), fallback.getFallbackFloat("Water_UnderwaterDayFog"), fallback.getFallbackFloat("Water_UnderwaterSunsetFog"), fallback.getFallbackFloat("Water_UnderwaterNightFog")) , mWeatherSettings() , mMasser("Masser", fallback) , mSecunda("Secunda", fallback) , mWindSpeed(0.f) , mIsStorm(false) , mStormDirection(0,1,0) , mCurrentRegion() , mTimePassed(0) , mFastForward(false) , mWeatherUpdateTime(mHoursBetweenWeatherChanges) , mTransitionFactor(0) , mCurrentWeather(0) , mNextWeather(0) , mQueuedWeather(0) , mRegions() , mResult() , mAmbientSound() , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; mTimeSettings.mNightEnd = mSunriseTime - 0.5f; mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; mTimeSettings.mDayEnd = mSunsetTime; mTimeSettings.mSunriseTime = mSunriseTime; mWeatherSettings.reserve(10); addWeather("Clear", fallback); // 0 addWeather("Cloudy", fallback); // 1 addWeather("Foggy", fallback); // 2 addWeather("Overcast", fallback); // 3 addWeather("Rain", fallback); // 4 addWeather("Thunderstorm", fallback); // 5 addWeather("Ashstorm", fallback, "meshes\\ashcloud.nif"); // 6 addWeather("Blight", fallback, "meshes\\blightcloud.nif"); // 7 addWeather("Snow", fallback, "meshes\\snow.nif"); // 8 addWeather("Blizzard", fallback, "meshes\\blizzard.nif"); // 9 Store::iterator it = store.get().begin(); for(; it != store.get().end(); ++it) { std::string regionID = Misc::StringUtils::lowerCase(it->mId); mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } forceWeather(0); } WeatherManager::~WeatherManager() { stopSounds(); } void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) { // In Morrowind, this seems to have the following behavior, when applied to the current region: // - When there is no transition in progress, start transitioning to the new weather. // - If there is a transition in progress, queue up the transition and process it when the current one completes. // - If there is a transition in progress, and a queued transition, overwrite the queued transition. // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. // If the region isn't current, Morrowind will store the new weather for the region in question. if(weatherID < mWeatherSettings.size()) { std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setWeather(weatherID); regionalWeatherChanged(it->first, it->second); } } } void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. // In Morrowind, this seems to have the following behavior when applied to the current region: // - If the region supports the current weather, no change in current weather occurs. // - If the region no longer supports the current weather, and there is no transition in progress, begin to // transition to a new supported weather type. // - If the region no longer supports the current weather, and there is a transition in progress, queue a // transition to a new supported weather type. std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { it->second.setChances(chances); regionalWeatherChanged(it->first, it->second); } } void WeatherManager::playerTeleported() { // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to // be changed immediately, and any transitions for the previous region discarded. MWBase::World* world = MWBase::Environment::get().getWorld(); if(world->isCellExterior() || world->isCellQuasiExterior()) { std::string playerRegion = Misc::StringUtils::lowerCase(world->getPlayerPtr().getCell()->getCell()->mRegion); std::map::iterator it = mRegions.find(playerRegion); if(it != mRegions.end() && playerRegion != mCurrentRegion) { mCurrentRegion = playerRegion; forceWeather(it->second.getWeather()); } } } void WeatherManager::update(float duration, bool paused) { MWWorld::ConstPtr player = MWMechanics::getPlayer(); MWBase::World& world = *MWBase::Environment::get().getWorld(); TimeStamp time = world.getTimeStamp(); if(!paused || mFastForward) { // Add new transitions when either the player's current external region changes. std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); if(updateWeatherTime() || updateWeatherRegion(playerRegion)) { std::map::iterator it = mRegions.find(mCurrentRegion); if(it != mRegions.end()) { addWeatherTransition(it->second.getWeather()); } } updateWeatherTransitions(duration); } const bool exterior = (world.isCellExterior() || world.isCellQuasiExterior()); if(!exterior) { mRendering.setSkyEnabled(false); stopSounds(); return; } calculateWeatherResult(time.getHour(), duration, paused); mWindSpeed = mResult.mWindSpeed; mIsStorm = mResult.mIsStorm; if (mIsStorm) { osg::Vec3f playerPos (player.getRefData().getPosition().asVec3()); osg::Vec3f redMountainPos (19950, 72032, 27831); mStormDirection = (playerPos - redMountainPos); mStormDirection.z() = 0; mStormDirection.normalize(); mRendering.getSkyManager()->setStormDirection(mStormDirection); } // disable sun during night if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) mRendering.getSkyManager()->sunDisable(); else mRendering.getSkyManager()->sunEnable(); // Update the sun direction. Run it east to west at a fixed angle from overhead. // The sun's speed at day and night may differ, since mSunriseTime and mNightStart // mark when the sun is level with the horizon. { // Shift times into a 24-hour window beginning at mSunriseTime... float adjustedHour = time.getHour(); float adjustedNightStart = mTimeSettings.mNightStart; if ( time.getHour() < mSunriseTime ) adjustedHour += 24.f; if ( mTimeSettings.mNightStart < mSunriseTime ) adjustedNightStart += 24.f; const bool is_night = adjustedHour >= adjustedNightStart; const float dayDuration = adjustedNightStart - mSunriseTime; const float nightDuration = 24.f - dayDuration; double theta; if ( !is_night ) { theta = M_PI * (adjustedHour - mSunriseTime) / dayDuration; } else { theta = M_PI * (1.f - (adjustedHour - adjustedNightStart) / nightDuration); } osg::Vec3f final( static_cast(cos(theta)), -0.268f, // approx tan( -15 degrees ) static_cast(sin(theta))); mRendering.setSunDirection( final * -1 ); } float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings); float peakHour = mSunriseTime + (mSunsetTime - mSunriseTime) / 2; if (time.getHour() < mSunriseTime || time.getHour() > mSunsetTime) mRendering.getSkyManager()->setGlareTimeOfDayFade(0); else if (time.getHour() < peakHour) mRendering.getSkyManager()->setGlareTimeOfDayFade(1 - (peakHour - time.getHour()) / (peakHour - mSunriseTime)); else mRendering.getSkyManager()->setGlareTimeOfDayFade(1 - (time.getHour() - peakHour) / (mSunsetTime - peakHour)); mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mFogColor); mRendering.setAmbientColour(mResult.mAmbientColor); mRendering.setSunColour(mResult.mSunColor); mRendering.getSkyManager()->setWeather(mResult); // Play sounds if (mPlayingSoundID != mResult.mAmbientLoopSoundID) { stopSounds(); if (!mResult.mAmbientLoopSoundID.empty()) mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound(mResult.mAmbientLoopSoundID, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); mPlayingSoundID = mResult.mAmbientLoopSoundID; } if (mAmbientSound.get()) mAmbientSound->setVolume(mResult.mAmbientSoundVolume); } void WeatherManager::stopSounds() { if (mAmbientSound.get()) MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); mAmbientSound.reset(); mPlayingSoundID.clear(); } float WeatherManager::getWindSpeed() const { return mWindSpeed; } bool WeatherManager::isInStorm() const { return mIsStorm; } osg::Vec3f WeatherManager::getStormDirection() const { return mStormDirection; } void WeatherManager::advanceTime(double hours, bool incremental) { // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are // immediately applied, regardless of whatever transition time might have been remaining. mTimePassed += hours; mFastForward = !incremental ? true : mFastForward; } unsigned int WeatherManager::getWeatherID() const { return mCurrentWeather; } bool WeatherManager::isDark() const { TimeStamp time = MWBase::Environment::get().getWorld()->getTimeStamp(); bool exterior = (MWBase::Environment::get().getWorld()->isCellExterior() || MWBase::Environment::get().getWorld()->isCellQuasiExterior()); return exterior && (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart - 1); } void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { ESM::WeatherState state; state.mCurrentRegion = mCurrentRegion; state.mTimePassed = mTimePassed; state.mFastForward = mFastForward; state.mWeatherUpdateTime = mWeatherUpdateTime; state.mTransitionFactor = mTransitionFactor; state.mCurrentWeather = mCurrentWeather; state.mNextWeather = mNextWeather; state.mQueuedWeather = mQueuedWeather; std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { state.mRegions.insert(std::make_pair(it->first, it->second)); } writer.startRecord(ESM::REC_WTHR); state.save(writer); writer.endRecord(ESM::REC_WTHR); } bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if(ESM::REC_WTHR == type) { static const int oldestCompatibleSaveFormat = 2; if(reader.getFormat() < oldestCompatibleSaveFormat) { // Weather state isn't really all that important, so to preserve older save games, we'll just discard the // older weather records, rather than fail to handle the record. reader.skipRecord(); } else { ESM::WeatherState state; state.load(reader); mCurrentRegion.swap(state.mCurrentRegion); mTimePassed = state.mTimePassed; mFastForward = state.mFastForward; mWeatherUpdateTime = state.mWeatherUpdateTime; mTransitionFactor = state.mTransitionFactor; mCurrentWeather = state.mCurrentWeather; mNextWeather = state.mNextWeather; mQueuedWeather = state.mQueuedWeather; mRegions.clear(); std::map::iterator it = state.mRegions.begin(); if(it == state.mRegions.end()) { // When loading an imported save, the region modifiers aren't currently being set, so just reset them. importRegions(); } else { for(; it != state.mRegions.end(); ++it) { mRegions.insert(std::make_pair(it->first, RegionWeather(it->second))); } } } return true; } return false; } void WeatherManager::clear() { stopSounds(); mCurrentRegion = ""; mTimePassed = 0.0f; mWeatherUpdateTime = 0.0f; forceWeather(0); mRegions.clear(); importRegions(); } inline void WeatherManager::addWeather(const std::string& name, const Fallback::Map& fallback, const std::string& particleEffect) { static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->getFloat(); Weather weather(name, fallback, fStromWindSpeed, mRainSpeed, particleEffect); mWeatherSettings.push_back(weather); } inline void WeatherManager::importRegions() { Store::iterator it = mStore.get().begin(); for(; it != mStore.get().end(); ++it) { std::string regionID = Misc::StringUtils::lowerCase(it->mId); mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); } } inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) { // If the region is current, then add a weather transition for it. MWWorld::ConstPtr player = MWMechanics::getPlayer(); if(player.isInCell()) { std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); if(!playerRegion.empty() && (playerRegion == regionID)) { addWeatherTransition(region.getWeather()); } } } inline bool WeatherManager::updateWeatherTime() { mWeatherUpdateTime -= mTimePassed; mTimePassed = 0.0f; if(mWeatherUpdateTime <= 0.0f) { // Expire all regional weather, so that any call to getWeather() will return a new weather ID. std::map::iterator it = mRegions.begin(); for(; it != mRegions.end(); ++it) { it->second.setWeather(invalidWeatherID); } mWeatherUpdateTime += mHoursBetweenWeatherChanges; return true; } return false; } inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) { if(!playerRegion.empty() && playerRegion != mCurrentRegion) { mCurrentRegion = playerRegion; return true; } return false; } inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) { // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last // weather type set, regardless of the remaining transition time. if(!mFastForward && inTransition()) { const float delta = mWeatherSettings[mNextWeather].transitionDelta(); mTransitionFactor -= elapsedRealSeconds * delta; if(mTransitionFactor <= 0.0f) { mCurrentWeather = mNextWeather; mNextWeather = mQueuedWeather; mQueuedWeather = invalidWeatherID; // We may have begun processing the queued transition, so we need to apply the remaining time towards it. if(inTransition()) { const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); const float remainingSeconds = -(mTransitionFactor / delta); mTransitionFactor = 1.0f - (remainingSeconds * newDelta); } else { mTransitionFactor = 0.0f; } } } else { if(mQueuedWeather != invalidWeatherID) { mCurrentWeather = mQueuedWeather; } else if(mNextWeather != invalidWeatherID) { mCurrentWeather = mNextWeather; } mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; mFastForward = false; } } inline void WeatherManager::forceWeather(const int weatherID) { mTransitionFactor = 0.0f; mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; } inline bool WeatherManager::inTransition() { return mNextWeather != invalidWeatherID; } inline void WeatherManager::addWeatherTransition(const int weatherID) { // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if // no transition is in progress, otherwise it queues it to be transitioned. assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); if(!inTransition() && (weatherID != mCurrentWeather)) { mNextWeather = weatherID; mTransitionFactor = 1.0f; } else if(inTransition() && (weatherID != mNextWeather)) { mQueuedWeather = weatherID; } } inline void WeatherManager::calculateWeatherResult(const float gameHour, const float elapsedSeconds, const bool isPaused) { float flash = 0.0f; if(!inTransition()) { calculateResult(mCurrentWeather, gameHour); flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); } else { calculateTransitionResult(1 - mTransitionFactor, gameHour); float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, elapsedSeconds, isPaused); float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, elapsedSeconds, isPaused); flash = currentFlash + nextFlash; } osg::Vec4f flashColor(flash, flash, flash, 0.0f); mResult.mFogColor += flashColor; mResult.mAmbientColor += flashColor; mResult.mSunColor += flashColor; } inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) { const Weather& current = mWeatherSettings[weatherID]; mResult.mCloudTexture = current.mCloudTexture; mResult.mCloudBlendFactor = 0; mResult.mWindSpeed = current.mWindSpeed; mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mAmbientSoundVolume = 1.f; mResult.mEffectFade = 1.f; mResult.mIsStorm = current.mIsStorm; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainFrequency = current.mRainFrequency; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart - 1); mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings); mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings); mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings); mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings); mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings); mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings); if (gameHour >= mSunsetTime - mSunPreSunsetTime) { float factor = (gameHour - (mSunsetTime - mSunPreSunsetTime)) / mSunPreSunsetTime; factor = std::min(1.f, factor); mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline // would then clamp the total lighting to (1,1,1). A noticable change in color tone can be observed when only one of the color components gets clamped. // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); for (int i=0; i<3; ++i) mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); } else mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); if (gameHour >= mSunsetTime) { float fade = std::min(1.f, (gameHour - mSunsetTime) / 2.f); fade = fade*fade; mResult.mSunDiscColor.a() = 1.f - fade; } else if (gameHour >= mSunriseTime && gameHour <= mSunriseTime + 1) { mResult.mSunDiscColor.a() = gameHour - mSunriseTime; } else mResult.mSunDiscColor.a() = 1; } inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) { calculateResult(mCurrentWeather, gameHour); const MWRender::WeatherResult current = mResult; calculateResult(mNextWeather, gameHour); const MWRender::WeatherResult other = mResult; mResult.mCloudTexture = current.mCloudTexture; mResult.mNextCloudTexture = other.mCloudTexture; mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); mResult.mWindSpeed = lerp(current.mWindSpeed, other.mWindSpeed, factor); mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); mResult.mNight = current.mNight; if(factor < 0.5) { mResult.mIsStorm = current.mIsStorm; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; mResult.mParticleEffect = current.mParticleEffect; mResult.mRainSpeed = current.mRainSpeed; mResult.mRainFrequency = current.mRainFrequency; mResult.mAmbientSoundVolume = 1-(factor*2); mResult.mEffectFade = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; } else { mResult.mIsStorm = other.mIsStorm; mResult.mParticleEffect = other.mParticleEffect; mResult.mRainEffect = other.mRainEffect; mResult.mParticleEffect = other.mParticleEffect; mResult.mRainSpeed = other.mRainSpeed; mResult.mRainFrequency = other.mRainFrequency; mResult.mAmbientSoundVolume = 2*(factor-0.5f); mResult.mEffectFade = mResult.mAmbientSoundVolume; mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; } }