diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a04b6a1cd..c7a1a403f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,7 +31,8 @@ stages: - build/install/ Coverity: - extends: .Debian + extends: .Debian_Image + stage: build rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' before_script: @@ -44,10 +45,10 @@ Coverity: - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw after_script: - tar cfz cov-int.tar.gz cov-int - - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME \ - --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL \ - --form file=@cov-int.tar.gz --form version="`git describe --tags`" \ - --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID + - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME + --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL + --form file=@cov-int.tar.gz --form version="`git describe --tags`" + --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID" variables: CC: gcc CXX: g++ @@ -119,7 +120,7 @@ Debian_Clang_tests: .MacOS: image: macos-11-xcode-12 tags: - - macos + - shared-macos-amd64 stage: build only: variables: @@ -128,12 +129,12 @@ Debian_Clang_tests: paths: - ccache/ script: - - rm -fr build/* # remove anything in the build directory + - rm -fr build # remove the build directory + - CI/before_install.osx.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" - ccache -z -M "${CCACHE_SIZE}" - - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.dmg"; done @@ -154,6 +155,7 @@ macOS11_Xcode12: macOS10.15_Xcode11: extends: .MacOS image: macos-10.15-xcode-11 + allow_failure: true cache: key: macOS10.15_Xcode11.v1 variables: diff --git a/AUTHORS.md b/AUTHORS.md index 1b49584d2..75302908e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -49,6 +49,7 @@ Programmers Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) + Cody Glassman (Wazabear) Coleman Smith (olcoal) Cory F. Cohen (cfcohen) Cris Mihalache (Mirceam) @@ -89,6 +90,7 @@ Programmers Internecine Jackerty Jacob Essex (Yacoby) + Jacob Turnbull (Tankinfrank) Jake Westrip (16bitint) James Carty (MrTopCat) James Moore (moore.work) @@ -150,6 +152,7 @@ Programmers Nathan Jeffords (blunted2night) NeveHanter Nialsy + Nick Crawford (nighthawk469) Nikolay Kasyanov (corristo) nobrakal Nolan Poe (nopoe) diff --git a/CHANGELOG.md b/CHANGELOG.md index a506079e8..87009f58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,8 @@ Bug #5914: BM: The Swimmer can't reach destination Bug #5923: Clicking on empty spaces between journal entries might show random topics Bug #5934: AddItem command doesn't accept negative values + Bug #5975: NIF controllers from sheath meshes are used + Bug #5995: NiUVController doesn't calculate the UV offset properly Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references Feature #1536: Show more information about level on menu @@ -127,6 +129,7 @@ Feature #3983: Wizard: Add link to buy Morrowind Feature #4894: Consider actors as obstacles for pathfinding Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing + Feature #4917: Do not trigger NavMesh update when RecastMesh update should not change NavMesh Feature #4977: Use the "default icon.tga" when an item's icon is not found Feature #5043: Head Bobbing Feature #5199: OpenMW-CS: Improve scene view colors @@ -152,6 +155,7 @@ Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. Feature #5813: Instanced groundcover support Feature #5814: Bsatool should be able to create BSA archives, not only to extract it + Feature #5828: Support more than 8 lights Feature #5910: Fall back to delta time when physics can't keep up Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/CI/activate_msvc.sh b/CI/activate_msvc.sh index 47f2c246f..233f01743 100644 --- a/CI/activate_msvc.sh +++ b/CI/activate_msvc.sh @@ -30,55 +30,11 @@ command -v unixPathAsWindows >/dev/null 2>&1 || function unixPathAsWindows { fi } -function windowsSystemPathAsUnix { - if command -v cygpath >/dev/null 2>&1; then - cygpath -u -p $1 - else - IFS=';' read -r -a paths <<< "$1" - declare -a convertedPaths - for entry in paths; do - convertedPaths+=(windowsPathAsUnix $entry) - done - convertedPath=printf ":%s" ${convertedPaths[@]} - echo ${convertedPath:1} - fi -} - -# capture CMD environment so we know what's been changed -declare -A originalCmdEnv -originalIFS="$IFS" -IFS=$'\n\r' -for pair in $(cmd //c "set"); do - IFS='=' read -r -a separatedPair <<< "${pair}" - if [ ${#separatedPair[@]} -ne 2 ]; then - echo "Parsed '$pair' as ${#separatedPair[@]} parts, expected 2." - continue - fi - originalCmdEnv["${separatedPair[0]}"]="${separatedPair[1]}" -done # capture CMD environment in a shell with MSVC activated -cmdEnv="$(cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" set)" - -declare -A cmdEnvChanges -for pair in $cmdEnv; do - if [ -n "$pair" ]; then - IFS='=' read -r -a separatedPair <<< "${pair}" - if [ ${#separatedPair[@]} -ne 2 ]; then - echo "Parsed '$pair' as ${#separatedPair[@]} parts, expected 2." - continue - fi - key="${separatedPair[0]}" - value="${separatedPair[1]}" - if ! [ ${originalCmdEnv[$key]+_} ] || [ "${originalCmdEnv[$key]}" != "$value" ]; then - if [ $key != 'PATH' ] && [ $key != 'path' ] && [ $key != 'Path' ]; then - export "$key=$value" - else - export PATH=$(windowsSystemPathAsUnix $value) - fi - fi - fi -done +cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" "bash" "-c" "declare -px > declared_env.sh" +source ./declared_env.sh +rm declared_env.sh MISSINGTOOLS=0 @@ -93,6 +49,4 @@ if [ $MISSINGTOOLS -ne 0 ]; then return 1 fi -IFS="$originalIFS" - -restoreOldSettings \ No newline at end of file +restoreOldSettings diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 6dad0abc1..51bcb9a59 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,9 +1,9 @@ #!/bin/sh -ex # workaround python issue on travis -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true -HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true +[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true +[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true +[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true # Some of these tools can come from places other than brew, so check before installing command -v ccache >/dev/null 2>&1 || brew install ccache @@ -15,5 +15,5 @@ ccache --version cmake --version qmake --version -curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-f8918dd.zip -o ~/openmw-deps.zip +curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-8f5ef6e.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null diff --git a/CMakeLists.txt b/CMakeLists.txt index 01c666a49..37d579d72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) +option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) option(BULLET_USE_DOUBLES "Use double precision for Bullet" ON) option(BUILD_OPENMW_MP "Build OpenMW-MP" ON) option(BUILD_BROWSER "Build tes3mp Server Browser" ON) @@ -533,10 +534,6 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.6) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") endif() - - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 5.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 5.0) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override") - endif() endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) # Extern @@ -614,6 +611,10 @@ if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) endif() +if (BUILD_BENCHMARKS) + add_subdirectory(apps/benchmarks) +endif() + if (WIN32) if (MSVC) if (OPENMW_MP_BUILD) @@ -644,64 +645,16 @@ if (WIN32) # Play a bit with the warning levels - set(WARNINGS "/Wall") # Since windows can only disable specific warnings, not enable them + set(WARNINGS "/W4") set(WARNINGS_DISABLE - # Warnings that aren't enabled normally and don't need to be enabled - # They're unneeded and sometimes completely retarded warnings that /Wall enables - # Not going to bother commenting them as they tend to warn on every standard library file - 4061 4263 4264 4266 4350 4371 4435 4514 4548 4571 4610 4619 4623 4625 - 4626 4628 4640 4668 4710 4711 4768 4820 4826 4917 4946 5032 5039 5045 - - # Warnings that are thrown on standard libraries and not OpenMW - 4347 # Non-template function with same name and parameter count as template function - 4365 # Variable signed/unsigned mismatch - 4510 4512 # Unable to generate copy constructor/assignment operator as it's not public in the base - 4706 # Assignment in conditional expression - 4738 # Storing 32-bit float result in memory, possible loss of performance - 4774 # Format string expected in argument is not a string literal - 4986 # Undocumented warning that occurs in the crtdbg.h file - 4987 # nonstandard extension used (triggered by setjmp.h) - 4996 # Function was declared deprecated - - # caused by OSG - 4589 # Constructor of abstract class 'osg::Operation' ignores initializer for virtual base class 'osg::Referenced' (False warning) - - # caused by boost - 4191 # 'type cast' : unsafe conversion (1.56, thread_primitives.hpp, normally off) - 4643 # Forward declaring 'X' in namespace std is not permitted by the C++ Standard. (in *_std_fwd.h files) - 5204 # Class has virtual functions, but its trivial destructor is not virtual - - # caused by MyGUI - 4275 # non dll-interface class 'std::exception' used as base for dll-interface class 'MyGUI::Exception' - 4297 # function assumed not to throw an exception but does - # OpenMW specific warnings - 4099 # Type mismatch, declared class or struct is defined with other type 4100 # Unreferenced formal parameter (-Wunused-parameter) - 4101 # Unreferenced local variable (-Wunused-variable) 4127 # Conditional expression is constant - 4242 # Storing value in a variable of a smaller type, possible loss of data 4244 # Storing value of one type in variable of another (size_t in int, for example) - 4245 # Signed/unsigned mismatch 4267 # Conversion from 'size_t' to 'int', possible loss of data - 4305 # Truncating value (double to float, for example) - 4309 # Variable overflow, trying to store 128 in a signed char for example - 4351 # New behavior: elements of array 'array' will be default initialized (desired behavior) - 4355 # Using 'this' in member initialization list - 4464 # relative include path contains '..' - 4505 # Unreferenced local function has been removed - 4701 # Potentially uninitialized local variable used - 4702 # Unreachable code - 4714 # function 'QString QString::trimmed(void) &&' marked as __forceinline not inlined - 4800 # Boolean optimization warning, e.g. myBool = (myInt != 0) instead of myBool = myInt + 4996 # Function was declared deprecated ) - - if (MSVC_VERSION GREATER 1800) - set(WARNINGS_DISABLE ${WARNINGS_DISABLE} 5026 5027 - 5031 # #pragma warning(pop): likely mismatch, popping warning state pushed in different file (config_begin.hpp, config_end.hpp) - ) - endif() if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.0" ) set(WARNINGS_DISABLE ${WARNINGS_DISABLE} @@ -709,6 +662,12 @@ if (WIN32) ) endif() + if( "${MyGUI_VERSION}" VERSION_LESS_EQUAL "3.4.1" ) + set(WARNINGS_DISABLE ${WARNINGS_DISABLE} + 4275 # non dll-interface class 'MyGUI::delegates::IDelegateUnlink' used as base for dll-interface class 'MyGUI::Widget' + ) + endif() + foreach(d ${WARNINGS_DISABLE}) set(WARNINGS "${WARNINGS} /wd${d}") endforeach(d) diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..b7170003e --- /dev/null +++ b/apps/benchmarks/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.11) + +set(BENCHMARK_ENABLE_TESTING OFF) +set(BENCHMARK_ENABLE_INSTALL OFF) +set(BENCHMARK_ENABLE_GTEST_TESTS OFF) + +set(SAVED_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + +string(REPLACE "-Wsuggest-override" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +string(REPLACE "-Wundef" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +include(FetchContent) +FetchContent_Declare(benchmark + URL https://github.com/google/benchmark/archive/refs/tags/v1.5.2.zip + URL_HASH MD5=49395b757a7c4656d70f1328d93efd00 + SOURCE_DIR fetched/benchmark +) +FetchContent_MakeAvailableExcludeFromAll(benchmark) + +set(CMAKE_CXX_FLAGS "${SAVED_CMAKE_CXX_FLAGS}") + +openmw_add_executable(openmw_detournavigator_navmeshtilescache_benchmark detournavigator/navmeshtilescache.cpp) +target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE -Wall) +target_compile_features(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE cxx_std_17) +target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark benchmark::benchmark components) + +if (UNIX AND NOT APPLE) + target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT}) +endif() + +if (MSVC) + if (CMAKE_CL_64) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") + endif (CMAKE_CL_64) +endif (MSVC) diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp new file mode 100644 index 000000000..10aa0672a --- /dev/null +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -0,0 +1,215 @@ +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace DetourNavigator; + + struct Key + { + osg::Vec3f mAgentHalfExtents; + TilePosition mTilePosition; + RecastMesh mRecastMesh; + std::vector mOffMeshConnections; + }; + + struct Item + { + Key mKey; + NavMeshData mValue; + }; + + template + TilePosition generateTilePosition(int max, Random& random) + { + std::uniform_int_distribution distribution(0, max); + return TilePosition(distribution(random), distribution(random)); + } + + template + osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) + { + std::uniform_int_distribution distribution(min, max); + return osg::Vec3f(distribution(random), distribution(random), distribution(random)); + } + + template + void generateVertices(OutputIterator out, std::size_t number, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); }); + } + + template + void generateIndices(OutputIterator out, int max, std::size_t number, Random& random) + { + std::uniform_int_distribution distribution(0, max); + std::generate_n(out, number - number % 3, [&] { return distribution(random); }); + } + + AreaType toAreaType(int index) + { + switch (index) + { + case 0: return AreaType_null; + case 1: return AreaType_water; + case 2: return AreaType_door; + case 3: return AreaType_pathgrid; + case 4: return AreaType_ground; + } + return AreaType_null; + } + + template + AreaType generateAreaType(Random& random) + { + std::uniform_int_distribution distribution(0, 4); + return toAreaType(distribution(random));; + } + + template + void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random) + { + std::generate_n(out, triangles, [&] { return generateAreaType(random); }); + } + + template + void generateWater(OutputIterator out, std::size_t count, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, count, [&] { + const btVector3 shift(distribution(random), distribution(random), distribution(random)); + return RecastMesh::Water {1, btTransform(btMatrix3x3::getIdentity(), shift)}; + }); + } + + template + void generateOffMeshConnection(OutputIterator out, std::size_t count, Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + std::generate_n(out, count, [&] { + const osg::Vec3f start(distribution(random), distribution(random), distribution(random)); + const osg::Vec3f end(distribution(random), distribution(random), distribution(random)); + return OffMeshConnection {start, end, generateAreaType(random)}; + }); + } + + template + Key generateKey(std::size_t triangles, Random& random) + { + const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); + const TilePosition tilePosition = generateTilePosition(10000, random); + const std::size_t generation = std::uniform_int_distribution(0, 100)(random); + const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); + std::vector vertices; + generateVertices(std::back_inserter(vertices), triangles * 1.98, random); + std::vector indices; + generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.53, random); + std::vector areaTypes; + generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); + std::vector water; + generateWater(std::back_inserter(water), 2, random); + const std::size_t trianglesPerChunk = 256; + RecastMesh recastMesh(generation, revision, std::move(indices), std::move(vertices), + std::move(areaTypes), std::move(water), trianglesPerChunk); + std::vector offMeshConnections; + generateOffMeshConnection(std::back_inserter(offMeshConnections), 300, random); + return Key {agentHalfExtents, tilePosition, std::move(recastMesh), std::move(offMeshConnections)}; + } + + constexpr std::size_t trianglesPerTile = 310; + + template + void generateKeys(OutputIterator out, std::size_t count, Random& random) + { + std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); + } + + template + void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache) + { + std::size_t size = cache.getStats().mNavMeshCacheSize; + + while (true) + { + Key key = generateKey(trianglesPerTile, random); + cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + *out++ = std::move(key); + const std::size_t newSize = cache.getStats().mNavMeshCacheSize; + if (size >= newSize) + break; + size = newSize; + } + } + + template + void getFromFilledCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections); + benchmark::DoNotOptimize(result); + } + } + + constexpr auto getFromFilledCache_1m_100hit = getFromFilledCache<1 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_4m_100hit = getFromFilledCache<4 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_16m_100hit = getFromFilledCache<16 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_64m_100hit = getFromFilledCache<64 * 1024 * 1024, 100>; + constexpr auto getFromFilledCache_1m_70hit = getFromFilledCache<1 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_4m_70hit = getFromFilledCache<4 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_16m_70hit = getFromFilledCache<16 * 1024 * 1024, 70>; + constexpr auto getFromFilledCache_64m_70hit = getFromFilledCache<64 * 1024 * 1024, 70>; + + template + void setToBoundedNonEmptyCache(benchmark::State& state) + { + NavMeshTilesCache cache(maxCacheSize); + std::minstd_rand random; + std::vector keys; + fillCache(std::back_inserter(keys), random, cache); + generateKeys(std::back_inserter(keys), keys.size() * 2, random); + std::reverse(keys.begin(), keys.end()); + std::size_t n = 0; + + while (state.KeepRunning()) + { + const auto& key = keys[n++ % keys.size()]; + const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + benchmark::DoNotOptimize(result); + } + } + + constexpr auto setToBoundedNonEmptyCache_1m = setToBoundedNonEmptyCache<1 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_4m = setToBoundedNonEmptyCache<4 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_16m = setToBoundedNonEmptyCache<16 * 1024 * 1024>; + constexpr auto setToBoundedNonEmptyCache_64m = setToBoundedNonEmptyCache<64 * 1024 * 1024>; +} // namespace + +BENCHMARK(getFromFilledCache_1m_100hit); +BENCHMARK(getFromFilledCache_4m_100hit); +BENCHMARK(getFromFilledCache_16m_100hit); +BENCHMARK(getFromFilledCache_64m_100hit); +BENCHMARK(getFromFilledCache_1m_70hit); +BENCHMARK(getFromFilledCache_4m_70hit); +BENCHMARK(getFromFilledCache_16m_70hit); +BENCHMARK(getFromFilledCache_64m_70hit); +BENCHMARK(setToBoundedNonEmptyCache_1m); +BENCHMARK(setToBoundedNonEmptyCache_4m); +BENCHMARK(setToBoundedNonEmptyCache_16m); +BENCHMARK(setToBoundedNonEmptyCache_64m); + +BENCHMARK_MAIN(); diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 7cea574c8..579f5f67d 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -322,7 +322,7 @@ int load(Arguments& info) std::string filename = info.filename; std::cout << "Loading file: " << filename << std::endl; - std::list skipped; + std::list skipped; try { diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp index ca81ebbbf..cb7947e40 100644 --- a/apps/essimporter/convertscpt.cpp +++ b/apps/essimporter/convertscpt.cpp @@ -11,6 +11,7 @@ namespace ESSImport { out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); out.mRunning = scpt.mRunning; + out.mTargetRef.unset(); // TODO: convert target reference of global script convertSCRI(scpt.mSCRI, out.mLocals); } diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp index dbf9ce0bd..e91c39452 100644 --- a/apps/essimporter/importinventory.cpp +++ b/apps/essimporter/importinventory.cpp @@ -19,6 +19,7 @@ namespace ESSImport item.mCount = contItem.mCount; item.mRelativeEquipmentSlot = -1; item.mLockLevel = 0; + item.mRefNum.unset(); unsigned int itemCount = std::abs(item.mCount); bool separateStacks = false; diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index d593669c3..9b969e35a 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -57,7 +57,7 @@ int main(int argc, char** argv) else { const std::string& ext = ".omwsave"; - if (boost::filesystem::exists(boost::filesystem::path(outputFile)) + if (bfs::exists(bfs::path(outputFile)) && (outputFile.size() < ext.size() || outputFile.substr(outputFile.size()-ext.size()) != ext)) { throw std::runtime_error("Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 683d44119..91e3842fa 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,5 +1,7 @@ #include "advancedpage.hpp" +#include + #include #include #include @@ -138,6 +140,13 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(activeGridObjectPagingCheckBox, "object paging active grid", "Terrain"); viewingDistanceComboBox->setValue(convertToCells(mEngineSettings.getInt("viewing distance", "Camera"))); + + int lightingMethod = 1; + if (mEngineSettings.getString("lighting method", "Shaders") == "legacy") + lightingMethod = 0; + else if (mEngineSettings.getString("lighting method", "Shaders") == "shaders") + lightingMethod = 2; + lightingMethodComboBox->setCurrentIndex(lightingMethod); } // Audio @@ -194,6 +203,7 @@ bool Launcher::AdvancedPage::loadSettings() showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + scalingSpinBox->setValue(mEngineSettings.getFloat("scaling factor", "GUI")); } // Bug fixes @@ -288,6 +298,9 @@ void Launcher::AdvancedPage::saveSettings() { mEngineSettings.setInt("viewing distance", "Camera", convertToUnits(viewingDistance)); } + + static std::array lightingMethodMap = {"legacy", "shaders compatibility", "shaders"}; + mEngineSettings.setString("lighting method", "Shaders", lightingMethodMap[lightingMethodComboBox->currentIndex()]); } // Audio @@ -348,6 +361,9 @@ void Launcher::AdvancedPage::saveSettings() mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); + float uiScalingFactor = scalingSpinBox->value(); + if (uiScalingFactor != mEngineSettings.getFloat("scaling factor", "GUI")) + mEngineSettings.setFloat("scaling factor", "GUI", uiScalingFactor); } // Bug fixes diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp index 6f76b7130..52ad20894 100644 --- a/apps/launcher/utils/openalutil.cpp +++ b/apps/launcher/utils/openalutil.cpp @@ -37,19 +37,25 @@ std::vector Launcher::enumerateOpenALDevicesHrtf() std::vector ret; ALCdevice *device = alcOpenDevice(nullptr); - if(device && alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) + if(device) { - LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; - void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); - memcpy(&alcGetStringiSOFT, &funcPtr, sizeof(funcPtr)); - ALCint num_hrtf; - alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); - ret.reserve(num_hrtf); - for(ALCint i = 0;i < num_hrtf && i < 20;++i) + if(alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) { - const ALCchar *entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); - ret.emplace_back(entry); + LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; + void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); + memcpy(&alcGetStringiSOFT, &funcPtr, sizeof(funcPtr)); + ALCint num_hrtf; + alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &num_hrtf); + ret.reserve(num_hrtf); + for(ALCint i = 0;i < num_hrtf;++i) + { + const ALCchar *entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); + if(strcmp(entry, "") == 0) + break; + ret.emplace_back(entry); + } } + alcCloseDevice(device); } return ret; -} \ No newline at end of file +} diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 65575580a..b73cd37b8 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -116,7 +116,7 @@ opencs_units (view/prefs opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut - shortcuteventhandler shortcutmanager shortcutsetting modifiersetting + shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting ) opencs_units_noqt (model/prefs diff --git a/apps/opencs/model/prefs/shortcutsetting.cpp b/apps/opencs/model/prefs/shortcutsetting.cpp index 1c065f7da..e0cd5bb07 100644 --- a/apps/opencs/model/prefs/shortcutsetting.cpp +++ b/apps/opencs/model/prefs/shortcutsetting.cpp @@ -13,8 +13,6 @@ namespace CSMPrefs { - const int ShortcutSetting::MaxKeys; - ShortcutSetting::ShortcutSetting(Category* parent, Settings::Manager* values, QMutex* mutex, const std::string& key, const std::string& label) : Setting(parent, values, mutex, key, label) diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 32b1ee33f..0958fa8d4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -421,6 +421,16 @@ void CSMPrefs::State::declare() declareSubcategory ("Script Editor"); declareShortcut ("script-editor-comment", "Comment Selection", QKeySequence()); declareShortcut ("script-editor-uncomment", "Uncomment Selection", QKeySequence()); + + declareCategory ("Models"); + declareString ("baseanim", "base animations", "meshes/base_anim.nif"). + setTooltip("3rd person base model with textkeys-data"); + declareString ("baseanimkna", "base animations, kna", "meshes/base_animkna.nif"). + setTooltip("3rd person beast race base model with textkeys-data"); + declareString ("baseanimfemale", "base animations, female", "meshes/base_anim_female.nif"). + setTooltip("3rd person female base model with textkeys-data"); + declareString ("wolfskin", "base animations, wolf", "meshes/wolf/skin.nif"). + setTooltip("3rd person werewolf skin"); } void CSMPrefs::State::declareCategory (const std::string& key) @@ -557,6 +567,24 @@ CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut (const std::string& return *setting; } +CSMPrefs::StringSetting& CSMPrefs::State::declareString (const std::string& key, const std::string& label, std::string default_) +{ + if (mCurrentCategory==mCategories.end()) + throw std::logic_error ("no category for setting"); + + setDefault (key, default_); + + default_ = mSettings.getString (key, mCurrentCategory->second.getKey()); + + CSMPrefs::StringSetting *setting = + new CSMPrefs::StringSetting (&mCurrentCategory->second, &mSettings, &mMutex, key, label, + default_); + + mCurrentCategory->second.addSetting (setting); + + return *setting; +} + CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier(const std::string& key, const std::string& label, int default_) { diff --git a/apps/opencs/model/prefs/state.hpp b/apps/opencs/model/prefs/state.hpp index a32583dc5..aa63de595 100644 --- a/apps/opencs/model/prefs/state.hpp +++ b/apps/opencs/model/prefs/state.hpp @@ -16,6 +16,7 @@ #include "category.hpp" #include "setting.hpp" #include "enumsetting.hpp" +#include "stringsetting.hpp" #include "shortcutmanager.hpp" class QColor; @@ -78,6 +79,8 @@ namespace CSMPrefs ShortcutSetting& declareShortcut (const std::string& key, const std::string& label, const QKeySequence& default_); + StringSetting& declareString (const std::string& key, const std::string& label, std::string default_); + ModifierSetting& declareModifier(const std::string& key, const std::string& label, int modifier_); void declareSeparator(); diff --git a/apps/opencs/model/prefs/stringsetting.cpp b/apps/opencs/model/prefs/stringsetting.cpp new file mode 100644 index 000000000..27290b6c7 --- /dev/null +++ b/apps/opencs/model/prefs/stringsetting.cpp @@ -0,0 +1,54 @@ + +#include "stringsetting.hpp" + +#include +#include + +#include + +#include "category.hpp" +#include "state.hpp" + +CSMPrefs::StringSetting::StringSetting (Category *parent, Settings::Manager *values, + QMutex *mutex, const std::string& key, const std::string& label, std::string default_) +: Setting (parent, values, mutex, key, label), mDefault (default_), mWidget(nullptr) +{} + +CSMPrefs::StringSetting& CSMPrefs::StringSetting::setTooltip (const std::string& tooltip) +{ + mTooltip = tooltip; + return *this; +} + +std::pair CSMPrefs::StringSetting::makeWidgets (QWidget *parent) +{ + mWidget = new QLineEdit (QString::fromUtf8 (mDefault.c_str()), parent); + + if (!mTooltip.empty()) + { + QString tooltip = QString::fromUtf8 (mTooltip.c_str()); + mWidget->setToolTip (tooltip); + } + + connect (mWidget, SIGNAL (textChanged (QString)), this, SLOT (textChanged (QString))); + + return std::make_pair (static_cast (nullptr), mWidget); +} + +void CSMPrefs::StringSetting::updateWidget() +{ + if (mWidget) + { + mWidget->setText(QString::fromStdString(getValues().getString(getKey(), getParent()->getKey()))); + } +} + +void CSMPrefs::StringSetting::textChanged (const QString& text) +{ + { + QMutexLocker lock (getMutex()); + getValues().setString (getKey(), getParent()->getKey(), text.toStdString()); + } + + getParent()->getState()->update (*this); +} diff --git a/apps/opencs/model/prefs/stringsetting.hpp b/apps/opencs/model/prefs/stringsetting.hpp new file mode 100644 index 000000000..36d020f29 --- /dev/null +++ b/apps/opencs/model/prefs/stringsetting.hpp @@ -0,0 +1,36 @@ +#ifndef CSM_PREFS_StringSetting_H +#define CSM_PREFS_StringSetting_H + +#include "setting.hpp" + +class QLineEdit; + +namespace CSMPrefs +{ + class StringSetting : public Setting + { + Q_OBJECT + + std::string mTooltip; + std::string mDefault; + QLineEdit* mWidget; + + public: + + StringSetting (Category *parent, Settings::Manager *values, + QMutex *mutex, const std::string& key, const std::string& label, std::string default_); + + StringSetting& setTooltip (const std::string& tooltip); + + /// Return label, input widget. + std::pair makeWidgets (QWidget *parent) override; + + void updateWidget() override; + + private slots: + + void textChanged (const QString& text); + }; +} + +#endif diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 34485a46d..5ec7401dc 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -76,6 +76,7 @@ void CSMWorld::ImportLandTexturesCommand::redo() } std::vector oldTextures; + oldTextures.reserve(texIndices.size()); for (int index : texIndices) { oldTextures.push_back(LandTexture::createUniqueRecordId(oldPlugin, index)); diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 70c496e3f..4ccd2a06d 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -83,6 +83,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat defines["clamp"] = "1"; // Clamp lighting defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; + defines["lightingModel"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); @@ -985,20 +986,6 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, nullptr, &metaData)); } - // Fix uninitialized master data index - for (std::vector::const_iterator masterData = mReader->getGameFiles().begin(); - masterData != mReader->getGameFiles().end(); ++masterData) - { - std::map::iterator nameResult = mContentFileNames.find(masterData->name); - if (nameResult != mContentFileNames.end()) - { - ESM::Header::MasterData& hackedMasterData = const_cast(*masterData); - - - hackedMasterData.index = nameResult->second; - } - } - return mReader->getRecordCount(); } diff --git a/apps/opencs/model/world/idtree.cpp b/apps/opencs/model/world/idtree.cpp index a8dfacb01..1e3398bbb 100644 --- a/apps/opencs/model/world/idtree.cpp +++ b/apps/opencs/model/world/idtree.cpp @@ -201,7 +201,7 @@ QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const const std::pair& address(unfoldIndexAddress(id)); if (address.first >= this->rowCount() || address.second >= this->columnCount()) - throw "Parent index is not present in the model"; + throw std::logic_error("Parent index is not present in the model"); return createIndex(address.first, address.second); } @@ -216,7 +216,7 @@ unsigned int CSMWorld::IdTree::foldIndexAddress (const QModelIndex& index) const std::pair< int, int > CSMWorld::IdTree::unfoldIndexAddress (unsigned int id) const { if (id == 0) - throw "Attempt to unfold index id of the top level data cell"; + throw std::runtime_error("Attempt to unfold index id of the top level data cell"); --id; int row = id / this->columnCount(); diff --git a/apps/opencs/view/render/cameracontroller.cpp b/apps/opencs/view/render/cameracontroller.cpp index 5dbb7a28c..f21224d73 100644 --- a/apps/opencs/view/render/cameracontroller.cpp +++ b/apps/opencs/view/render/cameracontroller.cpp @@ -126,7 +126,7 @@ namespace CSVRender { // Try again without any mask boundsVisitor.reset(); - boundsVisitor.setTraversalMask(~0); + boundsVisitor.setTraversalMask(~0u); root->accept(boundsVisitor); // Last resort, set a default @@ -458,7 +458,7 @@ namespace CSVRender , mDown(false) , mRollLeft(false) , mRollRight(false) - , mPickingMask(~0) + , mPickingMask(~0u) , mCenter(0,0,0) , mDistance(0) , mOrbitSpeed(osg::PI / 4) diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index ac260fe83..4d6155123 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -151,7 +151,7 @@ void CSVRender::CellArrow::buildShape() osg::Vec4Array *colours = new osg::Vec4Array; for (int i=0; i<6; ++i) - colours->push_back (osg::Vec4f (0.11, 0.6f, 0.95f, 1.0f)); + colours->push_back (osg::Vec4f (0.11f, 0.6f, 0.95f, 1.0f)); for (int i=0; i<6; ++i) colours->push_back (osg::Vec4f (0.08f, 0.44f, 0.7f, 1.0f)); diff --git a/apps/opencs/view/render/mask.hpp b/apps/opencs/view/render/mask.hpp index deeab4996..818be8b22 100644 --- a/apps/opencs/view/render/mask.hpp +++ b/apps/opencs/view/render/mask.hpp @@ -8,7 +8,7 @@ namespace CSVRender /// @note See the respective file in OpenMW (apps/openmw/mwrender/vismask.hpp) /// for general usage hints about node masks. /// @copydoc MWRender::VisMask - enum Mask + enum Mask : unsigned int { // elements that are part of the actual scene Mask_Reference = 0x2, diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 4d73cde15..534093996 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -120,7 +120,7 @@ void RenderWidget::flagAsModified() mView->requestRedraw(); } -void RenderWidget::setVisibilityMask(int mask) +void RenderWidget::setVisibilityMask(unsigned int mask) { mView->getCamera()->setCullMask(mask | Mask_ParticleSystem | Mask_Lighting); } diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index d7d9dba0c..922776e9f 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -55,7 +55,7 @@ namespace CSVRender /// Initiates a request to redraw the view void flagAsModified(); - void setVisibilityMask(int mask); + void setVisibilityMask(unsigned int mask); osg::Camera *getCamera(); diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index f4a3f461c..fa46cf673 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -606,7 +606,7 @@ void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName) newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter; } - catch (const std::exception& e) + catch (const std::exception&) { newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter); freeIndexFound = true; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 82a1459f2..6eb437daf 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -163,7 +163,7 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { - std::vector > selection = getSelection(~0); + std::vector > selection = getSelection(~0u); for (std::vector >::iterator it = selection.begin(); it!=selection.end(); ++it) { diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 1600c6336..15d977f48 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -237,6 +237,8 @@ namespace MWBase virtual bool getWorldMouseOver() = 0; + virtual float getScalingFactor() = 0; + virtual bool toggleFogOfWar() = 0; virtual bool toggleFullHelp() = 0; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 1d39f8b22..4425ad2c0 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -77,7 +77,7 @@ namespace MWClass CreatureCustomData() = default; CreatureCustomData(const CreatureCustomData& other); - CreatureCustomData(CreatureCustomData&& other) noexcept = default; + CreatureCustomData(CreatureCustomData&& other) = default; CreatureCustomData& asCreatureCustomData() override { diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp index 28e450e2b..fa7de97d2 100644 --- a/apps/openmw/mwdialogue/hypertextparser.cpp +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -17,7 +17,7 @@ namespace MWDialogue std::vector parseHyperText(const std::string & text) { std::vector result; - size_t pos_end, iteration_pos = 0; + size_t pos_end = std::string::npos, iteration_pos = 0; for(;;) { size_t pos_begin = text.find('@', iteration_pos); diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index a075ded67..b72b105ed 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -161,7 +161,7 @@ namespace MWGui // We need this copy for when @# hyperlinks are replaced std::string text = mText; - size_t pos_end; + size_t pos_end = std::string::npos; for(;;) { size_t pos_begin = text.find('@'); diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 572ffd467..2102c163b 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -81,13 +81,8 @@ namespace MWGui , mLastYSize(0) , mPreview(new MWRender::InventoryPreview(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) - , mScaleFactor(1.0f) , mUpdateTimer(0.f) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 0.f) - mScaleFactor = uiScale; - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); mPreview->rebuild(); @@ -483,10 +478,11 @@ namespace MWGui MyGUI::IntSize size = mAvatarImage->getSize(); int width = std::min(mPreview->getTextureWidth(), size.width); int height = std::min(mPreview->getTextureHeight(), size.height); - mPreview->setViewport(int(width*mScaleFactor), int(height*mScaleFactor)); + float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mPreview->setViewport(int(width*scalingFactor), int(height*scalingFactor)); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, - width*mScaleFactor/float(mPreview->getTextureWidth()), height*mScaleFactor/float(mPreview->getTextureHeight()))); + width*scalingFactor/float(mPreview->getTextureWidth()), height*scalingFactor/float(mPreview->getTextureHeight()))); } void InventoryWindow::onNameFilterChanged(MyGUI::EditBox* _sender) @@ -661,8 +657,9 @@ namespace MWGui y = (mAvatarImage->getHeight()-1) - y; // Scale coordinates - x = int(x*mScaleFactor); - y = int(y*mScaleFactor); + float scalingFactor = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + x = static_cast(x*scalingFactor); + y = static_cast(y*scalingFactor); int slot = mPreview->getSlotSelected (x, y); diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index dc3ee9e0c..214245767 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -104,7 +104,6 @@ namespace MWGui std::unique_ptr mPreview; bool mTrading; - float mScaleFactor; float mUpdateTimer; void toggleMaximized(); diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 426c3b437..6b38cd0d9 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -129,7 +129,7 @@ struct JournalViewModelImpl : JournalViewModel utf8text.replace(pos_begin, pos_end+1-pos_begin, displayName); - intptr_t value; + intptr_t value = 0; if (mModel->mKeywordSearch.containsKeyword(topicName, value)) mHyperLinks[std::make_pair(pos_begin, pos_begin+displayName.size())] = value; } diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 327cf6434..4540cc0a7 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -11,12 +11,16 @@ #include #include +#include #include #include #include #include #include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -106,11 +110,24 @@ namespace if (!widget->getUserString(settingMax).empty()) max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } + + void updateMaxLightsComboBox(MyGUI::ComboBox* box) + { + constexpr int min = 8; + constexpr int max = 32; + constexpr int increment = 8; + int maxLights = Settings::Manager::getInt("max lights", "Shaders"); + // show increments of 8 in dropdown + if (maxLights >= min && maxLights <= max && !(maxLights % increment)) + box->setIndexSelected((maxLights / increment)-1); + else + box->setIndexSelected(MyGUI::ITEM_NONE); + } } namespace MWGui { - void SettingsWindow::configureWidgets(MyGUI::Widget* widget) + void SettingsWindow::configureWidgets(MyGUI::Widget* widget, bool init) { MyGUI::EnumeratorWidgetPtr widgets = widget->getEnumerator(); while (widgets.next()) @@ -124,7 +141,8 @@ namespace MWGui getSettingCategory(current)) ? "#{sOn}" : "#{sOff}"; current->castType()->setCaptionWithReplacing(initialValue); - current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); + if (init) + current->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onButtonToggled); } if (type == sliderType) { @@ -159,6 +177,12 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); @@ -173,12 +197,13 @@ namespace MWGui valueStr = MyGUI::utility::toString(value); scroll->setScrollPosition(value); } - scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); + if (init) + scroll->eventScrollChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onSliderChangePosition); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } - configureWidgets(current); + configureWidgets(current, init); } } @@ -205,7 +230,7 @@ namespace MWGui getWidget(unusedSlider, widgetName); unusedSlider->setVisible(false); - configureWidgets(mMainWidget); + configureWidgets(mMainWidget, true); setTitle("#{sOptions}"); @@ -222,6 +247,9 @@ namespace MWGui getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mLightingMethodButton, "LightingMethodButton"); + getWidget(mLightsResetButton, "LightsResetButton"); + getWidget(mMaxLights, "MaxLights"); #ifndef WIN32 // hide gamma controls since it currently does not work under Linux @@ -247,6 +275,10 @@ namespace MWGui mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); + mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); + mMaxLights->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onMaxLightsChanged); + mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); @@ -292,6 +324,8 @@ namespace MWGui waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); + updateMaxLightsComboBox(mMaxLights); + mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); mKeyboardSwitch->setStateSelected(true); @@ -378,6 +412,54 @@ namespace MWGui apply(); } + void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) + { + if (pos == MyGUI::ITEM_NONE) + return; + + std::string message = "This change requires a restart to take effect."; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, {"#{sOK}"}, true); + + Settings::Manager::setString("lighting method", "Shaders", _sender->getItemNameAt(pos)); + apply(); + } + + void SettingsWindow::onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos) + { + int count = 8 * (pos + 1); + + Settings::Manager::setInt("max lights", "Shaders", count); + apply(); + configureWidgets(mMainWidget, false); + } + + void SettingsWindow::onLightsResetButtonClicked(MyGUI::Widget* _sender) + { + std::vector buttons = {"#{sYes}", "#{sNo}"}; + std::string message = "Resets to default values, would you like to continue? Changes to lighting method will require a restart."; + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons, true); + int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); + if (selectedButton == 1 || selectedButton == -1) + return; + + constexpr std::array settings = { + "light bounds multiplier", + "maximum light distance", + "light fade start", + "minimum interior brightness", + "max lights", + "lighting method", + }; + for (const auto& setting : settings) + Settings::Manager::setString(setting, "Shaders", Settings::Manager::mDefaultSettings[{"Shaders", setting}]); + + mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(Settings::Manager::mDefaultSettings[{"Shaders", "lighting method"}])); + updateMaxLightsComboBox(mMaxLights); + + apply(); + configureWidgets(mMainWidget, false); + } + void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { std::string on = MWBase::Environment::get().getWindowManager()->getGameSettingString("sOn", "On"); @@ -480,6 +562,12 @@ namespace MWGui ss << std::fixed << std::setprecision(2) << value/Constants::CellSizeInUnits; valueStr = ss.str(); } + else if (valueType == "Float") + { + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << value; + valueStr = ss.str(); + } else valueStr = MyGUI::utility::toString(int(value)); } @@ -570,6 +658,30 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::updateLightSettings() + { + auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); + std::string lightingMethodStr = SceneUtil::LightManager::getLightingMethodString(lightingMethod); + + mLightingMethodButton->removeAllItems(); + + std::array methods = { + SceneUtil::LightingMethod::FFP, + SceneUtil::LightingMethod::PerObjectUniform, + SceneUtil::LightingMethod::SingleUBO, + }; + + for (const auto& method : methods) + { + if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) + continue; + + mLightingMethodButton->addItem(SceneUtil::LightManager::getLightingMethodString(method)); + } + + mLightingMethodButton->setIndexSelected(mLightingMethodButton->findItemIndexWith(lightingMethodStr)); + } + void SettingsWindow::layoutControlsBox() { const int h = 18; @@ -632,6 +744,7 @@ namespace MWGui { highlightCurrentResolution(); updateControlsBox(); + updateLightSettings(); resetScrollbars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index c268514dc..9c28733f9 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -14,6 +14,8 @@ namespace MWGui void updateControlsBox(); + void updateLightSettings(); + void onResChange(int, int) override { center(); } protected: @@ -30,6 +32,10 @@ namespace MWGui MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::ComboBox* mMaxLights; + MyGUI::ComboBox* mLightingMethodButton; + MyGUI::Button* mLightsResetButton; + // controls MyGUI::ScrollView* mControlsBox; MyGUI::Button* mResetControlsButton; @@ -50,6 +56,10 @@ namespace MWGui void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); + void onLightsResetButtonClicked(MyGUI::Widget* _sender); + void onMaxLightsChanged(MyGUI::ComboBox* _sender, size_t pos); + void onRebindAction(MyGUI::Widget* _sender); void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); void onResetDefaultBindings(MyGUI::Widget* _sender); @@ -61,7 +71,7 @@ namespace MWGui void apply(); - void configureWidgets(MyGUI::Widget* widget); + void configureWidgets(MyGUI::Widget* widget, bool init); void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); void layoutControlsBox(); diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 4c4450ee0..8e6f95129 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -93,7 +93,7 @@ namespace MWGui int windowHeight = window->getSize().height; //initial values defined in openmw_stats_window.layout, if custom options are not present in .layout, a default is loaded - float leftPaneRatio = 0.44; + float leftPaneRatio = 0.44f; if (mLeftPane->isUserString("LeftPaneRatio")) leftPaneRatio = MyGUI::utility::parseFloat(mLeftPane->getUserString("LeftPaneRatio")); diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index e0a8e4d3e..c0694e403 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -273,7 +273,7 @@ namespace MWGui std::string widgetName = userStringPair.first.substr(underscorePos+1, userStringPair.first.size()-(underscorePos+1)); type = "Property"; - size_t caretPos = key.find("^"); + size_t caretPos = key.find('^'); if (caretPos != std::string::npos) { type = key.substr(0, caretPos); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 1b2a03dcd..4d4fc3263 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1,5 +1,6 @@ #include "windowmanagerimp.hpp" +#include #include #include #include @@ -199,8 +200,8 @@ namespace MWGui , mVersionDescription(versionDescription) , mWindowVisible(true) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), uiScale); + mScalingFactor = std::clamp(Settings::Manager::getFloat("scaling factor", "GUI"), 0.5f, 8.f); + mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), mScalingFactor); mGuiPlatform->initialise(resourcePath, (boost::filesystem::path(logpath) / "MyGUI.log").generic_string()); mGui = new MyGUI::Gui; @@ -211,7 +212,7 @@ namespace MWGui MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath)); + mFontLoader.reset(new Gui::FontLoader(encoding, resourceSystem->getVFS(), userDataPath, mScalingFactor)); mFontLoader->loadBitmapFonts(exportFonts); //Register own widgets with MyGUI @@ -1105,8 +1106,9 @@ namespace MWGui if(tag.compare(0, MyGuiPrefixLength, MyGuiPrefix) == 0) { tag = tag.substr(MyGuiPrefixLength, tag.length()); - std::string settingSection = tag.substr(0, tag.find(",")); - std::string settingTag = tag.substr(tag.find(",")+1, tag.length()); + size_t comma_pos = tag.find(','); + std::string settingSection = tag.substr(0, comma_pos); + std::string settingTag = tag.substr(comma_pos+1, tag.length()); _result = Settings::Manager::getString(settingTag, settingSection); } @@ -1415,6 +1417,11 @@ namespace MWGui return mHud->getWorldMouseOver(); } + float WindowManager::getScalingFactor() + { + return mScalingFactor; + } + void WindowManager::executeInConsole (const std::string& path) { mConsole->executeFile (path); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 7f00494c0..b786a4123 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -274,6 +274,8 @@ namespace MWGui bool getWorldMouseOver() override; + float getScalingFactor() override; + bool toggleFogOfWar() override; bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; @@ -624,6 +626,8 @@ namespace MWGui SDLUtil::VideoWrapper* mVideoWrapper; + float mScalingFactor; + /** * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. * Supported syntax: diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index d17a4bd95..03d492c9c 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -32,7 +32,6 @@ namespace MWInput , mMouseManager(mouseManager) , mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input")) , mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input")) - , mInvUiScalingFactor(1.f) , mSneakToggleShortcutTimer(0.f) , mGamepadZoom(0) , mGamepadGuiCursorEnabled(true) @@ -69,10 +68,6 @@ namespace MWInput } } - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 0.f) - mInvUiScalingFactor = 1.f / uiScale; - float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); @@ -102,8 +97,10 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor - float xMove = xAxis * dt * 1500.0f * mInvUiScalingFactor * mGamepadCursorSpeed; - float yMove = yAxis * dt * 1500.0f * mInvUiScalingFactor * mGamepadCursorSpeed; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + float xMove = xAxis * dt * 1500.0f / uiScale; + float yMove = yAxis * dt * 1500.0f / uiScale; + float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) { diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index 871f11102..d8c62d57c 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -52,7 +52,6 @@ namespace MWInput bool mJoystickEnabled; float mGamepadCursorSpeed; - float mInvUiScalingFactor; float mSneakToggleShortcutTimer; float mGamepadZoom; bool mGamepadGuiCursorEnabled; diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 8df116baa..a696332df 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -29,22 +29,18 @@ namespace MWInput , mCameraYMultiplier(Settings::Manager::getFloat("camera y multiplier", "Input")) , mBindingsManager(bindingsManager) , mInputWrapper(inputWrapper) - , mInvUiScalingFactor(1.f) , mGuiCursorX(0) , mGuiCursorY(0) , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) { - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 0.f) - mInvUiScalingFactor = 1.f / uiScale; - int w,h; SDL_GetWindowSize(window, &w, &h); - mGuiCursorX = mInvUiScalingFactor * w / 2.f; - mGuiCursorY = mInvUiScalingFactor * h / 2.f; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = w / (2.f * uiScale); + mGuiCursorY = h / (2.f * uiScale); } void MouseManager::processChangedSettings(const Settings::CategorySettingVector& changed) @@ -79,8 +75,9 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor - mGuiCursorX = static_cast(arg.x) * mInvUiScalingFactor; - mGuiCursorY = static_cast(arg.y) * mInvUiScalingFactor; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = static_cast(arg.x) / uiScale; + mGuiCursorY = static_cast(arg.y) / uiScale; mMouseWheel = static_cast(arg.z); @@ -249,6 +246,7 @@ namespace MWInput void MouseManager::warpMouse() { - mInputWrapper->warpMouse(static_cast(mGuiCursorX / mInvUiScalingFactor), static_cast(mGuiCursorY / mInvUiScalingFactor)); + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mInputWrapper->warpMouse(static_cast(mGuiCursorX*uiScale), static_cast(mGuiCursorY*uiScale)); } } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 3bf692bcf..000e7cd0b 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -47,7 +47,6 @@ namespace MWInput BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; - float mInvUiScalingFactor; float mGuiCursorX; float mGuiCursorY; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index f82f5dd9e..75a14bfad 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -465,7 +465,8 @@ namespace MWMechanics } void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance) + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, + bool inCombatOrPursue) { if (!actor.getRefData().getBaseNode()) return; @@ -486,7 +487,7 @@ namespace MWMechanics const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); - if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance)) + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance) && !inCombatOrPursue) return; // stop tracking when target is behind the actor @@ -494,7 +495,7 @@ namespace MWMechanics osg::Vec3f targetDirection(actor2Pos - actor1Pos); actorDirection.z() = 0; targetDirection.z() = 0; - if (actorDirection * targetDirection > 0 + if ((actorDirection * targetDirection > 0 || inCombatOrPursue) && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) { @@ -2194,28 +2195,25 @@ namespace MWMechanics MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); bool firstPersonPlayer = isPlayer && world->isFirstPerson(); bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); + MWWorld::Ptr activePackageTarget; // 1. Unconsious actor can not track target - // 2. Actors in combat and pursue mode do not bother to headtrack + // 2. Actors in combat and pursue mode do not bother to headtrack anyone except their target // 3. Player character does not use headtracking in the 1st-person view - if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue) + if (!stats.getKnockedDown() && !firstPersonPlayer) { - for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + if (inCombatOrPursue) + activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); + + for (PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { if (it->first == iter->first) continue; - updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance); - } - } - if (!stats.getKnockedDown() && !isPlayer && inCombatOrPursue) - { - // Actors in combat and pursue mode always look at their target. - for (const auto& package : stats.getAiSequence()) - { - headTrackTarget = package->getTarget(); - if (!headTrackTarget.isEmpty()) - break; + if (inCombatOrPursue && it->first != activePackageTarget) + continue; + + updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 59890aa16..f404e8c5b 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -131,7 +131,8 @@ namespace MWMechanics void turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir); void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, - MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance); + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, + bool inCombatOrPursue); void rest(double hours, bool sleep); ///< Update actors while the player is waiting or sleeping. diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 2740355b5..94e4ecd95 100644 --- a/apps/openmw/mwmechanics/aibreathe.cpp +++ b/apps/openmw/mwmechanics/aibreathe.cpp @@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; - smoothTurn(actor, -osg::PI / 2, 0); + smoothTurn(actor, static_cast(-osg::PI_2), 0); return false; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index b4901bfe4..c57530665 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -331,8 +331,9 @@ namespace MWMechanics && ((!storage.mReadyToAttack && !mPathFinder.isPathConstructed()) || (storage.mUseCustomDestination && (storage.mCustomDestination - vTargetPos).length() > rangeAttack))) { + const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. - const auto halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); const auto pathGridGraph = getPathGridGraph(actor.getCell()); @@ -342,11 +343,7 @@ namespace MWMechanics { // If there is no path, try to find a point on a line from the actor position to target projected // on navmesh to attack the target from there. - const MWBase::World* world = MWBase::Environment::get().getWorld(); - const auto halfExtents = world->getPathfindingHalfExtents(actor); const auto navigator = world->getNavigator(); - const auto navigatorFlags = getNavigatorFlags(actor); - const auto areaCosts = getAreaCosts(actor); const auto hit = navigator->raycast(halfExtents, vActorPos, vTargetPos, navigatorFlags); if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) @@ -602,7 +599,7 @@ namespace MWMechanics // Otherwise apply a random side step (kind of dodging) with some probability // if actor is within range of target's weapon. if (std::abs(angleToTarget) > osg::PI / 4) - moveDuration = 0.2; + moveDuration = 0.2f; else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); if (moveDuration > 0) @@ -810,16 +807,18 @@ osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& t float t_collision; float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; - - osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; - vTargetMoveDirNormalized.normalize(); - - float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product - projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); - if (projVelDirSquared > 0) + { + osg::Vec3f vTargetMoveDirNormalized = vTargetMoveDir; + vTargetMoveDirNormalized.normalize(); + + float projDistDiff = vDirToTarget * vTargetMoveDirNormalized; // dot product + projDistDiff = std::sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); + t_collision = projDistDiff / (std::sqrt(projVelDirSquared) - velDir); - else t_collision = 0; // speed of projectile is not enough to reach moving target + } + else + t_collision = 0; // speed of projectile is not enough to reach moving target return vDirToTarget + vTargetMoveDir * t_collision; } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 8dcf37355..204cf2f3f 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -25,6 +25,14 @@ #include +namespace +{ + float divOrMax(float dividend, float divisor) + { + return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; + } +} + MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : mTypeId(typeId), mOptions(options), @@ -416,14 +424,15 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: const MWWorld::Class& actorClass = actor.getClass(); DetourNavigator::Flags result = DetourNavigator::Flag_none; - if (actorClass.isPureWaterCreature(actor) - || (getTypeId() != AiPackageTypeId::Wander - && ((allowToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) - || actorClass.canSwim(actor) - || hasWaterWalking(actor)))) + if ((actorClass.isPureWaterCreature(actor) + || (getTypeId() != AiPackageTypeId::Wander + && ((allowToFollowOverWaterSurface && getTypeId() == AiPackageTypeId::Follow) + || actorClass.canSwim(actor) + || hasWaterWalking(actor))) + ) && actorClass.getSwimSpeed(actor) > 0) result |= DetourNavigator::Flag_swim; - if (actorClass.canWalk(actor)) + if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) result |= DetourNavigator::Flag_walk; if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) @@ -439,15 +448,15 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P const MWWorld::Class& actorClass = actor.getClass(); if (flags & DetourNavigator::Flag_swim) - costs.mWater = costs.mWater / actorClass.getSwimSpeed(actor); + costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); if (flags & DetourNavigator::Flag_walk) { float walkCost; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = 1.0 / actorClass.getWalkSpeed(actor); + walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); else - walkCost = 1.0 / actorClass.getRunSpeed(actor); + walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); costs.mDoor = costs.mDoor * walkCost; costs.mPathgrid = costs.mPathgrid * walkCost; costs.mGround = costs.mGround * walkCost; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 97a3acbf0..791f4fefa 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1037,7 +1037,7 @@ void CharacterController::handleTextKey(const std::string &groupname, SceneUtil: // The event can optionally contain volume and pitch modifiers float volume=1.f, pitch=1.f; - if (soundgen.find(" ") != std::string::npos) + if (soundgen.find(' ') != std::string::npos) { std::vector tokens; split(soundgen, ' ', tokens); diff --git a/apps/openmw/mwmechanics/spelllist.cpp b/apps/openmw/mwmechanics/spelllist.cpp index 891b28619..d8fbcf25a 100644 --- a/apps/openmw/mwmechanics/spelllist.cpp +++ b/apps/openmw/mwmechanics/spelllist.cpp @@ -153,23 +153,23 @@ namespace MWMechanics void SpellList::addListener(Spells* spells) { - for(const auto ptr : mListeners) - { - if(ptr == spells) - return; - } + if (std::find(mListeners.begin(), mListeners.end(), spells) != mListeners.end()) + return; mListeners.push_back(spells); } void SpellList::removeListener(Spells* spells) { - for(auto it = mListeners.begin(); it != mListeners.end(); it++) - { - if(*it == spells) - { - mListeners.erase(it); - break; - } - } + const auto it = std::find(mListeners.begin(), mListeners.end(), spells); + if (it != mListeners.end()) + mListeners.erase(it); + } + + void SpellList::updateListener(Spells* before, Spells* after) + { + const auto it = std::find(mListeners.begin(), mListeners.end(), before); + if (it == mListeners.end()) + return mListeners.push_back(after); + *it = after; } } diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index b01722fe8..c95ee812b 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -61,6 +61,8 @@ namespace MWMechanics void removeListener(Spells* spells); + void updateListener(Spells* before, Spells* after); + const std::vector getSpells() const; }; } diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 1f74602a1..8665e5b3d 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -43,6 +43,15 @@ namespace MWMechanics mSpellList->addListener(this); } + Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), + mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), + mSourcedEffects(std::move(spells.mSourcedEffects)) + { + if (mSpellList) + mSpellList->updateListener(&spells, this); + } + std::map::const_iterator Spells::begin() const { return mSpells.begin(); diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 3df89a537..055339795 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -59,7 +59,7 @@ namespace MWMechanics Spells(const Spells&); - Spells(const Spells&&) = delete; + Spells(Spells&& spells); ~Spells(); diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e6ea639e7..6dde0c16f 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -156,6 +156,7 @@ void Actor::updatePosition() mPreviousPosition = mWorldPosition; mPosition = mWorldPosition; mSimulationPosition = mWorldPosition; + mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; mSkipSimulation = true; } @@ -231,7 +232,6 @@ void Actor::applyOffsetChange() { if (mPositionOffset.length() == 0) return; - mWorldPosition += mPositionOffset; mPosition += mPositionOffset; mPreviousPosition += mPositionOffset; mSimulationPosition += mPositionOffset; diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index d552ed49e..eaeb308d5 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -17,10 +17,10 @@ namespace MWPhysics // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static constexpr int sMaxIterations = 8; // Allows for more precise movement solving without getting stuck or snagging too easily. - static constexpr float sCollisionMargin = 0.1; + static constexpr float sCollisionMargin = 0.1f; // Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily // Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues - static constexpr float sAllowedPenetration = 0.0; + static constexpr float sAllowedPenetration = 0.0f; } #endif diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index d29d6a922..37ee0e056 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -636,9 +636,6 @@ namespace MWPhysics return nullptr; }(); - if (caster == nullptr) - Log(Debug::Warning) << "No caster for projectile " << projectileId; - ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile); resultCallback.m_collisionFilterMask = 0xff; resultCallback.m_collisionFilterGroup = CollisionType_Projectile; diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 2a28381be..4f5a27e53 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -60,14 +60,14 @@ namespace MWPhysics // attempt 3: further, less tall fixed distance movement, same as above // If you're making a full conversion you should purge the logic for attempts 2 and 3. Attempts 2 and 3 just try to work around problems with vanilla Morrowind assets. int attempt = 0; - float downStepSize; + float downStepSize = 0; while(attempt < 3) { attempt++; if(attempt == 1) tracerDest = tracerPos + toMove; - else if (!firstIteration || !sDoExtraStairHacks) // first attempt failed and not on first movement solver iteration, can't retry -- or we have extra hacks disabled + else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled { return false; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index fcffe220b..845e917fb 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -358,6 +358,8 @@ void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) } mScabbard = attachMesh(scabbardName, boneName); + if (mScabbard) + resetControllers(mScabbard->getNode()); osg::Group* weaponNode = getBoneByName("Bip01 Weapon"); if (!weaponNode) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index d8c759935..b578ee25b 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -499,6 +499,11 @@ namespace MWRender mAlpha = alpha; } + void setLightSource(const osg::ref_ptr& lightSource) + { + mLightSource = lightSource; + } + protected: void setDefaults(osg::StateSet* stateset) override { @@ -521,10 +526,13 @@ namespace MWRender { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); + if (mLightSource) + mLightSource->setActorFade(mAlpha); } private: float mAlpha; + osg::ref_ptr mLightSource; }; struct Animation::AnimSource @@ -1613,7 +1621,7 @@ namespace MWRender { bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); - SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1763,6 +1771,7 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); + mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 04c5825c9..213a4f704 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -278,6 +278,7 @@ protected: osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; + osg::ref_ptr mExtraLightSource; float mAlpha; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 12e6ef3ce..3e5d1d0b3 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -433,7 +433,7 @@ namespace MWRender void Camera::setPitch(float angle) { const float epsilon = 0.000001f; - float limit = osg::PI_2 - epsilon; + float limit = static_cast(osg::PI_2) - epsilon; mPitch = osg::clampBetween(angle, -limit, limit); } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 2c9b28e78..b75e45906 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -174,7 +174,9 @@ namespace MWRender mCamera->setNodeMask(Mask_RenderToTexture); - osg::ref_ptr lightManager = new SceneUtil::LightManager; + bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; + + osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); lightManager->setStartLight(1); osg::ref_ptr stateset = lightManager->getOrCreateStateSet(); stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON); @@ -231,12 +233,22 @@ namespace MWRender float positionZ = std::cos(altitude); light->setPosition(osg::Vec4(positionX,positionY,positionZ, 0.0)); light->setDiffuse(osg::Vec4(diffuseR,diffuseG,diffuseB,1)); - light->setAmbient(osg::Vec4(ambientR,ambientG,ambientB,1)); + osg::Vec4 ambientRGBA = osg::Vec4(ambientR,ambientG,ambientB,1); + if (mResourceSystem->getSceneManager()->getForceShaders()) + { + // When using shaders, we now skip the ambient sun calculation as this is the only place it's used. + // Using the scene ambient will give identical results. + lightmodel->setAmbientIntensity(ambientRGBA); + light->setAmbient(osg::Vec4(0,0,0,1)); + } + else + light->setAmbient(ambientRGBA); light->setSpecular(osg::Vec4(0,0,0,0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); + lightManager->setSunlight(light); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); @@ -414,7 +426,7 @@ namespace MWRender visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); osg::Node::NodeMask nodeMask = mCamera->getNodeMask(); - mCamera->setNodeMask(~0); + mCamera->setNodeMask(~0u); mCamera->accept(visitor); mCamera->setNodeMask(nodeMask); diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 0baa85c52..fd2246253 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" @@ -271,6 +272,8 @@ namespace MWRender group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); group->getBound(); group->setNodeMask(Mask_Groundcover); + if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) + group->setCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", false, true); return group; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 25d859e54..ec2bf54a7 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -19,9 +19,13 @@ #include #include #include +#include #include +#include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" @@ -89,9 +93,8 @@ LocalMap::LocalMap(osg::Group* root) , mInterior(false) { // Increase map resolution, if use UI scaling - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 0.f) - mMapResolution *= uiScale; + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mMapResolution *= uiScale; SceneUtil::FindByNameVisitor find("Scene Root"); mRoot->accept(find); @@ -220,6 +223,9 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + // override sun for local map + SceneUtil::configureStateSetSunOverride(static_cast(mSceneRoot.get()), light, stateset); + camera->addChild(lightSource); camera->setStateSet(stateset); camera->setViewport(0, 0, mMapResolution, mMapResolution); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 7386c0069..064d3aa35 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -402,7 +402,7 @@ namespace MWRender refs[ref.mRefNum] = ref; } } - catch (std::exception& e) + catch (std::exception&) { continue; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index b6fe05d0f..5d5a349d4 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -52,6 +52,7 @@ #include "../mwgui/loadingscreen.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -195,14 +196,20 @@ namespace MWRender , mWorkQueue(workQueue) , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) + , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) { + auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); + resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); // Shadows and radial fog have problems with fixed-function mode - bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows"); + bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") + || Settings::Manager::getBool("force shaders", "Shaders") + || Settings::Manager::getBool("enable shadows", "Shadows") + || lightingMethod != SceneUtil::LightingMethod::FFP; resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); @@ -214,7 +221,13 @@ namespace MWRender resourceSystem->getSceneManager()->setApplyLightingToEnvMaps(Settings::Manager::getBool("apply lighting to environment maps", "Shaders")); resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1); - osg::ref_ptr sceneRoot = new SceneUtil::LightManager; + // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. + osg::ref_ptr sceneRoot = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); + resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); + resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); + sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); @@ -236,6 +249,7 @@ namespace MWRender mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); + Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++) @@ -247,9 +261,15 @@ namespace MWRender globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0"; globalDefines["useGPUShader4"] = "0"; + for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++) + globalDefines[itr->first] = itr->second; + + // Refactor this at some point - most shaders don't care about these defines float groundcoverDistance = (Constants::CellSizeInUnits * std::max(1, Settings::Manager::getInt("distance", "Groundcover")) - 1024) * 0.93; globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f); globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance); + globalDefines["groundcoverStompMode"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp mode", "Groundcover"), 0, 2)); + globalDefines["groundcoverStompIntensity"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp intensity", "Groundcover"), 0, 2)); // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); @@ -352,6 +372,7 @@ namespace MWRender mSunLight->setAmbient(osg::Vec4f(0,0,0,1)); mSunLight->setSpecular(osg::Vec4f(0,0,0,0)); mSunLight->setConstantAttenuation(1.f); + sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); sceneRoot->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::ON); @@ -520,7 +541,32 @@ namespace MWRender void RenderingManager::configureAmbient(const ESM::Cell *cell) { - setAmbientColour(SceneUtil::colourFromRGB(cell->mAmbi.mAmbient)); + bool needsAdjusting = false; + if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) + needsAdjusting = !cell->isExterior() && !(cell->mData.mFlags & ESM::Cell::QuasiEx); + + auto ambient = SceneUtil::colourFromRGB(cell->mAmbi.mAmbient); + + if (needsAdjusting) + { + constexpr float pR = 0.2126; + constexpr float pG = 0.7152; + constexpr float pB = 0.0722; + + // we already work in linear RGB so no conversions are needed for the luminosity function + float relativeLuminance = pR*ambient.r() + pG*ambient.g() + pB*ambient.b(); + if (relativeLuminance < mMinimumAmbientLuminance) + { + // brighten ambient so it reaches the minimum threshold but no more, we want to mess with content data as least we can + float targetBrightnessIncreaseFactor = mMinimumAmbientLuminance / relativeLuminance; + if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) + ambient = osg::Vec4(mMinimumAmbientLuminance, mMinimumAmbientLuminance, mMinimumAmbientLuminance, ambient.a()); + else + ambient *= targetBrightnessIncreaseFactor; + } + } + + setAmbientColour(ambient); osg::Vec4f diffuse = SceneUtil::colourFromRGB(cell->mAmbi.mSunlight); mSunLight->setDiffuse(diffuse); @@ -614,7 +660,7 @@ namespace MWRender } else if (mode == Render_Scene) { - int mask = mViewer->getCamera()->getCullMask(); + unsigned int mask = mViewer->getCamera()->getCullMask(); bool enabled = mask&Mask_Scene; enabled = !enabled; if (enabled) @@ -769,7 +815,7 @@ namespace MWRender return false; } - int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); + unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); @@ -814,8 +860,7 @@ namespace MWRender { RenderingManager::RayResult result; result.mHit = false; - result.mHitRefnum.mContentFile = -1; - result.mHitRefnum.mIndex = -1; + result.mHitRefnum.unset(); result.mRatio = 0; if (intersector->containsIntersections()) { @@ -871,7 +916,7 @@ namespace MWRender mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp()); mIntersectionVisitor->setIntersector(intersector); - int mask = ~0; + unsigned int mask = ~0u; mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); if (ignorePlayer) mask &= ~(Mask_Player); @@ -1103,9 +1148,47 @@ namespace MWRender else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) + { updateTextureFiltering(); + } else if (it->first == "Water") + { mWater->processChangedSettings(changed); + } + else if (it->first == "Shaders" && it->second == "minimum interior brightness") + { + mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); + if (MWMechanics::getPlayer().isInCell()) + configureAmbient(MWMechanics::getPlayer().getCell()->getCell()); + } + else if (it->first == "Shaders" && (it->second == "light bounds multiplier" || + it->second == "maximum light distance" || + it->second == "light fade start" || + it->second == "max lights")) + { + auto* lightManager = static_cast(getLightRoot()); + lightManager->processChangedSettings(changed); + + if (it->second == "max lights" && !lightManager->usingFFP()) + { + mViewer->stopThreading(); + + lightManager->updateMaxLights(); + + auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); + for (const auto& [name, key] : lightManager->getLightDefines()) + defines[name] = key; + mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); + + mSceneRoot->removeUpdateCallback(mStateUpdater); + mStateUpdater = new StateUpdater; + mSceneRoot->addUpdateCallback(mStateUpdater); + mStateUpdater->setFogEnd(mViewDistance); + updateAmbient(); + + mViewer->startThreading(); + } + } } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a7afa2fa0..a0a74bd5c 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -296,6 +296,7 @@ namespace MWRender osg::ref_ptr mStateUpdater; osg::Vec4f mAmbientColor; + float mMinimumAmbientLuminance; float mNightEyeFactor; float mNearClip; diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 2f2882368..67b24d60a 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -317,8 +317,8 @@ public: if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) ++numPlanes; - int mask = 0x1; - int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) { if (i >= numPlanes) @@ -441,7 +441,7 @@ private: class CelestialBody { public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0) + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) : mVisibleMask(visibleMask) { mGeom = createTexturedQuad(numUvSets); @@ -1315,7 +1315,8 @@ public: while (callback) { - if ((composite = dynamic_cast(callback))) + composite = dynamic_cast(callback); + if (composite) break; callback = callback->getNestedCallback(); @@ -1624,7 +1625,7 @@ void SkyManager::setEnabled(bool enabled) if (enabled && !mCreated) create(); - mRootNode->setNodeMask(enabled ? Mask_Sky : 0); + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); mEnabled = enabled; } @@ -1785,7 +1786,7 @@ void SkyManager::setWeather(const WeatherResult& weather) mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0 : 0); + mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); } if (mCloudColour != weather.mFogColor) @@ -1830,7 +1831,7 @@ void SkyManager::setWeather(const WeatherResult& weather) mAtmosphereNightUpdater->setFade(mStarsOpacity); } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0 : 0); + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); mPrecipitationAlpha = weather.mPrecipitationAlpha; } diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index bc3d3f192..87ca9415f 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -19,7 +19,7 @@ namespace MWRender /// another mask, or what type of node this mask is usually set on. /// @note The mask values are not serialized within models, nor used in any other way that would break backwards /// compatibility if the enumeration values were to be changed. Feel free to change them when it makes sense. - enum VisMask + enum VisMask : unsigned int { Mask_UpdateVisitor = 0x1, // reserved for separating UpdateVisitors from CullVisitors diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index caa0af434..c5fd1a363 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -670,6 +671,9 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R osg::ref_ptr program (new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); + auto method = mResourceSystem->getSceneManager()->getLightingMethod(); + if (method == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); node->setStateSet(shaderStateset); @@ -772,11 +776,11 @@ void Water::update(float dt) void Water::updateVisible() { bool visible = mEnabled && mToggled; - mWaterNode->setNodeMask(visible ? ~0 : 0); + mWaterNode->setNodeMask(visible ? ~0u : 0u); if (mRefraction) - mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0); + mRefraction->setNodeMask(visible ? Mask_RenderToTexture : 0u); if (mReflection) - mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0); + mReflection->setNodeMask(visible ? Mask_RenderToTexture : 0u); } bool Water::toggle() diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 31af5b24b..6bc60968c 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -84,7 +84,7 @@ namespace MWWorld mTerrain->cacheCell(mTerrainView.get(), mX, mY); mPreloadedObjects.insert(mLandManager->getLand(mX, mY)); } - catch(std::exception& e) + catch(std::exception&) { } } @@ -127,7 +127,7 @@ namespace MWWorld mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); } - catch (std::exception& e) + catch (std::exception&) { // ignore error for now, would spam the log too much // error will be shown when visiting the cell diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index 21e489ac0..9fa65bec7 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -157,7 +157,7 @@ MWWorld::Cells::Cells (const MWWorld::ESMStore& store, std::vector ("", (CellStore*)nullptr)); } diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index a4d935bf4..d0d55aa4a 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -88,27 +88,13 @@ namespace MWWorld::ResolutionListener::~ResolutionListener() { - if(!mStore.mModified && mStore.mResolved && !mStore.mPtr.isEmpty()) + try { - try - { - for(const auto&& ptr : mStore) - ptr.getRefData().setCount(0); - } - catch(const std::exception& e) - { - Log(Debug::Warning) << "Failed to clear temporary container contents of " << mStore.mPtr.get()->mBase->mId << ": " << e.what(); - } - mStore.fillNonRandom(mStore.mPtr.get()->mBase->mInventory, "", mStore.mSeed); - try - { - addScripts(mStore, mStore.mPtr.mCell); - } - catch(const std::exception& e) - { - Log(Debug::Warning) << "Failed to restart item scripts inside " << mStore.mPtr.get()->mBase->mId << ": " << e.what(); - } - mStore.mResolved = false; + mStore.unresolve(); + } + catch(const std::exception& e) + { + Log(Debug::Error) << "Failed to clear temporary container contents: " << e.what(); } } @@ -767,6 +753,21 @@ MWWorld::ResolutionHandle MWWorld::ContainerStore::resolveTemporarily() return {listener}; } +void MWWorld::ContainerStore::unresolve() +{ + if (mModified) + return; + + if (mResolved && !mPtr.isEmpty()) + { + for(const auto&& ptr : *this) + ptr.getRefData().setCount(0); + fillNonRandom(mPtr.get()->mBase->mInventory, "", mSeed); + addScripts(*this, mPtr.mCell); + mResolved = false; + } +} + float MWWorld::ContainerStore::getWeight() const { if (!mWeightUpToDate) diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index d3f3333c2..10ba78852 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -261,6 +261,7 @@ namespace MWWorld void resolve(); ResolutionHandle resolveTemporarily(); + void unresolve(); friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 986c4f858..a69d74b05 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -63,9 +63,9 @@ namespace // We will replace invalid entries by fixed ones std::vector npcsToReplace; - for (auto it : npcs) + for (auto npcIter : npcs) { - ESM::NPC npc = it.second; + ESM::NPC npc = npcIter.second; bool changed = false; const std::string npcFaction = npc.mFaction; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 45e6135cb..27bdaf3b9 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -1037,7 +1037,7 @@ namespace MWWorld { mSceneManager->getTemplate(mMesh); } - catch (std::exception& e) + catch (std::exception&) { } } @@ -1121,7 +1121,7 @@ namespace MWWorld exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); } } - catch (std::exception& e) + catch (std::exception&) { // ignore error for now, would spam the log too much } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index b60e534e8..ec7ad0d3f 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1444,11 +1444,12 @@ namespace MWWorld MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive) { auto* actor = mPhysics->getActor(ptr); + osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) actor->adjustPosition(vec); - - osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; - return moveObject(ptr, newpos.x(), newpos.y(), newpos.z(), false, moveToActive && ptr != getPlayerPtr()); + if (ptr.getClass().isActor()) + return moveObject(ptr, newpos.x(), newpos.y(), newpos.z(), false, moveToActive && ptr != getPlayerPtr()); + return moveObject(ptr, newpos.x(), newpos.y(), newpos.z()); } void World::scaleObject (const Ptr& ptr, float scale) @@ -4387,7 +4388,7 @@ namespace MWWorld if (!model.empty()) scene->preload(model, ref.getPtr().getClass().useAnim()); } - catch(std::exception& e) + catch(std::exception&) { } } diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index d377aed1e..3b7dc4915 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -835,4 +835,55 @@ namespace ASSERT_THAT(result, Optional(Vec3fEq(mEnd.x(), mEnd.y(), 1.87719))); } + + TEST_F(DetourNavigatorNavigatorTest, update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); + heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + + const btBoxShape oscillatingBoxShape(btVector3(20, 20, 20)); + const btVector3 oscillatingBoxShapePosition(32, 32, 400); + const btBoxShape boderBoxShape(btVector3(50, 50, 50)); + + mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition)); + // add this box to make navmesh bound box independent from oscillatingBoxShape rotations + mNavigator->addObject(ObjectId(&boderBoxShape), boderBoxShape, + btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200))); + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + + const auto navMeshes = mNavigator->getNavMeshes(); + ASSERT_EQ(navMeshes.size(), 1); + { + const auto navMesh = navMeshes.begin()->second->lockConst(); + ASSERT_EQ(navMesh->getGeneration(), 1); + ASSERT_EQ(navMesh->getNavMeshRevision(), 4); + } + + for (int n = 0; n < 10; ++n) + { + const btTransform transform(btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10), + oscillatingBoxShapePosition); + mNavigator->updateObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, transform); + mNavigator->update(mPlayerPosition); + mNavigator->wait(); + } + + ASSERT_EQ(navMeshes.size(), 1); + { + const auto navMesh = navMeshes.begin()->second->lockConst(); + ASSERT_EQ(navMesh->getGeneration(), 1); + ASSERT_EQ(navMesh->getNavMeshRevision(), 4); + } + } } diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index 5bc7af646..5d4b1602f 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -77,7 +77,7 @@ namespace EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); } - TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_throw_exception) + TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_return_cached_element) { const std::size_t navMeshDataSize = 1; const std::size_t navMeshKeySize = cRecastMeshKeySize; @@ -87,10 +87,9 @@ namespace NavMeshData anotherNavMeshData {anotherData, 1}; cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_THROW( - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)), - InvalidArgument - ); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)); + ASSERT_TRUE(result); + EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 00adfe0c1..6fe5e6f74 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -89,7 +89,7 @@ add_component_dir (esmterrain ) add_component_dir (misc - constants utf8stream stringops resourcehelpers rng messageformatparser weakcache + constants utf8stream stringops resourcehelpers rng messageformatparser weakcache thread ) add_component_dir (debug @@ -265,6 +265,8 @@ if (BUILD_OPENMW OR BUILD_OPENCS) navigator findrandompointaroundcircle raycast + navmeshtileview + oscillatingrecastmeshobject ) endif() # End of tes3mp change (major) diff --git a/components/bsa/bsa_file.cpp b/components/bsa/bsa_file.cpp index ef49a60d2..ec455ea1f 100644 --- a/components/bsa/bsa_file.cpp +++ b/components/bsa/bsa_file.cpp @@ -168,6 +168,14 @@ void BSAFile::readHeader() fs.setNameInfos(namesOffset, &mStringBuf); fs.hash = hashes[i]; + if (namesOffset >= mStringBuf.size()) { + fail("Archive contains names offset outside itself"); + } + const void* end = std::memchr(fs.name(), '\0', mStringBuf.size()-namesOffset); + if (!end) { + fail("Archive contains non-zero terminated string"); + } + endOfNameBuffer = std::max(endOfNameBuffer, namesOffset + std::strlen(fs.name())+1); assert(endOfNameBuffer <= mStringBuf.size()); diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index 0f3a26e15..09a05d2f5 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -35,7 +35,16 @@ #include #include -#include + +#if defined(_MSC_VER) + #pragma warning (push) + #pragma warning (disable : 4706) + #include + #pragma warning (pop) +#else + #include +#endif + #include #include diff --git a/components/bullethelpers/aabb.hpp b/components/bullethelpers/aabb.hpp new file mode 100644 index 000000000..6509b946c --- /dev/null +++ b/components/bullethelpers/aabb.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_AABB_H +#define OPENMW_COMPONENTS_BULLETHELPERS_AABB_H + +#include +#include +#include +#include + +inline bool operator==(const btAABB& lhs, const btAABB& rhs) +{ + return lhs.m_min == rhs.m_min && lhs.m_max == rhs.m_max; +} + +inline bool operator!=(const btAABB& lhs, const btAABB& rhs) +{ + return !(lhs == rhs); +} + +namespace BulletHelpers +{ + inline btAABB getAabb(const btCollisionShape& shape, const btTransform& transform) + { + btAABB result; + shape.getAabb(transform, result.m_min, result.m_max); + return result; + } +} + +#endif diff --git a/components/crashcatcher/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp index b4b2a4a0c..86571e1e3 100644 --- a/components/crashcatcher/crashcatcher.cpp +++ b/components/crashcatcher/crashcatcher.cpp @@ -43,7 +43,7 @@ namespace bfs = boost::filesystem; #include #endif -static const char crash_switch[] = "--cc-handle-crash"; +#include "crashcatcher.hpp" static const char fatal_err[] = "\n\n*** Fatal Error ***\n"; static const char pipe_err[] = "!!! Failed to create pipe\n"; @@ -150,6 +150,9 @@ static void gdb_info(pid_t pid) * So CoverityScan warning is valid only for ancient versions of stdlib. */ strcpy(respfile, "/tmp/gdb-respfile-XXXXXX"); +#ifdef __COVERITY__ + umask(0600); +#endif if((fd=mkstemp(respfile)) >= 0 && (f=fdopen(fd, "w")) != nullptr) { fprintf(f, "attach %d\n" diff --git a/components/crashcatcher/crashcatcher.hpp b/components/crashcatcher/crashcatcher.hpp index e3846ae8f..d8ecfe8f5 100644 --- a/components/crashcatcher/crashcatcher.hpp +++ b/components/crashcatcher/crashcatcher.hpp @@ -9,6 +9,8 @@ #define USE_CRASH_CATCHER 0 #endif +constexpr char crash_switch[] = "--cc-handle-crash"; + #if USE_CRASH_CATCHER extern void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath); #else diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windows_crashcatcher.cpp index 4c3dfa8f6..185eee6aa 100644 --- a/components/crashcatcher/windows_crashcatcher.cpp +++ b/components/crashcatcher/windows_crashcatcher.cpp @@ -136,7 +136,7 @@ namespace Crash memset(mShm->mStartup.mLogFilePath, 0, sizeof(mShm->mStartup.mLogFilePath)); int length = crashLogPath.length(); - if (length > MAX_LONG_PATH) length = MAX_LONG_PATH; + if (length >= MAX_LONG_PATH) length = MAX_LONG_PATH - 1; strncpy(mShm->mStartup.mLogFilePath, crashLogPath.c_str(), length); mShm->mStartup.mLogFilePath[length] = '\0'; @@ -178,8 +178,6 @@ namespace Crash sInstance->handleVectoredException(info); _Exit(1); - - return EXCEPTION_CONTINUE_SEARCH; } void CrashCatcher::handleVectoredException(PEXCEPTION_POINTERS info) diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index e9bcf8ad7..0e372942f 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -179,7 +179,12 @@ int wrapApplication(int (*innerApplication)(int argc, char *argv[]), int argc, c std::cerr.rdbuf (&sb); #else // Redirect cout and cerr to the log file - logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName)); + // If we are collecting a stack trace, append to existing log file + std::ios_base::openmode mode = std::ios::out; + if(argc == 2 && strcmp(argv[1], crash_switch) == 0) + mode |= std::ios::app; + + logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / logName), mode); coutsb.open (Debug::Tee(logfile, oldcout)); cerrsb.open (Debug::Tee(logfile, oldcerr)); diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 1ac928f07..7248850a6 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -2,8 +2,10 @@ #include "debug.hpp" #include "makenavmesh.hpp" #include "settings.hpp" +#include "version.hpp" #include +#include #include @@ -135,6 +137,7 @@ namespace DetourNavigator void AsyncNavMeshUpdater::process() noexcept { Log(Debug::Debug) << "Start process navigator jobs by thread=" << std::this_thread::get_id(); + Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { try @@ -178,6 +181,19 @@ namespace DetourNavigator const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile, offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache); + if (recastMesh != nullptr) + { + Version navMeshVersion; + { + const auto locked = navMeshCacheItem->lockConst(); + navMeshVersion.mGeneration = locked->getGeneration(); + navMeshVersion.mRevision = locked->getNavMeshRevision(); + } + mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, + Version {recastMesh->getGeneration(), recastMesh->getRevision()}, + navMeshVersion); + } + const auto finish = std::chrono::steady_clock::now(); writeDebugFiles(job, recastMesh.get()); @@ -246,7 +262,7 @@ namespace DetourNavigator if (jobs.top().mProcessTime > now) return {}; - Job job = std::move(jobs.top()); + Job job = jobs.top(); jobs.pop(); if (changeLastUpdate && job.mChangeType == ChangeType::update) @@ -257,7 +273,7 @@ namespace DetourNavigator if (it->second.empty()) pushed.erase(it); - return {std::move(job)}; + return job; } void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index 90b426610..22b047fbd 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -61,4 +61,9 @@ namespace DetourNavigator { return mImpl.isEmpty(); } + + void CachedRecastMeshManager::reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion) + { + mImpl.reportNavMeshChange(recastMeshVersion, navMeshVersion); + } } diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index 54574aa99..a19f017a4 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_CACHEDRECASTMESHMANAGER_H #include "recastmeshmanager.hpp" +#include "version.hpp" namespace DetourNavigator { @@ -25,6 +26,8 @@ namespace DetourNavigator bool isEmpty() const; + void reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion); + private: RecastMeshManager mImpl; std::shared_ptr mCached; diff --git a/components/detournavigator/chunkytrimesh.hpp b/components/detournavigator/chunkytrimesh.hpp index 9f6275ec8..7f98f5d22 100644 --- a/components/detournavigator/chunkytrimesh.hpp +++ b/components/detournavigator/chunkytrimesh.hpp @@ -46,6 +46,9 @@ namespace DetourNavigator ChunkyTriMesh(const std::vector& verts, const std::vector& tris, const std::vector& flags, const std::size_t trisPerChunk); + ChunkyTriMesh(ChunkyTriMesh&&) = default; + ChunkyTriMesh& operator=(ChunkyTriMesh&&) = default; + ChunkyTriMesh(const ChunkyTriMesh&) = delete; ChunkyTriMesh& operator=(const ChunkyTriMesh&) = delete; diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 7c7dcf186..9701b4950 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -576,17 +576,8 @@ namespace DetourNavigator return navMeshCacheItem->lock()->removeTile(changedTile); } - try - { - cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, - offMeshConnections, std::move(navMeshData)); - } - catch (const InvalidArgument&) - { - cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh, - offMeshConnections); - cached = static_cast(cachedNavMeshData); - } + cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, + offMeshConnections, std::move(navMeshData)); if (!cachedNavMeshData) { diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index 76f74f266..a9a4ebee6 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -5,6 +5,7 @@ #include "tileposition.hpp" #include "navmeshtilescache.hpp" #include "dtstatus.hpp" +#include "navmeshtileview.hpp" #include @@ -141,6 +142,12 @@ namespace DetourNavigator template UpdateNavMeshStatus updateTile(const TilePosition& position, T&& navMeshData) { + const dtMeshTile* currentTile = getTile(position); + if (currentTile != nullptr + && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(getRawData(navMeshData))) + { + return UpdateNavMeshStatus::ignored; + } const auto removed = removeTileImpl(position); const auto addStatus = addTileImpl(getRawData(navMeshData), getSize(navMeshData)); if (dtStatusSucceed(addStatus)) @@ -206,6 +213,12 @@ namespace DetourNavigator int* const dataSize = nullptr; return dtStatusSucceed(mImpl->removeTile(tileRef, data, dataSize)); } + + const dtMeshTile* getTile(const TilePosition& position) const + { + const int layer = 0; + return mImpl->getTileAt(position.x(), position.y(), layer); + } }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp index b6048da58..ccc41a666 100644 --- a/components/detournavigator/navmeshtilescache.cpp +++ b/components/detournavigator/navmeshtilescache.cpp @@ -1,5 +1,4 @@ #include "navmeshtilescache.hpp" -#include "exceptions.hpp" #include @@ -32,16 +31,8 @@ namespace DetourNavigator ++mGetCount; - const auto agentValues = mValues.find(agentHalfExtents); - if (agentValues == mValues.end()) - return Value(); - - const auto tileValues = agentValues->second.find(changedTile); - if (tileValues == agentValues->second.end()) - return Value(); - - const auto tile = tileValues->second.mMap.find(NavMeshKeyView(recastMesh, offMeshConnections)); - if (tile == tileValues->second.mMap.end()) + const auto tile = mValues.find(std::make_tuple(agentHalfExtents, changedTile, NavMeshKeyView(recastMesh, offMeshConnections))); + if (tile == mValues.end()) return Value(); acquireItemUnsafe(tile->second); @@ -71,76 +62,61 @@ namespace DetourNavigator }; const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, std::move(navMeshKey), itemSize); - const auto emplaced = mValues[agentHalfExtents][changedTile].mMap.emplace(iterator->mNavMeshKey, iterator); + const auto emplaced = mValues.emplace(std::make_tuple(agentHalfExtents, changedTile, NavMeshKeyRef(iterator->mNavMeshKey)), iterator); if (!emplaced.second) { mFreeItems.erase(iterator); - throw InvalidArgument("Set existing cache value"); + acquireItemUnsafe(emplaced.first->second); + ++mGetCount; + ++mHitCount; + return Value(*this, emplaced.first->second); } iterator->mNavMeshData = std::move(value); + ++iterator->mUseCount; mUsedNavMeshDataSize += itemSize; - mFreeNavMeshDataSize += itemSize; - - acquireItemUnsafe(iterator); + mBusyItems.splice(mBusyItems.end(), mFreeItems, iterator); return Value(*this, iterator); } - void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& stats) const + NavMeshTilesCache::Stats NavMeshTilesCache::getStats() const { - std::size_t navMeshCacheSize = 0; - std::size_t usedNavMeshTiles = 0; - std::size_t cachedNavMeshTiles = 0; - std::size_t hitCount = 0; - std::size_t getCount = 0; - + Stats result; { const std::lock_guard lock(mMutex); - navMeshCacheSize = mUsedNavMeshDataSize; - usedNavMeshTiles = mBusyItems.size(); - cachedNavMeshTiles = mFreeItems.size(); - hitCount = mHitCount; - getCount = mGetCount; + result.mNavMeshCacheSize = mUsedNavMeshDataSize; + result.mUsedNavMeshTiles = mBusyItems.size(); + result.mCachedNavMeshTiles = mFreeItems.size(); + result.mHitCount = mHitCount; + result.mGetCount = mGetCount; } + return result; + } - stats.setAttribute(frameNumber, "NavMesh CacheSize", navMeshCacheSize); - stats.setAttribute(frameNumber, "NavMesh UsedTiles", usedNavMeshTiles); - stats.setAttribute(frameNumber, "NavMesh CachedTiles", cachedNavMeshTiles); - stats.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(hitCount) / getCount * 100.0); + void NavMeshTilesCache::reportStats(unsigned int frameNumber, osg::Stats& out) const + { + const Stats stats = getStats(); + out.setAttribute(frameNumber, "NavMesh CacheSize", stats.mNavMeshCacheSize); + out.setAttribute(frameNumber, "NavMesh UsedTiles", stats.mUsedNavMeshTiles); + out.setAttribute(frameNumber, "NavMesh CachedTiles", stats.mCachedNavMeshTiles); + out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); } void NavMeshTilesCache::removeLeastRecentlyUsed() { const auto& item = mFreeItems.back(); - const auto agentValues = mValues.find(item.mAgentHalfExtents); - if (agentValues == mValues.end()) - return; - - const auto tileValues = agentValues->second.find(item.mChangedTile); - if (tileValues == agentValues->second.end()) - return; - - const auto value = tileValues->second.mMap.find(item.mNavMeshKey); - if (value == tileValues->second.mMap.end()) + const auto value = mValues.find(std::make_tuple(item.mAgentHalfExtents, item.mChangedTile, NavMeshKeyRef(item.mNavMeshKey))); + if (value == mValues.end()) return; mUsedNavMeshDataSize -= item.mSize; mFreeNavMeshDataSize -= item.mSize; - tileValues->second.mMap.erase(value); + mValues.erase(value); mFreeItems.pop_back(); - - if (!tileValues->second.mMap.empty()) - return; - - agentValues->second.erase(tileValues); - if (!agentValues->second.empty()) - return; - - mValues.erase(agentValues); } void NavMeshTilesCache::acquireItemUnsafe(ItemIterator iterator) diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index 25f4dc187..afa53beba 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -110,6 +110,14 @@ namespace DetourNavigator return lhs < rhs.mRef.get(); } + template + inline bool operator <(const std::tuple& lhs, const std::tuple& rhs) + { + const auto left = std::tie(std::get<0>(lhs), std::get<1>(lhs)); + const auto right = std::tie(std::get<0>(rhs), std::get<1>(rhs)); + return std::tie(left, std::get<2>(lhs)) < std::tie(right, std::get<2>(rhs)); + } + class NavMeshTilesCache { public: @@ -188,6 +196,15 @@ namespace DetourNavigator ItemIterator mIterator; }; + struct Stats + { + std::size_t mNavMeshCacheSize; + std::size_t mUsedNavMeshTiles; + std::size_t mCachedNavMeshTiles; + std::size_t mHitCount; + std::size_t mGetCount; + }; + NavMeshTilesCache(const std::size_t maxNavMeshDataSize); Value get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, @@ -197,14 +214,11 @@ namespace DetourNavigator const RecastMesh& recastMesh, const std::vector& offMeshConnections, NavMeshData&& value); + Stats getStats() const; + void reportStats(unsigned int frameNumber, osg::Stats& stats) const; private: - struct TileMap - { - std::map> mMap; - }; - mutable std::mutex mMutex; std::size_t mMaxNavMeshDataSize; std::size_t mUsedNavMeshDataSize; @@ -213,7 +227,7 @@ namespace DetourNavigator std::size_t mGetCount; std::list mBusyItems; std::list mFreeItems; - std::map> mValues; + std::map, ItemIterator, std::less<>> mValues; void removeLeastRecentlyUsed(); diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp new file mode 100644 index 000000000..96f07c6b1 --- /dev/null +++ b/components/detournavigator/navmeshtileview.cpp @@ -0,0 +1,183 @@ +#include "navmeshtileview.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace +{ + template + struct Ref + { + T& mRef; + + explicit Ref(T& ref) : mRef(ref) {} + + friend bool operator==(const Ref& lhs, const Ref& rhs) + { + return lhs.mRef == rhs.mRef; + } + }; + + template + struct ArrayRef + { + T (&mRef)[size]; + + explicit ArrayRef(T (&ref)[size]) : mRef(ref) {} + + friend bool operator==(const ArrayRef& lhs, const ArrayRef& rhs) + { + return std::equal(std::begin(lhs.mRef), std::end(lhs.mRef), std::begin(rhs.mRef)); + } + }; + + template + struct Span + { + T* mBegin; + T* mEnd; + + explicit Span(T* data, int size) : mBegin(data), mEnd(data + static_cast(size)) {} + + friend bool operator==(const Span& lhs, const Span& rhs) + { + // size is already equal if headers are equal + assert((lhs.mEnd - lhs.mBegin) == (rhs.mEnd - rhs.mBegin)); + return std::equal(lhs.mBegin, lhs.mEnd, rhs.mBegin); + } + }; + + auto makeTuple(const dtMeshHeader& v) + { + return std::tuple( + v.x, + v.y, + v.layer, + v.userId, + v.polyCount, + v.vertCount, + v.maxLinkCount, + v.detailMeshCount, + v.detailVertCount, + v.detailTriCount, + v.bvNodeCount, + v.offMeshConCount, + v.offMeshBase, + v.walkableHeight, + v.walkableRadius, + v.walkableClimb, + v.detailVertCount, + ArrayRef(v.bmin), + ArrayRef(v.bmax), + v.bvQuantFactor + ); + } + + auto makeTuple(const dtPoly& v) + { + return std::tuple(ArrayRef(v.verts), ArrayRef(v.neis), v.flags, v.vertCount, v.areaAndtype); + } + + auto makeTuple(const dtPolyDetail& v) + { + return std::tuple(v.vertBase, v.triBase, v.vertCount, v.triCount); + } + + auto makeTuple(const dtBVNode& v) + { + return std::tuple(ArrayRef(v.bmin), ArrayRef(v.bmax), v.i); + } + + auto makeTuple(const dtOffMeshConnection& v) + { + return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId); + } + + auto makeTuple(const DetourNavigator::NavMeshTileConstView& v) + { + return std::tuple( + Ref(*v.mHeader), + Span(v.mPolys, v.mHeader->polyCount), + Span(v.mVerts, v.mHeader->vertCount), + Span(v.mDetailMeshes, v.mHeader->detailMeshCount), + Span(v.mDetailVerts, v.mHeader->detailVertCount), + Span(v.mDetailTris, v.mHeader->detailTriCount), + Span(v.mBvTree, v.mHeader->bvNodeCount), + Span(v.mOffMeshCons, v.mHeader->offMeshConCount) + ); + } +} + +template +inline auto operator==(const T& lhs, const T& rhs) + -> std::enable_if_t, void>, bool> +{ + return makeTuple(lhs) == makeTuple(rhs); +} + +namespace DetourNavigator +{ + NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data) + { + const dtMeshHeader* header = reinterpret_cast(data); + + if (header->magic != DT_NAVMESH_MAGIC) + throw std::logic_error("Invalid navmesh magic"); + + if (header->version != DT_NAVMESH_VERSION) + throw std::logic_error("Invalid navmesh version"); + + // Similar code to https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/Detour/Source/DetourNavMesh.cpp#L978-L996 + const int headerSize = dtAlign4(sizeof(dtMeshHeader)); + const int vertsSize = dtAlign4(sizeof(float) * 3 * header->vertCount); + const int polysSize = dtAlign4(sizeof(dtPoly) * header->polyCount); + const int linksSize = dtAlign4(sizeof(dtLink) * (header->maxLinkCount)); + const int detailMeshesSize = dtAlign4(sizeof(dtPolyDetail) * header->detailMeshCount); + const int detailVertsSize = dtAlign4(sizeof(float) * 3 * header->detailVertCount); + const int detailTrisSize = dtAlign4(sizeof(unsigned char) * 4 * header->detailTriCount); + const int bvtreeSize = dtAlign4(sizeof(dtBVNode) * header->bvNodeCount); + const int offMeshLinksSize = dtAlign4(sizeof(dtOffMeshConnection) * header->offMeshConCount); + + const unsigned char* ptr = data + headerSize; + + NavMeshTileConstView view; + + view.mHeader = header; + view.mVerts = dtGetThenAdvanceBufferPointer(ptr, vertsSize); + view.mPolys = dtGetThenAdvanceBufferPointer(ptr, polysSize); + ptr += linksSize; + view.mDetailMeshes = dtGetThenAdvanceBufferPointer(ptr, detailMeshesSize); + view.mDetailVerts = dtGetThenAdvanceBufferPointer(ptr, detailVertsSize); + view.mDetailTris = dtGetThenAdvanceBufferPointer(ptr, detailTrisSize); + view.mBvTree = dtGetThenAdvanceBufferPointer(ptr, bvtreeSize); + view.mOffMeshCons = dtGetThenAdvanceBufferPointer(ptr, offMeshLinksSize); + + return view; + } + + NavMeshTileConstView asNavMeshTileConstView(const dtMeshTile& tile) + { + NavMeshTileConstView view; + + view.mHeader = tile.header; + view.mPolys = tile.polys; + view.mVerts = tile.verts; + view.mDetailMeshes = tile.detailMeshes; + view.mDetailVerts = tile.detailVerts; + view.mDetailTris = tile.detailTris; + view.mBvTree = tile.bvTree; + view.mOffMeshCons = tile.offMeshCons; + + return view; + } + + bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) + { + return makeTuple(lhs) == makeTuple(rhs); + } +} diff --git a/components/detournavigator/navmeshtileview.hpp b/components/detournavigator/navmeshtileview.hpp new file mode 100644 index 000000000..92017360c --- /dev/null +++ b/components/detournavigator/navmeshtileview.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILEVIEW_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILEVIEW_H + +struct dtMeshHeader; +struct dtPoly; +struct dtPolyDetail; +struct dtBVNode; +struct dtOffMeshConnection; +struct dtMeshTile; + +namespace DetourNavigator +{ + struct NavMeshTileConstView + { + const dtMeshHeader* mHeader; + const dtPoly* mPolys; + const float* mVerts; + const dtPolyDetail* mDetailMeshes; + const float* mDetailVerts; + const unsigned char* mDetailTris; + const dtBVNode* mBvTree; + const dtOffMeshConnection* mOffMeshCons; + + friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs); + }; + + NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data); + NavMeshTileConstView asNavMeshTileConstView(const dtMeshTile& tile); +} + +#endif diff --git a/components/detournavigator/oscillatingrecastmeshobject.cpp b/components/detournavigator/oscillatingrecastmeshobject.cpp new file mode 100644 index 000000000..5b8423183 --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.cpp @@ -0,0 +1,42 @@ +#include "oscillatingrecastmeshobject.hpp" + +#include + +namespace DetourNavigator +{ + OscillatingRecastMeshObject::OscillatingRecastMeshObject(RecastMeshObject&& impl, std::size_t lastChangeRevision) + : mImpl(std::move(impl)) + , mLastChangeRevision(lastChangeRevision) + , mAabb(BulletHelpers::getAabb(mImpl.getShape(), mImpl.getTransform())) + { + } + + OscillatingRecastMeshObject::OscillatingRecastMeshObject(const RecastMeshObject& impl, std::size_t lastChangeRevision) + : mImpl(impl) + , mLastChangeRevision(lastChangeRevision) + , mAabb(BulletHelpers::getAabb(mImpl.getShape(), mImpl.getTransform())) + { + } + + bool OscillatingRecastMeshObject::update(const btTransform& transform, const AreaType areaType, + std::size_t lastChangeRevision) + { + const btTransform oldTransform = mImpl.getTransform(); + if (!mImpl.update(transform, areaType)) + return false; + if (transform == oldTransform) + return true; + if (mLastChangeRevision != lastChangeRevision) + { + mLastChangeRevision = lastChangeRevision; + // btAABB doesn't have copy-assignment operator + const btAABB aabb = BulletHelpers::getAabb(mImpl.getShape(), transform); + mAabb.m_min = aabb.m_min; + mAabb.m_max = aabb.m_max; + return true; + } + const btAABB currentAabb = mAabb; + mAabb.merge(BulletHelpers::getAabb(mImpl.getShape(), transform)); + return currentAabb != mAabb; + } +} diff --git a/components/detournavigator/oscillatingrecastmeshobject.hpp b/components/detournavigator/oscillatingrecastmeshobject.hpp new file mode 100644 index 000000000..78a0c4b68 --- /dev/null +++ b/components/detournavigator/oscillatingrecastmeshobject.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OSCILLATINGRECASTMESHOBJECT_H + +#include "areatype.hpp" +#include "recastmeshobject.hpp" + +#include +#include + +namespace DetourNavigator +{ + class OscillatingRecastMeshObject + { + public: + explicit OscillatingRecastMeshObject(RecastMeshObject&& impl, std::size_t lastChangeRevision); + explicit OscillatingRecastMeshObject(const RecastMeshObject& impl, std::size_t lastChangeRevision); + + bool update(const btTransform& transform, const AreaType areaType, std::size_t lastChangeRevision); + + const RecastMeshObject& getImpl() const { return mImpl; } + + private: + RecastMeshObject mImpl; + std::size_t mLastChangeRevision; + btAABB mAabb; + }; +} + +#endif diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp index 3796c9816..146038ae6 100644 --- a/components/detournavigator/recastmeshmanager.cpp +++ b/components/detournavigator/recastmeshmanager.cpp @@ -13,7 +13,8 @@ namespace DetourNavigator bool RecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) { - const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), RecastMeshObject(shape, transform, areaType)); + const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), + OscillatingRecastMeshObject(RecastMeshObject(shape, transform, areaType), mRevision + 1)); if (!mObjects.emplace(id, iterator).second) { mObjectsOrder.erase(iterator); @@ -28,7 +29,9 @@ namespace DetourNavigator const auto object = mObjects.find(id); if (object == mObjects.end()) return false; - if (!object->second->update(transform, areaType)) + const std::size_t lastChangeRevision = mLastNavMeshReportedChange.has_value() + ? mLastNavMeshReportedChange->mRevision : mRevision; + if (!object->second->update(transform, areaType, lastChangeRevision)) return false; ++mRevision; return true; @@ -39,7 +42,7 @@ namespace DetourNavigator const auto object = mObjects.find(id); if (object == mObjects.end()) return std::nullopt; - const RemovedRecastMeshObject result {object->second->getShape(), object->second->getTransform()}; + const RemovedRecastMeshObject result {object->second->getImpl().getShape(), object->second->getImpl().getTransform()}; mObjectsOrder.erase(object->second); mObjects.erase(object); ++mRevision; @@ -74,7 +77,7 @@ namespace DetourNavigator std::shared_ptr RecastMeshManager::getMesh() { rebuild(); - return mMeshBuilder.create(mGeneration, mLastBuildRevision); + return mMeshBuilder.create(mGeneration, mRevision); } bool RecastMeshManager::isEmpty() const @@ -82,15 +85,27 @@ namespace DetourNavigator return mObjects.empty(); } + void RecastMeshManager::reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion) + { + if (recastMeshVersion.mGeneration != mGeneration) + return; + if (mLastNavMeshReport.has_value() && navMeshVersion < mLastNavMeshReport->mNavMeshVersion) + return; + mLastNavMeshReport = {recastMeshVersion.mRevision, navMeshVersion}; + if (!mLastNavMeshReportedChange.has_value() + || mLastNavMeshReportedChange->mNavMeshVersion < mLastNavMeshReport->mNavMeshVersion) + mLastNavMeshReportedChange = mLastNavMeshReport; + } + void RecastMeshManager::rebuild() { - if (mLastBuildRevision == mRevision) - return; mMeshBuilder.reset(); for (const auto& v : mWaterOrder) mMeshBuilder.addWater(v.mCellSize, v.mTransform); - for (const auto& v : mObjectsOrder) + for (const auto& object : mObjectsOrder) + { + const RecastMeshObject& v = object.getImpl(); mMeshBuilder.addObject(v.getShape(), v.getTransform(), v.getAreaType()); - mLastBuildRevision = mRevision; + } } } diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp index 5b568e004..daa123fcb 100644 --- a/components/detournavigator/recastmeshmanager.hpp +++ b/components/detournavigator/recastmeshmanager.hpp @@ -2,8 +2,9 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H #include "recastmeshbuilder.hpp" -#include "recastmeshobject.hpp" +#include "oscillatingrecastmeshobject.hpp" #include "objectid.hpp" +#include "version.hpp" #include @@ -50,15 +51,24 @@ namespace DetourNavigator bool isEmpty() const; + void reportNavMeshChange(Version recastMeshVersion, Version navMeshVersion); + private: + struct Report + { + std::size_t mRevision; + Version mNavMeshVersion; + }; + std::size_t mRevision = 0; - std::size_t mLastBuildRevision = 0; std::size_t mGeneration; RecastMeshBuilder mMeshBuilder; - std::list mObjectsOrder; - std::unordered_map::iterator> mObjects; + std::list mObjectsOrder; + std::unordered_map::iterator> mObjects; std::list mWaterOrder; std::map::iterator> mWater; + std::optional mLastNavMeshReportedChange; + std::optional mLastNavMeshReport; void rebuild(); }; diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index aac0b4c3c..3f3546278 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -8,6 +8,22 @@ namespace DetourNavigator { + namespace + { + bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, + std::vector& children) + { + assert(static_cast(shape.getNumChildShapes()) == children.size()); + bool result = false; + for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) + { + assert(shape.getChildShape(i) == std::addressof(children[static_cast(i)].getShape())); + result = children[static_cast(i)].update(shape.getChildTransform(i), areaType) || result; + } + return result; + } + } + RecastMeshObject::RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) : mShape(shape) @@ -42,19 +58,6 @@ namespace DetourNavigator return result; } - bool RecastMeshObject::updateCompoundObject(const btCompoundShape& shape, - const AreaType areaType, std::vector& children) - { - assert(static_cast(shape.getNumChildShapes()) == children.size()); - bool result = false; - for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - { - assert(shape.getChildShape(i) == std::addressof(children[static_cast(i)].mShape.get())); - result = children[static_cast(i)].update(shape.getChildTransform(i), areaType) || result; - } - return result; - } - std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) { if (shape.isCompound()) diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index f25647ae5..e659300e6 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -41,9 +41,6 @@ namespace DetourNavigator AreaType mAreaType; btVector3 mLocalScaling; std::vector mChildren; - - static bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children); }; std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType); diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index fddaa88f1..20ebc8fea 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -145,6 +145,15 @@ namespace DetourNavigator return mRevision; } + void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) + { + const auto tiles = mTiles.lock(); + const auto it = tiles->find(tilePosition); + if (it == tiles->end()) + return; + it->second.reportNavMeshChange(recastMeshVersion, navMeshVersion); + } + bool TileCachedRecastMeshManager::addTile(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border, std::map& tiles) diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index fa192bd73..68683f410 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -5,6 +5,7 @@ #include "tileposition.hpp" #include "settingsutils.hpp" #include "gettilespositions.hpp" +#include "version.hpp" #include @@ -88,6 +89,8 @@ namespace DetourNavigator std::size_t getRevision() const; + void reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion); + private: const Settings& mSettings; Misc::ScopeGuarded> mTiles; diff --git a/components/detournavigator/version.hpp b/components/detournavigator/version.hpp new file mode 100644 index 000000000..c9de98459 --- /dev/null +++ b/components/detournavigator/version.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_VERSION_H + +#include +#include + +namespace DetourNavigator +{ + struct Version + { + std::size_t mGeneration; + std::size_t mRevision; + + friend inline bool operator<(const Version& lhs, const Version& rhs) + { + return std::tie(lhs.mGeneration, lhs.mRevision) < std::tie(rhs.mGeneration, rhs.mRevision); + } + }; +} + +#endif diff --git a/components/esm/loadpgrd.cpp b/components/esm/loadpgrd.cpp index 708685e72..9abeba260 100644 --- a/components/esm/loadpgrd.cpp +++ b/components/esm/loadpgrd.cpp @@ -100,6 +100,8 @@ namespace ESM for(PointList::const_iterator it = mPoints.begin(); it != mPoints.end(); ++it, ++pointIndex) { unsigned char connectionNum = (*it).mConnectionNum; + if (rawConnections.end() - rawIt < connectionNum) + esm.fail("Not enough connections"); for (int i = 0; i < connectionNum; ++i) { Edge edge; edge.mV0 = pointIndex; diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index 98edca48f..a56b9f247 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -115,7 +115,7 @@ namespace ESM void Region::blank() { mData.mClear = mData.mCloudy = mData.mFoggy = mData.mOvercast = mData.mRain = - mData.mThunder = mData.mAsh, mData.mBlight = mData.mA = mData.mB = 0; + mData.mThunder = mData.mAsh = mData.mBlight = mData.mA = mData.mB = 0; mMapColor = 0; diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index a7f348cb1..1ad90dbc3 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -30,7 +30,7 @@ namespace ESM // The tmp buffer is a null-byte separated string list, we // just have to pick out one string at a time. char* str = tmp.data(); - if (!str) + if (tmp.empty()) { if (mVarNames.size() > 0) Log(Debug::Warning) << "SCVR with no variable names"; @@ -41,7 +41,7 @@ namespace ESM // Support '\r' terminated strings like vanilla. See Bug #1324. std::replace(tmp.begin(), tmp.end(), '\r', '\0'); // Avoid heap corruption - if (!tmp.empty() && tmp[tmp.size()-1] != '\0') + if (tmp.back() != '\0') { tmp.emplace_back('\0'); std::stringstream ss; @@ -51,23 +51,28 @@ namespace ESM ss << "\n Subrecord: " << "SCVR"; ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); Log(Debug::Verbose) << ss.str(); + str = tmp.data(); } + const auto tmpEnd = tmp.data() + tmp.size(); for (size_t i = 0; i < mVarNames.size(); i++) { mVarNames[i] = std::string(str); str += mVarNames[i].size() + 1; - if (static_cast(str - tmp.data()) > tmp.size()) + if (str >= tmpEnd) { - // SCVR subrecord is unused and variable names are determined - // from the script source, so an overflow is not fatal. - std::stringstream ss; - ss << "String table overflow"; - ss << "\n File: " << esm.getName(); - ss << "\n Record: " << esm.getContext().recName.toString(); - ss << "\n Subrecord: " << "SCVR"; - ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); - Log(Debug::Verbose) << ss.str(); + if(str > tmpEnd) + { + // SCVR subrecord is unused and variable names are determined + // from the script source, so an overflow is not fatal. + std::stringstream ss; + ss << "String table overflow"; + ss << "\n File: " << esm.getName(); + ss << "\n Record: " << esm.getContext().recName.toString(); + ss << "\n Subrecord: " << "SCVR"; + ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); + Log(Debug::Verbose) << ss.str(); + } // Get rid of empty strings in the list. mVarNames.resize(i+1); break; diff --git a/components/esm/loadtes3.cpp b/components/esm/loadtes3.cpp index 84a31b3bd..d953f1dc2 100644 --- a/components/esm/loadtes3.cpp +++ b/components/esm/loadtes3.cpp @@ -42,7 +42,6 @@ void ESM::Header::load (ESMReader &esm) MasterData m; m.name = esm.getHString(); m.size = esm.getHNLong ("DATA"); - m.index = -1; mMaster.push_back (m); } diff --git a/components/esm/loadtes3.hpp b/components/esm/loadtes3.hpp index 5b26ac7d2..014e2a136 100644 --- a/components/esm/loadtes3.hpp +++ b/components/esm/loadtes3.hpp @@ -49,7 +49,6 @@ namespace ESM { std::string name; uint64_t size; - int index; // Position of the parent file in the global list of loaded files }; GMDT mGameData; // Used in .ess savegames only diff --git a/components/esm/variantimp.cpp b/components/esm/variantimp.cpp index 74d1351ec..b9cd9a853 100644 --- a/components/esm/variantimp.cpp +++ b/components/esm/variantimp.cpp @@ -1,6 +1,7 @@ #include "variantimp.hpp" #include +#include #include "esmreader.hpp" #include "esmwriter.hpp" @@ -52,12 +53,10 @@ void ESM::readESMVariantValue(ESMReader& esm, Variant::Format format, VarType ty esm.getHNT (value, "FLTV"); if (type==VT_Short) - { - if (value!=value) - out = 0; // nan + if (std::isnan(value)) + out = 0; else out = static_cast (value); - } else if (type==VT_Long) out = static_cast (value); else diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 98fce32d2..b4739110f 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -146,10 +146,11 @@ namespace namespace Gui { - FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath) + FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor) : mVFS(vfs) , mUserDataPath(userDataPath) , mFontHeight(16) + , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) mEncoding = ToUTF8::CP437; @@ -566,11 +567,7 @@ namespace Gui // to allow to configure font size via config file, without need to edit XML files. // Also we should take UI scaling factor in account. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); - - float uiScale = Settings::Manager::getFloat("scaling factor", "GUI"); - if (uiScale > 0.f) - resolution *= uiScale; + resolution = std::min(960, std::max(48, resolution)) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp index 94b022501..8a46a0069 100644 --- a/components/fontloader/fontloader.hpp +++ b/components/fontloader/fontloader.hpp @@ -27,7 +27,7 @@ namespace Gui class FontLoader { public: - FontLoader (ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath); + FontLoader (ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor); ~FontLoader(); /// @param exportToFile export the converted fonts (Images and XML with glyph metrics) to files? @@ -43,6 +43,7 @@ namespace Gui const VFS::Manager* mVFS; std::string mUserDataPath; int mFontHeight; + float mScalingFactor; std::vector mTextures; std::vector mFonts; diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp index 0ceed80d5..d2e7067c6 100644 --- a/components/interpreter/defines.cpp +++ b/components/interpreter/defines.cpp @@ -176,7 +176,8 @@ namespace Interpreter{ transform(temp.begin(), temp.end(), temp.begin(), ::tolower); } - if((found = check(temp, globals[j], &i, &start))){ + found = check(temp, globals[j], &i, &start); + if(found){ char type = context.getGlobalType(globals[j]); switch(type){ diff --git a/components/misc/frameratelimiter.hpp b/components/misc/frameratelimiter.hpp index b727074d2..e3689cbcf 100644 --- a/components/misc/frameratelimiter.hpp +++ b/components/misc/frameratelimiter.hpp @@ -13,8 +13,8 @@ namespace Misc explicit FrameRateLimiter(std::chrono::duration maxFrameDuration, std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) : mMaxFrameDuration(std::chrono::duration_cast(maxFrameDuration)) - , mLastFrameDuration(0) , mLastMeasurement(now) + , mLastFrameDuration(0) {} std::chrono::steady_clock::duration getLastFrameDuration() const diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp new file mode 100644 index 000000000..30a9c41ee --- /dev/null +++ b/components/misc/hash.hpp @@ -0,0 +1,15 @@ +#ifndef MISC_HASH_H +#define MISC_HASH_H + +namespace Misc +{ + /// Implemented similar to the boost::hash_combine + template + inline void hashCombine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + } +} + +#endif \ No newline at end of file diff --git a/components/misc/thread.cpp b/components/misc/thread.cpp new file mode 100644 index 000000000..c78bf17e5 --- /dev/null +++ b/components/misc/thread.cpp @@ -0,0 +1,73 @@ +#include "thread.hpp" + +#include + +#include +#include + +#ifdef __linux__ + +#include +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + sched_param param; + param.sched_priority = 0; + if (pthread_setschedparam(pthread_self(), SCHED_IDLE, ¶m) == 0) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << std::strerror(errno); + } +} + +#elif defined(WIN32) + +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN + +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST)) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << GetLastError(); + } +} + +#elif defined(__FreeBSD__) + +#include +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + struct rtprio prio; + prio.type = RTP_PRIO_IDLE; + prio.prio = RTP_PRIO_MAX; + if (rtprio_thread(RTP_SET, 0, &prio) == 0) + Log(Debug::Verbose) << "Using idle priority for thread=" << std::this_thread::get_id(); + else + Log(Debug::Warning) << "Failed to set idle priority for thread=" << std::this_thread::get_id() << ": " << std::strerror(errno); + } +} + +#else + +namespace Misc +{ + void setCurrentThreadIdlePriority() + { + Log(Debug::Warning) << "Idle thread priority is not supported on this system"; + } +} + +#endif diff --git a/components/misc/thread.hpp b/components/misc/thread.hpp new file mode 100644 index 000000000..191c43bba --- /dev/null +++ b/components/misc/thread.hpp @@ -0,0 +1,11 @@ +#ifndef OPENMW_COMPONENTS_MISC_THREAD_H +#define OPENMW_COMPONENTS_MISC_THREAD_H + +#include + +namespace Misc +{ + void setCurrentThreadIdlePriority(); +} + +#endif diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 415a013b3..b6674611b 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -363,11 +363,11 @@ void NiSkinPartition::read(NIFStream *nif) void NiSkinPartition::Partition::read(NIFStream *nif) { - unsigned short numVertices = nif->getUShort(); - unsigned short numTriangles = nif->getUShort(); - unsigned short numBones = nif->getUShort(); - unsigned short numStrips = nif->getUShort(); - unsigned short bonesPerVertex = nif->getUShort(); + size_t numVertices = nif->getUShort(); + size_t numTriangles = nif->getUShort(); + size_t numBones = nif->getUShort(); + size_t numStrips = nif->getUShort(); + size_t bonesPerVertex = nif->getUShort(); if (numBones) nif->getUShorts(bones, numBones); @@ -395,7 +395,7 @@ void NiSkinPartition::Partition::read(NIFStream *nif) if (numStrips) { strips.resize(numStrips); - for (unsigned short i = 0; i < numStrips; i++) + for (size_t i = 0; i < numStrips; i++) nif->getUShorts(strips[i], stripLengths[i]); } else if (numTriangles) diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index d72cef194..72c740af3 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -126,8 +126,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) { Nif::Record* r = nif.getRoot(i); assert(r != nullptr); - Nif::Node* node = nullptr; - if ((node = dynamic_cast(r))) + if (Nif::Node* node = dynamic_cast(r)) roots.emplace_back(node); } const std::string filename = nif.getFilename(); diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 31fd92b43..d48c55ad7 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -277,19 +277,18 @@ void UVController::apply(osg::StateSet* stateset, osg::NodeVisitor* nv) if (hasInput()) { float value = getInputValue(nv); - float uTrans = mUTrans.interpKey(value); - float vTrans = mVTrans.interpKey(value); - float uScale = mUScale.interpKey(value); - float vScale = mVScale.interpKey(value); - osg::Matrix flipMat; - flipMat.preMultTranslate(osg::Vec3f(0,1,0)); - flipMat.preMultScale(osg::Vec3f(1,-1,1)); + // First scale the UV relative to its center, then apply the offset. + // U offset is flipped regardless of the graphics library, + // while V offset is flipped to account for OpenGL Y axis convention. + osg::Vec3f uvOrigin(0.5f, 0.5f, 0.f); + osg::Vec3f uvScale(mUScale.interpKey(value), mVScale.interpKey(value), 1.f); + osg::Vec3f uvTrans(-mUTrans.interpKey(value), -mVTrans.interpKey(value), 0.f); - osg::Matrixf mat = osg::Matrixf::scale(uScale, vScale, 1); - mat.setTrans(uTrans, vTrans, 0); - - mat = flipMat * mat * flipMat; + osg::Matrixf mat = osg::Matrixf::translate(uvOrigin); + mat.preMultScale(uvScale); + mat.preMultTranslate(-uvOrigin); + mat.setTrans(mat.getTrans() + uvTrans); // setting once is enough because all other texture units share the same TexMat (see setDefaults). if (!mTextureUnits.empty()) diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index ae1726f06..49d52074a 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -214,7 +214,7 @@ namespace NifOsg return sHiddenNodeMask; } - unsigned int Loader::sIntersectionDisabledNodeMask = ~0; + unsigned int Loader::sIntersectionDisabledNodeMask = ~0u; void Loader::setIntersectionDisabledNodeMask(unsigned int mask) { @@ -238,7 +238,7 @@ namespace NifOsg std::string mFilename; unsigned int mVersion, mUserVersion, mBethVersion; - size_t mFirstRootTextureIndex = -1; + size_t mFirstRootTextureIndex{~0u}; bool mFoundFirstRootTexturingProperty = false; bool mHasNightDayLabel = false; @@ -312,8 +312,7 @@ namespace NifOsg for (size_t i = 0; i < numRoots; ++i) { const Nif::Record* r = nif->getRoot(i); - const Nif::Node* nifNode = nullptr; - if ((nifNode = dynamic_cast(r))) + if (const Nif::Node* nifNode = dynamic_cast(r)) roots.emplace_back(nifNode); } if (roots.empty()) @@ -609,7 +608,8 @@ namespace NifOsg bool hasVisController = false; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { - if ((hasVisController |= (ctrl->recType == Nif::RC_NiVisController))) + hasVisController |= (ctrl->recType == Nif::RC_NiVisController); + if (hasVisController) break; } diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index f4ab79519..d7eeeeb97 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -77,7 +77,7 @@ namespace Resource mTarget.mTextKeys.emplace(parseTimeSignature(line), parseTextKey(line)); } } - catch (std::exception& e) + catch (std::exception&) { Log(Debug::Warning) << "No textkey file found for " << mNormalized; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 287365a83..a3c751f7a 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -226,6 +226,7 @@ namespace Resource , mAutoUseNormalMaps(false) , mAutoUseSpecularMaps(false) , mApplyLightingToEnvMaps(false) + , mLightingMethod(SceneUtil::LightingMethod::FFP) , mConvertAlphaTestToAlphaToCoverage(false) , mInstanceCache(new MultiObjectCache) , mSharedStateManager(new SharedStateManager) @@ -304,6 +305,26 @@ namespace Resource mApplyLightingToEnvMaps = apply; } + void SceneManager::setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported) + { + mSupportedLightingMethods = supported; + } + + bool SceneManager::isSupportedLightingMethod(SceneUtil::LightingMethod method) const + { + return mSupportedLightingMethods[static_cast(method)]; + } + + void SceneManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + } + + SceneUtil::LightingMethod SceneManager::getLightingMethod() const + { + return mLightingMethod; + } + void SceneManager::setConvertAlphaTestToAlphaToCoverage(bool convert) { mConvertAlphaTestToAlphaToCoverage = convert; diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index bf69a8c4b..7635cd20f 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -12,6 +12,8 @@ #include "resourcemanager.hpp" +#include + namespace Resource { class ImageManager; @@ -105,6 +107,12 @@ namespace Resource void setApplyLightingToEnvMaps(bool apply); + void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); + bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + + void setLightingMethod(SceneUtil::LightingMethod method); + SceneUtil::LightingMethod getLightingMethod() const; + void setConvertAlphaTestToAlphaToCoverage(bool convert); void setShaderPath(const std::string& path); @@ -191,6 +199,8 @@ namespace Resource bool mAutoUseSpecularMaps; std::string mSpecularMapPattern; bool mApplyLightingToEnvMaps; + SceneUtil::LightingMethod mLightingMethod; + SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; bool mConvertAlphaTestToAlphaToCoverage; osg::ref_ptr mInstanceCache; diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp index c759fabc7..cc320aecf 100644 --- a/components/sceneutil/lightcontroller.cpp +++ b/components/sceneutil/lightcontroller.cpp @@ -62,7 +62,8 @@ namespace SceneUtil mPhase = mPhase <= 0.5f ? 1.f : 0.25f; } - static_cast(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness); + auto* lightSource = static_cast(node); + lightSource->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness * lightSource->getActorFade()); traverse(node, nv); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 2ebce241d..b5c7af780 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,34 +1,351 @@ #include "lightmanager.hpp" +#include + +#include +#include +#include +#include +#include + #include #include +#include +#include + +#include + +namespace +{ + bool sortLights(const SceneUtil::LightManager::LightSourceViewBound* left, const SceneUtil::LightManager::LightSourceViewBound* right) + { + static auto constexpr illuminationBias = 81.f; + return left->mViewBound.center().length2() - left->mViewBound.radius2()*illuminationBias < right->mViewBound.center().length2() - right->mViewBound.radius2()*illuminationBias; + } + + float getLightRadius(const osg::Light* light) + { + float value = 0.0; + light->getUserValue("radius", value); + return value; + } + + void setLightRadius(osg::Light* light, float value) + { + light->setUserValue("radius", value); + } + + void configurePosition(osg::Matrixf& mat, const osg::Vec4& pos) + { + mat(0, 0) = pos.x(); + mat(0, 1) = pos.y(); + mat(0, 2) = pos.z(); + } + + void configureAmbient(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(1, 0) = color.r(); + mat(1, 1) = color.g(); + mat(1, 2) = color.b(); + } + + void configureDiffuse(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(2, 0) = color.r(); + mat(2, 1) = color.g(); + mat(2, 2) = color.b(); + } + + void configureSpecular(osg::Matrixf& mat, const osg::Vec4& color) + { + mat(3, 0) = color.r(); + mat(3, 1) = color.g(); + mat(3, 2) = color.b(); + mat(3, 3) = color.a(); + } + + void configureAttenuation(osg::Matrixf& mat, float c, float l, float q, float r) + { + mat(0, 3) = c; + mat(1, 3) = l; + mat(2, 3) = q; + mat(3, 3) = r; + } + + bool isReflectionCamera(osg::Camera* camera) + { + return (camera->getName() == "ReflectionCamera"); + } +} + namespace SceneUtil { + static int sLightId = 0; + + // Handles a GLSL shared layout by using configured offsets and strides to fill a continuous buffer, making the data upload to GPU simpler. + class LightBuffer : public osg::Referenced + { + public: + + enum LayoutOffset + { + Diffuse, + DiffuseSign, + Ambient, + Specular, + Position, + AttenuationRadius + }; + + LightBuffer(int count) + : mData(new osg::FloatArray(3*4*count)) + , mEndian(osg::getCpuByteOrder()) + , mCount(count) + , mStride(12) + , mCachedSunPos(osg::Vec4()) + { + mOffsets[Diffuse] = 0; + mOffsets[Ambient] = 1; + mOffsets[Specular] = 2; + mOffsets[DiffuseSign] = 3; + mOffsets[Position] = 4; + mOffsets[AttenuationRadius] = 8; + } + + LightBuffer(const LightBuffer& copy) + : osg::Referenced() + , mData(copy.mData) + , mEndian(copy.mEndian) + , mCount(copy.mCount) + , mStride(copy.mStride) + , mOffsets(copy.mOffsets) + , mCachedSunPos(copy.mCachedSunPos) + {} + + void setDiffuse(int index, const osg::Vec4& value) + { + // Deal with negative lights (negative diffuse) by passing a sign bit in the unused alpha component + auto positiveColor = value; + unsigned int signBit = 1; + if (value[0] < 0) + { + positiveColor *= -1.0; + signBit = ~0u; + } + unsigned int packedColor = asRGBA(positiveColor); + std::memcpy(&(*mData)[getOffset(index, Diffuse)], &packedColor, sizeof(unsigned int)); + std::memcpy(&(*mData)[getOffset(index, DiffuseSign)], &signBit, sizeof(unsigned int)); + } + + void setAmbient(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Ambient)], &packed, sizeof(unsigned int)); + } + + void setSpecular(int index, const osg::Vec4& value) + { + unsigned int packed = asRGBA(value); + std::memcpy(&(*mData)[getOffset(index, Specular)], &packed, sizeof(unsigned int)); + } + + void setPosition(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, Position)], value.ptr(), sizeof(osg::Vec4f)); + } + + void setAttenuationRadius(int index, const osg::Vec4& value) + { + std::memcpy(&(*mData)[getOffset(index, AttenuationRadius)], value.ptr(), sizeof(osg::Vec4f)); + } + + auto& getData() + { + return mData; + } + + void dirty() + { + mData->dirty(); + } + + static constexpr int queryBlockSize(int sz) + { + return 3 * osg::Vec4::num_components * sizeof(GL_FLOAT) * sz; + } + + void setCachedSunPos(const osg::Vec4& pos) + { + mCachedSunPos = pos; + } + + void uploadCachedSunPos(const osg::Matrix& viewMat) + { + osg::Vec4 viewPos = mCachedSunPos * viewMat; + std::memcpy(&(*mData)[getOffset(0, Position)], viewPos.ptr(), sizeof(osg::Vec4f)); + } + + unsigned int asRGBA(const osg::Vec4& value) const + { + return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA(); + } + + int getOffset(int index, LayoutOffset slot) + { + return mStride * index + mOffsets[slot]; + } + + void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) + { + constexpr auto sizeofFloat = sizeof(GL_FLOAT); + constexpr auto sizeofVec4 = sizeofFloat * osg::Vec4::num_components; + + LightBuffer oldBuffer = LightBuffer(*this); + + mOffsets[Diffuse] = offsetColors / sizeofFloat; + mOffsets[Ambient] = mOffsets[Diffuse] + 1; + mOffsets[Specular] = mOffsets[Diffuse] + 2; + mOffsets[DiffuseSign] = mOffsets[Diffuse] + 3; + mOffsets[Position] = offsetPosition / sizeofFloat; + mOffsets[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; + mStride = (offsetAttenuationRadius + sizeofVec4 + stride) / 4; + + // Copy over previous buffers light data. Buffers populate before we know the layout. + mData->resize(size / sizeofFloat); + for (int i = 0; i < oldBuffer.mCount; ++i) + { + std::memcpy(&(*mData)[getOffset(i, Diffuse)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Diffuse)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, Position)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, Position)], sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[getOffset(i, AttenuationRadius)], &(*oldBuffer.mData)[oldBuffer.getOffset(i, AttenuationRadius)], sizeof(osg::Vec4f)); + } + } + + private: + osg::ref_ptr mData; + osg::Endian mEndian; + int mCount; + int mStride; + std::array mOffsets; + osg::Vec4 mCachedSunPos; + }; class LightStateCache { public: - osg::Light* lastAppliedLight[8]; + std::vector lastAppliedLight; }; - LightStateCache* getLightStateCache(unsigned int contextid) + LightStateCache* getLightStateCache(size_t contextid, size_t size = 8) { static std::vector cacheVector; if (cacheVector.size() < contextid+1) cacheVector.resize(contextid+1); + cacheVector[contextid].lastAppliedLight.resize(size); return &cacheVector[contextid]; } - // Resets the modelview matrix to just the view matrix before applying lights. - class LightStateAttribute : public osg::StateAttribute + void configureStateSetSunOverride(LightManager* lightManager, const osg::Light* light, osg::StateSet* stateset, int mode) + { + auto method = lightManager->getLightingMethod(); + switch (method) + { + case LightingMethod::FFP: + { + break; + } + case LightingMethod::PerObjectUniform: + { + osg::Matrixf lightMat; + configurePosition(lightMat, light->getPosition()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureSpecular(lightMat, light->getSpecular()); + + osg::ref_ptr uni = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", lightManager->getMaxLights()); + uni->setElement(0, lightMat); + stateset->addUniform(uni, mode); + break; + } + case LightingMethod::SingleUBO: + { + osg::ref_ptr buffer = new LightBuffer(lightManager->getMaxLightsInScene()); + + buffer->setDiffuse(0, light->getDiffuse()); + buffer->setAmbient(0, light->getAmbient()); + buffer->setSpecular(0, light->getSpecular()); + buffer->setPosition(0, light->getPosition()); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + buffer->getData()->setBufferObject(ubo); +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, mode); + + break; + } + } + } + + class DisableLight : public osg::StateAttribute { public: - LightStateAttribute() : mIndex(0) {} - LightStateAttribute(unsigned int index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + DisableLight() : mIndex(0) {} + DisableLight(int index) : mIndex(index) {} - LightStateAttribute(const LightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} + + osg::Object* cloneType() const override { return new DisableLight(mIndex); } + osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); } + bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast(obj)!=nullptr; } + const char* libraryName() const override { return "SceneUtil"; } + const char* className() const override { return "DisableLight"; } + Type getType() const override { return LIGHT; } + + unsigned int getMember() const override + { + return mIndex; + } + + bool getModeUsage(ModeUsage & usage) const override + { + usage.usesMode(GL_LIGHT0 + mIndex); + return true; + } + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("DisableLight::compare: unimplemented"); + } + + void apply(osg::State& state) const override + { + int lightNum = GL_LIGHT0 + mIndex; + glLightfv(lightNum, GL_AMBIENT, mNullptr.ptr()); + glLightfv(lightNum, GL_DIFFUSE, mNullptr.ptr()); + glLightfv(lightNum, GL_SPECULAR, mNullptr.ptr()); + + LightStateCache* cache = getLightStateCache(state.getContextID()); + cache->lastAppliedLight[mIndex] = nullptr; + } + + private: + size_t mIndex; + osg::Vec4f mNullptr; + }; + + class FFPLightStateAttribute : public osg::StateAttribute + { + public: + FFPLightStateAttribute() : mIndex(0) {} + FFPLightStateAttribute(size_t index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + + FFPLightStateAttribute(const FFPLightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex), mLights(copy.mLights) {} unsigned int getMember() const override @@ -38,17 +355,17 @@ namespace SceneUtil bool getModeUsage(ModeUsage & usage) const override { - for (unsigned int i=0; ilastAppliedLight[i+mIndex]; if (current != mLights[i].get()) @@ -75,29 +392,28 @@ namespace SceneUtil void applyLight(GLenum lightNum, const osg::Light* light) const { - glLightfv( lightNum, GL_AMBIENT, light->getAmbient().ptr() ); - glLightfv( lightNum, GL_DIFFUSE, light->getDiffuse().ptr() ); - glLightfv( lightNum, GL_SPECULAR, light->getSpecular().ptr() ); - glLightfv( lightNum, GL_POSITION, light->getPosition().ptr() ); + glLightfv(lightNum, GL_AMBIENT, light->getAmbient().ptr()); + glLightfv(lightNum, GL_DIFFUSE, light->getDiffuse().ptr()); + glLightfv(lightNum, GL_SPECULAR, light->getSpecular().ptr()); + glLightfv(lightNum, GL_POSITION, light->getPosition().ptr()); // TODO: enable this once spot lights are supported // need to transform SPOT_DIRECTION by the world matrix? - //glLightfv( lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr() ); - //glLightf ( lightNum, GL_SPOT_EXPONENT, light->getSpotExponent() ); - //glLightf ( lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff() ); - glLightf ( lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation() ); - glLightf ( lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation() ); - glLightf ( lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation() ); + //glLightfv(lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr()); + //glLightf(lightNum, GL_SPOT_EXPONENT, light->getSpotExponent()); + //glLightf(lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff()); + glLightf(lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation()); + glLightf(lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation()); + glLightf(lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation()); } private: - unsigned int mIndex; - - std::vector > mLights; + size_t mIndex; + std::vector> mLights; }; LightManager* findLightManager(const osg::NodePath& path) { - for (unsigned int i=0;i(path[i])) return lightManager; @@ -105,6 +421,169 @@ namespace SceneUtil return nullptr; } + class LightStateAttributePerObjectUniform : public osg::StateAttribute + { + public: + LightStateAttributePerObjectUniform() : mLightManager(nullptr) {} + LightStateAttributePerObjectUniform(const std::vector>& lights, LightManager* lightManager) : mLights(lights), mLightManager(lightManager) {} + + LightStateAttributePerObjectUniform(const LightStateAttributePerObjectUniform& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLights(copy.mLights), mLightManager(copy.mLightManager) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightStateAttributePerObjectUniform::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightStateAttributePerObjectUniform, osg::StateAttribute::LIGHT) + + void resize(int numLights) + { + mLights.resize(std::min(static_cast(numLights), mLights.size())); + } + + void apply(osg::State &state) const override + { + auto* lightUniform = mLightManager->getStateSet()->getUniform("LightBuffer"); + for (size_t i = 0; i < mLights.size(); ++i) + { + auto light = mLights[i]; + osg::Matrixf lightMat; + + configurePosition(lightMat, light->getPosition() * state.getInitialViewMatrix()); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), getLightRadius(light)); + + lightUniform->setElement(i+1, lightMat); + } + + auto sun = mLightManager->getSunlightBuffer(state.getFrameStamp()->getFrameNumber()); + configurePosition(sun, osg::Vec4(sun(0,0), sun(0,1), sun(0,2), 0.0) * state.getInitialViewMatrix()); + lightUniform->setElement(0, sun); + + lightUniform->dirty(); + } + + private: + std::vector> mLights; + LightManager* mLightManager; + }; + + struct StateSetGenerator + { + LightManager* mLightManager; + + virtual ~StateSetGenerator() {} + + virtual osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) = 0; + + virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {} + }; + + struct StateSetGeneratorFFP : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + std::vector> lights; + lights.reserve(lightList.size()); + for (size_t i = 0; i < lightList.size(); ++i) + lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum)); + + // the first light state attribute handles the actual state setting for all lights + // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary + // don't use setAttributeAndModes, that does not support light indices! + stateset->setAttribute(new FFPLightStateAttribute(mLightManager->getStartLight(), std::move(lights)), osg::StateAttribute::ON); + + for (size_t i = 0; i < lightList.size(); ++i) + stateset->setMode(GL_LIGHT0 + mLightManager->getStartLight() + i, osg::StateAttribute::ON); + + // need to push some dummy attributes to ensure proper state tracking + // lights need to reset to their default when the StateSet is popped + for (size_t i = 1; i < lightList.size(); ++i) + stateset->setAttribute(mLightManager->getDummies()[i + mLightManager->getStartLight()].get(), osg::StateAttribute::ON); + + return stateset; + } + }; + + struct StateSetGeneratorSingleUBO : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr indices = new osg::IntArray(mLightManager->getMaxLights()); + osg::ref_ptr indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", indices->size()); + int pointCount = 0; + + for (size_t i = 0; i < lightList.size(); ++i) + { + int bufIndex = mLightManager->getLightIndexMap(frameNum)[lightList[i]->mLightSource->getId()]; + indices->at(pointCount++) = bufIndex; + } + indicesUni->setArray(indices); + stateset->addUniform(indicesUni); + stateset->addUniform(new osg::Uniform("PointLightCount", pointCount)); + + return stateset; + } + + // Cached statesets must be revalidated in case the light indices change. There is no actual link between + // a light's ID and the buffer index it will eventually be assigned (or reassigned) to. + void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) override + { + int newCount = 0; + int oldCount; + + auto uOldArray = stateset->getUniform("PointLightIndex"); + auto uOldCount = stateset->getUniform("PointLightCount"); + + uOldCount->get(oldCount); + + // max lights count can change during runtime + oldCount = std::min(mLightManager->getMaxLights(), oldCount); + + auto& lightData = mLightManager->getLightIndexMap(frameNum); + + for (int i = 0; i < oldCount; ++i) + { + auto* lightSource = lightList[i]->mLightSource; + auto it = lightData.find(lightSource->getId()); + if (it != lightData.end()) + uOldArray->setElement(newCount++, it->second); + } + + uOldArray->dirty(); + uOldCount->set(newCount); + } + }; + + struct StateSetGeneratorPerObjectUniform : StateSetGenerator + { + osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override + { + osg::ref_ptr stateset = new osg::StateSet; + + std::vector> lights(lightList.size()); + + for (size_t i = 0; i < lightList.size(); ++i) + { + auto* light = lightList[i]->mLightSource->getLight(frameNum); + lights[i] = light; + setLightRadius(light, lightList[i]->mLightSource->getRadius()); + } + + stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); + + return stateset; + } + }; + // Set on a LightSource. Adds the light source to its light manager for the current frame. // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager. class CollectLightCallback : public osg::NodeCallback @@ -154,194 +633,473 @@ namespace SceneUtil void operator()(osg::Node* node, osg::NodeVisitor* nv) override { LightManager* lightManager = static_cast(node); - lightManager->update(); + lightManager->update(nv->getTraversalNumber()); traverse(node, nv); } }; - LightManager::LightManager() + class LightManagerCullCallback : public osg::NodeCallback + { + public: + LightManagerCullCallback(LightManager* lightManager) : mLightManager(lightManager), mLastFrameNumber(0) {} + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + + if (mLastFrameNumber != cv->getTraversalNumber()) + { + mLastFrameNumber = cv->getTraversalNumber(); + + if (mLightManager->getLightingMethod() == LightingMethod::SingleUBO) + { + auto stateset = mLightManager->getStateSet(); + auto bo = mLightManager->getLightBuffer(mLastFrameNumber); + +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); +#else + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); +#endif + stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); + } + + auto sun = mLightManager->getSunlight(); + + if (sun) + { + // we must defer uploading the transformation to view-space position to deal with different cameras (e.g. reflection RTT). + if (mLightManager->getLightingMethod() == LightingMethod::PerObjectUniform) + { + osg::Matrixf lightMat; + configurePosition(lightMat, sun->getPosition()); + configureAmbient(lightMat, sun->getAmbient()); + configureDiffuse(lightMat, sun->getDiffuse()); + configureSpecular(lightMat, sun->getSpecular()); + mLightManager->setSunlightBuffer(lightMat, mLastFrameNumber); + } + else + { + auto buf = mLightManager->getLightBuffer(mLastFrameNumber); + + buf->setCachedSunPos(sun->getPosition()); + buf->setAmbient(0, sun->getAmbient()); + buf->setDiffuse(0, sun->getDiffuse()); + buf->setSpecular(0, sun->getSpecular()); + } + } + } + + traverse(node, nv); + } + + private: + LightManager* mLightManager; + size_t mLastFrameNumber; + }; + + class LightManagerStateAttribute : public osg::StateAttribute + { + public: + LightManagerStateAttribute() + : mLightManager(nullptr) + , mInitLayout(false) + { + } + + LightManagerStateAttribute(LightManager* lightManager) + : mLightManager(lightManager) + , mDummyProgram(new osg::Program) + , mInitLayout(false) + { + static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene()); + + // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably + // available, regardless of extensions, until GLSL 140. + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); + } + + LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager), mInitLayout(copy.mInitLayout) {} + + int compare(const StateAttribute &sa) const override + { + throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + } + + META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT) + + void initSharedLayout(osg::GLExtensions* ext, int handle) const + { + constexpr std::array index = { static_cast(Shader::UBOBinding::LightBuffer) }; + int totalBlockSize = -1; + int stride = -1; + + ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); + ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); + + std::array names = { + "LightBuffer[0].packedColors", + "LightBuffer[0].position", + "LightBuffer[0].attenuation", + }; + std::vector indices(names.size()); + std::vector offsets(names.size()); + + ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); + ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); + + for (int i = 0; i < 2; ++i) + { + auto& buf = mLightManager->getLightBuffer(i); + buf->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } + } + + void apply(osg::State& state) const override + { + if (!mInitLayout) + { + mDummyProgram->apply(state); + auto handle = mDummyProgram->getPCP(state)->getHandle(); + auto* ext = state.get(); + + int activeUniformBlocks = 0; + ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); + + // wait until the UBO binding is created + if (activeUniformBlocks > 0) + { + initSharedLayout(ext, handle); + mInitLayout = true; + } + } + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->uploadCachedSunPos(state.getInitialViewMatrix()); + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); + } + + private: + + std::string generateDummyShader(int maxLightsInScene) + { + const std::string define = "@maxLightsInScene"; + + std::string shader = R"GLSL( + #version 120 + #extension GL_ARB_uniform_buffer_object : require + struct LightData { + ivec4 packedColors; + vec4 position; + vec4 attenuation; + }; + uniform LightBufferBinding { + LightData LightBuffer[@maxLightsInScene]; + }; + void main() + { + gl_Position = vec4(0.0); + } + )GLSL"; + + shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); + return shader; + } + + LightManager* mLightManager; + osg::ref_ptr mDummyProgram; + mutable bool mInitLayout; + }; + + const std::unordered_map LightManager::mLightingMethodSettingMap = { + {"legacy", LightingMethod::FFP} + ,{"shaders compatibility", LightingMethod::PerObjectUniform} + ,{"shaders", LightingMethod::SingleUBO} + }; + + LightingMethod LightManager::getLightingMethodFromString(const std::string& value) + { + auto it = LightManager::mLightingMethodSettingMap.find(value); + if (it != LightManager::mLightingMethodSettingMap.end()) + return it->second; + + constexpr const char* fallback = "shaders compatibility"; + Log(Debug::Warning) << "Unknown lighting method '" << value << "', returning fallback '" << fallback << "'"; + return LightingMethod::PerObjectUniform; + } + + std::string LightManager::getLightingMethodString(LightingMethod method) + { + for (const auto& p : LightManager::mLightingMethodSettingMap) + if (p.second == method) + return p.first; + return ""; + } + + LightManager::LightManager(bool ffp) : mStartLight(0) , mLightingMask(~0u) + , mSun(nullptr) + , mPointLightRadiusMultiplier(1.f) + , mPointLightFadeEnd(0.f) + , mPointLightFadeStart(0.f) { + osg::GLExtensions* exts = osg::GLExtensions::Get(0, false); + bool supportsUBO = exts && exts->isUniformBufferObjectSupported; + bool supportsGPU4 = exts && exts->isGpuShader4Supported; + + mSupported[static_cast(LightingMethod::FFP)] = true; + mSupported[static_cast(LightingMethod::PerObjectUniform)] = true; + mSupported[static_cast(LightingMethod::SingleUBO)] = supportsUBO && supportsGPU4; + setUpdateCallback(new LightManagerUpdateCallback); - for (unsigned int i=0; i<8; ++i) - mDummies.push_back(new LightStateAttribute(i, std::vector >())); + + if (ffp) + { + initFFP(mFFPMaxLights); + return; + } + + std::string lightingMethodString = Settings::Manager::getString("lighting method", "Shaders"); + auto lightingMethod = LightManager::getLightingMethodFromString(lightingMethodString); + + updateSettings(); + + static bool hasLoggedWarnings = false; + + if (lightingMethod == LightingMethod::SingleUBO && !hasLoggedWarnings) + { + if (!supportsUBO) + Log(Debug::Warning) << "GL_ARB_uniform_buffer_object not supported: switching to shader compatibility lighting mode"; + if (!supportsGPU4) + Log(Debug::Warning) << "GL_EXT_gpu_shader4 not supported: switching to shader compatibility lighting mode"; + hasLoggedWarnings = true; + } + + int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit); + + if (!supportsUBO || !supportsGPU4 || lightingMethod == LightingMethod::PerObjectUniform) + initPerObjectUniform(targetLights); + else + initSingleUBO(targetLights); + + getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); + + addCullCallback(new LightManagerCullCallback(this)); } LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) : osg::Group(copy, copyop) , mStartLight(copy.mStartLight) , mLightingMask(copy.mLightingMask) + , mSun(copy.mSun) + , mLightingMethod(copy.mLightingMethod) + , mPointLightRadiusMultiplier(copy.mPointLightRadiusMultiplier) + , mPointLightFadeEnd(copy.mPointLightFadeEnd) + , mPointLightFadeStart(copy.mPointLightFadeStart) + , mMaxLights(copy.mMaxLights) { - } - void LightManager::setLightingMask(unsigned int mask) + LightingMethod LightManager::getLightingMethod() const + { + return mLightingMethod; + } + + bool LightManager::usingFFP() const + { + return mLightingMethod == LightingMethod::FFP; + } + + int LightManager::getMaxLights() const + { + return mMaxLights; + } + + void LightManager::setMaxLights(int value) + { + mMaxLights = value; + } + + int LightManager::getMaxLightsInScene() const + { + static constexpr int max = 16384 / LightBuffer::queryBlockSize(1); + return max; + } + + Shader::ShaderManager::DefineMap LightManager::getLightDefines() const + { + Shader::ShaderManager::DefineMap defines; + + defines["maxLights"] = std::to_string(getMaxLights()); + defines["maxLightsInScene"] = std::to_string(getMaxLightsInScene()); + defines["lightingMethodFFP"] = getLightingMethod() == LightingMethod::FFP ? "1" : "0"; + defines["lightingMethodPerObjectUniform"] = getLightingMethod() == LightingMethod::PerObjectUniform ? "1" : "0"; + defines["lightingMethodUBO"] = getLightingMethod() == LightingMethod::SingleUBO ? "1" : "0"; + defines["useUBO"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + // exposes bitwise operators + defines["useGPUShader4"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); + defines["getLight"] = getLightingMethod() == LightingMethod::FFP ? "gl_LightSource" : "LightBuffer"; + defines["startLight"] = getLightingMethod() == LightingMethod::SingleUBO ? "0" : "1"; + defines["endLight"] = getLightingMethod() == LightingMethod::FFP ? defines["maxLights"] : "PointLightCount"; + + return defines; + } + + void LightManager::processChangedSettings(const Settings::CategorySettingVector& changed) + { + updateSettings(); + } + + void LightManager::updateMaxLights() + { + if (usingFFP()) + return; + + int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit); + setMaxLights(targetLights); + + if (getLightingMethod() == LightingMethod::PerObjectUniform) + { + auto* prevUniform = getStateSet()->getUniform("LightBuffer"); + osg::ref_ptr newUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); + + for (int i = 0; i < getMaxLights(); ++i) + { + osg::Matrixf prevLightData; + prevUniform->getElement(i, prevLightData); + newUniform->setElement(i, prevLightData); + } + + getStateSet()->removeUniform(prevUniform); + getStateSet()->addUniform(newUniform); + + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + static_cast(pair.second->getAttribute(osg::StateAttribute::LIGHT))->resize(getMaxLights()); + mStateSetCache[i].clear(); + } + } + else + { + for (int i = 0; i < 2; ++i) + { + for (auto& pair : mStateSetCache[i]) + { + auto& stateset = pair.second; + osg::Uniform* uOldArray = stateset->getUniform("PointLightIndex"); + osg::Uniform* uOldCount = stateset->getUniform("PointLightCount"); + + int prevCount; + uOldCount->get(prevCount); + int newCount = std::min(getMaxLights(), prevCount); + uOldCount->set(newCount); + + osg::ref_ptr newArray = uOldArray->getIntArray(); + newArray->resize(newCount); + + stateset->removeUniform(uOldArray); + stateset->addUniform(new osg::Uniform("PointLightIndex", newArray)); + } + mStateSetCache[i].clear(); + } + } + } + + void LightManager::updateSettings() + { + if (getLightingMethod() == LightingMethod::FFP) + return; + + mPointLightRadiusMultiplier = std::clamp(Settings::Manager::getFloat("light bounds multiplier", "Shaders"), 0.f, 5.f); + + mPointLightFadeEnd = std::max(0.f, Settings::Manager::getFloat("maximum light distance", "Shaders")); + if (mPointLightFadeEnd > 0) + { + mPointLightFadeStart = std::clamp(Settings::Manager::getFloat("light fade start", "Shaders"), 0.f, 1.f); + mPointLightFadeStart = mPointLightFadeEnd * mPointLightFadeStart; + } + } + + void LightManager::initFFP(int targetLights) + { + setLightingMethod(LightingMethod::FFP); + setMaxLights(targetLights); + + for (int i = 0; i < getMaxLights(); ++i) + mDummies.push_back(new FFPLightStateAttribute(i, std::vector>())); + } + + void LightManager::initPerObjectUniform(int targetLights) + { + auto* stateset = getOrCreateStateSet(); + + setLightingMethod(LightingMethod::PerObjectUniform); + setMaxLights(targetLights); + + // ensures sunlight element in our uniform array is updated when there are no point lights in scene + stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform({}, this), osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights())); + } + + void LightManager::initSingleUBO(int targetLights) + { + setLightingMethod(LightingMethod::SingleUBO); + setMaxLights(targetLights); + + for (int i = 0; i < 2; ++i) + { + mLightBuffers[i] = new LightBuffer(getMaxLightsInScene()); + + osg::ref_ptr ubo = new osg::UniformBufferObject; + ubo->setUsage(GL_STREAM_DRAW); + + mLightBuffers[i]->getData()->setBufferObject(ubo); + } + + getOrCreateStateSet()->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON); + } + + void LightManager::setLightingMethod(LightingMethod method) + { + mLightingMethod = method; + switch (method) + { + case LightingMethod::FFP: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::SingleUBO: + mStateSetGenerator = std::make_unique(); + break; + case LightingMethod::PerObjectUniform: + mStateSetGenerator = std::make_unique(); + break; + } + mStateSetGenerator->mLightManager = this; + } + + void LightManager::setLightingMask(size_t mask) { mLightingMask = mask; } - unsigned int LightManager::getLightingMask() const + size_t LightManager::getLightingMask() const { return mLightingMask; } - void LightManager::update() - { - mLights.clear(); - mLightsInViewSpace.clear(); - - // do an occasional cleanup for orphaned lights - for (int i=0; i<2; ++i) - { - if (mStateSetCache[i].size() > 5000) - mStateSetCache[i].clear(); - } - } - - void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum) - { - LightSourceTransform l; - l.mLightSource = lightSource; - l.mWorldMatrix = worldMat; - lightSource->getLight(frameNum)->setPosition(osg::Vec4f(worldMat.getTrans().x(), - worldMat.getTrans().y(), - worldMat.getTrans().z(), 1.f)); - mLights.push_back(l); - } - - /* similar to the boost::hash_combine */ - template - inline void hash_combine(std::size_t& seed, const T& v) - { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); - } - - osg::ref_ptr LightManager::getLightListStateSet(const LightList &lightList, unsigned int frameNum) - { - // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - size_t hash = 0; - for (unsigned int i=0; imLightSource->getId()); - - LightStateSetMap& stateSetCache = mStateSetCache[frameNum%2]; - - LightStateSetMap::iterator found = stateSetCache.find(hash); - if (found != stateSetCache.end()) - return found->second; - else - { - osg::ref_ptr stateset = new osg::StateSet; - std::vector > lights; - lights.reserve(lightList.size()); - for (unsigned int i=0; imLightSource->getLight(frameNum)); - - // the first light state attribute handles the actual state setting for all lights - // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary - // don't use setAttributeAndModes, that does not support light indices! - stateset->setAttribute(new LightStateAttribute(mStartLight, std::move(lights)), osg::StateAttribute::ON); - - for (unsigned int i=0; isetMode(GL_LIGHT0 + mStartLight + i, osg::StateAttribute::ON); - - // need to push some dummy attributes to ensure proper state tracking - // lights need to reset to their default when the StateSet is popped - for (unsigned int i=1; isetAttribute(mDummies[i+mStartLight].get(), osg::StateAttribute::ON); - - stateSetCache.emplace(hash, stateset); - return stateset; - } - } - - const std::vector& LightManager::getLights() const - { - return mLights; - } - - const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix) - { - osg::observer_ptr camPtr (camera); - std::map, LightSourceViewBoundCollection>::iterator it = mLightsInViewSpace.find(camPtr); - - if (it == mLightsInViewSpace.end()) - { - it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; - - for (std::vector::iterator lightIt = mLights.begin(); lightIt != mLights.end(); ++lightIt) - { - osg::Matrixf worldViewMat = lightIt->mWorldMatrix * (*viewMatrix); - osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), lightIt->mLightSource->getRadius()); - transformBoundingSphere(worldViewMat, viewBound); - - LightSourceViewBound l; - l.mLightSource = lightIt->mLightSource; - l.mViewBound = viewBound; - it->second.push_back(l); - } - } - return it->second; - } - - class DisableLight : public osg::StateAttribute - { - public: - DisableLight() : mIndex(0) {} - DisableLight(int index) : mIndex(index) {} - - DisableLight(const DisableLight& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop), mIndex(copy.mIndex) {} - - osg::Object* cloneType() const override { return new DisableLight(mIndex); } - osg::Object* clone(const osg::CopyOp& copyop) const override { return new DisableLight(*this,copyop); } - bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast(obj)!=nullptr; } - const char* libraryName() const override { return "SceneUtil"; } - const char* className() const override { return "DisableLight"; } - Type getType() const override { return LIGHT; } - - unsigned int getMember() const override - { - return mIndex; - } - - bool getModeUsage(ModeUsage & usage) const override - { - usage.usesMode(GL_LIGHT0 + mIndex); - return true; - } - - int compare(const StateAttribute &sa) const override - { - throw std::runtime_error("DisableLight::compare: unimplemented"); - } - - void apply(osg::State& state) const override - { - int lightNum = GL_LIGHT0 + mIndex; - glLightfv( lightNum, GL_AMBIENT, mnullptr.ptr() ); - glLightfv( lightNum, GL_DIFFUSE, mnullptr.ptr() ); - glLightfv( lightNum, GL_SPECULAR, mnullptr.ptr() ); - - LightStateCache* cache = getLightStateCache(state.getContextID()); - cache->lastAppliedLight[mIndex] = nullptr; - } - - private: - unsigned int mIndex; - osg::Vec4f mnullptr; - }; - void LightManager::setStartLight(int start) { mStartLight = start; + if (!usingFFP()) return; + // Set default light state to zero // This is necessary because shaders don't respect glDisable(GL_LIGHTX) so in addition to disabling // we'll have to set a light state that has no visible effect - for (int i=start; i<8; ++i) + for (int i = start; i < getMaxLights(); ++i) { osg::ref_ptr defaultLight (new DisableLight(i)); getOrCreateStateSet()->setAttributeAndModes(defaultLight, osg::StateAttribute::OFF); @@ -353,10 +1111,142 @@ namespace SceneUtil return mStartLight; } - static int sLightId = 0; + void LightManager::update(size_t frameNum) + { + getLightIndexMap(frameNum).clear(); + mLights.clear(); + mLightsInViewSpace.clear(); + + // Do an occasional cleanup for orphaned lights. + for (int i = 0; i < 2; ++i) + { + if (mStateSetCache[i].size() > 5000) + mStateSetCache[i].clear(); + } + } + + void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum) + { + LightSourceTransform l; + l.mLightSource = lightSource; + l.mWorldMatrix = worldMat; + osg::Vec3f pos = osg::Vec3f(worldMat.getTrans().x(), worldMat.getTrans().y(), worldMat.getTrans().z()); + lightSource->getLight(frameNum)->setPosition(osg::Vec4f(pos, 1.f)); + + mLights.push_back(l); + } + + void LightManager::setSunlight(osg::ref_ptr sun) + { + if (usingFFP()) return; + + mSun = sun; + } + + osg::ref_ptr LightManager::getSunlight() + { + return mSun; + } + + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) + { + // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) + size_t hash = 0; + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + Misc::hashCombine(hash, id); + + if (getLightingMethod() != LightingMethod::SingleUBO) + continue; + + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; + + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + } + + auto& stateSetCache = mStateSetCache[frameNum%2]; + + auto found = stateSetCache.find(hash); + if (found != stateSetCache.end()) + { + mStateSetGenerator->update(found->second, lightList, frameNum); + return found->second; + } + + auto stateset = mStateSetGenerator->generate(lightList, frameNum); + stateSetCache.emplace(hash, stateset); + return stateset; + } + + const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum) + { + bool isReflection = isReflectionCamera(camera); + osg::observer_ptr camPtr (camera); + auto it = mLightsInViewSpace.find(camPtr); + + if (it == mLightsInViewSpace.end()) + { + it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; + + for (const auto& transform : mLights) + { + osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); + + float radius = transform.mLightSource->getRadius(); + + osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius * mPointLightRadiusMultiplier); + transformBoundingSphere(worldViewMat, viewBound); + + if (!isReflection && mPointLightFadeEnd != 0.f) + { + const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; + float fade = 1 - std::clamp((viewBound.center().length() - mPointLightFadeStart) / fadeDelta, 0.f, 1.f); + if (fade == 0.f) + continue; + + auto* light = transform.mLightSource->getLight(frameNum); + light->setDiffuse(light->getDiffuse() * fade); + } + + LightSourceViewBound l; + l.mLightSource = transform.mLightSource; + l.mViewBound = viewBound; + it->second.push_back(l); + } + } + + if (getLightingMethod() == LightingMethod::SingleUBO) + { + if (it->second.size() > static_cast(getMaxLightsInScene() - 1)) + { + auto sorter = [] (const LightSourceViewBound& left, const LightSourceViewBound& right) { + return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2(); + }; + std::sort(it->second.begin() + 1, it->second.end(), sorter); + it->second.erase((it->second.begin() + 1) + (getMaxLightsInScene() - 2), it->second.end()); + } + } + + return it->second; + } + + void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix) + { + auto* light = lightSource->getLight(frameNum); + auto& buf = getLightBuffer(frameNum); + buf->setDiffuse(index, light->getDiffuse()); + buf->setAmbient(index, light->getAmbient()); + buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); + buf->setPosition(index, light->getPosition() * (*viewMatrix)); + } LightSource::LightSource() : mRadius(0.f) + , mActorFade(1.f) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; @@ -365,19 +1255,14 @@ namespace SceneUtil LightSource::LightSource(const LightSource ©, const osg::CopyOp ©op) : osg::Node(copy, copyop) , mRadius(copy.mRadius) + , mActorFade(copy.mActorFade) { mId = sLightId++; - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } - - bool sortLights (const LightManager::LightSourceViewBound* left, const LightManager::LightSourceViewBound* right) - { - return left->mViewBound.center().length2() - left->mViewBound.radius2()*81 < right->mViewBound.center().length2() - right->mViewBound.radius2()*81; - } - void LightListCallback::operator()(osg::Node *node, osg::NodeVisitor *nv) { osgUtil::CullVisitor* cv = static_cast(nv); @@ -413,7 +1298,7 @@ namespace SceneUtil // Don't use Camera::getViewMatrix, that one might be relative to another camera! const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); - const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix); + const std::vector& lights = mLightManager->getLightsInViewSpace(cv->getCurrentCamera(), viewMatrix, mLastFrameNumber); // get the node bounds in view space // NB do not node->getBound() * modelView, that would apply the node's transformation twice @@ -421,7 +1306,7 @@ namespace SceneUtil osg::Transform* transform = node->asTransform(); if (transform) { - for (unsigned int i=0; igetNumChildren(); ++i) + for (size_t i = 0; i < transform->getNumChildren(); ++i) nodeBound.expandBy(transform->getChild(i)->getBound()); } else @@ -430,7 +1315,7 @@ namespace SceneUtil transformBoundingSphere(mat, nodeBound); mLightList.clear(); - for (unsigned int i=0; i (8 - mLightManager->getStartLight()); + size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); osg::StateSet* stateset = nullptr; @@ -451,12 +1336,12 @@ namespace SceneUtil { // remove lights culled by this camera LightManager::LightList lightList = mLightList; - for (LightManager::LightList::iterator it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights; ) + for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) { osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius*2; + bs._radius = bs._radius * 2.0; osg::CullingSet& cullingSet = stack.front(); if (cullingSet.isCulled(bs)) { @@ -474,10 +1359,10 @@ namespace SceneUtil while (lightList.size() > maxLights) lightList.pop_back(); } - stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber()); + stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); } else - stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber()); + stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); cv->pushStateSet(stateset); diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index c370f1b7f..dc7f36e96 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -2,6 +2,10 @@ #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H #include +#include +#include +#include +#include #include @@ -9,6 +13,10 @@ #include #include +#include + +#include + namespace osgUtil { class CullVisitor; @@ -16,6 +24,15 @@ namespace osgUtil namespace SceneUtil { + class LightBuffer; + struct StateSetGenerator; + + enum class LightingMethod + { + FFP, + PerObjectUniform, + SingleUBO, + }; /// LightSource managed by a LightManager. /// @par Typically used for point lights. Spot lights are not supported yet. Directional lights affect the whole scene @@ -35,6 +52,8 @@ namespace SceneUtil int mId; + float mActorFade; + public: META_Node(SceneUtil, LightSource) @@ -54,10 +73,20 @@ namespace SceneUtil mRadius = radius; } + void setActorFade(float alpha) + { + mActorFade = alpha; + } + + float getActorFade() const + { + return mActorFade; + } + /// Get the osg::Light safe for modification in the given frame. /// @par May be used externally to animate the light's color/attenuation properties, /// and is used internally to synchronize the light's position with the position of the LightSource. - osg::Light* getLight(unsigned int frame) + osg::Light* getLight(size_t frame) { return mLight[frame % 2]; } @@ -83,31 +112,9 @@ namespace SceneUtil class LightManager : public osg::Group { public: - - META_Node(SceneUtil, LightManager) - - LightManager(); - - LightManager(const LightManager& copy, const osg::CopyOp& copyop); - - /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired. - /// By default, it's ~0u i.e. always on. - /// If you have some views that do not require lighting, then set the Camera's cull mask to not include - /// the lightingMask for a much faster cull and rendering. - void setLightingMask (unsigned int mask); - - unsigned int getLightingMask() const; - - /// Set the first light index that should be used by this manager, typically the number of directional lights in the scene. - void setStartLight(int start); - - int getStartLight() const; - - /// Internal use only, called automatically by the LightManager's UpdateCallback - void update(); - - /// Internal use only, called automatically by the LightSource's UpdateCallback - void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, unsigned int frameNum); + static LightingMethod getLightingMethodFromString(const std::string& value); + /// Returns string as used in settings file, or the empty string if the method is undefined + static std::string getLightingMethodString(LightingMethod method); struct LightSourceTransform { @@ -115,36 +122,125 @@ namespace SceneUtil osg::Matrixf mWorldMatrix; }; - const std::vector& getLights() const; - struct LightSourceViewBound { LightSource* mLightSource; osg::BoundingSphere mViewBound; }; - const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix); + using LightList = std::vector; + using SupportedMethods = std::array; - typedef std::vector LightList; + META_Node(SceneUtil, LightManager) - osg::ref_ptr getLightListStateSet(const LightList& lightList, unsigned int frameNum); + LightManager(bool ffp = true); + + LightManager(const LightManager& copy, const osg::CopyOp& copyop); + + /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired. + /// By default, it's ~0u i.e. always on. + /// If you have some views that do not require lighting, then set the Camera's cull mask to not include + /// the lightingMask for a much faster cull and rendering. + void setLightingMask(size_t mask); + size_t getLightingMask() const; + + /// Set the first light index that should be used by this manager, typically the number of directional lights in the scene. + void setStartLight(int start); + int getStartLight() const; + + /// Internal use only, called automatically by the LightManager's UpdateCallback + void update(size_t frameNum); + + /// Internal use only, called automatically by the LightSource's UpdateCallback + void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum); + + const std::vector& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix, size_t frameNum); + + osg::ref_ptr getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix); + + void setSunlight(osg::ref_ptr sun); + osg::ref_ptr getSunlight(); + + bool usingFFP() const; + + LightingMethod getLightingMethod() const; + + int getMaxLights() const; + + int getMaxLightsInScene() const; + + auto& getDummies() { return mDummies; } + + auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; } + + auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + + osg::Matrixf getSunlightBuffer(size_t frameNum) const { return mSunlightBuffers[frameNum%2]; } + void setSunlightBuffer(const osg::Matrixf& buffer, size_t frameNum) { mSunlightBuffers[frameNum%2] = buffer; } + + SupportedMethods getSupportedLightingMethods() { return mSupported; } + + std::map getLightDefines() const; + + void processChangedSettings(const Settings::CategorySettingVector& changed); + + /// Not thread safe, it is the responsibility of the caller to stop/start threading on the viewer + void updateMaxLights(); private: - // Lights collected from the scene graph. Only valid during the cull traversal. + void initFFP(int targetLights); + void initPerObjectUniform(int targetLights); + void initSingleUBO(int targetLights); + + void updateSettings(); + + void setLightingMethod(LightingMethod method); + void setMaxLights(int value); + + void updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum, const osg::RefMatrix* viewMatrix); + std::vector mLights; - typedef std::vector LightSourceViewBoundCollection; + using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; // < Light list hash , StateSet > - typedef std::map > LightStateSetMap; + using LightStateSetMap = std::map>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; int mStartLight; - unsigned int mLightingMask; + size_t mLightingMask; + + osg::ref_ptr mSun; + + osg::ref_ptr mLightBuffers[2]; + + osg::Matrixf mSunlightBuffers[2]; + + // < Light ID , Buffer Index > + using LightIndexMap = std::unordered_map; + LightIndexMap mLightIndexMaps[2]; + + std::unique_ptr mStateSetGenerator; + + LightingMethod mLightingMethod; + + float mPointLightRadiusMultiplier; + float mPointLightFadeEnd; + float mPointLightFadeStart; + + int mMaxLights; + + SupportedMethods mSupported; + + static constexpr auto mMaxLightsLowerLimit = 2; + static constexpr auto mMaxLightsUpperLimit = 64; + static constexpr auto mFFPMaxLights = 8; + + static const std::unordered_map mLightingMethodSettingMap; }; /// To receive lighting, objects must be decorated by a LightListCallback. Light list callbacks must be added via @@ -180,11 +276,13 @@ namespace SceneUtil private: LightManager* mLightManager; - unsigned int mLastFrameNumber; + size_t mLastFrameNumber; LightManager::LightList mLightList; std::set mIgnoredLightSources; }; + void configureStateSetSunOverride(LightManager* lightManager, const osg::Light* light, osg::StateSet* stateset, int mode = osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } #endif diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index e9be05908..6a1a1376e 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -58,7 +58,7 @@ namespace SceneUtil light->setQuadraticAttenuation(quadraticAttenuation); } - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior) + osg::ref_ptr addLight(osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior) { SceneUtil::FindByNameVisitor visitor("AttachLight"); node->accept(visitor); @@ -85,8 +85,9 @@ namespace SceneUtil attachTo = trans; } - osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior); + osg::ref_ptr lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1)); attachTo->addChild(lightSource); + return lightSource; } osg::ref_ptr createLightSource(const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient) diff --git a/components/sceneutil/lightutil.hpp b/components/sceneutil/lightutil.hpp index 7096c38b2..1ddfa3d45 100644 --- a/components/sceneutil/lightutil.hpp +++ b/components/sceneutil/lightutil.hpp @@ -32,7 +32,7 @@ namespace SceneUtil /// @param partsysMask Node mask to ignore when computing the sub graph's bounding box. /// @param lightMask Mask to assign to the newly created LightSource. /// @param isExterior Is the light outside? May be used for deciding which attenuation settings to use. - void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior); + osg::ref_ptr addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior); /// @brief Convert an ESM::Light to a SceneUtil::LightSource, and return it. /// @param esmLight The light definition coming from the game files containing radius, color, flicker, etc. diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 89e001a98..bf6581ae6 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -2113,7 +2113,7 @@ struct ConvexHull if (edge.first == vertex) otherEnd = edge.second; else if (edge.second == vertex) - otherEnd - edge.first; + otherEnd = edge.first; else continue; diff --git a/components/sceneutil/pathgridutil.cpp b/components/sceneutil/pathgridutil.cpp index ed6894dfc..f37a8ba59 100644 --- a/components/sceneutil/pathgridutil.cpp +++ b/components/sceneutil/pathgridutil.cpp @@ -142,7 +142,7 @@ namespace SceneUtil osg::Vec3f dir = toPos - fromPos; dir.normalize(); - osg::Quat rot = osg::Quat(-osg::PI / 2, osg::Vec3(0, 0, 1)); + osg::Quat rot(static_cast(-osg::PI_2), osg::Vec3f(0, 0, 1)); dir = rot * dir; unsigned short diamondIndex = 0; diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp index 5a4096f5c..abc1fa8b4 100644 --- a/components/sceneutil/shadowsbin.cpp +++ b/components/sceneutil/shadowsbin.cpp @@ -194,8 +194,9 @@ void ShadowsBin::sortImplementation() // noTestRoot is now a stategraph with useDiffuseMapForShadowAlpha disabled but minimal other state bool cullFaceOverridden = false; - while ((root = root->_parent)) + while (root->_parent) { + root = root->_parent; if (!root->getStateSet()) continue; unsigned int cullFaceFlags = root->getStateSet()->getMode(GL_CULL_FACE); diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index cc8a2f44b..1fe368572 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -266,7 +266,9 @@ bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg:: { #if OSG_VERSION_LESS_THAN(3, 6, 6) // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028 - osg::GLExtensions::Get(0, false)->glRenderbufferStorageMultisampleCoverageNV = nullptr; + osg::ref_ptr extensions = osg::GLExtensions::Get(0, false); + if (extensions) + extensions->glRenderbufferStorageMultisampleCoverageNV = nullptr; #endif unsigned int samples = 0; unsigned int colourSamples = 0; diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index ca223ae3b..7ebfd710f 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -361,13 +361,10 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v /// \brief Package mouse and mousewheel motions into a single event MouseMotionEvent InputWrapper::_packageMouseMotion(const SDL_Event &evt) { - MouseMotionEvent pack_evt; + MouseMotionEvent pack_evt = {}; pack_evt.x = mMouseX; - pack_evt.xrel = 0; pack_evt.y = mMouseY; - pack_evt.yrel = 0; pack_evt.z = mMouseZ; - pack_evt.zrel = 0; if(evt.type == SDL_MOUSEMOTION) { diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 4f887e659..3a5b46440 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -9,17 +9,28 @@ #include #include +#include #include #include namespace Shader { + ShaderManager::ShaderManager() + : mLightingMethod(SceneUtil::LightingMethod::FFP) + { + } + void ShaderManager::setShaderPath(const std::string &path) { mPath = path; } + void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method) + { + mLightingMethod = method; + } + bool addLineDirectivesAfterConditionalBlocks(std::string& source) { for (size_t position = 0; position < source.length(); ) @@ -344,6 +355,8 @@ namespace Shader program->addShader(fragmentShader); program->addBindAttribLocation("aOffset", 6); program->addBindAttribLocation("aRotation", 7); + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 13db30b01..2450f0d6d 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -11,16 +11,38 @@ #include +#include + +namespace Resource +{ + class SceneManager; +} + +namespace SceneUtil +{ + enum class LightingMethod; +} + namespace Shader { + enum class UBOBinding + { + LightBuffer + }; + /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's. /// @par Shader templates can get the value of a define with the syntax @define. class ShaderManager { public: + + ShaderManager(); + void setShaderPath(const std::string& path); + void setLightingMethod(SceneUtil::LightingMethod method); + typedef std::map DefineMap; /// Create or retrieve a shader instance. @@ -59,6 +81,8 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; + SceneUtil::LightingMethod mLightingMethod; + std::mutex mMutex; }; diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index b0013538f..e8ac7d9c3 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -278,8 +278,7 @@ namespace Shader const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); osg::StateSet::AttributeList removedAttributes; - osg::ref_ptr removedState; - if (removedState = getRemovedState(*stateset)) + if (osg::ref_ptr removedState = getRemovedState(*stateset)) removedAttributes = removedState->getAttributeList(); for (const auto& attributeMap : { attributes, removedAttributes }) { @@ -475,8 +474,7 @@ namespace Shader writableStateSet->removeAttribute(osg::StateAttribute::PROGRAM); - osg::ref_ptr removedState; - if (removedState = getRemovedState(*writableStateSet)) + if (osg::ref_ptr removedState = getRemovedState(*writableStateSet)) { // user data is normally shallow copied so shared with the original stateset osg::ref_ptr writableUserData; diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 7f184c70e..70b44935c 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -241,7 +241,7 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask, int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize) +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize) : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) @@ -256,7 +256,7 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour mChunkManagers.push_back(mChunkManager.get()); } -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize) +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize) : TerrainGrid(parent, storage, nodeMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index aba2dccf3..2ddea4204 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -20,9 +20,9 @@ namespace Terrain class QuadTreeWorld : public TerrainGrid // note: derived from TerrainGrid is only to render default cells (see loadCell) { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize); - QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize); + QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize); ~QuadTreeWorld(); diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index cf8debc69..bbd0508ef 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -20,13 +20,13 @@ public: void reset() override {} }; -TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) +TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask) : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mNumSplits(4) { } -TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, int nodeMask) +TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, unsigned int nodeMask) : Terrain::World(parent, storage, nodeMask) , mNumSplits(4) { diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index dc9203466..3f44b18b0 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -14,8 +14,8 @@ namespace Terrain class TerrainGrid : public Terrain::World { public: - TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); - TerrainGrid(osg::Group* parent, Storage* storage, int nodeMask=~0); + TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask=~0u, unsigned int borderMask=0); + TerrainGrid(osg::Group* parent, Storage* storage, unsigned int nodeMask=~0u); ~TerrainGrid(); void cacheCell(View* view, int x, int y) override; diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 15ec72973..d1581724e 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -13,7 +13,7 @@ namespace Terrain { -World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) +World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask) : mStorage(storage) , mParent(parent) , mResourceSystem(resourceSystem) @@ -49,7 +49,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mResourceSystem->addResourceManager(mTextureManager.get()); } -World::World(osg::Group* parent, Storage* storage, int nodeMask) +World::World(osg::Group* parent, Storage* storage, unsigned int nodeMask) : mStorage(storage) , mParent(parent) , mCompositeMapCamera(nullptr) diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index a4be57e8e..5797d894e 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -105,8 +105,8 @@ namespace Terrain /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param nodeMask mask for the terrain root /// @param preCompileMask mask for pre compiling textures - World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask); - World(osg::Group* parent, Storage* storage, int nodeMask); + World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask); + World(osg::Group* parent, Storage* storage, unsigned int nodeMask); virtual ~World(); /// Set a WorkQueue to delete objects in the background thread. diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst b/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst index 48a19cb7c..47096a83e 100644 --- a/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst +++ b/docs/source/reference/modding/custom-models/pipeline-blender-collada.rst @@ -1,79 +1,122 @@ ############################## -Blender to OpenMW with Collada +Blender to OpenMW with COLLADA ############################## -First, let's take a look at the pipeline requirements and how the fundamental properties of a scene translate from Blender to OpenMW. - Requirements ------------- +************ +To use the Blender to OpenMW pipeline via COLLADA, you will need the following. + * `OpenMW 0.47 `_ or later * `Blender 2.81 `_ or later. Latest confirmed, working version is Blender 2.91 * `Better COLLADA Exporter `_ tuned for OpenMW * A model you would like to export -In addition, OpenMW needs to be configured to read COLLADA (dae) files instead of the default format (nif). In settings.cfg under [Models] section... TODO + +Static Models +************* +Static models are those that don't have any animations included in the exported file. First, let's take a look at how the fundamental properties of a scene in Blender translate to a COLLADA model suitable for use in OpenMW. These apply the same to static and animated models. Location --------- +======== Objects keep their visual location and origin they had in the original scene. - Rotation --------- +======== * Blender’s +Z axis is up axis in OpenMW * Blender’s +Y axis is front axis in OpenMW * Blender’s X axis is left-right axis in OpenMW - Scale ------ +===== -Scale ratio between Blender and OpenMW is 70 to 1. This means 70 blender units translate to 1 m in OpenMW. +Scale ratio between Blender and OpenMW is 70 to 1. This means 70 units in Blender translate to 1 m in OpenMW. -However, a scale factor like this is impractical to work with. A better approach is to work with a scale of 1 Blender unit = 1m and apply the 70 scale factor in the Better COLLADA Exporter. The exporter will automatically scale all object, mesh, armature and animation data. +However, a scale factor like this is impractical to work with. A better approach is to work with a scale of 1 Blender unit = 1 m and apply the 70 scale factor at export. The exporter will automatically scale all object, mesh, armature and animation data. + +Materials +========= + +OpenMW uses the classic, specular material setup and currently doesn't use the more mainstream `PBR `_ way. In Blender, the mesh needs a default material with a diffuse texture connected to the ``Base Color`` socket. This is enough for the material to be included in the exported COLLADA file. + +Additional texture types, such as specular or normal maps, will be automatically recognized and used by OpenMW. They need an identical base name as the diffuse texture, a suffix, and be in the same folder. How to enable this and what suffix is used for what texture type is covered in more detail in :doc:`../../modding/texture-modding/texture-basics`. + +Collision Shapes +================ + +In Blender, create an empty and name it ``collision``. Any mesh that is a child of this empty will be used for physics collision and will not be visible. There can be multiple child meshes under ``collision`` and they will all contribute to the collision shapes. The meshes themselves can have an arbitrary name, it's only the name of the empty that is important. The ``tcb`` command in OpenMW's in-game console will make the collision shapes visible and you will be able to inspect them. + +Exporter Settings +================= + +For static models, use the following exporter settings. Before export, select all objects you wish to include in the exported file and have the "Selected Objects" option enabled. Without this, the exporter could fail. -Exporter settings - static models ---------------------------------- - -Better COLLADA Exporter offers various options which are rather straightforward for static models. The important one is last in the list, to apply a scaling factor of 70 to the whole scene, so 1 blender unit equals 1 m in OpenMW. The following settings should be good for general use. -It's also very important to have "export selected" box checked, as otherwise the exporter may just fail with an error message. It's also important to have the correct window open, and the models selected before exporting. +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_exporter_static.jpg + :align: center -Animated models to OpenMW -------------------------- +Animated Models +*************** Animated models are those where a hierarchy of bones, known as armature, deforms the mesh and makes things move. Besides the topics covered above, the following requirements apply. Armature --------- +======== -* For animated models, a single armature per COLLADA file is advised to avoid any potential problems. +* A single armature per COLLADA file is advised to avoid any potential problems. * There needs to be a single top-most bone in the armature’s hierarchy, where both the deformation and control bones fall under it. * Not all bones need to be exported. By disabing the bone’s “Deform” property and using the corresponding option in the exporter, it is possible to export only the bones needed for animation. Animations ----------- +========== Every action in Blender is exported as its own animation clip in COLLADA. Actions you don't wish to export need to have "-noexp" added to their name, with the corresponding option enabled in the exporter. -Due to current limitations of the format / exporter, the keyframes of any action must not overlap the keyframes of any other action. Thus in practice, the keyframes for each action need to be manually offset to their unique range on the timeline. +Due to current limitations of the format and exporter, the keyframes of individual actions must not overlap with other actions. The keyframes need to be manually offset to a unique range on the timeline as shown in this example. -An animated .dae file needs a corresponding animation definition file, or textkeys, for OpenMW to understand. Textkeys are set in .txt file with the same name as the model. E.g. OpenMWDude.dae -> OpenMWDude.txt , each line having a textkey and a double number for timesignature. E.g. idle: start 0.03333333333333333. +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_animations_on_timeline.jpg + :align: center + +Textkeys +-------- + +The exported COLLADA file requires a corresponding textkeys file for OpenMW to properly read the animations. Textkeys is a .txt file containing animation definitions. Textkeys file is placed in the same folder as the model and uses a name matching the model. + + - ``OpenMWDude.dae`` + - ``OpenMWDude.txt`` + +Textkeys use a simple format as shown in the example. Name, start and stop values can be taken from the corresponding COLLADA file for each ````. + +.. code:: + + idle: start 0.03333333333333333 + idle: stop 2.033333333333333 + runforward: start 2.0666666666666664 + runforward: stop 3.0666666666666664 + runback: start 3.1 + runback: stop 4.1 + ... + Root Motion ------------ +=========== -OpenMW can read the movement of the root (top-most) bone and use it to move objects in the game world. For this to work, the root bone must be animated to move through space. The root bone must, in its default pose, be alligned with the world. +OpenMW can read the movement of the root (top-most in hierarchy) bone and use it to move objects in the game world. For this to work, the root bone must be animated to move through space. The root bone must, in its default pose, be alligned with the world. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_rig_root.jpg + :align: center Exporter Settings ------------------ +================= -For animated models, use the following exporter settings. Before export, select all objects you wish to include in the exported file. TODO +For animated models, use the following exporter settings. Before export, select all objects you wish to include in the exported file and have the "Selected Objects" option enabled. Without this, the exporter could fail. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/custom-models/_static/dae_exporter_animated.jpg + :align: center diff --git a/docs/source/reference/modding/index.rst b/docs/source/reference/modding/index.rst index df98137d8..90491d34d 100644 --- a/docs/source/reference/modding/index.rst +++ b/docs/source/reference/modding/index.rst @@ -24,5 +24,6 @@ about creating new content for OpenMW, please refer to texture-modding/index custom-models/index font + sky-system extended paths diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index cad04ab5c..00f99e47b 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -5,12 +5,13 @@ scaling factor -------------- :Type: floating point -:Range: > 0.0 +:Range: 0.5 to 8.0 :Default: 1.0 -This setting scales the GUI interface windows. +This setting scales GUI windows. A value of 1.0 results in the normal scale. Larger values are useful to increase the scale of the GUI for high resolution displays. -This setting can only be configured by editing the settings configuration file. + +This setting can be configured in the Interface section of Advanced tab of the launcher. font size --------- diff --git a/docs/source/reference/modding/settings/cells.rst b/docs/source/reference/modding/settings/cells.rst index 620b545ac..ab707faf8 100644 --- a/docs/source/reference/modding/settings/cells.rst +++ b/docs/source/reference/modding/settings/cells.rst @@ -175,7 +175,7 @@ pointers cache size ------------------- :Type: integer -:Range: >0 +:Range: 40 to 1000 :Default: 40 The count of object pointers that will be saved for a faster search by object ID. diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index f0c37b738..ef97caeec 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -54,3 +54,57 @@ chunks near player will have size 4096x4096 game units. Larger chunks reduce CPU Smaller values do an opposite. This setting can only be configured by editing the settings configuration file. + +stomp mode +---------- + +:Type: integer +:Range: 0, 1, 2 +:Default: 2 + +Determines whether grass should respond to the player treading on it. + +.. list-table:: Modes + :header-rows: 1 + * - Mode number + - Meaning + * - 0 + - Grass cannot be trampled. + * - 1 + - The player's XY position is taken into account. + * - 2 + - The player's height above the ground is taken into account, too. + +In MGE XE, which existing grass mods were made for, only the player's XY position was taken into account. +However, presumably due to a bug, jumping straight up would change the XY position, so grass *does* respond to the player jumping. +Levitating above grass in MGE XE still considers it stood-on, which can look bad. +OpenMW's height-aware system ensures grass does not act as if it's being stood on when the player levitates above it, but that means grass rapidly snaps back to its original position when the player jumps out of it. +Therefore, it is not recommended to use MGE XE's intensity constants if this setting is set to 2, i.e. :ref:`stomp intensity` should be 0 or 1 when :ref:`stomp mode` is 2. + +stomp intensity +--------------- + +:Type: integer +:Range: 0, 1, 2 +:Default: 1 + +How far away from the player grass can be before it's unaffected by being trod on, and how far it moves when it is. + +.. list-table:: Presets + :header-rows: 1 + * - Preset number + - Range (Units) + - Distance (Units) + - Description + * - 2 + - 150 + - 60 + - MGE XE levels. Generally excessive/comical, but what existing mods were made with in mind. + * - 1 + - 80 + - 40 + - Reduced levels. Usually looks better. + * - 0 + - 50 + - 20 + - Gentle levels. diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index acc848299..03b7805de 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -40,7 +40,7 @@ Only affects objects that render with shaders (see 'force shaders' option). Always affects terrain. Leaving this option at its default makes the lighting compatible with Morrowind's fixed-function method, -but the lighting may appear dull and there might be colour shifts. +but the lighting may appear dull and there might be colour shifts. Setting this option to 'false' results in more dynamic lighting. auto use object normal maps @@ -148,6 +148,117 @@ By default, the fog becomes thicker proportionally to your distance from the cli This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV. Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain. +lighting method +--------------- + +:Type: string +:Range: legacy|shaders compatibility|shaders +:Default: default + +Sets the internal handling of light sources. + +'legacy' is restricted to 8 lights per object and emulates fixed function +pipeline compatible lighting. + +'shaders compatibility' removes the light limit controllable through :ref:`max +lights` and follows a modifed attenuation formula which can drastically reduce +light popping and seams. This mode also enables lighting on groundcover and a +configurable light fade. It is recommended to use this with older hardware and a +light limit closer to 8. Because of its wide range of compatibility it is set as +the default. + +'shaders' carries all of the benefits that 'shaders compatibility' does, but +uses a modern approach that allows for a higher :ref:`max lights` count with +little to no performance penalties on modern hardware. It is recommended to use +this mode when supported and where the GPU is not a bottleneck. On some weaker +devices, using this mode along with :ref:`force per pixel lighting` can carry +performance penalties. + +When enabled, groundcover lighting is forced to be vertex lighting, unless +normal maps are provided. This is due to some groundcover mods using the Z-Up +normals technique to avoid some common issues with shading. As a consequence, +per pixel lighting would give undesirable results. + +Note that the rendering will act as if you have 'force shaders' option enabled +when not set to 'legacy'. This means that shaders will be used to render all objects and +the terrain. + +light bounds multiplier +----------------------- + +:Type: float +:Range: 0.0-5.0 +:Default: 1.65 + +Controls the bounding sphere radius of point lights, which is used to determine +if an object should receive lighting from a particular light source. Note, this +has no direct effect on the overall illumination of lights. Larger multipliers +will allow for smoother transitions of light sources, but may require an +increase in :ref:`max lights` and thus carries a performance penalty. This +especially helps with abrupt light popping with handheld light sources such as +torches and lanterns. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +maximum light distance +---------------------- + +:Type: float +:Range: The whole range of 32-bit floating point +:Default: 8192 + +The maximum distance from the camera that lights will be illuminated, applies to +both interiors and exteriors. A lower distance will improve performance. Set +this to a non-positive value to disable fading. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +light fade start +---------------- + +:Type: float +:Range: 0.0-1.0 +:Default: 0.85 + +The fraction of the maximum distance at which lights will begin to fade away. +Tweaking it will make the transition proportionally more or less smooth. + +This setting has no effect if the :ref:`maximum light distance` is non-positive +or :ref:`lighting method` is 'legacy'. + +max lights +---------- + +:Type: integer +:Range: 2-64 +:Default: 8 + +Sets the maximum number of lights that each object can receive lighting from. +Increasing this too much can cause significant performance loss, especially if +:ref:`lighting method` is not set to 'shaders' or :ref:`force per pixel +lighting` is on. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + +minimum interior brightness +------------------------ + +:Type: float +:Range: 0.0-1.0 +:Default: 0.08 + +Sets the minimum interior ambient brightness for interior cells when +:ref:`lighting method` is not 'legacy'. A consequence of the new lighting system +is that interiors will sometimes be darker since light sources now have sensible +fall-offs. A couple solutions are to either add more lights or increase their +radii to compensate, but these require content changes. For best results it is +recommended to set this to 0.0 to retain the colors that level designers +intended. If brighter interiors are wanted, however, this setting should be +increased. Note, it is advised to keep this number small (< 0.1) to avoid the +aforementioned changes in visuals. + +This setting has no effect if :ref:`lighting method` is 'legacy'. + antialias alpha test --------------------------------------- diff --git a/docs/source/reference/modding/settings/terrain.rst b/docs/source/reference/modding/settings/terrain.rst index 149ef979e..e6018a865 100644 --- a/docs/source/reference/modding/settings/terrain.rst +++ b/docs/source/reference/modding/settings/terrain.rst @@ -129,7 +129,7 @@ object paging active grid ------------------------- :Type: boolean :Range: True/False -:Default: False +:Default: True Controls whether the objects in the active cells use the mentioned paging algorithms. Active grid paging significantly improves the framerate when your setup is CPU-limited. diff --git a/docs/source/reference/modding/sky-system.rst b/docs/source/reference/modding/sky-system.rst new file mode 100644 index 000000000..fa71295e3 --- /dev/null +++ b/docs/source/reference/modding/sky-system.rst @@ -0,0 +1,79 @@ +######################## +Sky System and Structure +######################## + +Overview +******** + +The sky system is made from multiple components that contribute to the final result. + +1. Background colour and fog +2. Atmosphere +3. Clouds +4. Stars +5. Sun +6. Moons +7. Weather effects + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/_static/sky-system-overview.png + :align: center + +Background Colour and Fog +************************* + +Even when nothing else is being rendered, OpenMW will always show the basic background colour. The distant fog effect is based off this colour as well. Other elements of the sky are rendered on top of the background to compose the sky. The colour of the background changes depending on the time of day and current weather. + +Atmosphere +********** + +A mesh that contributes the blue colour of the sky. It is a rough cylinder in shape without the bottom face and with the normals pointing inwards. During the day, it is light blue and transitions to a very dark blue, almost black, during the night. + +Towards the bottom edge the mesh gradually becomes transparent and blends with the background colour. Transparency is done per vertex and OpenMW decides which vertices are transparent based on their index. This adds a requirement for a very strict vertex and face order on this mesh to blend properly. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/_static/sky-system-mesh-atmosphere.jpg + :align: center + +Clouds +****** + +A mesh that renders a tiling, scrolling texture of the clouds. It is a flat dome in shape with the normals pointing inwards. Towards the boundary edge the mesh becomes transparent and blends with the background colour. As with the atmosphere, there is a very strict vertex order on this mesh to blend properly. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/_static/sky-system-mesh-clouds.jpg + :align: center + +By default, the UVs of this mesh are scaled so the clouds texture repeats five times across the sky. The weather system blends between different cloud textures depending on the current weather. Speed of the clouds moving across the sky can be set per weather type. + +Stars +***** + +A dome shaped mesh that shows the stars during the night. It starts to become visible during sunset and goes back to transparent during sunrise. At its bottom edge it blends to transparency which is defined with the vertex colour. White is full opacity while black is full transparency. The mesh ends above the horizon to prevent the stars being visible near the horizon when underwater. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/_static/sky-system-mesh-stars.jpg + :align: center + +It can use multiple textures to show different star constellations. In addition, extra meshes can be used to blend nebulae across the sky. + +Sun +*** + +The sun is a billboard that moves across the sky. It is composed of two texures. One shows the regular sun sphere, the other is the sun glare that is added on top of the sun. Glare strength adjusts dynamically depending on how obstructed the view to the sun is. + +Moons +***** + +The moons are two separate billboards moving across the sky and are both rendered the same way. First, a circle texture is used to mask the background. A moon texture is then added on top of the mask. Depending on the current moon phase, a variant of the moon texture is used. The texture on top is additively blended so any transparent area is achieved with black pixels. The following image shows all the separate textures needed for one moon. + +.. image:: https://gitlab.com/OpenMW/openmw-docs/-/raw/master/docs/source/reference/modding/_static/sky-system-moon-textures.jpg + :align: center + + +Weather Effects +*************** + +These are particle emitters used to display weather phenomena such as rain, snow, or dust clouds. Originally, Morrowind used .nif files with a number of planes and baked animations. In OpenMW, these effects are done through code and are currently hardcoded. The particle emmitters emit particles around the player in a big enough area so it looks like the whole world is affected by the weather. + +Settings +******** + +Colour and other settings for each weather type can be edited in ``openmw.cfg`` configuration file. + diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index babb5c28f..92c185413 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -457,6 +457,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +