Add OpenMW commits up to 11 Mar 2020

pull/558/head
David Cernat 5 years ago
commit 43e7df6df8

@ -23,6 +23,7 @@
Bug #3977: Non-ASCII characters in object ID's are not supported Bug #3977: Non-ASCII characters in object ID's are not supported
Bug #4009: Launcher does not show data files on the first run after installing Bug #4009: Launcher does not show data files on the first run after installing
Bug #4077: Enchanted items are not recharged if they are not in the player's inventory Bug #4077: Enchanted items are not recharged if they are not in the player's inventory
Bug #4141: PCSkipEquip isn't set to 1 when reading books/scrolls
Bug #4202: Open .omwaddon files without needing toopen openmw-cs first Bug #4202: Open .omwaddon files without needing toopen openmw-cs first
Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect Bug #4240: Ash storm origin coordinates and hand shielding animation behavior are incorrect
Bug #4262: Rain settings are hardcoded Bug #4262: Rain settings are hardcoded
@ -36,6 +37,8 @@
Bug #4411: Reloading a saved game while falling prevents damage in some cases Bug #4411: Reloading a saved game while falling prevents damage in some cases
Bug #4449: Value returned by GetWindSpeed is incorrect Bug #4449: Value returned by GetWindSpeed is incorrect
Bug #4456: AiActivate should not be cancelled after target activation Bug #4456: AiActivate should not be cancelled after target activation
Bug #4493: If the setup doesn't find what it is expecting, it fails silently and displays the requester again instead of letting the user know what wasn't found.
Bug #4523: "player->ModCurrentFatigue -0.001" in global script does not cause the running player to fall
Bug #4540: Rain delay when exiting water Bug #4540: Rain delay when exiting water
Bug #4594: Actors without AI packages don't use Hello dialogue Bug #4594: Actors without AI packages don't use Hello dialogue
Bug #4598: Script parser does not support non-ASCII characters Bug #4598: Script parser does not support non-ASCII characters
@ -200,7 +203,8 @@
Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0 Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0
Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted
Bug #5278: Console command Show doesn't fall back to global variable after local var not found Bug #5278: Console command Show doesn't fall back to global variable after local var not found
Feature #1415: Infinite fall failsafe Bug #5300: NPCs don't switch from torch to shield when starting combat
Bug #5308: World map copying makes save loading much slower
Feature #1774: Handle AvoidNode Feature #1774: Handle AvoidNode
Feature #2229: Improve pathfinding AI Feature #2229: Improve pathfinding AI
Feature #3025: Analogue gamepad movement controls Feature #3025: Analogue gamepad movement controls
@ -224,6 +228,7 @@
Feature #4730: Native animated containers support Feature #4730: Native animated containers support
Feature #4784: Launcher: Duplicate Content Lists Feature #4784: Launcher: Duplicate Content Lists
Feature #4812: Support NiSwitchNode Feature #4812: Support NiSwitchNode
Feature #4831: Item search in the player's inventory
Feature #4836: Daytime node switch Feature #4836: Daytime node switch
Feature #4840: Editor: Transient terrain change support Feature #4840: Editor: Transient terrain change support
Feature #4859: Make water reflections more configurable Feature #4859: Make water reflections more configurable
@ -253,9 +258,11 @@
Feature #5146: Safe Dispose corpse Feature #5146: Safe Dispose corpse
Feature #5147: Show spell magicka cost in spell buying window Feature #5147: Show spell magicka cost in spell buying window
Feature #5170: Editor: Land shape editing, land selection Feature #5170: Editor: Land shape editing, land selection
Feature #5172: Editor: Delete instances/references with keypress in scene window
Feature #5193: Weapon sheathing Feature #5193: Weapon sheathing
Feature #5219: Impelement TestCells console command Feature #5219: Impelement TestCells console command
Feature #5224: Handle NiKeyframeController for NiTriShape Feature #5224: Handle NiKeyframeController for NiTriShape
Feature #5304: Morrowind-style bump-mapping
Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4686: Upgrade media decoder to a more current FFmpeg API
Task #4695: Optimize Distant Terrain memory consumption Task #4695: Optimize Distant Terrain memory consumption
Task #4789: Optimize cell transitions Task #4789: Optimize cell transitions

@ -42,6 +42,7 @@ New Editor Features:
- "Faction Ranks" table for "Faction" records (#4209) - "Faction Ranks" table for "Faction" records (#4209)
- Changes to height editing can be cancelled without changes to data (press esc to cancel) (#4840) - Changes to height editing can be cancelled without changes to data (press esc to cancel) (#4840)
- Land heightmap/shape editing and vertex selection (#5170) - Land heightmap/shape editing and vertex selection (#5170)
- Deleting instances with a keypress (#5172)
Bug Fixes: Bug Fixes:
- The Mouse Wheel can now be used for key bindings (#2679) - The Mouse Wheel can now be used for key bindings (#2679)

@ -355,6 +355,7 @@ void CSMPrefs::State::declare()
declareShortcut ("scene-select-secondary", "Secondary Select", declareShortcut ("scene-select-secondary", "Secondary Select",
QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton)); QKeySequence(Qt::ControlModifier | (int)Qt::MiddleButton));
declareModifier ("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift); declareModifier ("scene-speed-modifier", "Speed Modifier", Qt::Key_Shift);
declareShortcut ("scene-delete", "Delete Instance", QKeySequence(Qt::Key_Delete));
declareShortcut ("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5)); declareShortcut ("scene-load-cam-cell", "Load Camera Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_5));
declareShortcut ("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6)); declareShortcut ("scene-load-cam-eastcell", "Load East Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_6));
declareShortcut ("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8)); declareShortcut ("scene-load-cam-northcell", "Load North Cell", QKeySequence(Qt::KeypadModifier | Qt::Key_8));

@ -79,8 +79,9 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat
Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); Shader::ShaderManager::DefineMap defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines();
Shader::ShaderManager::DefineMap shadowDefines = SceneUtil::ShadowManager::getShadowsDisabledDefines(); Shader::ShaderManager::DefineMap shadowDefines = SceneUtil::ShadowManager::getShadowsDisabledDefines();
defines["forcePPL"] = "0"; defines["forcePPL"] = "0"; // Don't force per-pixel lighting
defines["clamp"] = "1"; defines["clamp"] = "1"; // Clamp lighting
defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind
for (const auto& define : shadowDefines) for (const auto& define : shadowDefines)
defines[define.first] = define.second; defines[define.first] = define.second;
mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines);

@ -10,6 +10,7 @@
#include "../../model/world/idtree.hpp" #include "../../model/world/idtree.hpp"
#include "../../model/world/commands.hpp" #include "../../model/world/commands.hpp"
#include "../../model/world/commandmacro.hpp" #include "../../model/world/commandmacro.hpp"
#include "../../model/prefs/shortcut.hpp"
#include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolbar.hpp"
#include "../widget/scenetoolmode.hpp" #include "../widget/scenetoolmode.hpp"
@ -96,6 +97,9 @@ CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, QWidg
{ {
connect(this, SIGNAL(requestFocus(const std::string&)), connect(this, SIGNAL(requestFocus(const std::string&)),
worldspaceWidget, SIGNAL(requestFocus(const std::string&))); worldspaceWidget, SIGNAL(requestFocus(const std::string&)));
CSMPrefs::Shortcut* deleteShortcut = new CSMPrefs::Shortcut("scene-delete", worldspaceWidget);
connect(deleteShortcut, SIGNAL(activated(bool)), this, SLOT(deleteSelectedInstances(bool)));
} }
void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar) void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar)
@ -659,3 +663,21 @@ void CSVRender::InstanceMode::subModeChanged (const std::string& id)
getWorldspaceWidget().abortDrag(); getWorldspaceWidget().abortDrag();
getWorldspaceWidget().setSubMode (getSubModeFromId (id), SceneUtil::Mask_EditorReference); getWorldspaceWidget().setSubMode (getSubModeFromId (id), SceneUtil::Mask_EditorReference);
} }
void CSVRender::InstanceMode::deleteSelectedInstances(bool active)
{
std::vector<osg::ref_ptr<TagBase> > selection = getWorldspaceWidget().getSelection (SceneUtil::Mask_EditorReference);
if (selection.empty()) return;
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
CSMWorld::IdTable& referencesTable = dynamic_cast<CSMWorld::IdTable&> (
*document.getData().getTableModel (CSMWorld::UniversalId::Type_References));
QUndoStack& undoStack = document.getUndoStack();
CSMWorld::CommandMacro macro (undoStack, "Delete Instances");
for(osg::ref_ptr<TagBase> tag: selection)
if (CSVRender::ObjectTag *objectTag = dynamic_cast<CSVRender::ObjectTag *> (tag.get()))
macro.push(new CSMWorld::DeleteCommand(referencesTable, objectTag->mObject->getReferenceId()));
getWorldspaceWidget().clearSelection (SceneUtil::Mask_EditorReference);
}

@ -92,6 +92,7 @@ namespace CSVRender
private slots: private slots:
void subModeChanged (const std::string& id); void subModeChanged (const std::string& id);
void deleteSelectedInstances(bool active);
}; };
} }

@ -461,6 +461,8 @@ namespace MWBase
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0; virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0;
virtual bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) = 0;
virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0;
virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0; virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0;
@ -825,6 +827,8 @@ namespace MWBase
virtual osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const = 0; virtual osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const = 0;
virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0;
virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0;
}; };
} }

@ -46,9 +46,11 @@ CompanionWindow::CompanionWindow(DragAndDrop *dragAndDrop, MessageBoxManager* ma
getWidget(mCloseButton, "CloseButton"); getWidget(mCloseButton, "CloseButton");
getWidget(mProfitLabel, "ProfitLabel"); getWidget(mProfitLabel, "ProfitLabel");
getWidget(mEncumbranceBar, "EncumbranceBar"); getWidget(mEncumbranceBar, "EncumbranceBar");
getWidget(mFilterEdit, "FilterEdit");
getWidget(mItemView, "ItemView"); getWidget(mItemView, "ItemView");
mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &CompanionWindow::onBackgroundSelected); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &CompanionWindow::onBackgroundSelected);
mItemView->eventItemClicked += MyGUI::newDelegate(this, &CompanionWindow::onItemSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &CompanionWindow::onItemSelected);
mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &CompanionWindow::onNameFilterChanged);
mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked);
@ -92,6 +94,12 @@ void CompanionWindow::onItemSelected(int index)
dragItem (nullptr, count); dragItem (nullptr, count);
} }
void CompanionWindow::onNameFilterChanged(MyGUI::EditBox* _sender)
{
mSortModel->setNameFilter(_sender->getCaption());
mItemView->update();
}
void CompanionWindow::dragItem(MyGUI::Widget* sender, int count) void CompanionWindow::dragItem(MyGUI::Widget* sender, int count)
{ {
mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count);
@ -113,6 +121,7 @@ void CompanionWindow::setPtr(const MWWorld::Ptr& npc)
mModel = new CompanionItemModel(npc); mModel = new CompanionItemModel(npc);
mSortModel = new SortFilterItemModel(mModel); mSortModel = new SortFilterItemModel(mModel);
mFilterEdit->setCaption(std::string());
mItemView->setModel(mSortModel); mItemView->setModel(mSortModel);
mItemView->resetScrollBars(); mItemView->resetScrollBars();

@ -39,11 +39,13 @@ namespace MWGui
DragAndDrop* mDragAndDrop; DragAndDrop* mDragAndDrop;
MyGUI::Button* mCloseButton; MyGUI::Button* mCloseButton;
MyGUI::EditBox* mFilterEdit;
MyGUI::TextBox* mProfitLabel; MyGUI::TextBox* mProfitLabel;
Widgets::MWDynamicStat* mEncumbranceBar; Widgets::MWDynamicStat* mEncumbranceBar;
MessageBoxManager* mMessageBoxManager; MessageBoxManager* mMessageBoxManager;
void onItemSelected(int index); void onItemSelected(int index);
void onNameFilterChanged(MyGUI::EditBox* _sender);
void onBackgroundSelected(); void onBackgroundSelected();
void dragItem(MyGUI::Widget* sender, int count); void dragItem(MyGUI::Widget* sender, int count);

@ -8,6 +8,7 @@
#include <MyGUI_RenderManager.h> #include <MyGUI_RenderManager.h>
#include <MyGUI_InputManager.h> #include <MyGUI_InputManager.h>
#include <MyGUI_Button.h> #include <MyGUI_Button.h>
#include <MyGUI_EditBox.h>
#include <osg/Texture2D> #include <osg/Texture2D>
@ -35,12 +36,10 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/scriptmanager.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/actionequip.hpp" #include "../mwworld/actionequip.hpp"
#include "../mwscript/interpretercontext.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
@ -104,6 +103,7 @@ namespace MWGui
getWidget(mLeftPane, "LeftPane"); getWidget(mLeftPane, "LeftPane");
getWidget(mRightPane, "RightPane"); getWidget(mRightPane, "RightPane");
getWidget(mArmorRating, "ArmorRating"); getWidget(mArmorRating, "ArmorRating");
getWidget(mFilterEdit, "FilterEdit");
mAvatarImage->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onAvatarClicked); mAvatarImage->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onAvatarClicked);
mAvatarImage->setRenderItemTexture(mPreviewTexture.get()); mAvatarImage->setRenderItemTexture(mPreviewTexture.get());
@ -118,6 +118,7 @@ namespace MWGui
mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged);
mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged);
mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged);
mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &InventoryWindow::onNameFilterChanged);
mFilterAll->setStateSelected(true); mFilterAll->setStateSelected(true);
@ -147,6 +148,8 @@ namespace MWGui
else else
mSortModel = new SortFilterItemModel(mTradeModel); mSortModel = new SortFilterItemModel(mTradeModel);
mSortModel->setNameFilter(mFilterEdit->getCaption());
mItemView->setModel(mSortModel); mItemView->setModel(mSortModel);
mFilterAll->setStateSelected(true); mFilterAll->setStateSelected(true);
@ -402,6 +405,11 @@ namespace MWGui
void InventoryWindow::onOpen() void InventoryWindow::onOpen()
{ {
// Reset the filter focus when opening the window
MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget();
if (focus == mFilterEdit)
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr);
if (!mPtr.isEmpty()) if (!mPtr.isEmpty())
{ {
updateEncumbranceBar(); updateEncumbranceBar();
@ -479,6 +487,12 @@ namespace MWGui
width*mScaleFactor/float(mPreview->getTextureWidth()), height*mScaleFactor/float(mPreview->getTextureHeight()))); width*mScaleFactor/float(mPreview->getTextureWidth()), height*mScaleFactor/float(mPreview->getTextureHeight())));
} }
void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender)
{
mSortModel->setNameFilter(_sender->getCaption());
mItemView->update();
}
void InventoryWindow::onFilterChanged(MyGUI::Widget* _sender) void InventoryWindow::onFilterChanged(MyGUI::Widget* _sender)
{ {
if (_sender == mFilterAll) if (_sender == mFilterAll)
@ -491,7 +505,6 @@ namespace MWGui
mSortModel->setCategory(SortFilterItemModel::Category_Magic); mSortModel->setCategory(SortFilterItemModel::Category_Magic);
else if (_sender == mFilterMisc) else if (_sender == mFilterMisc)
mSortModel->setCategory(SortFilterItemModel::Category_Misc); mSortModel->setCategory(SortFilterItemModel::Category_Misc);
mFilterAll->setStateSelected(false); mFilterAll->setStateSelected(false);
mFilterWeapon->setStateSelected(false); mFilterWeapon->setStateSelected(false);
mFilterApparel->setStateSelected(false); mFilterApparel->setStateSelected(false);
@ -521,6 +534,16 @@ namespace MWGui
void InventoryWindow::useItem(const MWWorld::Ptr &ptr, bool force) void InventoryWindow::useItem(const MWWorld::Ptr &ptr, bool force)
{ {
const std::string& script = ptr.getClass().getScript(ptr); const std::string& script = ptr.getClass().getScript(ptr);
if (!script.empty())
{
// Don't try to equip the item if PCSkipEquip is set to 1
if (ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 1)
{
ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1);
return;
}
ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 0);
}
MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::Ptr player = MWMechanics::getPlayer();
@ -540,43 +563,28 @@ namespace MWGui
if (canEquip.first == 0) if (canEquip.first == 0)
{ {
/// If PCSkipEquip is set, set OnPCEquip to 1 and don't message anything MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second);
if (!script.empty() && ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 1)
ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1);
else
MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second);
updateItemView(); updateItemView();
return; return;
} }
} }
} }
// If the item has a script, set its OnPcEquip to 1 // If the item has a script, set OnPCEquip or PCSkipEquip to 1
if (!script.empty() if (!script.empty())
// Another morrowind oddity: when an item has skipped equipping and pcskipequip is reset to 0 afterwards,
// the next time it is equipped will work normally, but will not set onpcequip
&& (ptr != mSkippedToEquip || ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 1))
ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1);
// Give the script a chance to run once before we do anything else
// this is important when setting pcskipequip as a reaction to onpcequip being set (bk_treasuryreport does this)
if (!force && !script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled())
{ {
MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here
MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); const std::string& type = ptr.getTypeName();
bool isBook = type == typeid(ESM::Book).name();
if (!isBook && type != typeid(ESM::Ingredient).name() && type != typeid(ESM::Repair).name())
ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1);
// Books must have PCSkipEquip set to 1 instead
else if (isBook)
ptr.getRefData().getLocals().setVarByInt(script, "pcskipequip", 1);
} }
mSkippedToEquip = MWWorld::Ptr(); std::shared_ptr<MWWorld::Action> action = ptr.getClass().use(ptr, force);
if (ptr.getRefData().getCount()) // make sure the item is still there, the script might have removed it action->execute(player);
{
if (script.empty() || ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 0)
{
std::shared_ptr<MWWorld::Action> action = ptr.getClass().use(ptr, force);
action->execute(player);
}
else
mSkippedToEquip = ptr;
}
if (isVisible()) if (isVisible())
{ {

@ -92,8 +92,8 @@ namespace MWGui
MyGUI::Button* mFilterApparel; MyGUI::Button* mFilterApparel;
MyGUI::Button* mFilterMagic; MyGUI::Button* mFilterMagic;
MyGUI::Button* mFilterMisc; MyGUI::Button* mFilterMisc;
MWWorld::Ptr mSkippedToEquip; MyGUI::EditBox* mFilterEdit;
GuiMode mGuiMode; GuiMode mGuiMode;
@ -121,6 +121,7 @@ namespace MWGui
void onWindowResize(MyGUI::Window* _sender); void onWindowResize(MyGUI::Window* _sender);
void onFilterChanged(MyGUI::Widget* _sender); void onFilterChanged(MyGUI::Widget* _sender);
void onNameFilterChanged(MyGUI::EditBox* _sender);
void onAvatarClicked(MyGUI::Widget* _sender); void onAvatarClicked(MyGUI::Widget* _sender);
void onPinToggled(); void onPinToggled();

@ -16,9 +16,6 @@ namespace MWGui
bool shouldAcceptKeyFocus(MyGUI::Widget* w) bool shouldAcceptKeyFocus(MyGUI::Widget* w)
{ {
if (w && w->getUserString("IgnoreTabKey") == "y")
return false;
return w && !w->castType<MyGUI::Window>(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled(); return w && !w->castType<MyGUI::Window>(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled();
} }

@ -250,6 +250,10 @@ namespace MWGui
return false; return false;
} }
std::string compare = Misc::StringUtils::lowerCase(item.mBase.getClass().getName(item.mBase));
if(compare.find(mNameFilter) == std::string::npos)
return false;
return true; return true;
} }
@ -277,6 +281,11 @@ namespace MWGui
mFilter = filter; mFilter = filter;
} }
void SortFilterItemModel::setNameFilter (const std::string& filter)
{
mNameFilter = Misc::StringUtils::lowerCase(filter);
}
void SortFilterItemModel::update() void SortFilterItemModel::update()
{ {
mSourceModel->update(); mSourceModel->update();

@ -25,6 +25,7 @@ namespace MWGui
void setCategory (int category); void setCategory (int category);
void setFilter (int filter); void setFilter (int filter);
void setNameFilter (const std::string& filter);
/// Use ItemStack::Type for sorting? /// Use ItemStack::Type for sorting?
void setSortByType(bool sort) { mSortByType = sort; } void setSortByType(bool sort) { mSortByType = sort; }
@ -57,6 +58,8 @@ namespace MWGui
int mCategory; int mCategory;
int mFilter; int mFilter;
bool mSortByType; bool mSortByType;
std::string mNameFilter; // filter by item name
}; };
} }

@ -56,8 +56,6 @@ namespace MWGui
getWidget(mEffectBox, "EffectsBox"); getWidget(mEffectBox, "EffectsBox");
getWidget(mFilterEdit, "FilterEdit"); getWidget(mFilterEdit, "FilterEdit");
mFilterEdit->setUserString("IgnoreTabKey", "y");
mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected);
mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged);
deleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onDeleteClicked); deleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onDeleteClicked);

@ -81,6 +81,7 @@ namespace MWGui
getWidget(mTotalBalance, "TotalBalance"); getWidget(mTotalBalance, "TotalBalance");
getWidget(mTotalBalanceLabel, "TotalBalanceLabel"); getWidget(mTotalBalanceLabel, "TotalBalanceLabel");
getWidget(mBottomPane, "BottomPane"); getWidget(mBottomPane, "BottomPane");
getWidget(mFilterEdit, "FilterEdit");
getWidget(mItemView, "ItemView"); getWidget(mItemView, "ItemView");
mItemView->eventItemClicked += MyGUI::newDelegate(this, &TradeWindow::onItemSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &TradeWindow::onItemSelected);
@ -92,6 +93,7 @@ namespace MWGui
mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged);
mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged);
mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged);
mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &TradeWindow::onNameFilterChanged);
mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onCancelButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onCancelButtonClicked);
mOfferButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onOfferButtonClicked); mOfferButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onOfferButtonClicked);
@ -176,8 +178,7 @@ namespace MWGui
setTitle(actor.getClass().getName(actor)); setTitle(actor.getClass().getName(actor));
onFilterChanged(mFilterAll); onFilterChanged(mFilterAll);
mFilterEdit->setCaption("");
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTotalBalance);
} }
void TradeWindow::onFrame(float dt) void TradeWindow::onFrame(float dt)
@ -185,6 +186,12 @@ namespace MWGui
checkReferenceAvailable(); checkReferenceAvailable();
} }
void TradeWindow::onNameFilterChanged(MyGUI::EditBox* _sender)
{
mSortModel->setNameFilter(_sender->getCaption());
mItemView->update();
}
void TradeWindow::onFilterChanged(MyGUI::Widget* _sender) void TradeWindow::onFilterChanged(MyGUI::Widget* _sender)
{ {
if (_sender == mFilterAll) if (_sender == mFilterAll)

@ -59,6 +59,8 @@ namespace MWGui
MyGUI::Button* mFilterMagic; MyGUI::Button* mFilterMagic;
MyGUI::Button* mFilterMisc; MyGUI::Button* mFilterMisc;
MyGUI::EditBox* mFilterEdit;
MyGUI::Button* mIncreaseButton; MyGUI::Button* mIncreaseButton;
MyGUI::Button* mDecreaseButton; MyGUI::Button* mDecreaseButton;
MyGUI::TextBox* mTotalBalanceLabel; MyGUI::TextBox* mTotalBalanceLabel;
@ -86,6 +88,7 @@ namespace MWGui
void sellItem (MyGUI::Widget* sender, int count); void sellItem (MyGUI::Widget* sender, int count);
void onFilterChanged(MyGUI::Widget* _sender); void onFilterChanged(MyGUI::Widget* _sender);
void onNameFilterChanged(MyGUI::EditBox* _sender);
void onOfferButtonClicked(MyGUI::Widget* _sender); void onOfferButtonClicked(MyGUI::Widget* _sender);
void onAccept(MyGUI::EditBox* sender); void onAccept(MyGUI::EditBox* sender);
void onCancelButtonClicked(MyGUI::Widget* _sender); void onCancelButtonClicked(MyGUI::Widget* _sender);

@ -1309,6 +1309,11 @@ namespace MWMechanics
if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name())
inventoryStore.unequipItem(*heldIter, ptr); inventoryStore.unequipItem(*heldIter, ptr);
} }
else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name())
{
// For hostile NPCs, see if they have anything better to equip first
inventoryStore.autoEquip(ptr);
}
heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);

@ -28,6 +28,10 @@ void AiSequence::copy (const AiSequence& sequence)
for (std::list<AiPackage *>::const_iterator iter (sequence.mPackages.begin()); for (std::list<AiPackage *>::const_iterator iter (sequence.mPackages.begin());
iter!=sequence.mPackages.end(); ++iter) iter!=sequence.mPackages.end(); ++iter)
mPackages.push_back ((*iter)->clone()); mPackages.push_back ((*iter)->clone());
// We need to keep an AiWander storage, if present - it has a state machine.
// Not sure about another temporary storages
sequence.mAiState.copy<AiWanderStorage>(mAiState);
} }
AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(-1) {} AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(-1) {}

@ -38,6 +38,14 @@ namespace MWMechanics
//return a reference to the (new allocated) object //return a reference to the (new allocated) object
return *result; return *result;
} }
template< class Derived >
void copy(DerivedClassStorage& destination) const
{
Derived* result = dynamic_cast<Derived*>(mStorage);
if (result != nullptr)
destination.store<Derived>(*result);
}
template< class Derived > template< class Derived >
void store( const Derived& payload ) void store( const Derived& payload )

@ -61,6 +61,34 @@ namespace MWMechanics
rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0)); rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0));
return position + osg::Vec3f(distance, 0.0, 0.0) * rotation; return position + osg::Vec3f(distance, 0.0, 0.0) * rotation;
} }
bool isDestinationHidden(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination)
{
const auto position = actor.getRefData().getPosition().asVec3();
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor);
osg::Vec3f direction = destination - position;
direction.normalize();
const auto visibleDestination = (
isWaterCreature || isFlyingCreature
? destination
: destination + osg::Vec3f(0, 0, halfExtents.z())
) + direction * std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
const int mask = MWPhysics::CollisionType_World
| MWPhysics::CollisionType_HeightMap
| MWPhysics::CollisionType_Door
| MWPhysics::CollisionType_Actor;
return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor);
}
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination)
{
const auto world = MWBase::Environment::get().getWorld();
const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor);
const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor);
}
} }
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat): AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
@ -265,6 +293,11 @@ namespace MWMechanics
completeManualWalking(actor, storage); completeManualWalking(actor, storage);
} }
if (wanderState == AiWanderStorage::Wander_Walking
&& (isDestinationHidden(actor, mPathFinder.getPath().back())
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
completeManualWalking(actor, storage);
return false; // AiWander package not yet completed return false; // AiWander package not yet completed
} }
@ -328,7 +361,10 @@ namespace MWMechanics
if (!isWaterCreature && destinationIsAtWater(actor, mDestination)) if (!isWaterCreature && destinationIsAtWater(actor, mDestination))
continue; continue;
if ((isWaterCreature || isFlyingCreature) && destinationThroughGround(currentPosition, mDestination)) if (isDestinationHidden(actor, mDestination))
continue;
if (isAreaOccupiedByOtherActor(actor, mDestination))
continue; continue;
if (isWaterCreature || isFlyingCreature) if (isWaterCreature || isFlyingCreature)
@ -357,16 +393,6 @@ namespace MWMechanics
return MWBase::Environment::get().getWorld()->isUnderwater(actor.getCell(), positionBelowSurface); return MWBase::Environment::get().getWorld()->isUnderwater(actor.getCell(), positionBelowSurface);
} }
/*
* Returns true if the start to end point travels through a collision point (land).
*/
bool AiWander::destinationThroughGround(const osg::Vec3f& startPoint, const osg::Vec3f& destination) {
const int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door;
return MWBase::Environment::get().getWorld()->castRay(startPoint.x(), startPoint.y(), startPoint.z(),
destination.x(), destination.y(), destination.z(),
mask);
}
void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) {
stopWalking(actor, storage); stopWalking(actor, storage);
mObstacleCheck.clear(); mObstacleCheck.clear();

@ -136,7 +136,6 @@ namespace MWMechanics
bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage); bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
bool destinationThroughGround(const osg::Vec3f& startPoint, const osg::Vec3f& destination);
void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage); void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage);
int mDistance; // how far the actor can wander from the spawn point int mDistance; // how far the actor can wander from the spawn point

@ -120,6 +120,7 @@ namespace MWMechanics
mWalkState = WalkState::Norm; mWalkState = WalkState::Norm;
mStateDuration = 0; mStateDuration = 0;
mPrev = position; mPrev = position;
mInitialDistance = (destination - position).length();
return; return;
} }
@ -129,10 +130,11 @@ namespace MWMechanics
const float prevDistance = (destination - mPrev).length(); const float prevDistance = (destination - mPrev).length();
const float currentDistance = (destination - position).length(); const float currentDistance = (destination - position).length();
const float movedDistance = prevDistance - currentDistance; const float movedDistance = prevDistance - currentDistance;
const float movedFromInitialDistance = mInitialDistance - currentDistance;
mPrev = position; mPrev = position;
if (movedDistance >= distSameSpot) if (movedDistance >= distSameSpot && movedFromInitialDistance >= distSameSpot)
{ {
mWalkState = WalkState::Norm; mWalkState = WalkState::Norm;
mStateDuration = 0; mStateDuration = 0;
@ -143,6 +145,7 @@ namespace MWMechanics
{ {
mWalkState = WalkState::CheckStuck; mWalkState = WalkState::CheckStuck;
mStateDuration = duration; mStateDuration = duration;
mInitialDistance = (destination - position).length();
return; return;
} }

@ -54,6 +54,7 @@ namespace MWMechanics
float mStateDuration; float mStateDuration;
int mEvadeDirectionIndex; int mEvadeDirectionIndex;
float mInitialDistance = 0;
void chooseEvasionDirection(); void chooseEvasionDirection();
}; };

@ -0,0 +1,72 @@
#ifndef OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H
#define OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H
#include <LinearMath/btVector3.h>
#include <BulletCollision/BroadphaseCollision/btBroadphaseInterface.h>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <algorithm>
namespace MWPhysics
{
// https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection
bool testAabbAgainstSphere(const btVector3& aabbMin, const btVector3& aabbMax,
const btVector3& position, const btScalar radius)
{
const btVector3 nearest(
std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())),
std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())),
std::max(aabbMin.z(), std::min(aabbMax.z(), position.z()))
);
return nearest.distance(position) < radius;
}
class HasSphereCollisionCallback final : public btBroadphaseAabbCallback
{
public:
HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object,
const int mask, const int group)
: mPosition(position),
mRadius(radius),
mCollisionObject(object),
mCollisionFilterMask(mask),
mCollisionFilterGroup(group)
{
}
bool process(const btBroadphaseProxy* proxy) final
{
if (mResult)
return false;
const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject);
if (collisionObject == mCollisionObject)
return true;
if (needsCollision(*proxy))
mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius);
return !mResult;
}
bool getResult() const
{
return mResult;
}
private:
btVector3 mPosition;
btScalar mRadius;
btCollisionObject* mCollisionObject;
int mCollisionFilterMask;
int mCollisionFilterGroup;
bool mResult = false;
bool needsCollision(const btBroadphaseProxy& proxy) const
{
bool collides = (proxy.m_collisionFilterGroup & mCollisionFilterMask) != 0;
collides = collides && (mCollisionFilterGroup & proxy.m_collisionFilterMask);
return collides;
}
};
}
#endif

@ -45,6 +45,7 @@
#include "trace.h" #include "trace.h"
#include "object.hpp" #include "object.hpp"
#include "heightfield.hpp" #include "heightfield.hpp"
#include "hasspherecollisioncallback.hpp"
namespace MWPhysics namespace MWPhysics
{ {
@ -1466,4 +1467,20 @@ namespace MWPhysics
mCollisionWorld->addCollisionObject(mWaterCollisionObject.get(), CollisionType_Water, mCollisionWorld->addCollisionObject(mWaterCollisionObject.get(), CollisionType_Water,
CollisionType_Actor); CollisionType_Actor);
} }
bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const
{
btCollisionObject* object = nullptr;
const auto it = mActors.find(ignore);
if (it != mActors.end())
object = it->second->getCollisionObject();
const auto bulletPosition = Misc::Convert::toBullet(position);
const auto aabbMin = bulletPosition - btVector3(radius, radius, radius);
const auto aabbMax = bulletPosition + btVector3(radius, radius, radius);
const int mask = MWPhysics::CollisionType_Actor;
const int group = 0xff;
HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group);
mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult();
}
} }

@ -197,6 +197,8 @@ namespace MWPhysics
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
} }
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const;
private: private:
void updateWater(); void updateWater();

@ -504,7 +504,7 @@ namespace MWRender
if (map.mImageData.empty()) if (map.mImageData.empty())
return; return;
Files::IMemStream istream(&map.mImageData[0], map.mImageData.size()); Files::IMemStream istream(map.mImageData.data(), map.mImageData.size());
osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png");
if (!readerwriter) if (!readerwriter)
@ -569,7 +569,7 @@ namespace MWRender
if (srcBox == destBox && imageWidth == mWidth && imageHeight == mHeight) if (srcBox == destBox && imageWidth == mWidth && imageHeight == mHeight)
{ {
mOverlayImage->copySubImage(0, 0, 0, image); mOverlayImage = image;
requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false);
} }

@ -253,6 +253,7 @@ namespace MWRender
globalDefines["forcePPL"] = Settings::Manager::getBool("force per pixel lighting", "Shaders") ? "1" : "0"; globalDefines["forcePPL"] = Settings::Manager::getBool("force per pixel lighting", "Shaders") ? "1" : "0";
globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0"; globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0";
globalDefines["preLightEnv"] = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0";
// It is unnecessary to stop/start the viewer as no frames are being rendered yet. // It is unnecessary to stop/start the viewer as no frames are being rendered yet.
mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines);

@ -16,6 +16,7 @@
#include <osg/BlendFunc> #include <osg/BlendFunc>
#include <osg/AlphaFunc> #include <osg/AlphaFunc>
#include <osg/PolygonOffset> #include <osg/PolygonOffset>
#include <osg/Version>
#include <osg/observer_ptr> #include <osg/observer_ptr>
#include <osgParticle/BoxPlacer> #include <osgParticle/BoxPlacer>
@ -558,19 +559,25 @@ private:
osg::ref_ptr<osg::OcclusionQueryNode> oqn = new osg::OcclusionQueryNode; osg::ref_ptr<osg::OcclusionQueryNode> oqn = new osg::OcclusionQueryNode;
oqn->setQueriesEnabled(true); oqn->setQueriesEnabled(true);
#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5)
// With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced
osg::ref_ptr<osg::QueryGeometry> queryGeom = new osg::QueryGeometry(oqn->getName());
#else
osg::ref_ptr<osg::QueryGeometry> queryGeom = oqn->getQueryGeometry();
#endif
// Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry,
// so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry
// is only called once. // is only called once.
// Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal.
oqn->getQueryGeometry()->setDataVariance(osg::Object::STATIC); queryGeom->setDataVariance(osg::Object::STATIC);
// Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this,
// normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to
// circumvent this. // circumvent this.
osg::Geometry* queryGeom = oqn->getQueryGeometry();
queryGeom->setVertexArray(mGeom->getVertexArray()); queryGeom->setVertexArray(mGeom->getVertexArray());
queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX);
queryGeom->removePrimitiveSet(0, oqn->getQueryGeometry()->getNumPrimitiveSets()); queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets());
queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0));
// Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound.
@ -578,6 +585,10 @@ private:
// Still need a proper bounding sphere. // Still need a proper bounding sphere.
oqn->setInitialBound(queryGeom->getBound()); oqn->setInitialBound(queryGeom->getBound());
#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5)
oqn->setQueryGeometry(queryGeom.release());
#endif
osg::StateSet* queryStateSet = new osg::StateSet; osg::StateSet* queryStateSet = new osg::StateSet;
if (queryVisible) if (queryVisible)
{ {

@ -1373,6 +1373,7 @@ namespace MWScript
msg << "[Deleted]" << std::endl; msg << "[Deleted]" << std::endl;
msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl;
msg << "Memory address: " << ptr.getBase() << std::endl;
if (ptr.isInCell()) if (ptr.isInCell())
{ {

@ -302,8 +302,15 @@ namespace MWScript
MWMechanics::DynamicStat<float> stat (ptr.getClass().getCreatureStats (ptr) MWMechanics::DynamicStat<float> stat (ptr.getClass().getCreatureStats (ptr)
.getDynamic (mIndex)); .getDynamic (mIndex));
// for fatigue, a negative current value is allowed and means the actor will be knocked down bool allowDecreaseBelowZero = false;
bool allowDecreaseBelowZero = (mIndex == 2); if (mIndex == 2) // Fatigue-specific logic
{
// For fatigue, a negative current value is allowed and means the actor will be knocked down
allowDecreaseBelowZero = true;
// Knock down the actor immediately if a non-positive new value is the case
if (diff + current <= 0.f)
ptr.getClass().getCreatureStats(ptr).setKnockedDown(true);
}
stat.setCurrent (diff + current, allowDecreaseBelowZero); stat.setCurrent (diff + current, allowDecreaseBelowZero);
ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat);

@ -44,7 +44,6 @@
#include "../mwphysics/object.hpp" #include "../mwphysics/object.hpp"
#include "../mwphysics/heightfield.hpp" #include "../mwphysics/heightfield.hpp"
#include "actionteleport.hpp"
#include "player.hpp" #include "player.hpp"
#include "localscripts.hpp" #include "localscripts.hpp"
#include "esmstore.hpp" #include "esmstore.hpp"
@ -269,20 +268,10 @@ namespace
struct PositionVisitor struct PositionVisitor
{ {
float mLowestPos = std::numeric_limits<float>::max();
bool operator() (const MWWorld::Ptr& ptr) bool operator() (const MWWorld::Ptr& ptr)
{ {
if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled())
{
if (!ptr.getClass().isActor())
{
float objectPosZ = ptr.getRefData().getPosition().pos[2];
if (objectPosZ < mLowestPos)
mLowestPos = objectPosZ;
}
ptr.getClass().adjustPosition (ptr, false); ptr.getClass().adjustPosition (ptr, false);
}
return true; return true;
} }
}; };
@ -540,16 +529,6 @@ namespace MWWorld
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3()); navigator->update(player.getRefData().getPosition().asVec3());
const float fallThreshold = 256.f;
if (mCurrentCell && !mCurrentCell->isExterior() && pos.z() < mLowestPos - fallThreshold)
{
ESM::Position newPos;
std::string cellName = mCurrentCell->getCell()->mName;
MWBase::Environment::get().getWorld()->findInteriorPosition(cellName, newPos);
if (newPos.pos[2] >= mLowestPos)
MWWorld::ActionTeleport(cellName, newPos, false).execute(player);
}
if (!mCurrentCell || !mCurrentCell->isExterior()) if (!mCurrentCell || !mCurrentCell->isExterior())
return; return;
@ -970,10 +949,8 @@ namespace MWWorld
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });
// do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order // do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order
// Also note the lowest object position in the cell to allow infinite fall fail safe to work
PositionVisitor posVisitor; PositionVisitor posVisitor;
cell.forEach (posVisitor); cell.forEach (posVisitor);
mLowestPos = posVisitor.mLowestPos;
} }
void Scene::addObjectToScene (const Ptr& ptr) void Scene::addObjectToScene (const Ptr& ptr)

@ -84,7 +84,6 @@ namespace MWWorld
float mPredictionTime; float mPredictionTime;
osg::Vec3f mLastPlayerPos; osg::Vec3f mLastPlayerPos;
float mLowestPos;
void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false); void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false);

@ -1871,6 +1871,11 @@ namespace MWWorld
return result.mHit; return result.mHit;
} }
bool World::castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore)
{
return mPhysics->castRay(from, to, ignore, std::vector<MWWorld::Ptr>(), mask).mHit;
}
bool World::rotateDoor(const Ptr door, MWWorld::DoorState state, float duration) bool World::rotateDoor(const Ptr door, MWWorld::DoorState state, float duration)
{ {
const ESM::Position& objPos = door.getRefData().getPosition(); const ESM::Position& objPos = door.getRefData().getPosition();
@ -4458,4 +4463,9 @@ namespace MWWorld
btVector3 hitNormal; btVector3 hitNormal;
return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal);
} }
bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const
{
return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore);
}
} }

@ -560,6 +560,8 @@ namespace MWWorld
bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) override; bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) override;
bool castRay(const osg::Vec3f& from, const osg::Vec3f& to, int mask, const MWWorld::ConstPtr& ignore) override;
void setActorCollisionMode(const Ptr& ptr, bool internal, bool external) override; void setActorCollisionMode(const Ptr& ptr, bool internal, bool external) override;
bool isActorCollisionEnabled(const Ptr& ptr) override; bool isActorCollisionEnabled(const Ptr& ptr) override;
@ -914,6 +916,8 @@ namespace MWWorld
osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const override; osg::Vec3f getPathfindingHalfExtents(const MWWorld::ConstPtr& actor) const override;
bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override;
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override;
}; };
} }

@ -266,7 +266,7 @@ namespace
MOCK_CONST_METHOD0(numRecords, std::size_t ()); MOCK_CONST_METHOD0(numRecords, std::size_t ());
MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t)); MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t));
MOCK_CONST_METHOD0(numRoots, std::size_t ()); MOCK_CONST_METHOD0(numRoots, std::size_t ());
MOCK_CONST_METHOD1(getString, std::string (std::size_t)); MOCK_CONST_METHOD1(getString, std::string (uint32_t));
MOCK_METHOD1(setUseSkinning, void (bool)); MOCK_METHOD1(setUseSkinning, void (bool));
MOCK_CONST_METHOD0(getUseSkinning, bool ()); MOCK_CONST_METHOD0(getUseSkinning, bool ());
MOCK_CONST_METHOD0(getFilename, std::string ()); MOCK_CONST_METHOD0(getFilename, std::string ());

@ -154,6 +154,14 @@ void Wizard::InstallationPage::showFileDialog(Wizard::Component component)
name = QLatin1String("Bloodmoon"); name = QLatin1String("Bloodmoon");
break; break;
} }
logTextEdit->appendHtml(tr("<p>Attempting to install component %1.</p>").arg(name));
mWizard->addLogText(tr("Attempting to install component %1.").arg(name));
QMessageBox msgBox;
msgBox.setWindowTitle(tr("%1 Installation").arg(name));
msgBox.setIcon(QMessageBox::Information);
msgBox.setText(QObject::tr("Select a valid %1 installation media.<br><b>Hint</b>: make sure that it contains at least one <b>.cab</b> file.").arg(name));
msgBox.exec();
QString path = QFileDialog::getExistingDirectory(this, QString path = QFileDialog::getExistingDirectory(this,
tr("Select %1 installation media").arg(name), tr("Select %1 installation media").arg(name),

@ -493,7 +493,9 @@ bool Wizard::UnshieldWorker::setupComponent(Component component)
} }
if (!found) { if (!found)
{
emit textChanged(tr("Failed to find a valid archive containing %1.bsa! Retrying.").arg(name));
QReadLocker readLock(&mLock); QReadLocker readLock(&mLock);
emit requestFileDialog(component); emit requestFileDialog(component);
mWait.wait(&mLock); mWait.wait(&mLock);

@ -422,7 +422,8 @@ void CompressedBSAFile::convertCompressedSizesToUncompressed()
std::uint64_t CompressedBSAFile::generateHash(std::string stem, std::string extension) const std::uint64_t CompressedBSAFile::generateHash(std::string stem, std::string extension) const
{ {
size_t len = stem.length(); size_t len = stem.length();
if (len == 0) return 0; if (len == 0)
return 0;
std::uint64_t hash = 0; std::uint64_t hash = 0;
unsigned int hash2 = 0; unsigned int hash2 = 0;
Misc::StringUtils::lowerCaseInPlace(stem); Misc::StringUtils::lowerCaseInPlace(stem);
@ -434,12 +435,19 @@ std::uint64_t CompressedBSAFile::generateHash(std::string stem, std::string exte
for (const char &c : extension) for (const char &c : extension)
hash = hash * 0x1003f + c; hash = hash * 0x1003f + c;
} }
for (size_t i = 1; i < len-2 && len > 3; i++) if (len >= 4)
hash2 = hash2 * 0x1003f + stem[i]; {
for (size_t i = 1; i < len-2; i++)
hash2 = hash2 * 0x1003f + stem[i];
}
hash = (hash + hash2) << 32; hash = (hash + hash2) << 32;
hash2 = (stem[0] << 24) | (len << 16); hash2 = (stem[0] << 24) | (len << 16);
if (len >= 3) hash2 |= stem[len-2] << 8; if (len >= 2)
if (len >= 2) hash2 |= stem[len-1]; {
if (len >= 3)
hash2 |= stem[len-2] << 8;
hash2 |= stem[len-1];
}
if (!extension.empty()) if (!extension.empty())
{ {
if (extension == ".kf") hash2 |= 0x80; if (extension == ".kf") hash2 |= 0x80;

@ -116,7 +116,7 @@ namespace Compiler
void registerExtensions (Extensions& extensions) void registerExtensions (Extensions& extensions)
{ {
extensions.registerInstruction ("additem", "clX", opcodeAddItem, opcodeAddItemExplicit); extensions.registerInstruction ("additem", "clX", opcodeAddItem, opcodeAddItemExplicit);
extensions.registerFunction ("getitemcount", 'l', "c", opcodeGetItemCount, extensions.registerFunction ("getitemcount", 'l', "cX", opcodeGetItemCount,
opcodeGetItemCountExplicit); opcodeGetItemCountExplicit);
extensions.registerInstruction ("removeitem", "clX", opcodeRemoveItem, extensions.registerInstruction ("removeitem", "clX", opcodeRemoveItem,
opcodeRemoveItemExplicit); opcodeRemoveItemExplicit);

@ -26,7 +26,7 @@ struct File
virtual size_t numRoots() const = 0; virtual size_t numRoots() const = 0;
virtual std::string getString(size_t index) const = 0; virtual std::string getString(uint32_t index) const = 0;
virtual void setUseSkinning(bool skinning) = 0; virtual void setUseSkinning(bool skinning) = 0;
@ -129,8 +129,10 @@ public:
size_t numRoots() const override { return roots.size(); } size_t numRoots() const override { return roots.size(); }
/// Get a given string from the file's string table /// Get a given string from the file's string table
std::string getString(size_t index) const override std::string getString(uint32_t index) const override
{ {
if (index == std::numeric_limits<uint32_t>::max())
return std::string();
return strings.at(index); return strings.at(index);
} }

@ -46,12 +46,10 @@ void NiTexturingProperty::read(NIFStream *nif)
for (unsigned int i = 0; i < numTextures; i++) for (unsigned int i = 0; i < numTextures; i++)
{ {
textures[i].read(nif); textures[i].read(nif);
// Ignore these at the moment
if (i == 5 && textures[5].inUse) // Bump map settings if (i == 5 && textures[5].inUse) // Bump map settings
{ {
/*float lumaScale =*/ nif->getFloat(); envMapLumaBias = nif->getVector2();
/*float lumaOffset =*/ nif->getFloat(); bumpMapMatrix = nif->getVector4();
/*const Vector4 *lumaMatrix =*/ nif->getVector4();
} }
} }
} }

@ -93,6 +93,9 @@ public:
std::vector<Texture> textures; std::vector<Texture> textures;
osg::Vec2f envMapLumaBias;
osg::Vec4f bumpMapMatrix;
void read(NIFStream *nif); void read(NIFStream *nif);
void post(NIFFile *nif); void post(NIFFile *nif);
}; };

@ -168,10 +168,10 @@ namespace
namespace NifOsg namespace NifOsg
{ {
class CollisionSwitch : public osg::Group class CollisionSwitch : public osg::MatrixTransform
{ {
public: public:
CollisionSwitch(bool enabled) : osg::Group() CollisionSwitch(const osg::Matrixf& transformations, bool enabled) : osg::MatrixTransform(transformations)
{ {
setEnabled(enabled); setEnabled(enabled);
} }
@ -477,7 +477,7 @@ namespace NifOsg
case Nif::RC_NiCollisionSwitch: case Nif::RC_NiCollisionSwitch:
{ {
bool enabled = nifNode->flags & Nif::NiNode::Flag_ActiveCollision; bool enabled = nifNode->flags & Nif::NiNode::Flag_ActiveCollision;
node = new CollisionSwitch(enabled); node = new CollisionSwitch(nifNode->trafo.toMatrix(), enabled);
dataVariance = osg::Object::STATIC; dataVariance = osg::Object::STATIC;
break; break;
@ -1536,6 +1536,10 @@ namespace NifOsg
{ {
// Set this texture to Off by default since we can't render it with the fixed-function pipeline // Set this texture to Off by default since we can't render it with the fixed-function pipeline
stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF);
osg::Matrix2 bumpMapMatrix(texprop->bumpMapMatrix.x(), texprop->bumpMapMatrix.y(),
texprop->bumpMapMatrix.z(), texprop->bumpMapMatrix.w());
stateset->addUniform(new osg::Uniform("bumpMapMatrix", bumpMapMatrix));
stateset->addUniform(new osg::Uniform("envMapLumaBias", texprop->envMapLumaBias));
} }
else if (i == Nif::NiTexturingProperty::DecalTexture) else if (i == Nif::NiTexturingProperty::DecalTexture)
{ {
@ -1559,7 +1563,7 @@ namespace NifOsg
texture2d->setName("diffuseMap"); texture2d->setName("diffuseMap");
break; break;
case Nif::NiTexturingProperty::BumpTexture: case Nif::NiTexturingProperty::BumpTexture:
texture2d->setName("normalMap"); texture2d->setName("bumpMap");
break; break;
case Nif::NiTexturingProperty::GlowTexture: case Nif::NiTexturingProperty::GlowTexture:
texture2d->setName("emissiveMap"); texture2d->setName("emissiveMap");

@ -12,6 +12,7 @@
#include <components/vfs/manager.hpp> #include <components/vfs/manager.hpp>
#include <components/sceneutil/riggeometry.hpp> #include <components/sceneutil/riggeometry.hpp>
#include <components/sceneutil/morphgeometry.hpp> #include <components/sceneutil/morphgeometry.hpp>
#include <components/settings/settings.hpp>
#include "shadermanager.hpp" #include "shadermanager.hpp"
@ -75,7 +76,7 @@ namespace Shader
return newStateSet.get(); return newStateSet.get();
} }
const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap" }; const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap", "bumpMap" };
bool isTextureNameRecognized(const std::string& name) bool isTextureNameRecognized(const std::string& name)
{ {
for (unsigned int i=0; i<sizeof(defaultTextures)/sizeof(defaultTextures[0]); ++i) for (unsigned int i=0; i<sizeof(defaultTextures)/sizeof(defaultTextures[0]); ++i)
@ -95,6 +96,7 @@ namespace Shader
const osg::Texture* diffuseMap = nullptr; const osg::Texture* diffuseMap = nullptr;
const osg::Texture* normalMap = nullptr; const osg::Texture* normalMap = nullptr;
const osg::Texture* specularMap = nullptr; const osg::Texture* specularMap = nullptr;
const osg::Texture* bumpMap = nullptr;
for(unsigned int unit=0;unit<texAttributes.size();++unit) for(unsigned int unit=0;unit<texAttributes.size();++unit)
{ {
const osg::StateAttribute *attr = stateset->getTextureAttribute(unit, osg::StateAttribute::TEXTURE); const osg::StateAttribute *attr = stateset->getTextureAttribute(unit, osg::StateAttribute::TEXTURE);
@ -130,6 +132,21 @@ namespace Shader
diffuseMap = texture; diffuseMap = texture;
else if (texName == "specularMap") else if (texName == "specularMap")
specularMap = texture; specularMap = texture;
else if (texName == "bumpMap")
{
bumpMap = texture;
mRequirements.back().mShaderRequired = true;
if (!writableStateSet)
writableStateSet = getWritableStateSet(node);
// Bump maps are off by default as well
writableStateSet->setTextureMode(unit, GL_TEXTURE_2D, osg::StateAttribute::ON);
}
else if (texName == "envMap")
{
static const bool preLightEnv = Settings::Manager::getBool("apply lighting to environment maps", "Shaders");
if (preLightEnv)
mRequirements.back().mShaderRequired = true;
}
} }
else else
Log(Debug::Error) << "ShaderVisitor encountered unknown texture " << texture; Log(Debug::Error) << "ShaderVisitor encountered unknown texture " << texture;
@ -158,8 +175,11 @@ namespace Shader
image = mImageManager.getImage(normalMapFileName); image = mImageManager.getImage(normalMapFileName);
} }
} }
// Avoid using the auto-detected normal map if it's already being used as a bump map.
// It's probably not an actual normal map.
bool hasNamesakeBumpMap = image && bumpMap && bumpMap->getImage(0) && image->getFileName() == bumpMap->getImage(0)->getFileName();
if (image) if (!hasNamesakeBumpMap && image)
{ {
osg::ref_ptr<osg::Texture2D> normalMapTex (new osg::Texture2D(image)); osg::ref_ptr<osg::Texture2D> normalMapTex (new osg::Texture2D(image));
normalMapTex->setTextureSize(image->s(), image->t()); normalMapTex->setTextureSize(image->s(), image->t());

@ -124,3 +124,14 @@ terrain specular map pattern
:Default: _diffusespec :Default: _diffusespec
The filename pattern to probe for when detecting terrain specular maps (see 'auto use terrain specular maps') The filename pattern to probe for when detecting terrain specular maps (see 'auto use terrain specular maps')
apply lighting to environment maps
----------------------------------
:Type: boolean
:Range: True/False
:Default: False
Normally environment map reflections aren't affected by lighting, which makes environment-mapped (and thus bump-mapped objects) glow in the dark.
Morrowind Code Patch includes an option to remedy that by doing environment-mapping before applying lighting, this is the equivalent of that option.
Affected objects will use shaders.

@ -3,59 +3,59 @@ Normal maps from Morrowind to OpenMW
==================================== ====================================
- `General introduction to normal map conversion`_ - `General introduction to normal map conversion`_
- `Normal Mapping in OpenMW`_ - `OpenMW normal-mapping`_
- `Activating normal mapping shaders in OpenMW`_ - `Activating normal-mapping shaders in OpenMW`_
- `Normal mapping in Morrowind with Morrowind Code Patch`_ - `Morrowind bump-mapping`_
- `Normal mapping in Morrowind with MGE XE`_ - `MGE XE normal-mapping`_
- `Converting PeterBitt's Scamp Replacer`_ (Mod made for the MGE XE PBR prototype) - `Converting PeterBitt's Scamp Replacer`_ (Mod made for the MGE XE PBR prototype)
- `Tutorial - MGE`_ - `Tutorial - MGE`_
- `Converting Lougian's Hlaalu Bump mapped`_ (MCP's fake bump map function, part 1: *without* custom models) - `Converting Lougian's Hlaalu Bump mapped`_ (Morrowind's bump-mapping, part 1: *without* custom models)
- `Tutorial - MCP, Part 1`_ - `Tutorial - Morrowind, Part 1`_
- `Converting Apel's Various Things - Sacks`_ (MCP's fake bump map function, part 2: *with* custom models) - `Converting Apel's Various Things - Sacks`_ (Morrowind's bump-mapping, part 2: *with* custom models)
- `Tutorial - MCP, Part 2`_ - `Tutorial - Morrowind, Part 2`_
General introduction to normal map conversion General introduction to normal map conversion
------------------------------------------------ ------------------------------------------------
:Authors: Joakim (Lysol) Berg :Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov
:Updated: 2016-11-11 :Updated: 2020-03-03
This page has general information and tutorials on how normal mapping works in OpenMW and how you can make mods using This page has general information and tutorials on how normal-mapping works in OpenMW and how you can make mods using
the old fake normal mapping technique (such as `Netch Bump mapped`_ and `Hlaalu Bump mapped`_, and maybe the most the old environment-mapped bump-mapping technique (such as `Netch Bump mapped`_ and `Hlaalu Bump mapped`_, and maybe the most
(in)famous one to give shiny rocks in OpenMW, the mod `On the Rocks`_!, featured in MGSO and Morrowind Rebirth) work in OpenMW. (in)famous one to previously give shiny rocks in OpenMW, the mod `On the Rocks`_!, featured in MGSO and Morrowind Rebirth) work better in OpenMW.
*Note:* The conversion made in the `Converting Apel's Various Things - Sacks`_-part of this tutorial require the use of the application NifSkope_. *Note:* The conversion made in the `Converting Apel's Various Things - Sacks`_-part of this tutorial require the use of the application NifSkope_.
*Another note:* I will use the terms bump mapping and normal mapping simultaneously. *Another note:* I will use the terms bump-mapping and normal-mapping simultaneously.
Normal mapping is one form of bump mapping. In other words, normal mapping is bump mapping, Normal-mapping is one form of bump-mapping. In other words, normal-mapping is bump-mapping,
but bump mapping isn't necessarily normal mapping. but bump-mapping isn't necessarily normal-mapping.
There are several techniques for bump mapping, and normal mapping is the most common one today. There are several techniques for bump-mapping, and normal-mapping is the most common one today.
So let's get on with it. So let's get on with it.
Normal Mapping in OpenMW OpenMW normal-mapping
************************ ************************
Normal mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix, Normal-mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix,
and you're done. and you're done.
So to expand on this a bit, let's take a look at how a model seeks for textures. So to expand on this a bit, let's take a look at how a model looks up textures.
Let us assume we have the model *example.nif*. In this model file, Let us assume we have the model *example.nif*. In this model file,
there should be a tag (NiSourceTexture) that states what texture it should use and where to find it. Typically, there should be a tag (NiTexturingProperty) that states what textures it should use and where to find them. Typically,
it will point to something like *exampletexture_01.dds*. This texture is supposed to be located directly in the the model's base (diffuse) texture reference will point to something named like *exampletexture_01.dds*. This texture is supposed to be located directly in the
Textures folder since it does not state anything else. If the model is a custom made one, modders tend to group Textures folder since it does not state anything else.
their textures in separate folders, just to easily keep track of them. Modders tend to group textures for custom-made models in dedicated folders to keep track of them easily,
It might be something like *./Textures/moddername/exampletexture_02.dds*. so it might be something like *./Textures/moddername/exampletexture_02.dds*.
When OpenMW finally adds normal mapping, it simply takes the NiSourceTexture file path, e.g., OpenMW will pick the diffuse map file path from the mesh, e.g.
*exampletexture_01.dds*, and looks for a *exampletexture_01_n.dds*. If it can't find this file, no normal mapping is added. *exampletexture_01.dds*, and look up a texture named *exampletexture_01_n.dds*.
If it *does* find this file, the model will use this texture as a normal map. Simple. That file will be the normal map if it's present. Simple.
Activating normal mapping shaders in OpenMW Activating normal-mapping shaders in OpenMW
******************************************* *******************************************
Before normal (and specular and parallax) maps will show up in OpenMW, you'll need to activate them in the Before normal (and specular and parallax) maps can show up in OpenMW, their auto-detection needs to be turned on in
settings.cfg_-file. Add these rows where it would make sense: settings.cfg_-file. Add these rows where it would make sense:
:: ::
@ -64,66 +64,51 @@ settings.cfg_-file. Add these rows where it would make sense:
auto use object normal maps = true auto use object normal maps = true
auto use terrain normal maps = true auto use terrain normal maps = true
And while we're at it, why not activate specular maps too just for the sake of it?
::
auto use object specular maps = true auto use object specular maps = true
auto use terrain specular maps = true auto use terrain specular maps = true
Lastly, if you want really nice lights in OpenMW, add these rows: See OpenMW's wiki page about `texture modding`_ to read more about it.
::
force shaders = true
clamp lighting = false
See OpenMW's wiki page about `texture modding`_ to read further about this.
Normal mapping in Morrowind with Morrowind Code Patch Morrowind bump-mapping
***************************************************** *****************************************************
**Conversion difficulty:** **Conversion difficulty:**
*Varies. Sometimes quick and easy, sometimes time-consuming and hard.* *Varies. Sometimes quick and easy, sometimes time-consuming and hard.*
You might have bumped (pun intended) on a few bump-mapped texture packs for Morrowind that require the You might have bumped (pun intended) on a few bump-mapped texture packs for Morrowind that require
Morrowind Code Patch (MCP). You might even be thinking: Why doesn't OpenMW just support these instead of reinventing Morrowind Code Patch (MCP). OpenMW supports them, and like MCP can optionally apply lighting after environment maps
the wheel? I know it sounds strange, but it will make sense. Here's how MCP handles normal maps: are processed which makes bump-mapped models look a bit better,
can make use of the gloss map channel in the bump map and can apply bump-mapping to skinned models.
Add this to settings.cfg_-file:
Morrowind does not recognize normal maps (they weren't really a "thing" yet in 2002), so even if you have a normal map, ::
Morrowind will not load and display it. MCP has a clever way to solve this issue, by using something Morrowind *does* support,
namely environment maps. You could add a tag for an environment map and then add a normal map as the environment map,
but you'd end up with a shiny ugly model in the game. MCP solves this by turning down the brightness of the environment maps,
making the model look *kind of* as if it had a normal map applied to it.
I say kind of because it does not really look as good as normal mapping usually does. It was a hacky way to do it,
but it was the only way at the time, and therefore the best way.
The biggest problem with this is not that it doesn't look as good as it could no, [Shaders]
the biggest problem in my opinion is that it requires you to state the file paths for your normal map textures *in the models*! apply lighting to environment maps = true
For buildings, which often use several textures for one single model file, it could take *ages* to do this,
and you had to do it for dozens of model files too. You also had to ship your texture pack with model files,
making your mod bigger in file size.
These are basically the reasons why OpenMW does not support fake bump maps like MCP does. But sometimes you may want them to look a bit better than in vanilla.
It is just a really bad way to enhance your models, all the more when you have the possibility to do it in a better way. Technically you aren't supposed to convert bump maps because they shouldn't be normal maps that are supported by OpenMW as well,
but artists may use actual normal maps as bump maps either because they look better in vanilla... or because they're lazy.
In this case you can benefit from OpenMW's normal-mapping support by using these bump maps the way normal maps are used.
This means that you will have to drop the bump-mapping references from the model and sometimes rename the texture.
Normal mapping in Morrowind with MGE XE MGE XE normal-mapping
*************************************** ***************************************
**Conversion difficulty:** **Conversion difficulty:**
*Easy* *Easy*
The most recent feature on this topic is that the Morrowind Graphics Extender (MGE) finally started to support real The most recent feature on this topic is that the Morrowind Graphics Extender (MGE) finally started to support real
normal mapping in an experimental version available here: `MGE XE`_ (you can't use MGE with OpenMW!). normal-mapping in an experimental version available here: `MGE XE`_ (you can't use MGE with OpenMW!).
Not only this but it also adds full support for physically based rendering (PBR), Not only this but it also adds full support for physically based rendering (PBR),
making it one step ahead of OpenMW in terms of texturing techniques. However, making it one step ahead of OpenMW in terms of texturing techniques. However,
OpenMW will probably have this feature in the future too and let's hope that OpenMW and MGE will handle PBR in a OpenMW will probably have this feature in the future too and let's hope that OpenMW and MGE will handle PBR in a
similar fashion in the future so that mods can be used for both MGE and OpenMW without any hassle. similar fashion in the future so that mods can be used for both MGE and OpenMW without any hassle.
I haven't researched that much on the MGE variant yet but it does support real implementation of normal mapping, I haven't researched that much on the MGE variant yet but it does support real implementation of normal-mapping,
making it really easy to convert mods made for MGE into OpenMW (I'm only talking about the normal map textures though). making it really easy to convert mods made for MGE into OpenMW (I'm only talking about the normal map textures though).
There's some kind of text file if I understood it correctly that MGE uses to find the normal map. There's some kind of text file if I understood it correctly that MGE uses to find the normal map.
OpenMW does not need this, you just have to make sure the normal map has the same name as the diffuse texture but with OpenMW does not need this, you just have to make sure the normal map has the same name as the diffuse texture but with
the correct suffix after. the correct suffix after.
Now, on to the tutorials. Now, on to the tutorials.
@ -135,20 +120,20 @@ Converting PeterBitt's Scamp Replacer
:Authors: Joakim (Lysol) Berg :Authors: Joakim (Lysol) Berg
:Updated: 2016-11-11 :Updated: 2016-11-11
So, let's say you've found out that PeterBitt_ makes awesome models and textures featuring physically based rendering So, let's say you've found out that PeterBitt_ makes awesome models and textures featuring physically based rendering
(PBR) and normal maps. Let's say that you tried to run his `PBR Scamp Replacer`_ in OpenMW and that you were greatly (PBR) and normal maps. Let's say that you tried to run his `PBR Scamp Replacer`_ in OpenMW and that you were greatly
disappointed when the normal map didn't seem to work. Lastly, let's say you came here, looking for some answers. disappointed when the normal map didn't seem to work. Lastly, let's say you came here, looking for some answers.
Am I right? Great. Because you've come to the right place! Am I right? Great. Because you've come to the right place!
*A quick note before we begin*: Please note that you can only use the normal map texture and not the rest of the materials, *A quick note before we begin*: Please note that you can only use the normal map texture and not the rest of the materials,
since PBR isn't implemented in OpenMW yet. Sometimes PBR textures can look dull without all of the texture files, since PBR isn't implemented in OpenMW yet. Sometimes PBR textures can look dull without all of the texture files,
so have that in mind. so have that in mind.
Tutorial - MGE Tutorial - MGE
************** **************
In this tutorial, I will use PeterBitt's `PBR Scamp Replacer`_ as an example, In this tutorial, I will use PeterBitt's `PBR Scamp Replacer`_ as an example,
but any mod featuring PBR that requires the PBR version of MGE will do, but any mod featuring PBR that requires the PBR version of MGE will do,
provided it also includes a normal map (which it probably does). provided it also includes a normal map (which it probably does).
So, follow these steps: So, follow these steps:
@ -163,67 +148,67 @@ So, follow these steps:
#. Rename your newly extracted file (``tx_Scamp_normals.dds``) to ``tx_Scamp_n.dds`` (which is exactly the same name as the diffuse texture file, except for the added *_n* suffix before the filename extention). #. Rename your newly extracted file (``tx_Scamp_normals.dds``) to ``tx_Scamp_n.dds`` (which is exactly the same name as the diffuse texture file, except for the added *_n* suffix before the filename extention).
#. You're actually done! #. You're actually done!
So as you might notice, converting these mods is very simple and takes just a couple of minutes. So as you might notice, converting these mods is very simple and takes just a couple of minutes.
It's more or less just a matter of renaming and moving a few files. It's more or less just a matter of renaming and moving a few files.
I totally recommend you to also try this on PeterBitt's Nix Hound replacer and Flash3113's various replacers. I totally recommend you to also try this on PeterBitt's Nix Hound replacer and Flash3113's various replacers.
It should be the same principle to get those to work. It should be the same principle to get those to work.
And let's hope that some one implements PBR shaders to OpenMW too, And let's hope that some one implements PBR shaders to OpenMW too,
so that we can use all the material files of these mods in the future. so that we can use all the material files of these mods in the future.
Converting Lougian's Hlaalu Bump mapped Converting Lougian's Hlaalu Bump mapped
--------------------------------------- ---------------------------------------
**Mod made for MCP's fake bump function, without custom models** **Mod made for Morrowind's bump-mapping, without custom models**
:Authors: Joakim (Lysol) Berg :Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov
:Updated: 2016-11-11 :Updated: 2020-03-03
Converting textures made for the Morrowind Code Patch (MCP) fake bump mapping can be really easy or a real pain, Converting normal maps made for the Morrowind's bump-mapping can be really easy or a real pain,
depending on a few circumstances. In this tutorial, we will look at a very easy, depending on a few circumstances. In this tutorial, we will look at a very easy,
although in some cases a bit time-consuming, example. although in some cases a bit time-consuming, example.
Tutorial - MCP, Part 1 Tutorial - Morrowind, Part 1
********************** **********************
We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_. We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_.
Since this is just a texture pack and not a model replacer, Since this is just a texture pack and not a model replacer,
we can convert the mod in a few minutes by just renaming a few dozen files and by *not* extracting the included model we can convert the mod in a few minutes by just renaming a few dozen files and by *not* extracting the included model
(``.nif``) files when installing the mod. (``.nif``) files when installing the mod.
#. Download Lougian's `Hlaalu Bump mapped`_. #. Download Lougian's `Hlaalu Bump mapped`_.
#. Install the mod by extracting the ``./Textures`` folder to a data folder the way you usually install mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!). #. Install the mod by extracting the ``./Textures`` folder to a data folder the way you usually install mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!).
- Again, yes, *only* the ``./Textures`` folder. Do *not* extract the Meshes folder. They are only there to make the MCP hack work, which is not of any interest to us. - Again, yes, *only* the ``./Textures`` folder. Do not extract the Meshes folder. They are there to make Morrowind bump-mapping work.
#. Go to your new texture folder. If you installed the mod like I recommended, you won't have any trouble finding the files. If you instead placed all your files in Morrowinds main Data Files folder (sigh), you need to check with the mod's .rar file to see what files you should look for. Because you'll be scrolling through a lot of files. #. Go to your new texture folder. If you installed the mod like I recommended, you won't have any trouble finding the files. If you instead placed all your files in Morrowinds main Data Files folder (sigh), you need to check with the mod's .rar file to see what files you should look for. Because you'll be scrolling through a lot of files.
#. Find all the textures related to the texture pack in the Textures folder and take note of all the ones that ends with a *_nm.dds*. #. Find all the textures related to the texture pack in the Textures folder and take note of all the ones that ends with a *_nm.dds*.
#. The *_nm.dds* files are normal map files. OpenMW's standard format is to have the normal maps with a *_n.dds* instead. Rename all the normal map textures to only have a *_n.dds* instead of the *_nm.dds*. #. The *_nm.dds* files are normal map files. OpenMW's standard format is to have the normal maps with a *_n.dds* instead. Rename all the normal map textures to only have a *_n.dds* instead of the *_nm.dds*.
- As a nice bonus to this tutorial, this pack actually included one specularity texture too. We should use it of course. It's the one called "``tx_glass_amber_02_reflection.dds``". For OpenMW to recognize this file and use it as a specular map, you need to change the *_reflection.dds* part to *_spec.dds*, resulting in the name ``tx_glass_amber_01_spec.dds``. - As a nice bonus to this tutorial, this pack actually included one specularity texture too. We should use it of course. It's the one called "``tx_glass_amber_02_reflection.dds``". For OpenMW to recognize this file and use it as a specular map, you need to change the *_reflection.dds* part to *_spec.dds*, resulting in the name ``tx_glass_amber_01_spec.dds``.
#. That should be it. Really simple, but I do know that it takes a few minutes to rename all those files. #. That should be it. Really simple, but I do know that it takes a few minutes to rename all those files.
Now if the mod you want to change includes custom made models it gets a bit more complicated I'm afraid. Now if the mod you want to change includes custom made models it gets a bit more complicated I'm afraid.
But that is for the next tutorial. But that is for the next tutorial.
Converting Apel's Various Things - Sacks Converting Apel's Various Things - Sacks
---------------------------------------- ----------------------------------------
**Mod made for MCP's fake bump function, with custom models** **Mod made for Morrowind bump-mapping, with custom models**
:Authors: Joakim (Lysol) Berg :Authors: Joakim (Lysol) Berg, Alexei (Capostrophic) Dobrohotov
:Updated: 2016-11-09 :Updated: 2020-03-03
In part one of this tutorial, we converted a mod that only included modified Morrowind model (``.nif``) In part one of this tutorial, we converted a mod that only included modified Morrowind model (``.nif``)
files so that the normal maps could be loaded in Morrowind with MCP. files so that the bump maps could be loaded as normal maps.
We ignored those model files since they are not needed with OpenMW. In this tutorial however, We ignored those model files since they are not needed with OpenMW. In this tutorial however,
we will convert a mod that includes new, custom made models. In other words, we cannot just ignore those files this time. we will convert a mod that includes new, custom-made models. In other words, we cannot just ignore those files this time.
Tutorial - MCP, Part 2 Tutorial - Morrowind, Part 2
********************** **********************
The sacks included in Apel's `Various Things - Sacks`_ come in two versions Without bump mapping, and with bump mapping. The sacks included in Apel's `Various Things - Sacks`_ come in two versions without bump-mapping, and with bump-mapping.
Since we want the glory of normal mapping in our OpenMW setup, we will go with the bump-mapped version. Since we want the glory of normal-mapping in our OpenMW setup, we will go with the bump-mapped version.
#. Start by downloading Apel's `Various Things - Sacks`_ from Nexus. #. Start by downloading Apel's `Various Things - Sacks`_ from Nexus.
#. Once downloaded, install it the way you'd normally install your mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!). #. Once downloaded, install it the way you'd normally install your mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!).
#. Now, if you ran the mod right away, your sacks will be made out of lead_. This is because the normal map is loaded as an environment map which MCP fixes so that it looks less shiny. We don't use MCP, so therefore, it looks kind of like the shack was made out of lead. #. Now, if you ran the mod right away, your sacks may look... wetter than expected. This is because the mod assumes you have the MCP feature which makes the sacks less shiny enabled. You can have its equivalent enabled to make the sacks look like in Morrowind with MCP, or you may proceed on the tutorial.
#. We need to fix this by removing some tags in the model files. You need to download NifSkope_ for this, which, again, only have binaries available for Windows. #. We need to fix this by removing some tags in the model files. You need to download NifSkope_ for this, which, again, only have binaries available for Windows.
#. Go the place where you installed the mod and go to ``./Meshes/o/`` to find the model files. #. Go the place where you installed the mod and go to ``./Meshes/o/`` to find the model files.
- If you installed the mod like I suggested, finding the files will be easy as a pie, but if you installed it by dropping everything into your main Morrowind Data Files folder, then you'll have to scroll a lot to find them. Check the mod's zip file for the file names of the models if this is the case. The same thing applies to when fixing the textures. - If you installed the mod like I suggested, finding the files will be easy as a pie, but if you installed it by dropping everything into your main Morrowind Data Files folder, then you'll have to scroll a lot to find them. Check the mod's zip file for the file names of the models if this is the case. The same thing applies to when fixing the textures.
@ -232,14 +217,14 @@ Since we want the glory of normal mapping in our OpenMW setup, we will go with t
- NiSourceTexture with the value that appears to be a normal map file, in this mod, they have the suffix *_nm.dds*. - NiSourceTexture with the value that appears to be a normal map file, in this mod, they have the suffix *_nm.dds*.
#. Remove all these tags by selecting them one at a time and press right click>Block>Remove Branch. (Ctrl-Del) #. Remove all these tags by selecting them one at a time and press right click>Block>Remove Branch. (Ctrl-Del)
#. Repeat this on all the affected models. #. Repeat this on all the affected models.
#. If you launch OpenMW now, you'll `no longer have shiny models`_. But one thing is missing. Can you see it? It's actually hard to spot on still pictures, but we have no normal maps here. #. If you launch OpenMW now, you'll `no longer have wet models`_. But one thing is missing. Can you see it? It's actually hard to spot on still pictures, but we have no normal maps here.
#. Now, go back to the root of where you installed the mod. Now go to ``./Textures/`` and you'll find the texture files in question. #. Now, go back to the root of where you installed the mod. Now go to ``./Textures/`` and you'll find the texture files in question.
#. OpenMW detects normal maps if they have the same name as the base diffuse texture, but with a *_n.dds* suffix. In this mod, the normal maps has a suffix of *_nm.dds*. Change all the files that ends with *_nm.dds* to instead end with *_n.dds*. #. OpenMW detects normal maps if they have the same name as the base diffuse texture, but with a *_n.dds* suffix. In this mod, the normal maps has a suffix of *_nm.dds*. Change all the files that ends with *_nm.dds* to instead end with *_n.dds*.
#. Finally, `we are done`_! #. Finally, `we are done`_!
Since these models have one or two textures applied to them, the fix was not that time-consuming. The process continues to work for more complex models that use more textures, but looking through each category for texture effects and normal mapped textures rapidly becomes tedious. Luckily, NifSkope provides a feature to do the same automatically. Since these models have one or two textures applied to them, the fix was not that time-consuming. The process continues to work for more complex models that use more textures, but looking through each category for texture effects and normal mapped textures rapidly becomes tedious. Luckily, NifSkope provides a feature to do the same automatically.
Rightclick in NifSkope to access the *Spells* dropdown menu, also available via the top bar, hover over the *Blocks* section, and `choose the action to Remove by ID`_. You can then input the RegEx expression ``^NiTextureEffect`` (directing it to remove any block whose name starts with "NiTextureEffect") to automatically remove all texture effect blocks within the NIF. This also has the helpful side effect of listing `all the blocks within the NIF in the bottom section`_, allowing you to additionally root out any blocks referencing *_nm.dds* textures without having to painstakingly open each category. Right-click in NifSkope to access the *Spells* dropdown menu, also available via the top bar, hover over the *Blocks* section, and `choose the action to Remove by ID`_. You can then input the RegEx expression ``^NiTextureEffect`` (directing it to remove any block whose name starts with "NiTextureEffect") to automatically remove all texture effect blocks within the NIF. This also has the helpful side effect of listing `all the blocks within the NIF in the bottom section`_, allowing you to additionally root out any blocks referencing *_nm.dds* textures without having to painstakingly open each category.
.. _`Netch Bump mapped`: https://www.nexusmods.com/morrowind/mods/42851/? .. _`Netch Bump mapped`: https://www.nexusmods.com/morrowind/mods/42851/?
.. _`Hlaalu Bump mapped`: https://www.nexusmods.com/morrowind/mods/42396/? .. _`Hlaalu Bump mapped`: https://www.nexusmods.com/morrowind/mods/42396/?
@ -251,10 +236,9 @@ Rightclick in NifSkope to access the *Spells* dropdown menu, also available via
.. _settings.cfg: https://wiki.openmw.org/index.php?title=Settings .. _settings.cfg: https://wiki.openmw.org/index.php?title=Settings
.. _`Multiple data folders`: https://wiki.openmw.org/index.php?title=Mod_installation .. _`Multiple data folders`: https://wiki.openmw.org/index.php?title=Mod_installation
.. _`Various Things - Sacks`: https://www.nexusmods.com/morrowind/mods/42558/? .. _`Various Things - Sacks`: https://www.nexusmods.com/morrowind/mods/42558/?
.. _Lead: https://imgur.com/bwpcYlc
.. _NifSkope: https://wiki.openmw.org/index.php?title=Tools#NifSkope .. _NifSkope: https://wiki.openmw.org/index.php?title=Tools#NifSkope
.. _Blocks: https://imgur.com/VmQC0WG .. _Blocks: https://imgur.com/VmQC0WG
.. _`no longer have shiny models`: https://imgur.com/vu1k7n1 .. _`no longer have wet models`: https://imgur.com/vu1k7n1
.. _`we are done`: https://imgur.com/yyZxlTw .. _`we are done`: https://imgur.com/yyZxlTw
.. _`choose the action to Remove by ID`: https://imgur.com/a/qs2t0tC .. _`choose the action to Remove by ID`: https://imgur.com/a/qs2t0tC
.. _`all the blocks within the NIF in the bottom section`: https://imgur.com/a/UFFNyWt .. _`all the blocks within the NIF in the bottom section`: https://imgur.com/a/UFFNyWt

@ -23,18 +23,6 @@ To plug in a normal map, you name the normal map as the diffuse texture but with
OpenMW will then recognise the file and load it as a normal map, provided you have set up your settings file correctly. OpenMW will then recognise the file and load it as a normal map, provided you have set up your settings file correctly.
See the section `Automatic use`_ further down below for detailed information. See the section `Automatic use`_ further down below for detailed information.
.. note::
While the original Morrowind engine does support the loading of a BumpTexture slot in the NIF,
it will not display it as a normal map. Morrowind Code Patch (MCP)
added a way to hack normal maps into the engine by first enabling the engine to load the BumpTexture slot as an
environment map and then turn down the brightness of the environment map.
This will imitate how a real normal map shader would display a normal map, but it will not look exactly the same.
OpenMW uses standard normal mapping, which achieves much better results.
Unfortunately, this difference can result in incompatibilities.
Some mods
(e.g. `Redoran Bump Mapped <http://www.nexusmods.com/morrowind/mods/42406/?>`_)
look much darker compared to the vanilla engine and will have to be recalibrated.
Specular Mapping Specular Mapping
################ ################
@ -43,14 +31,12 @@ The alpha channel specifies shininess in range [0, 255].
If a specular map is used, it will override the shininess and specular color If a specular map is used, it will override the shininess and specular color
set in the NiMaterialProperty / osg::Material. set in the NiMaterialProperty / osg::Material.
NIF files do not support specular maps. Morrowind format NIF files do not support normal maps or specular maps.
In order to use them anyway, see the next section. In order to use them anyway, see the next section.
Automatic Use Automatic Use
############# #############
In addition to editing mesh files,
there is another way of plugging in these texture maps.
Simply create the textures with appropriate naming convention Simply create the textures with appropriate naming convention
(e.g. when the base texture is called foo.dds, (e.g. when the base texture is called foo.dds,
the normal map would have to be called foo_n.dds). the normal map would have to be called foo_n.dds).

@ -4,8 +4,15 @@
<Widget type="Window" skin="MW_Window" layer="Windows" position="0 0 600 300" name="_Main"> <Widget type="Window" skin="MW_Window" layer="Windows" position="0 0 600 300" name="_Main">
<Property key="MinSize" value="245 145"/> <Property key="MinSize" value="245 145"/>
<!-- Search box-->
<Widget type="HBox" position="5 5 575 23" align="Left Top HStretch" name="_Filter">
<Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="0 0 0 23" name="FilterEdit">
<UserString key="HStretch" value="true"/>
<UserString key="AcceptTab" value="true"/>
</Widget>
</Widget>
<!-- Items --> <!-- Items -->
<Widget type="ItemView" skin="MW_ItemView" position="5 5 575 225" name="ItemView" align="Left Top Stretch"> <Widget type="ItemView" skin="MW_ItemView" position="5 33 575 197" name="ItemView" align="Left Top Stretch">
</Widget> </Widget>
<Widget type="HBox" position="5 235 575 24" align="Bottom HStretch"> <Widget type="HBox" position="5 235 575 24" align="Bottom HStretch">

@ -50,6 +50,11 @@
<Property key="Caption" value="#{sMiscTab}"/> <Property key="Caption" value="#{sMiscTab}"/>
<Property key="NeedKey" value="false"/> <Property key="NeedKey" value="false"/>
</Widget> </Widget>
<!-- Search box-->
<Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="0 0 0 23" name="FilterEdit">
<UserString key="HStretch" value="true"/>
<UserString key="AcceptTab" value="true"/>
</Widget>
</Widget> </Widget>
</Widget> </Widget>

@ -24,6 +24,7 @@
<!-- Search box--> <!-- Search box-->
<Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="8 535 268 23" align="Left Bottom HStretch" name="FilterEdit"> <Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="8 535 268 23" align="Left Bottom HStretch" name="FilterEdit">
<UserString key="AcceptTab" value="true"/>
</Widget> </Widget>
</Widget> </Widget>

@ -27,6 +27,11 @@
<Property key="Caption" value="#{sMiscTab}"/> <Property key="Caption" value="#{sMiscTab}"/>
<Property key="NeedKey" value="false"/> <Property key="NeedKey" value="false"/>
</Widget> </Widget>
<!-- Search box-->
<Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="0 0 0 23" name="FilterEdit">
<UserString key="HStretch" value="true"/>
<UserString key="AcceptTab" value="true"/>
</Widget>
</Widget> </Widget>
<!-- Items --> <!-- Items -->

@ -346,6 +346,10 @@ specular map pattern = _spec
# The filename pattern to probe for when detecting terrain specular maps (see 'auto use terrain specular maps') # The filename pattern to probe for when detecting terrain specular maps (see 'auto use terrain specular maps')
terrain specular map pattern = _diffusespec terrain specular map pattern = _diffusespec
# Apply lighting to reflections on the environment-mapped objects like in Morrowind Code Patch.
# Affected objects use shaders.
apply lighting to environment maps = false
[Input] [Input]
# Capture control of the cursor prevent movement outside the window. # Capture control of the cursor prevent movement outside the window.

@ -42,6 +42,13 @@ uniform sampler2D specularMap;
varying vec2 specularMapUV; varying vec2 specularMapUV;
#endif #endif
#if @bumpMap
uniform sampler2D bumpMap;
varying vec2 bumpMapUV;
uniform vec2 envMapLumaBias;
uniform mat2 bumpMapMatrix;
#endif
varying float depth; varying float depth;
#define PER_PIXEL_LIGHTING (@normalMap || @forcePPL) #define PER_PIXEL_LIGHTING (@normalMap || @forcePPL)
@ -112,6 +119,31 @@ void main()
#if @decalMap #if @decalMap
vec4 decalTex = texture2D(decalMap, decalMapUV); vec4 decalTex = texture2D(decalMap, decalMapUV);
gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a); gl_FragData[0].xyz = mix(gl_FragData[0].xyz, decalTex.xyz, decalTex.a);
#endif
#if @envMap
vec2 envTexCoordGen = envMapUV;
float envLuma = 1.0;
#if @normalMap
// if using normal map + env map, take advantage of per-pixel normals for envTexCoordGen
vec3 viewVec = normalize(passViewPos.xyz);
vec3 r = reflect( viewVec, viewNormal );
float m = 2.0 * sqrt( r.x*r.x + r.y*r.y + (r.z+1.0)*(r.z+1.0) );
envTexCoordGen = vec2(r.x/m + 0.5, r.y/m + 0.5);
#endif
#if @bumpMap
vec4 bumpTex = texture2D(bumpMap, bumpMapUV);
envTexCoordGen += bumpTex.rg * bumpMapMatrix;
envLuma = clamp(bumpTex.b * envMapLumaBias.x + envMapLumaBias.y, 0.0, 1.0);
#endif
#if @preLightEnv
gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma;
#endif
#endif #endif
float shadowing = unshadowedLightRatio(depth); float shadowing = unshadowedLightRatio(depth);
@ -128,24 +160,12 @@ void main()
gl_FragData[0] *= doLighting(passViewPos, normalize(viewNormal), passColor, shadowing); gl_FragData[0] *= doLighting(passViewPos, normalize(viewNormal), passColor, shadowing);
#endif #endif
#if @emissiveMap #if @envMap && !@preLightEnv
gl_FragData[0].xyz += texture2D(emissiveMap, emissiveMapUV).xyz; gl_FragData[0].xyz += texture2D(envMap, envTexCoordGen).xyz * envMapColor.xyz * envLuma;
#endif
#if @envMap
#if @normalMap
// if using normal map + env map, take advantage of per-pixel normals for texCoordGen
vec3 viewVec = normalize(passViewPos.xyz);
vec3 r = reflect( viewVec, viewNormal );
float m = 2.0 * sqrt( r.x*r.x + r.y*r.y + (r.z+1.0)*(r.z+1.0) );
vec2 texCoordGen = vec2(r.x/m + 0.5, r.y/m + 0.5);
gl_FragData[0].xyz += texture2D(envMap, texCoordGen).xyz * envMapColor.xyz;
#else
gl_FragData[0].xyz += texture2D(envMap, envMapUV).xyz * envMapColor.xyz;
#endif #endif
#if @emissiveMap
gl_FragData[0].xyz += texture2D(emissiveMap, emissiveMapUV).xyz;
#endif #endif
#if @specularMap #if @specularMap

@ -29,6 +29,10 @@ varying vec4 passTangent;
varying vec2 envMapUV; varying vec2 envMapUV;
#endif #endif
#if @bumpMap
varying vec2 bumpMapUV;
#endif
#if @specularMap #if @specularMap
varying vec2 specularMapUV; varying vec2 specularMapUV;
#endif #endif
@ -91,6 +95,10 @@ void main(void)
passTangent = gl_MultiTexCoord7.xyzw; passTangent = gl_MultiTexCoord7.xyzw;
#endif #endif
#if @bumpMap
bumpMapUV = (gl_TextureMatrix[@bumpMapUV] * gl_MultiTexCoord@bumpMapUV).xy;
#endif
#if @specularMap #if @specularMap
specularMapUV = (gl_TextureMatrix[@specularMapUV] * gl_MultiTexCoord@specularMapUV).xy; specularMapUV = (gl_TextureMatrix[@specularMapUV] * gl_MultiTexCoord@specularMapUV).xy;
#endif #endif

Loading…
Cancel
Save